nx 22.7.7 → 22.7.9
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.
- package/dist/src/ai/set-up-ai-agents/set-up-ai-agents.js +2 -1
- package/dist/src/command-line/configure-ai-agents/configure-ai-agents.js +1 -1
- package/dist/src/command-line/init/init-v2.js +1 -1
- package/dist/src/command-line/migrate/migrate-ui-api.js +17 -12
- package/dist/src/command-line/reset/reset.js +6 -1
- package/dist/src/daemon/client/client.d.ts +45 -3
- package/dist/src/daemon/client/client.js +138 -42
- package/dist/src/daemon/client/daemon-socket-messenger.js +4 -0
- package/dist/src/daemon/message-types/daemon-message.d.ts +33 -0
- package/dist/src/daemon/message-types/daemon-message.js +48 -0
- package/dist/src/daemon/server/latest-nx.js +3 -1
- package/dist/src/daemon/server/server.js +22 -0
- package/dist/src/daemon/server/shutdown-utils.d.ts +5 -0
- package/dist/src/daemon/server/shutdown-utils.js +11 -4
- package/dist/src/daemon/socket-utils.d.ts +1 -0
- package/dist/src/daemon/socket-utils.js +24 -3
- package/dist/src/daemon/tmp-dir.d.ts +43 -3
- package/dist/src/daemon/tmp-dir.js +437 -15
- package/dist/src/devkit-internals.d.ts +1 -0
- package/dist/src/devkit-internals.js +4 -1
- package/dist/src/native/index.js +71 -28
- package/dist/src/native/native-file-cache-location.d.ts +17 -1
- package/dist/src/native/native-file-cache-location.js +86 -12
- package/dist/src/native/nx.wasm32-wasi.debug.wasm +0 -0
- package/dist/src/native/nx.wasm32-wasi.wasm +0 -0
- package/dist/src/project-graph/file-utils.js +5 -1
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.d.ts +1 -0
- package/dist/src/project-graph/plugins/isolation/isolated-plugin.js +18 -1
- package/dist/src/project-graph/plugins/isolation/messaging.d.ts +16 -3
- package/dist/src/project-graph/plugins/isolation/messaging.js +3 -0
- package/dist/src/project-graph/plugins/isolation/plugin-worker.js +58 -8
- package/dist/src/project-graph/project-graph.js +20 -0
- package/dist/src/utils/command-line-utils.js +30 -20
- package/dist/src/utils/git-revision.d.ts +12 -0
- package/dist/src/utils/git-revision.js +25 -0
- package/dist/src/utils/git-utils.d.ts +1 -3
- package/dist/src/utils/git-utils.js +83 -47
- package/dist/src/utils/nx-tmp-dir.d.ts +18 -0
- package/dist/src/utils/nx-tmp-dir.js +40 -0
- package/dist/src/utils/owned-private-dir.d.ts +132 -0
- package/dist/src/utils/owned-private-dir.js +323 -0
- package/dist/src/utils/package-json.d.ts +3 -3
- package/dist/src/utils/package-json.js +30 -12
- package/dist/src/utils/package-manager.d.ts +13 -0
- package/dist/src/utils/package-manager.js +27 -0
- package/dist/src/utils/safe-spawn.d.ts +24 -0
- package/dist/src/utils/safe-spawn.js +104 -0
- package/dist/src/utils/shell-quoting.d.ts +15 -0
- package/dist/src/utils/shell-quoting.js +35 -0
- package/dist/src/utils/wait-for-socket-connection.d.ts +11 -0
- package/dist/src/utils/wait-for-socket-connection.js +14 -8
- package/package.json +17 -13
|
@@ -1 +1,17 @@
|
|
|
1
|
-
|
|
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.
|
|
4
|
-
|
|
3
|
+
exports.NATIVE_CACHE_ROOT = void 0;
|
|
4
|
+
exports.getNativeFileCacheLocationToDelete = getNativeFileCacheLocationToDelete;
|
|
5
|
+
exports.ensureSecureNativeFileCacheLocation = ensureSecureNativeFileCacheLocation;
|
|
5
6
|
const path_1 = require("path");
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
Binary file
|
|
@@ -10,6 +10,7 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const fs_1 = require("fs");
|
|
11
11
|
const path_1 = require("path");
|
|
12
12
|
const fileutils_1 = require("../utils/fileutils");
|
|
13
|
+
const git_revision_1 = require("../utils/git-revision");
|
|
13
14
|
const ignore_1 = require("../utils/ignore");
|
|
14
15
|
const json_diff_1 = require("../utils/json-diff");
|
|
15
16
|
const workspace_root_1 = require("../utils/workspace-root");
|
|
@@ -77,6 +78,9 @@ function calculateFileChanges(files, nxArgs, readFileAtRevision = defaultReadFil
|
|
|
77
78
|
}
|
|
78
79
|
exports.TEN_MEGABYTES = 1024 * 10000;
|
|
79
80
|
function defaultReadFileAtRevision(file, revision) {
|
|
81
|
+
if (revision) {
|
|
82
|
+
(0, git_revision_1.assertValidGitRevision)(revision);
|
|
83
|
+
}
|
|
80
84
|
try {
|
|
81
85
|
const fileFullPath = `${workspace_root_1.workspaceRoot}${path_1.sep}${file}`;
|
|
82
86
|
const gitRepositoryPath = (0, child_process_1.execSync)('git rev-parse --show-toplevel', {
|
|
@@ -89,7 +93,7 @@ function defaultReadFileAtRevision(file, revision) {
|
|
|
89
93
|
.join('/');
|
|
90
94
|
return !revision
|
|
91
95
|
? (0, fs_1.readFileSync)(file, 'utf-8')
|
|
92
|
-
: (0, child_process_1.
|
|
96
|
+
: (0, child_process_1.execFileSync)('git', ['show', `${revision}:${filePathInGitRepository}`], {
|
|
93
97
|
maxBuffer: exports.TEN_MEGABYTES,
|
|
94
98
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
95
99
|
windowsHide: true,
|
|
@@ -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");
|
|
@@ -11,6 +13,7 @@ const installation_directory_1 = require("../../../utils/installation-directory"
|
|
|
11
13
|
const logger_2 = require("../../../utils/logger");
|
|
12
14
|
const progress_topics_1 = require("../../../utils/progress-topics");
|
|
13
15
|
const wait_for_socket_connection_1 = require("../../../utils/wait-for-socket-connection");
|
|
16
|
+
const workspace_root_1 = require("../../../utils/workspace-root");
|
|
14
17
|
const resolve_plugin_1 = require("../resolve-plugin");
|
|
15
18
|
const messaging_1 = require("./messaging");
|
|
16
19
|
const plugin_lifecycle_manager_1 = require("./plugin-lifecycle-manager");
|
|
@@ -335,6 +338,10 @@ class IsolatedPlugin {
|
|
|
335
338
|
exports.IsolatedPlugin = IsolatedPlugin;
|
|
336
339
|
// --- Worker Spawning Utilities ---
|
|
337
340
|
global.nxPluginWorkerCount ??= 0;
|
|
341
|
+
function getPluginWorkerSocketId() {
|
|
342
|
+
const counter = global.nxPluginWorkerCount++;
|
|
343
|
+
return `${process.pid}-${counter.toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
|
|
344
|
+
}
|
|
338
345
|
async function startPluginWorker(name) {
|
|
339
346
|
performance.mark(`start-plugin-worker:${name}`);
|
|
340
347
|
const isWorkerTypescript = path.extname(__filename) === '.ts';
|
|
@@ -351,12 +358,22 @@ async function startPluginWorker(name) {
|
|
|
351
358
|
}
|
|
352
359
|
: {}),
|
|
353
360
|
};
|
|
354
|
-
|
|
361
|
+
// Keep the host pid readable for diagnostics, the counter collision-free
|
|
362
|
+
// within this process, and entropy for a reused pid across process runs. The
|
|
363
|
+
// suffix is a base36 counter plus four random bytes: eleven characters after
|
|
364
|
+
// the pid until the counter passes 36, and fixed-length where the
|
|
365
|
+
// `performance.now()` stamp it replaced varied with process uptime. That
|
|
366
|
+
// keeps the socket path inside the 95-character budget `assertValidSocketPath`
|
|
367
|
+
// enforces.
|
|
368
|
+
const ipcPath = (0, socket_utils_1.getPluginOsSocketPath)(getPluginWorkerSocketId());
|
|
355
369
|
const worker = (0, child_process_1.spawn)(process.execPath, [
|
|
356
370
|
...(isWorkerTypescript ? ['--require', 'ts-node/register'] : []),
|
|
357
371
|
workerPath,
|
|
358
372
|
ipcPath,
|
|
359
373
|
name,
|
|
374
|
+
// The host's root. The worker validates against this rather than re-resolving,
|
|
375
|
+
// so the two agree by construction.
|
|
376
|
+
workspace_root_1.workspaceRoot,
|
|
360
377
|
], {
|
|
361
378
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
362
379
|
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");
|
|
@@ -22,6 +23,19 @@ global.NX_PLUGIN_WORKER = true;
|
|
|
22
23
|
let plugin;
|
|
23
24
|
const socketPath = process.argv[2];
|
|
24
25
|
const expectedPluginName = process.argv[3];
|
|
26
|
+
// The host's root, passed explicitly rather than re-resolved: a host that set
|
|
27
|
+
// its root at runtime would resolve a different one here and drop every
|
|
28
|
+
// legitimate message as foreign.
|
|
29
|
+
const hostWorkspaceRoot = process.argv[4];
|
|
30
|
+
// Positional, so inserting an argument host-side shifts all of them — and the
|
|
31
|
+
// symptom is silent and total: an undefined hostWorkspaceRoot makes every
|
|
32
|
+
// message look foreign while the host waits out its plugin timeout.
|
|
33
|
+
if (!socketPath || !expectedPluginName || !hostWorkspaceRoot) {
|
|
34
|
+
console.error(`[plugin-worker] started with an incomplete argument list ` +
|
|
35
|
+
`(socketPath=${socketPath}, pluginName=${expectedPluginName}, hostWorkspaceRoot=${hostWorkspaceRoot}). ` +
|
|
36
|
+
`This is an Nx bug — please report it at https://github.com/nrwl/nx/issues.`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
25
39
|
const CONNECT_TIMEOUT_MS = 30_000;
|
|
26
40
|
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. ` +
|
|
27
41
|
'Plugin workers expect to receive a socket connection from the parent process shortly after being started. ' +
|
|
@@ -44,6 +58,16 @@ const server = (0, net_1.createServer)((socket) => {
|
|
|
44
58
|
if (!(0, messaging_1.isPluginWorkerMessage)(message)) {
|
|
45
59
|
return;
|
|
46
60
|
}
|
|
61
|
+
// Same check the daemon applies to its own socket. Dropped rather than thrown:
|
|
62
|
+
// a stray foreign message must not kill a worker serving its host. The daemon
|
|
63
|
+
// has a response channel and surfaces it to the client instead.
|
|
64
|
+
try {
|
|
65
|
+
(0, daemon_message_1.assertNotForeignWorkspaceMessage)(message, hostWorkspaceRoot, `The Nx plugin worker "${expectedPluginName}" (pid: ${process.pid})`);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
logger_1.logger.verbose(`[plugin-worker] ignored a "${message.type}" message: ${e instanceof Error ? e.message : e}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
47
71
|
return (0, messaging_1.consumeMessage)(socket, message, {
|
|
48
72
|
load: async ({ plugin: pluginConfiguration, root, name, pluginPath, shouldRegisterTSTranspiler, }) => {
|
|
49
73
|
loadErrorTimeout?.clear();
|
|
@@ -105,8 +129,24 @@ const server = (0, net_1.createServer)((socket) => {
|
|
|
105
129
|
process.exit(0);
|
|
106
130
|
});
|
|
107
131
|
});
|
|
108
|
-
server.
|
|
109
|
-
|
|
132
|
+
server.on('error', (err) => {
|
|
133
|
+
// Without this the host only sees "exited before the connection was
|
|
134
|
+
// established"; the errno distinguishes a denied bind from EADDRINUSE.
|
|
135
|
+
console.error(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) failed to listen on ${socketPath}: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
138
|
+
// A worker killed without its 'end' handler leaves the socket behind. Colliding
|
|
139
|
+
// takes a recycled host pid landing on the same counter *and* the same 4 random
|
|
140
|
+
// bytes; the random component is what this unlink rests on, and on Windows it is
|
|
141
|
+
// the only barrier, since the endpoint is a namespace object and the mode never
|
|
142
|
+
// applies. A failed bind surfaces through the error handler above.
|
|
143
|
+
try {
|
|
144
|
+
(0, fs_1.unlinkSync)(socketPath);
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
server.listen(socketPath, () => {
|
|
148
|
+
logger_1.logger.verbose(`[plugin-worker] "${expectedPluginName}" (pid: ${process.pid}) listening on ${socketPath}`);
|
|
149
|
+
});
|
|
110
150
|
async function withErrorHandling(cb) {
|
|
111
151
|
try {
|
|
112
152
|
const result = await cb();
|
|
@@ -137,15 +177,25 @@ function setErrorTimeout(timeoutMs, errorMessage) {
|
|
|
137
177
|
},
|
|
138
178
|
};
|
|
139
179
|
}
|
|
140
|
-
const
|
|
180
|
+
const cleanup = () => {
|
|
141
181
|
server.close();
|
|
142
182
|
try {
|
|
143
183
|
(0, fs_1.unlinkSync)(socketPath);
|
|
144
184
|
}
|
|
145
185
|
catch (e) { }
|
|
146
|
-
process.exit(exitCode);
|
|
147
186
|
};
|
|
148
|
-
const events = ['SIGINT', 'SIGTERM', 'SIGQUIT'
|
|
149
|
-
events.forEach((event) => process.once(event,
|
|
150
|
-
|
|
151
|
-
process.
|
|
187
|
+
const events = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
188
|
+
events.forEach((event) => process.once(event, () => {
|
|
189
|
+
cleanup();
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}));
|
|
192
|
+
// Cleanup only: process.exit() here would override the real exit code.
|
|
193
|
+
process.once('exit', cleanup);
|
|
194
|
+
const fatalHandler = (error) => {
|
|
195
|
+
// Registering this handler suppresses Node's default reporting.
|
|
196
|
+
console.error(error);
|
|
197
|
+
cleanup();
|
|
198
|
+
process.exit(1);
|
|
199
|
+
};
|
|
200
|
+
process.once('uncaughtException', fatalHandler);
|
|
201
|
+
process.once('unhandledRejection', fatalHandler);
|
|
@@ -350,6 +350,26 @@ async function createProjectGraphAndSourceMapsAsync(opts = {
|
|
|
350
350
|
(0, tmp_dir_1.markDaemonAsDisabled)(e.message);
|
|
351
351
|
return buildProjectGraphAndSourceMapsWithoutDaemon();
|
|
352
352
|
}
|
|
353
|
+
if (e.daemonPermissionError) {
|
|
354
|
+
// Deliberately not disabled: unlike the inotify limit above, a socket
|
|
355
|
+
// owned by someone else stops being there when it is removed or the
|
|
356
|
+
// machine reboots, and disabling until `nx reset` would outlive the
|
|
357
|
+
// cause and hide the fix from anyone who followed the advice.
|
|
358
|
+
// The first line carries the errno, and it is the one token that tells
|
|
359
|
+
// the two causes apart: EACCES is a socket owned by someone else (delete
|
|
360
|
+
// it), EPERM is a sandbox refusing the connect syscall (allow unix
|
|
361
|
+
// sockets under the Nx socket root). The message hedges between exactly
|
|
362
|
+
// those two because it cannot tell them apart, and this branch does not
|
|
363
|
+
// call writeDaemonLogs, so dropping the line loses the errno for good.
|
|
364
|
+
const [summary, ...details] = e.message.split('\n');
|
|
365
|
+
output_1.output.note({
|
|
366
|
+
title: `${summary} Continuing without the daemon.`,
|
|
367
|
+
// The blank line after the summary separates paragraphs when the
|
|
368
|
+
// message is printed as one blob; as bodyLines it is a leading gap.
|
|
369
|
+
bodyLines: details[0] === '' ? details.slice(1) : details,
|
|
370
|
+
});
|
|
371
|
+
return buildProjectGraphAndSourceMapsWithoutDaemon();
|
|
372
|
+
}
|
|
353
373
|
if (e.internalDaemonError) {
|
|
354
374
|
const errorLogFile = (0, tmp_dir_1.writeDaemonLogs)(e.message);
|
|
355
375
|
output_1.output.warn({
|
|
@@ -11,6 +11,7 @@ const yargs_parser_1 = tslib_1.__importDefault(require("yargs-parser"));
|
|
|
11
11
|
const file_utils_1 = require("../project-graph/file-utils");
|
|
12
12
|
const output_1 = require("./output");
|
|
13
13
|
const child_process_1 = require("child_process");
|
|
14
|
+
const git_revision_1 = require("./git-revision");
|
|
14
15
|
const workspace_root_1 = require("./workspace-root");
|
|
15
16
|
const shared_options_1 = require("../command-line/yargs-utils/shared-options");
|
|
16
17
|
function createOverrides(__overrides_unparsed__ = []) {
|
|
@@ -192,30 +193,27 @@ function parseFiles(options) {
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
function getUncommittedFiles() {
|
|
195
|
-
return parseGitOutput(
|
|
196
|
+
return parseGitOutput([
|
|
197
|
+
'diff',
|
|
198
|
+
'--name-only',
|
|
199
|
+
'--no-renames',
|
|
200
|
+
'--relative',
|
|
201
|
+
'HEAD',
|
|
202
|
+
'.',
|
|
203
|
+
]);
|
|
196
204
|
}
|
|
197
205
|
function getUntrackedFiles() {
|
|
198
|
-
return parseGitOutput(
|
|
206
|
+
return parseGitOutput(['ls-files', '--others', '--exclude-standard']);
|
|
199
207
|
}
|
|
200
208
|
function getMergeBase(base, head = 'HEAD') {
|
|
209
|
+
(0, git_revision_1.assertValidGitRevision)(base);
|
|
210
|
+
(0, git_revision_1.assertValidGitRevision)(head);
|
|
201
211
|
try {
|
|
202
|
-
return (
|
|
203
|
-
maxBuffer: file_utils_1.TEN_MEGABYTES,
|
|
204
|
-
cwd: workspace_root_1.workspaceRoot,
|
|
205
|
-
stdio: 'pipe',
|
|
206
|
-
windowsHide: true,
|
|
207
|
-
})
|
|
208
|
-
.toString()
|
|
209
|
-
.trim();
|
|
212
|
+
return runGit(['merge-base', base, head]).toString().trim();
|
|
210
213
|
}
|
|
211
214
|
catch {
|
|
212
215
|
try {
|
|
213
|
-
return (
|
|
214
|
-
maxBuffer: file_utils_1.TEN_MEGABYTES,
|
|
215
|
-
cwd: workspace_root_1.workspaceRoot,
|
|
216
|
-
stdio: 'pipe',
|
|
217
|
-
windowsHide: true,
|
|
218
|
-
})
|
|
216
|
+
return runGit(['merge-base', '--fork-point', base, head])
|
|
219
217
|
.toString()
|
|
220
218
|
.trim();
|
|
221
219
|
}
|
|
@@ -225,15 +223,27 @@ function getMergeBase(base, head = 'HEAD') {
|
|
|
225
223
|
}
|
|
226
224
|
}
|
|
227
225
|
function getFilesUsingBaseAndHead(base, head) {
|
|
228
|
-
|
|
226
|
+
(0, git_revision_1.assertValidGitRevision)(base);
|
|
227
|
+
(0, git_revision_1.assertValidGitRevision)(head);
|
|
228
|
+
return parseGitOutput([
|
|
229
|
+
'diff',
|
|
230
|
+
'--name-only',
|
|
231
|
+
'--no-renames',
|
|
232
|
+
'--relative',
|
|
233
|
+
base,
|
|
234
|
+
head,
|
|
235
|
+
]);
|
|
229
236
|
}
|
|
230
|
-
function
|
|
231
|
-
return (0, child_process_1.
|
|
237
|
+
function runGit(args) {
|
|
238
|
+
return (0, child_process_1.execFileSync)('git', args, {
|
|
232
239
|
maxBuffer: file_utils_1.TEN_MEGABYTES,
|
|
233
240
|
cwd: workspace_root_1.workspaceRoot,
|
|
234
241
|
stdio: 'pipe',
|
|
235
242
|
windowsHide: true,
|
|
236
|
-
})
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function parseGitOutput(args) {
|
|
246
|
+
return runGit(args)
|
|
237
247
|
.toString('utf-8')
|
|
238
248
|
.split('\n')
|
|
239
249
|
.map((a) => a.trim())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git parses any argument beginning with `-` as an option rather than a
|
|
3
|
+
* revision, so a flag-shaped revision could smuggle options such as
|
|
4
|
+
* `--upload-pack` into the git commands Nx runs.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertValidGitRevision(revision: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* For refs Nx itself recorded from `git rev-parse` and later read back off
|
|
9
|
+
* disk, where anything other than a commit sha means the value was tampered
|
|
10
|
+
* with rather than that the user picked an unusual revision.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertValidGitSha(sha: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertValidGitRevision = assertValidGitRevision;
|
|
4
|
+
exports.assertValidGitSha = assertValidGitSha;
|
|
5
|
+
/**
|
|
6
|
+
* Git parses any argument beginning with `-` as an option rather than a
|
|
7
|
+
* revision, so a flag-shaped revision could smuggle options such as
|
|
8
|
+
* `--upload-pack` into the git commands Nx runs.
|
|
9
|
+
*/
|
|
10
|
+
function assertValidGitRevision(revision) {
|
|
11
|
+
if (revision.startsWith('-')) {
|
|
12
|
+
throw new Error(`Invalid git revision: "${revision}". Git revisions cannot start with "-".`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const COMMIT_SHA = /^[0-9a-f]{7,40}$/i;
|
|
16
|
+
/**
|
|
17
|
+
* For refs Nx itself recorded from `git rev-parse` and later read back off
|
|
18
|
+
* disk, where anything other than a commit sha means the value was tampered
|
|
19
|
+
* with rather than that the user picked an unusual revision.
|
|
20
|
+
*/
|
|
21
|
+
function assertValidGitSha(sha) {
|
|
22
|
+
if (!COMMIT_SHA.test(sha)) {
|
|
23
|
+
throw new Error(`Invalid git commit sha: "${sha}". Expected a hexadecimal commit sha.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -28,9 +28,7 @@ export declare class GitRepository {
|
|
|
28
28
|
hasFilterRepoInstalled(): Promise<boolean>;
|
|
29
29
|
filterRepo(source: string, destination: string): Promise<void>;
|
|
30
30
|
filterBranch(source: string, destination: string, branchName: string): Promise<void>;
|
|
31
|
-
private
|
|
32
|
-
private quotePath;
|
|
33
|
-
private quoteArg;
|
|
31
|
+
private execGit;
|
|
34
32
|
}
|
|
35
33
|
export interface VcsRemoteInfo {
|
|
36
34
|
domain: string;
|