nx 23.2.0-beta.5 → 23.2.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/src/command-line/init/init-v2.js +2 -0
  2. package/dist/src/command-line/migrate/migrate.js +44 -27
  3. package/dist/src/command-line/migrate/version-skew-guard.d.ts +3 -4
  4. package/dist/src/command-line/migrate/version-skew-guard.js +5 -6
  5. package/dist/src/command-line/reset/reset.js +6 -1
  6. package/dist/src/command-line/run/run-one.js +1 -0
  7. package/dist/src/command-line/run-many/run-many.js +1 -0
  8. package/dist/src/core/graph/main.js +1 -1
  9. package/dist/src/core/graph/styles.js +1 -1
  10. package/dist/src/daemon/client/client.d.ts +45 -1
  11. package/dist/src/daemon/client/client.js +128 -32
  12. package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
  13. package/dist/src/daemon/message-types/daemon-message.d.ts +32 -0
  14. package/dist/src/daemon/message-types/daemon-message.js +25 -0
  15. package/dist/src/daemon/server/server.js +22 -0
  16. package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
  17. package/dist/src/daemon/server/shutdown-utils.js +11 -4
  18. package/dist/src/daemon/socket-utils.d.ts +1 -0
  19. package/dist/src/daemon/socket-utils.js +24 -3
  20. package/dist/src/daemon/tmp-dir.d.ts +43 -3
  21. package/dist/src/daemon/tmp-dir.js +437 -15
  22. package/dist/src/devkit-internals.d.ts +2 -1
  23. package/dist/src/devkit-internals.js +14 -5
  24. package/dist/src/generators/tree.d.ts +4 -2
  25. package/dist/src/native/index.d.ts +10 -0
  26. package/dist/src/native/index.js +71 -28
  27. package/dist/src/native/native-bindings.js +1 -0
  28. package/dist/src/native/native-file-cache-location.d.ts +17 -1
  29. package/dist/src/native/native-file-cache-location.js +86 -12
  30. package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
  31. package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
  32. package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
  33. package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
  34. package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
  35. package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
  36. package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
  37. package/dist/src/project-graph/project-graph.js +20 -0
  38. package/dist/src/utils/child-process.d.ts +31 -1
  39. package/dist/src/utils/child-process.js +102 -1
  40. package/dist/src/utils/ignore.d.ts +128 -1
  41. package/dist/src/utils/ignore.js +182 -12
  42. package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
  43. package/dist/src/utils/nx-tmp-dir.js +40 -0
  44. package/dist/src/utils/owned-private-dir.d.ts +132 -0
  45. package/dist/src/utils/owned-private-dir.js +323 -0
  46. package/dist/src/utils/plugins/core-plugins.js +4 -0
  47. package/dist/src/utils/shell-quoting.d.ts +15 -0
  48. package/dist/src/utils/shell-quoting.js +35 -0
  49. package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
  50. package/dist/src/utils/wait-for-socket-connection.js +14 -8
  51. package/package.json +12 -11
@@ -1,15 +1,11 @@
1
1
  const { join, basename } = require('path');
2
- const {
3
- copyFileSync,
4
- existsSync,
5
- mkdirSync,
6
- renameSync,
7
- statSync,
8
- unlinkSync,
9
- } = require('fs');
2
+ const { copyFileSync, renameSync, statSync, unlinkSync } = require('fs');
3
+ const { createHash } = require('crypto');
10
4
  const Module = require('module');
11
5
  const { nxVersion } = require('../utils/versions');
12
- const { getNativeFileCacheLocation } = require('./native-file-cache-location');
6
+ const {
7
+ ensureSecureNativeFileCacheLocation,
8
+ } = require('./native-file-cache-location');
13
9
 
14
10
  const MAX_COPY_RETRIES = 3;
15
11
 
@@ -73,6 +69,14 @@ function statsOrNull(path) {
73
69
  }
74
70
  }
75
71
 
72
+ function removeIfPresent(path) {
73
+ try {
74
+ unlinkSync(path);
75
+ } catch {
76
+ // Ignore cleanup errors
77
+ }
78
+ }
79
+
76
80
  function isNoExecError(e) {
77
81
  return e.code === 'EACCES' || e.code === 'EPERM';
78
82
  }
@@ -95,20 +99,44 @@ Module._load = function (request, parent, isMain) {
95
99
  const nativeLocation = require.resolve(modulePath);
96
100
  const fileName = basename(nativeLocation);
97
101
 
98
- // we copy the file to a workspace-scoped tmp directory and prefix with nxVersion to avoid stale files being loaded
99
- const nativeFileCacheLocation = getNativeFileCacheLocation();
102
+ // null when the cache dir cannot be created *and trusted*; loading in place
103
+ // from node_modules is safer than loading out of a directory we don't own.
104
+ const nativeFileCacheLocation = ensureSecureNativeFileCacheLocation();
105
+ if (!nativeFileCacheLocation) {
106
+ return originalLoad.apply(this, [nativeLocation, parent, isMain]);
107
+ }
108
+
109
+ // nxVersion alone is not enough: in a source checkout it is the placeholder
110
+ // 0.0.1 for every worktree, so hashing the resolved binding path keeps each
111
+ // checkout in its own entry.
112
+ const cacheKey =
113
+ nxVersion +
114
+ '-' +
115
+ createHash('sha256')
116
+ .update(nativeLocation)
117
+ .digest('hex')
118
+ .substring(0, 12);
119
+
100
120
  // This is a path to copy to, not the one that gets loaded
101
121
  const tmpTmpFile = join(
102
122
  nativeFileCacheLocation,
103
- nxVersion + '-' + Math.random() + fileName
123
+ cacheKey + '-' + Math.random() + fileName
104
124
  );
105
125
  // This is the path that will get loaded
106
- const tmpFile = join(nativeFileCacheLocation, nxVersion + '-' + fileName);
107
- const expectedFileSize = statSync(nativeLocation).size;
126
+ const tmpFile = join(nativeFileCacheLocation, cacheKey + '-' + fileName);
127
+ const sourceStats = statSync(nativeLocation);
128
+ const expectedFileSize = sourceStats.size;
108
129
  const existingFileStats = statsOrNull(tmpFile);
109
130
 
110
- // If the file to be loaded already exists, just load it
111
- if (existingFileStats?.size === expectedFileSize) {
131
+ // Size alone does not detect a rebuilt binding: a small Rust edit routinely
132
+ // produces a byte-identical size. The copy is taken after the build, so a
133
+ // cache entry older than the source means the source was rebuilt since.
134
+ const isFresh =
135
+ existingFileStats?.size === expectedFileSize &&
136
+ existingFileStats.mtimeMs >= sourceStats.mtimeMs;
137
+
138
+ // If the file to be loaded already exists and is current, just load it
139
+ if (isFresh) {
112
140
  try {
113
141
  return originalLoad.apply(this, [tmpFile, parent, isMain]);
114
142
  } catch (e) {
@@ -119,20 +147,36 @@ Module._load = function (request, parent, isMain) {
119
147
  throw e;
120
148
  }
121
149
  }
122
- if (!existsSync(nativeFileCacheLocation)) {
123
- mkdirSync(nativeFileCacheLocation, { recursive: true });
124
- }
125
150
 
126
151
  // Retry copying up to 3 times, validating after each copy
152
+ let attemptsMade = 0;
153
+ let cacheError = null;
127
154
  for (let attempt = 1; attempt <= MAX_COPY_RETRIES; attempt++) {
155
+ attemptsMade = attempt;
128
156
  // First copy to a unique location for each process
129
- copyFileSync(nativeLocation, tmpTmpFile);
157
+ try {
158
+ copyFileSync(nativeLocation, tmpTmpFile);
159
+ } catch (e) {
160
+ // Permission errors won't heal on retry, and a throwing copy can still
161
+ // have left a partial file behind.
162
+ cacheError = e;
163
+ removeIfPresent(tmpTmpFile);
164
+ break;
165
+ }
130
166
 
131
167
  // Validate the copy - check file size matches expected
132
168
  const copiedFileStats = statsOrNull(tmpTmpFile);
133
169
  if (copiedFileStats?.size === expectedFileSize) {
134
170
  // Copy succeeded, rename to final location and load
135
- renameSync(tmpTmpFile, tmpFile);
171
+ try {
172
+ renameSync(tmpTmpFile, tmpFile);
173
+ } catch (e) {
174
+ // Unguarded, this throws out of `require` and native-bindings.js
175
+ // reports it as a missing optional dependency, which it is not.
176
+ cacheError = e;
177
+ removeIfPresent(tmpTmpFile);
178
+ break;
179
+ }
136
180
  try {
137
181
  return originalLoad.apply(this, [tmpFile, parent, isMain]);
138
182
  } catch (e) {
@@ -145,16 +189,15 @@ Module._load = function (request, parent, isMain) {
145
189
  }
146
190
 
147
191
  // Copy failed validation, clean up the malformed file
148
- try {
149
- unlinkSync(tmpTmpFile);
150
- } catch {
151
- // Ignore cleanup errors
152
- }
192
+ removeIfPresent(tmpTmpFile);
153
193
  }
154
194
 
155
- // All retries failed - warn and load from original location
195
+ // Name the errno: ENOSPC, EROFS, EACCES and EDQUOT are all actionable and
196
+ // otherwise indistinguishable.
156
197
  console.warn(
157
- `Warning: Failed to copy native module to cache after ${MAX_COPY_RETRIES} attempts. ` +
198
+ `Warning: Failed to copy native module to cache after ${attemptsMade} attempt${
199
+ attemptsMade === 1 ? '' : 's'
200
+ }${cacheError ? ` (${cacheError.code ?? cacheError.message})` : ''}. ` +
158
201
  `Loading from original location instead. ` +
159
202
  `This may cause file locking issues on Windows.`
160
203
  );
@@ -610,6 +610,7 @@ module.exports.getBinaryTarget = nativeBinding.getBinaryTarget
610
610
  module.exports.getDefaultMaxCacheSize = nativeBinding.getDefaultMaxCacheSize
611
611
  module.exports.getEventDimensions = nativeBinding.getEventDimensions
612
612
  module.exports.getFilesForOutputsBatch = nativeBinding.getFilesForOutputsBatch
613
+ module.exports.getHardcodedIgnorePatterns = nativeBinding.getHardcodedIgnorePatterns
613
614
  module.exports.getMainWorktreeRoot = nativeBinding.getMainWorktreeRoot
614
615
  module.exports.getTransformableOutputs = nativeBinding.getTransformableOutputs
615
616
  module.exports.GroupType = nativeBinding.GroupType
@@ -1 +1,17 @@
1
- export declare function getNativeFileCacheLocation(): string;
1
+ /**
2
+ * Path of the current user's native binary cache. Nothing about the constant is
3
+ * owner-only — `ensureSecureNativeFileCacheLocation` is what establishes that,
4
+ * and on Windows nothing does, since the OS temp dir is already per-account.
5
+ */
6
+ export declare const NATIVE_CACHE_ROOT: string;
7
+ export declare function getNativeFileCacheLocationToDelete(): string | null;
8
+ /**
9
+ * Create the native file cache dir, or return `null` if it cannot be created
10
+ * *securely* — in which case the caller loads the binding in place.
11
+ *
12
+ * The stable top-level container is verified as safe for private children. The
13
+ * uid directory and every directory loaded through are owner-only.
14
+ * `ensureOwnedPrivateDir` refuses a directory or symlink another local user
15
+ * planted before us.
16
+ */
17
+ export declare function ensureSecureNativeFileCacheLocation(cacheRoot?: string): string | null;
@@ -1,23 +1,97 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getNativeFileCacheLocation = getNativeFileCacheLocation;
4
- const os_1 = require("os");
3
+ exports.NATIVE_CACHE_ROOT = void 0;
4
+ exports.getNativeFileCacheLocationToDelete = getNativeFileCacheLocationToDelete;
5
+ exports.ensureSecureNativeFileCacheLocation = ensureSecureNativeFileCacheLocation;
5
6
  const path_1 = require("path");
6
- const crypto_1 = require("crypto");
7
- const workspace_root_1 = require("../utils/workspace-root");
7
+ const fs_1 = require("fs");
8
+ const nx_tmp_dir_1 = require("../utils/nx-tmp-dir");
8
9
  const versions_1 = require("../utils/versions");
9
- function getNativeFileCacheLocation() {
10
- if (process.env.NX_NATIVE_FILE_CACHE_DIRECTORY) {
11
- return process.env.NX_NATIVE_FILE_CACHE_DIRECTORY;
10
+ const owned_private_dir_1 = require("../utils/owned-private-dir");
11
+ /**
12
+ * Path of the current user's native binary cache. Nothing about the constant is
13
+ * owner-only — `ensureSecureNativeFileCacheLocation` is what establishes that,
14
+ * and on Windows nothing does, since the OS temp dir is already per-account.
15
+ */
16
+ exports.NATIVE_CACHE_ROOT = (0, path_1.join)(nx_tmp_dir_1.NX_USER_TMP_DIR, 'native-cache');
17
+ /**
18
+ * The configured cache dir, normalized. `resolve` strips a trailing slash, which
19
+ * would otherwise defeat the guards downstream: `lstat` on a path ending in `/`
20
+ * resolves a symlink rather than reporting it, and `O_NOFOLLOW` then opens the
21
+ * target. `NX_SOCKET_DIR` is normalized for the same reason.
22
+ */
23
+ function configuredCacheDir() {
24
+ const dir = process.env.NX_NATIVE_FILE_CACHE_DIRECTORY;
25
+ return dir ? (0, path_1.resolve)(dir) : undefined;
26
+ }
27
+ function getNativeFileCacheLocationToDelete() {
28
+ const configured = configuredCacheDir();
29
+ if (configured !== undefined) {
30
+ // Checked for ownership before it is handed to a recursive delete: being
31
+ // configured does not make it ours. Weaker than the load path below, which
32
+ // also enforces mode and validates the version directory.
33
+ return (0, owned_private_dir_1.isOwnedRealDirectory)(configured) ? configured : null;
12
34
  }
13
- else {
14
- const hash = (0, crypto_1.createHash)('sha256').update(workspace_root_1.workspaceRoot).update(versions_1.nxVersion);
35
+ return (0, owned_private_dir_1.isSafeSharedRoot)(nx_tmp_dir_1.NX_TMP_DIR).status === 'ok' &&
36
+ (0, owned_private_dir_1.isOwnedRealDirectory)(nx_tmp_dir_1.NX_USER_TMP_DIR) &&
37
+ (0, owned_private_dir_1.isOwnedRealDirectory)(exports.NATIVE_CACHE_ROOT)
38
+ ? (0, path_1.join)(exports.NATIVE_CACHE_ROOT, versions_1.nxVersion)
39
+ : null;
40
+ }
41
+ /**
42
+ * Create the native file cache dir, or return `null` if it cannot be created
43
+ * *securely* — in which case the caller loads the binding in place.
44
+ *
45
+ * The stable top-level container is verified as safe for private children. The
46
+ * uid directory and every directory loaded through are owner-only.
47
+ * `ensureOwnedPrivateDir` refuses a directory or symlink another local user
48
+ * planted before us.
49
+ */
50
+ function ensureSecureNativeFileCacheLocation(
51
+ // Test seam: lets a spec plant a hostile directory under a root it controls.
52
+ cacheRoot = exports.NATIVE_CACHE_ROOT) {
53
+ const configured = configuredCacheDir();
54
+ if (configured !== undefined) {
55
+ const dir = configured;
15
56
  try {
16
- hash.update((0, os_1.userInfo)().username);
57
+ // Held to the same 0700 bar as the default location: a `.node` is loaded
58
+ // out of here. Unlike NX_SOCKET_DIR it is not additionally refused for
59
+ // naming one of Nx's own roots, which is the only case that throws.
60
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(dir), { recursive: true });
61
+ const established = (0, owned_private_dir_1.ensureOwnedPrivateDir)(dir);
62
+ if (established.status !== 'ok') {
63
+ // The guard's own reason rather than a fixed sentence: `not-created
64
+ // (EACCES)` and `foreign-owner` are exactly what the catch below
65
+ // promises to tell apart, and one string for all of them cannot.
66
+ throw new owned_private_dir_1.DirectoryRefusedError(established.refusal);
67
+ }
68
+ return dir;
17
69
  }
18
70
  catch (e) {
19
- // if there's no user, we only use the workspace root for the hash and move on
71
+ // Never discard a configured directory silently — the socket directory
72
+ // follows the same rule. A typo, EACCES or EROFS is otherwise
73
+ // indistinguishable from a working cache.
74
+ console.warn(`Nx could not use the configured native file cache directory ${dir} (${e?.code ?? e?.message ?? e}). Loading the native binding in place instead.`);
75
+ return null;
76
+ }
77
+ }
78
+ const userRoot = (0, path_1.dirname)(cacheRoot);
79
+ const sharedRoot = (0, path_1.dirname)(userRoot);
80
+ // Outermost first: the stable shared container must either belong to root or
81
+ // to us, and must be sticky if peers can write there. The uid directory then
82
+ // becomes the owner-only boundary for sockets and native-cache alike.
83
+ if ((0, owned_private_dir_1.ensureSafeSharedRoot)(sharedRoot).status !== 'ok') {
84
+ return null;
85
+ }
86
+ for (const root of [userRoot, cacheRoot]) {
87
+ if ((0, owned_private_dir_1.ensureOwnedPrivateDir)(root).status !== 'ok') {
88
+ return null;
20
89
  }
21
- return (0, path_1.join)((0, os_1.tmpdir)(), `nx-native-file-cache-${hash.digest('hex').substring(0, 7)}`);
22
90
  }
91
+ // Verified, not assumed: this is the directory we load a `.node` out of.
92
+ const versionDir = (0, path_1.join)(cacheRoot, versions_1.nxVersion);
93
+ if ((0, owned_private_dir_1.ensureOwnedPrivateDir)(versionDir).status !== 'ok') {
94
+ return null;
95
+ }
96
+ return versionDir;
23
97
  }
Binary file
@@ -60,3 +60,4 @@ export declare class IsolatedPlugin implements LoadedNxPlugin {
60
60
  shutdown(): void;
61
61
  private registerProcessMetrics;
62
62
  }
63
+ export declare function getPluginWorkerSocketId(): string;
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.IsolatedPlugin = void 0;
4
+ exports.getPluginWorkerSocketId = getPluginWorkerSocketId;
4
5
  const child_process_1 = require("child_process");
6
+ const crypto_1 = require("crypto");
5
7
  const net_1 = require("net");
6
8
  const path = require("path");
7
9
  const logger_1 = require("../../../daemon/logger");
@@ -12,6 +14,7 @@ const installation_directory_1 = require("../../../utils/installation-directory"
12
14
  const logger_2 = require("../../../utils/logger");
13
15
  const progress_topics_1 = require("../../../utils/progress-topics");
14
16
  const wait_for_socket_connection_1 = require("../../../utils/wait-for-socket-connection");
17
+ const workspace_root_1 = require("../../../utils/workspace-root");
15
18
  const resolve_plugin_1 = require("../resolve-plugin");
16
19
  const messaging_1 = require("./messaging");
17
20
  const plugin_lifecycle_manager_1 = require("./plugin-lifecycle-manager");
@@ -336,6 +339,10 @@ class IsolatedPlugin {
336
339
  exports.IsolatedPlugin = IsolatedPlugin;
337
340
  // --- Worker Spawning Utilities ---
338
341
  global.nxPluginWorkerCount ??= 0;
342
+ function getPluginWorkerSocketId() {
343
+ const counter = global.nxPluginWorkerCount++;
344
+ return `${process.pid}-${counter.toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
345
+ }
339
346
  async function startPluginWorker(name) {
340
347
  performance.mark(`start-plugin-worker:${name}`);
341
348
  const isWorkerTypescript = path.extname(__filename) === '.ts';
@@ -352,7 +359,14 @@ async function startPluginWorker(name) {
352
359
  }
353
360
  : {}),
354
361
  };
355
- const ipcPath = (0, socket_utils_1.getPluginOsSocketPath)([process.pid, global.nxPluginWorkerCount++, performance.now()].join('-'));
362
+ // Keep the host pid readable for diagnostics, the counter collision-free
363
+ // within this process, and entropy for a reused pid across process runs. The
364
+ // suffix is a base36 counter plus four random bytes: eleven characters after
365
+ // the pid until the counter passes 36, and fixed-length where the
366
+ // `performance.now()` stamp it replaced varied with process uptime. That
367
+ // keeps the socket path inside the 95-character budget `assertValidSocketPath`
368
+ // enforces.
369
+ const ipcPath = (0, socket_utils_1.getPluginOsSocketPath)(getPluginWorkerSocketId());
356
370
  const worker = (0, child_process_1.spawn)(process.execPath, [
357
371
  // Spawn the worker with the same resolve conditions Nx uses for plugin
358
372
  // entries so the plugin's transitive workspace imports resolve to source.
@@ -361,6 +375,9 @@ async function startPluginWorker(name) {
361
375
  workerPath,
362
376
  ipcPath,
363
377
  name,
378
+ // The host's root. The worker validates against this rather than re-resolving,
379
+ // so the two agree by construction.
380
+ workspace_root_1.workspaceRoot,
364
381
  ], {
365
382
  stdio: ['ignore', 'pipe', 'pipe'],
366
383
  env,
@@ -107,10 +107,23 @@ type PluginMessageDefs = DefineMessages<{
107
107
  };
108
108
  };
109
109
  }>;
110
+ /**
111
+ * The sender's workspace root. Only the *worker* asserts on it, for host→worker
112
+ * messages; results and notifications carry the field but the host does not
113
+ * check it. The daemon channel is checked in the same one direction — the
114
+ * server validates inbound client messages and the client does not validate
115
+ * responses — so the two are alike rather than one being weaker.
116
+ * Optional and stamped centrally in `sendMessageOverSocket`, so the many
117
+ * message constructors need not set it; undefined is treated as not foreign,
118
+ * which is why this detects a shared socket directory rather than an attacker.
119
+ */
120
+ export type WorkspaceStampedMessage = {
121
+ workspaceRoot?: string;
122
+ };
110
123
  /** Union of all plugin worker message types */
111
- export type PluginWorkerMessage = AllMessages<PluginMessageDefs>;
124
+ export type PluginWorkerMessage = AllMessages<PluginMessageDefs> & WorkspaceStampedMessage;
112
125
  /** Union of all plugin worker result types */
113
- export type PluginWorkerResult = AllResults<PluginMessageDefs>;
126
+ export type PluginWorkerResult = AllResults<PluginMessageDefs> & WorkspaceStampedMessage;
114
127
  /** Result type for the load message */
115
128
  export type PluginWorkerLoadResult = ResultOf<PluginMessageDefs, 'load'>;
116
129
  /**
@@ -122,7 +135,7 @@ export type PluginWorkerEmitLogNotification = {
122
135
  type: 'emitLog';
123
136
  level: 'log' | 'warn' | 'error';
124
137
  message: string;
125
- };
138
+ } & WorkspaceStampedMessage;
126
139
  export type PluginWorkerNotification = PluginWorkerEmitLogNotification;
127
140
  export declare function isPluginWorkerNotification(message: Serializable): message is PluginWorkerNotification;
128
141
  export declare function isPluginWorkerMessage(message: Serializable): message is PluginWorkerMessage;
@@ -7,6 +7,7 @@ exports.consumeMessage = consumeMessage;
7
7
  exports.sendMessageOverSocket = sendMessageOverSocket;
8
8
  const socket_utils_1 = require("../../../daemon/socket-utils");
9
9
  const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket");
10
+ const workspace_root_1 = require("../../../utils/workspace-root");
10
11
  const NOTIFICATION_TYPES = [
11
12
  'emitLog',
12
13
  ];
@@ -81,6 +82,8 @@ async function consumeMessage(socket, raw, handlers) {
81
82
  * Sends a message over the socket with proper formatting.
82
83
  */
83
84
  function sendMessageOverSocket(socket, message) {
85
+ // Stamped here rather than in each constructor, mirroring the daemon.
86
+ message.workspaceRoot = workspace_root_1.workspaceRoot;
84
87
  socket.write((0, socket_utils_1.serialize)(message));
85
88
  socket.write(consume_messages_from_socket_1.MESSAGE_END_SEQ);
86
89
  }
@@ -7,6 +7,7 @@ node_perf_hooks_1.performance.mark(`plugin worker ${process.pid} code loading --
7
7
  const consume_messages_from_socket_1 = require("../../../utils/consume-messages-from-socket");
8
8
  const logger_1 = require("../../../utils/logger");
9
9
  const serializable_error_1 = require("../../../utils/serializable-error");
10
+ const daemon_message_1 = require("../../../daemon/message-types/daemon-message");
10
11
  const messaging_1 = require("./messaging");
11
12
  const worker_streaming_1 = require("./worker-streaming");
12
13
  const fs_1 = require("fs");
@@ -23,6 +24,19 @@ global.NX_PLUGIN_WORKER = true;
23
24
  let plugin;
24
25
  const socketPath = process.argv[2];
25
26
  const expectedPluginName = process.argv[3];
27
+ // The host's root, passed explicitly rather than re-resolved: a host that set
28
+ // its root at runtime would resolve a different one here and drop every
29
+ // legitimate message as foreign.
30
+ const hostWorkspaceRoot = process.argv[4];
31
+ // Positional, so inserting an argument host-side shifts all of them — and the
32
+ // symptom is silent and total: an undefined hostWorkspaceRoot makes every
33
+ // message look foreign while the host waits out its plugin timeout.
34
+ if (!socketPath || !expectedPluginName || !hostWorkspaceRoot) {
35
+ console.error(`[plugin-worker] started with an incomplete argument list ` +
36
+ `(socketPath=${socketPath}, pluginName=${expectedPluginName}, hostWorkspaceRoot=${hostWorkspaceRoot}). ` +
37
+ `This is an Nx bug — please report it at https://github.com/nrwl/nx/issues.`);
38
+ process.exit(1);
39
+ }
26
40
  const CONNECT_TIMEOUT_MS = 30_000;
27
41
  let connectErrorTimeout = setErrorTimeout(CONNECT_TIMEOUT_MS, `The plugin worker for ${expectedPluginName} is exiting as it was not connected to within ${CONNECT_TIMEOUT_MS / 1000} seconds. ` +
28
42
  'Plugin workers expect to receive a socket connection from the parent process shortly after being started. ' +
@@ -45,6 +59,16 @@ const server = (0, net_1.createServer)((socket) => {
45
59
  if (!(0, messaging_1.isPluginWorkerMessage)(message)) {
46
60
  return;
47
61
  }
62
+ // Same check the daemon applies to its own socket. Dropped rather than thrown:
63
+ // a stray foreign message must not kill a worker serving its host. The daemon
64
+ // has a response channel and surfaces it to the client instead.
65
+ try {
66
+ (0, daemon_message_1.assertNotForeignWorkspaceMessage)(message, hostWorkspaceRoot, `The Nx plugin worker "${expectedPluginName}" (pid: ${process.pid})`);
67
+ }
68
+ catch (e) {
69
+ logger_1.logger.verbose(`[plugin-worker] ignored a "${message.type}" message: ${e instanceof Error ? e.message : e}`);
70
+ return;
71
+ }
48
72
  return (0, messaging_1.consumeMessage)(socket, message, {
49
73
  load: async ({ plugin: pluginConfiguration, root, name, pluginPath, shouldRegisterTSTranspiler, }) => {
50
74
  loadErrorTimeout?.clear();
@@ -104,8 +128,24 @@ const server = (0, net_1.createServer)((socket) => {
104
128
  process.exit(0);
105
129
  });
106
130
  });
107
- server.listen(socketPath);
108
- logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) listening on ${socketPath}`);
131
+ server.on('error', (err) => {
132
+ // Without this the host only sees "exited before the connection was
133
+ // established"; the errno distinguishes a denied bind from EADDRINUSE.
134
+ console.error(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) failed to listen on ${socketPath}: ${err.message}`);
135
+ process.exit(1);
136
+ });
137
+ // A worker killed without its 'end' handler leaves the socket behind. Colliding
138
+ // takes a recycled host pid landing on the same counter *and* the same 4 random
139
+ // bytes; the random component is what this unlink rests on, and on Windows it is
140
+ // the only barrier, since the endpoint is a namespace object and the mode never
141
+ // applies. A failed bind surfaces through the error handler above.
142
+ try {
143
+ (0, fs_1.unlinkSync)(socketPath);
144
+ }
145
+ catch { }
146
+ server.listen(socketPath, () => {
147
+ logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) listening on ${socketPath}`);
148
+ });
109
149
  async function withErrorHandling(cb) {
110
150
  try {
111
151
  const result = await cb();
@@ -136,15 +176,25 @@ function setErrorTimeout(timeoutMs, errorMessage) {
136
176
  },
137
177
  };
138
178
  }
139
- const exitHandler = (exitCode) => () => {
179
+ const cleanup = () => {
140
180
  server.close();
141
181
  try {
142
182
  (0, fs_1.unlinkSync)(socketPath);
143
183
  }
144
184
  catch (e) { }
145
- process.exit(exitCode);
146
185
  };
147
- const events = ['SIGINT', 'SIGTERM', 'SIGQUIT', 'exit'];
148
- events.forEach((event) => process.once(event, exitHandler(0)));
149
- process.once('uncaughtException', exitHandler(1));
150
- process.once('unhandledRejection', exitHandler(1));
186
+ const events = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
187
+ events.forEach((event) => process.once(event, () => {
188
+ cleanup();
189
+ process.exit(0);
190
+ }));
191
+ // Cleanup only: process.exit() here would override the real exit code.
192
+ process.once('exit', cleanup);
193
+ const fatalHandler = (error) => {
194
+ // Registering this handler suppresses Node's default reporting.
195
+ console.error(error);
196
+ cleanup();
197
+ process.exit(1);
198
+ };
199
+ process.once('uncaughtException', fatalHandler);
200
+ process.once('unhandledRejection', fatalHandler);
@@ -354,6 +354,26 @@ async function createProjectGraphAndSourceMapsAsync(opts = {
354
354
  (0, tmp_dir_1.markDaemonAsDisabled)(e.message);
355
355
  return buildProjectGraphAndSourceMapsWithoutDaemon();
356
356
  }
357
+ if (e.daemonPermissionError) {
358
+ // Deliberately not disabled: unlike the inotify limit above, a socket
359
+ // owned by someone else stops being there when it is removed or the
360
+ // machine reboots, and disabling until `nx reset` would outlive the
361
+ // cause and hide the fix from anyone who followed the advice.
362
+ // The first line carries the errno, and it is the one token that tells
363
+ // the two causes apart: EACCES is a socket owned by someone else (delete
364
+ // it), EPERM is a sandbox refusing the connect syscall (allow unix
365
+ // sockets under the Nx socket root). The message hedges between exactly
366
+ // those two because it cannot tell them apart, and this branch does not
367
+ // call writeDaemonLogs, so dropping the line loses the errno for good.
368
+ const [summary, ...details] = e.message.split('\n');
369
+ output_1.output.note({
370
+ title: `${summary} Continuing without the daemon.`,
371
+ // The blank line after the summary separates paragraphs when the
372
+ // message is printed as one blob; as bodyLines it is a leading gap.
373
+ bodyLines: details[0] === '' ? details.slice(1) : details,
374
+ });
375
+ return buildProjectGraphAndSourceMapsWithoutDaemon();
376
+ }
357
377
  if (e.internalDaemonError) {
358
378
  const errorLogFile = (0, tmp_dir_1.writeDaemonLogs)(e.message);
359
379
  output_1.output.warn({
@@ -1,7 +1,37 @@
1
- import { type ExecOptions, type ExecSyncOptions } from 'child_process';
1
+ import { type ExecOptions, type ExecSyncOptions, type SpawnSyncOptions } from 'child_process';
2
2
  import { PackageManagerCommands } from './package-manager';
3
3
  import { ChildProcess } from '../native';
4
4
  export declare function getRunNxBaseCommand(packageManagerCommand?: PackageManagerCommands, cwd?: string): string;
5
+ /**
6
+ * Locate an nx entry point to spawn for the workspace at `root`, so a caller
7
+ * can run it directly instead of going through a shell. `findInstalledNxBin`
8
+ * decides which one.
9
+ *
10
+ * Null means nothing may be spawned directly, leaving the caller to fall back
11
+ * to `getRunNxBaseCommand`. Null is therefore always safe: it costs the
12
+ * argument fidelity a direct spawn buys, never the ability to run.
13
+ */
14
+ export declare function getNxBin(root?: string): string | null;
15
+ /**
16
+ * The entry point the nx installed directly under `dir` names, with no ascent
17
+ * to `dir`'s ancestors. For an installation that declares nx itself, such as
18
+ * the temp CLI `nx migrate` builds, an ancestor's nx is never the right answer.
19
+ */
20
+ export declare function readInstalledNxBin(dir: string): string | null;
21
+ /**
22
+ * Run a nx command, passing the arguments through as an argv array.
23
+ *
24
+ * When `getNxBin` names an entry point, the child is spawned directly with no
25
+ * shell in between, so every argument reaches the child exactly as provided:
26
+ * shell metacharacters (`(`, `%`, `^`, spaces, quotes) are data, not syntax.
27
+ * Otherwise falls back to the package-manager + shell path, where every
28
+ * argument goes through `quoteShellArg` and the Windows limits it documents
29
+ * apply.
30
+ */
31
+ export declare function runNxArgvSync(argv: string[], options?: SpawnSyncOptions & {
32
+ cwd?: string;
33
+ nxBin?: string;
34
+ }): void;
5
35
  export declare function runNxSync(cmd: string, options?: ExecSyncOptions & {
6
36
  cwd?: string;
7
37
  packageManagerCommand?: PackageManagerCommands;