@orkestrel/process 0.0.2 → 0.0.4
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/README.md +28 -22
- package/dist/src/core/index.cjs +85 -8
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +330 -90
- package/dist/src/core/index.d.ts +330 -90
- package/dist/src/core/index.js +79 -9
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +1099 -267
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +645 -101
- package/dist/src/server/index.d.ts +645 -101
- package/dist/src/server/index.js +1076 -265
- package/dist/src/server/index.js.map +1 -1
- package/package.json +17 -6
|
@@ -1,40 +1,16 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let node_buffer = require("node:buffer");
|
|
3
3
|
let node_child_process = require("node:child_process");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
4
5
|
let node_path = require("node:path");
|
|
6
|
+
let node_process = require("node:process");
|
|
7
|
+
let _orkestrel_contract = require("@orkestrel/contract");
|
|
5
8
|
let _src_core = require("../core/index.cjs");
|
|
6
9
|
let node_readline = require("node:readline");
|
|
7
10
|
let node_string_decoder = require("node:string_decoder");
|
|
8
11
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
9
12
|
//#region src/server/helpers.ts
|
|
10
13
|
/**
|
|
11
|
-
* Signals one owned child process, or its detached process group on a POSIX host.
|
|
12
|
-
*
|
|
13
|
-
* @remarks
|
|
14
|
-
* On a POSIX host a child spawned `detached` leads its own process group, so signalling the
|
|
15
|
-
* negated pid reaches the whole tree. On Windows, or when no pid is available, the signal goes to
|
|
16
|
-
* the child directly. A throw during signalling is swallowed: the process can exit and win the
|
|
17
|
-
* signal race, and the child's close event remains the authoritative terminal state.
|
|
18
|
-
*
|
|
19
|
-
* @param child - The child process signal boundary
|
|
20
|
-
* @param signal - The signal to deliver
|
|
21
|
-
* @returns Nothing
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```ts
|
|
25
|
-
* killProcess(child, 'SIGTERM')
|
|
26
|
-
* ```
|
|
27
|
-
*/
|
|
28
|
-
function killProcess(child, signal) {
|
|
29
|
-
try {
|
|
30
|
-
if (process.platform === "win32" || child.pid === void 0) {
|
|
31
|
-
child.kill(signal);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
process.kill(-child.pid, signal);
|
|
35
|
-
} catch {}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
14
|
* Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
|
|
39
15
|
*
|
|
40
16
|
* @remarks
|
|
@@ -42,7 +18,7 @@ function killProcess(child, signal) {
|
|
|
42
18
|
* multibyte sequence, the start advances past the leading continuation bytes so the retained
|
|
43
19
|
* bytes always begin on a code-point boundary.
|
|
44
20
|
*
|
|
45
|
-
* @param
|
|
21
|
+
* @param bytes - The accumulated bytes
|
|
46
22
|
* @param limit - The maximum retained byte count
|
|
47
23
|
* @returns The trailing bytes, at most `limit` and never starting mid-sequence
|
|
48
24
|
*
|
|
@@ -51,7 +27,8 @@ function killProcess(child, signal) {
|
|
|
51
27
|
* trimTail(Buffer.from('hello'), 3) // <Buffer 6c 6c 6f>
|
|
52
28
|
* ```
|
|
53
29
|
*/
|
|
54
|
-
function trimTail(
|
|
30
|
+
function trimTail(bytes, limit) {
|
|
31
|
+
const buffer = node_buffer.Buffer.isBuffer(bytes) ? bytes : node_buffer.Buffer.from(bytes);
|
|
55
32
|
if (buffer.byteLength <= limit) return buffer;
|
|
56
33
|
let start = buffer.byteLength - limit;
|
|
57
34
|
while (start < buffer.byteLength) {
|
|
@@ -69,7 +46,7 @@ function trimTail(buffer, limit) {
|
|
|
69
46
|
* a multibyte sequence, the end retreats to the sequence's start so the retained bytes always end
|
|
70
47
|
* on a code-point boundary and decode without a replacement character.
|
|
71
48
|
*
|
|
72
|
-
* @param
|
|
49
|
+
* @param bytes - The accumulated bytes
|
|
73
50
|
* @param limit - The maximum retained byte count
|
|
74
51
|
* @returns The leading bytes, at most `limit` and never ending mid-sequence
|
|
75
52
|
*
|
|
@@ -78,7 +55,8 @@ function trimTail(buffer, limit) {
|
|
|
78
55
|
* trimHead(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
|
|
79
56
|
* ```
|
|
80
57
|
*/
|
|
81
|
-
function trimHead(
|
|
58
|
+
function trimHead(bytes, limit) {
|
|
59
|
+
const buffer = node_buffer.Buffer.isBuffer(bytes) ? bytes : node_buffer.Buffer.from(bytes);
|
|
82
60
|
if (buffer.byteLength <= limit) return buffer;
|
|
83
61
|
let end = limit;
|
|
84
62
|
while (end > 0) {
|
|
@@ -89,256 +67,630 @@ function trimHead(buffer, limit) {
|
|
|
89
67
|
return node_buffer.Buffer.from(buffer.subarray(0, end));
|
|
90
68
|
}
|
|
91
69
|
/**
|
|
92
|
-
*
|
|
70
|
+
* Takes one owned frozen snapshot of a caller's command.
|
|
93
71
|
*
|
|
94
72
|
* @remarks
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
73
|
+
* Every public entry point snapshots before it validates, so the object validated is the object
|
|
74
|
+
* spawned. Each property is read exactly once, because reading one runs the caller's own getter: a
|
|
75
|
+
* command whose `file` changes between reads would otherwise validate one executable and spawn
|
|
76
|
+
* another. The argument vector and the environment record are copied and frozen, so a caller
|
|
77
|
+
* mutating either after the call cannot reach the spawn. An absent optional stays absent rather than
|
|
78
|
+
* becoming an explicit `undefined`.
|
|
100
79
|
*
|
|
101
|
-
* @param
|
|
102
|
-
* @returns
|
|
80
|
+
* @param command - The caller's command, whose properties may be getters
|
|
81
|
+
* @returns A frozen command carrying the values read at this instant
|
|
103
82
|
*
|
|
104
83
|
* @example
|
|
105
84
|
* ```ts
|
|
106
|
-
*
|
|
85
|
+
* snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
|
|
107
86
|
* ```
|
|
108
87
|
*/
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
88
|
+
function snapshotCommand(command) {
|
|
89
|
+
const file = command.file;
|
|
90
|
+
const argumentsList = Object.freeze([...command.arguments]);
|
|
91
|
+
const sourceEnvironment = command.environment;
|
|
92
|
+
const environment = sourceEnvironment === void 0 ? void 0 : Object.freeze({ ...sourceEnvironment });
|
|
93
|
+
const input = command.input;
|
|
94
|
+
const isolated = command.isolated;
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
file,
|
|
97
|
+
arguments: argumentsList,
|
|
98
|
+
...environment === void 0 ? {} : { environment },
|
|
99
|
+
...input === void 0 ? {} : { input },
|
|
100
|
+
...isolated === void 0 ? {} : { isolated }
|
|
101
|
+
});
|
|
113
102
|
}
|
|
114
103
|
/**
|
|
115
104
|
* Renders one command into its diagnostic command line.
|
|
116
105
|
*
|
|
117
106
|
* @param command - The executable and its argument vector
|
|
118
|
-
* @returns The space-joined command line, for
|
|
107
|
+
* @returns The space-joined command line, for an {@link ExecuteResult} and error messages
|
|
119
108
|
*
|
|
120
109
|
* @example
|
|
121
110
|
* ```ts
|
|
122
|
-
*
|
|
111
|
+
* formatCommand({ file: 'git', arguments: ['status'] }) // 'git status'
|
|
123
112
|
* ```
|
|
124
113
|
*/
|
|
125
|
-
function
|
|
114
|
+
function formatCommand(command) {
|
|
126
115
|
return [command.file, ...command.arguments].join(" ");
|
|
127
116
|
}
|
|
128
117
|
/**
|
|
129
|
-
*
|
|
118
|
+
* Reads one environment variable under an explicit platform's key rules.
|
|
130
119
|
*
|
|
131
120
|
* @remarks
|
|
132
|
-
*
|
|
133
|
-
*
|
|
121
|
+
* Windows environment keys are case-insensitive, so a merged record can hold `Path` where the
|
|
122
|
+
* caller asks for `PATH`. The exact key is tried first and a folded scan runs only for `win32`.
|
|
123
|
+
*
|
|
124
|
+
* @param environment - The environment record to read
|
|
125
|
+
* @param name - The variable name
|
|
126
|
+
* @param platform - The platform whose key rules apply
|
|
127
|
+
* @returns The value, or `undefined` when the environment declares no such variable
|
|
134
128
|
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* readPlatformVariable({ Path: 'C:\\Windows' }, 'PATH', 'win32') // 'C:\\Windows'
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
function readPlatformVariable(environment, name, platform) {
|
|
135
|
+
const direct = environment[name];
|
|
136
|
+
if (direct !== void 0) return direct;
|
|
137
|
+
if (platform !== "win32") return void 0;
|
|
138
|
+
const target = name.toUpperCase();
|
|
139
|
+
for (const [key, value] of Object.entries(environment)) if (key.toUpperCase() === target) return value;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reads one environment variable the way the current host resolves it.
|
|
143
|
+
*
|
|
144
|
+
* @param environment - The environment record to read
|
|
145
|
+
* @param name - The variable name
|
|
146
|
+
* @returns The value, or `undefined` when the environment declares no such variable
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```ts
|
|
150
|
+
* readVariable({ PATH: '/usr/bin' }, 'PATH') // '/usr/bin'
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function readVariable(environment, name) {
|
|
154
|
+
return readPlatformVariable(environment, name, node_process.platform);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Merges environment layers under an explicit platform's key rules.
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* Later maps override earlier ones and an `undefined` value unsets a key. For `win32` the keys fold
|
|
161
|
+
* case-insensitively and the last writer wins, so `PATH` followed by `Path` yields one variable
|
|
162
|
+
* rather than two the host would resolve unpredictably.
|
|
163
|
+
*
|
|
164
|
+
* @param platform - The platform whose key rules apply
|
|
165
|
+
* @param parent - The parent environment layer
|
|
166
|
+
* @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
|
|
135
167
|
* @param base - The command's own environment overrides
|
|
136
168
|
* @param override - Per-invocation overrides applied last
|
|
137
|
-
* @returns The
|
|
169
|
+
* @returns The environment for a spawn, carrying no unset key
|
|
138
170
|
*
|
|
139
171
|
* @example
|
|
140
172
|
* ```ts
|
|
141
|
-
*
|
|
173
|
+
* mergePlatformEnvironment('linux', {}, false, { TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
|
|
142
174
|
* ```
|
|
143
175
|
*/
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
176
|
+
function mergePlatformEnvironment(platform, parent, isolated, base, override) {
|
|
177
|
+
const folded = platform === "win32";
|
|
178
|
+
const layers = [
|
|
179
|
+
isolated ? {} : parent,
|
|
180
|
+
base ?? {},
|
|
181
|
+
override ?? {}
|
|
182
|
+
];
|
|
183
|
+
const entries = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const layer of layers) for (const [key, value] of Object.entries(layer)) {
|
|
185
|
+
const token = folded ? key.toUpperCase() : key;
|
|
186
|
+
if (value === void 0) {
|
|
187
|
+
entries.delete(token);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
entries.set(token, [key, value]);
|
|
191
|
+
}
|
|
192
|
+
const merged = {};
|
|
193
|
+
for (const [key, value] of entries.values()) merged[key] = value;
|
|
194
|
+
return merged;
|
|
150
195
|
}
|
|
151
196
|
/**
|
|
152
|
-
*
|
|
197
|
+
* Merges environment overrides into the environment one child receives on the current host.
|
|
198
|
+
*
|
|
199
|
+
* @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
|
|
200
|
+
* @param base - The command's own environment overrides
|
|
201
|
+
* @param override - Per-invocation overrides applied last
|
|
202
|
+
* @returns The environment for a spawn, carrying no unset key
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* ```ts
|
|
206
|
+
* mergeEnvironment(true, { TOKEN: 'a' }) // { TOKEN: 'a' }
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
function mergeEnvironment(isolated, base, override) {
|
|
210
|
+
return mergePlatformEnvironment(node_process.platform, process.env, isolated, base, override);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Checks whether a path names a regular file.
|
|
153
214
|
*
|
|
154
215
|
* @remarks
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* @
|
|
160
|
-
*
|
|
161
|
-
* @
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
216
|
+
* Existence alone does not make a lookup hit: a directory named `git` on `PATH` is not the `git`
|
|
217
|
+
* command. An unreadable or malformed path reports `false` rather than throwing.
|
|
218
|
+
*
|
|
219
|
+
* @param target - The absolute or relative path to inspect
|
|
220
|
+
* @returns True when the path resolves to a regular file; false otherwise
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```ts
|
|
224
|
+
* isFile(process.execPath) // true
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
function isFile(target) {
|
|
228
|
+
return (0, _orkestrel_contract.holds)(() => (0, node_fs.statSync)(target, { throwIfNoEntry: false })?.isFile() === true);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Builds the executable candidates an explicit platform would search.
|
|
232
|
+
*
|
|
233
|
+
* @remarks
|
|
234
|
+
* Windows alone needs this list: the host searches the working directory before `PATH` and applies
|
|
235
|
+
* `PATHEXT`, and Node reproduces neither for a direct spawn. Within each searched directory the
|
|
236
|
+
* literal name is tried first and each `PATHEXT` candidate after it, whether or not the name already
|
|
237
|
+
* carries an extension — so `report.txt` resolves to a `report.txt` file where one exists and to
|
|
238
|
+
* `report.txt.cmd` where none does. The lookup reads the child's effective environment, so an
|
|
239
|
+
* overridden `PATH` selects the executable the child would have found. A POSIX host resolves the
|
|
240
|
+
* file itself, so the candidate list there is empty. An appended extension is spelled the way
|
|
241
|
+
* `PATHEXT` spells it rather than the way the directory entry does, which the case-insensitive host
|
|
242
|
+
* treats as the same file. A non-Windows platform returns an empty list because `execvp` performs
|
|
243
|
+
* its own lookup.
|
|
244
|
+
*
|
|
245
|
+
* @param file - The command executable name or path
|
|
246
|
+
* @param workspace - The directory searched first
|
|
247
|
+
* @param environment - The child's effective environment
|
|
248
|
+
* @param platform - The platform whose lookup rules apply
|
|
249
|
+
* @returns The ordered absolute candidate paths
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* buildExecutableCandidates('git', 'C:\\work', { PATH: 'C:\\bin' }, 'win32')
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
function buildExecutableCandidates(file, workspace, environment, platform) {
|
|
257
|
+
if (platform !== "win32") return Object.freeze([]);
|
|
258
|
+
const candidates = [file, ...(readPlatformVariable(environment, "PATHEXT", platform) ?? _src_core.PROCESS_PATHEXT).split(";").filter((extension) => extension.length > 0).map((extension) => `${file}${extension}`)];
|
|
259
|
+
const directories = file.includes("/") || file.includes("\\") ? [workspace] : [workspace, ...(readPlatformVariable(environment, "PATH", platform) ?? "").split(";")];
|
|
260
|
+
const targets = [];
|
|
261
|
+
for (const directory of directories) {
|
|
262
|
+
if (directory.length === 0) continue;
|
|
263
|
+
for (const candidate of candidates) targets.push(node_path.win32.resolve(directory, candidate));
|
|
264
|
+
}
|
|
265
|
+
return Object.freeze(targets);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Resolves a command file to the executable path the host would launch.
|
|
269
|
+
*
|
|
270
|
+
* @param file - The command executable name or path
|
|
271
|
+
* @param options - The directory searched first and the child's effective environment
|
|
272
|
+
* @returns The resolved absolute path, or `undefined` when no candidate is a regular file
|
|
167
273
|
*
|
|
168
274
|
* @example
|
|
169
275
|
* ```ts
|
|
170
|
-
*
|
|
276
|
+
* resolveExecutable('git', {}) ?? 'git' // an absolute path on Windows, 'git' elsewhere
|
|
171
277
|
* ```
|
|
172
278
|
*/
|
|
173
|
-
function
|
|
279
|
+
function resolveExecutable(file, options) {
|
|
280
|
+
const environment = options?.environment ?? process.env;
|
|
281
|
+
const candidates = buildExecutableCandidates(file, options?.workspace ?? process.cwd(), environment, node_process.platform);
|
|
282
|
+
for (const target of candidates) if (isFile(target)) return target;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Quotes one command-line token for a `cmd.exe` command line.
|
|
286
|
+
*
|
|
287
|
+
* @remarks
|
|
288
|
+
* A token carrying whitespace or a shell metacharacter is wrapped in double quotes so `cmd.exe`
|
|
289
|
+
* passes it as one literal argument; an embedded double quote is doubled, which is how `cmd.exe`
|
|
290
|
+
* reads a literal quote inside a quoted token. The quoted set includes `%`, although quoting cannot
|
|
291
|
+
* stop `cmd.exe` expansion; {@link buildSpawn} therefore refuses `%` in an argument to a Windows
|
|
292
|
+
* batch target.
|
|
293
|
+
*
|
|
294
|
+
* @param value - The token to quote
|
|
295
|
+
* @returns The token, quoted only when `cmd.exe` would otherwise split or interpret it
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* ```ts
|
|
299
|
+
* quoteArgument('a&b') // '"a&b"'
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
function quoteArgument(value) {
|
|
303
|
+
if (value.length > 0 && !/[\s"&|<>^()%!]/.test(value)) return value;
|
|
304
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Builds a spawn form from a resolved file and an explicit platform.
|
|
308
|
+
*
|
|
309
|
+
* @remarks
|
|
310
|
+
* A resolved `.cmd` or `.bat` script cannot be spawned directly **on Windows**, so it runs there
|
|
311
|
+
* through an explicitly quoted `cmd.exe /d /s /c` command line with the argument vector passed
|
|
312
|
+
* verbatim. No path uses a shell, so a metacharacter in an argument is never interpreted. `cmd.exe`
|
|
313
|
+
* expands `%NAME%` before it parses quotes, so no quoting can carry a percent sign through to a
|
|
314
|
+
* Windows batch target: an argument carrying one is refused there rather than silently rewritten.
|
|
315
|
+
* The whole batch path is Windows-only. A POSIX host has no `cmd.exe` and no restriction on spawning
|
|
316
|
+
* a file directly, so a target named `worker.cmd` spawns directly there and receives a percent sign
|
|
317
|
+
* as literal text.
|
|
318
|
+
*
|
|
319
|
+
* @param command - The executable and its argument vector
|
|
320
|
+
* @param file - The resolved executable file
|
|
321
|
+
* @param environment - The child's effective environment
|
|
322
|
+
* @param platform - The platform whose batch rules apply
|
|
323
|
+
* @returns The file, argument vector, and verbatim flag to spawn with
|
|
324
|
+
* @throws A {@link ProcessError} coded `invalid` when `platform` is `win32`, the resolved target is a batch script, and an argument carries a percent sign
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* buildPlatformSpawn({ file: 'node', arguments: ['--version'] }, 'node', {}, 'linux').verbatim // false
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
function buildPlatformSpawn(command, file, environment, platform) {
|
|
332
|
+
const extension = (platform === "win32" ? node_path.win32.extname(file) : node_path.posix.extname(file)).toLowerCase();
|
|
333
|
+
if (!(platform === "win32" && (extension === ".cmd" || extension === ".bat"))) return Object.freeze({
|
|
334
|
+
file,
|
|
335
|
+
arguments: Object.freeze([...command.arguments]),
|
|
336
|
+
verbatim: false
|
|
337
|
+
});
|
|
338
|
+
for (const argument of command.arguments) if (argument.includes("%")) throw (0, _src_core.createInvalidError)("command argument", argument);
|
|
339
|
+
const line = [`"${file}"`, ...command.arguments.map(quoteArgument)].join(" ");
|
|
174
340
|
return Object.freeze({
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
341
|
+
file: readPlatformVariable(environment, "ComSpec", platform) ?? "cmd.exe",
|
|
342
|
+
arguments: Object.freeze([
|
|
343
|
+
"/d",
|
|
344
|
+
"/s",
|
|
345
|
+
"/c",
|
|
346
|
+
`"${line}"`
|
|
347
|
+
]),
|
|
348
|
+
verbatim: true
|
|
182
349
|
});
|
|
183
350
|
}
|
|
184
351
|
/**
|
|
185
|
-
*
|
|
352
|
+
* Builds the resolved spawn form of one command for the current host.
|
|
353
|
+
*
|
|
354
|
+
* @param command - The executable and its argument vector
|
|
355
|
+
* @param options - The directory searched first and the child's effective environment
|
|
356
|
+
* @returns The file, argument vector, and verbatim flag to spawn with
|
|
357
|
+
* @throws A {@link ProcessError} coded `invalid` when the host is Windows, the resolved target is a batch script, and an argument carries a percent sign
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```ts
|
|
361
|
+
* buildSpawn({ file: 'node', arguments: ['--version'] }).verbatim // false
|
|
362
|
+
* ```
|
|
363
|
+
*/
|
|
364
|
+
function buildSpawn(command, options) {
|
|
365
|
+
return buildPlatformSpawn(command, resolveExecutable(command.file, options) ?? command.file, options?.environment ?? process.env, node_process.platform);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Validates one spawn-bound string.
|
|
186
369
|
*
|
|
187
370
|
* @remarks
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}). A positive `timeout` arms a
|
|
191
|
-
* timer that terminates the child through `SIGTERM`, then `SIGKILL` after `grace`
|
|
192
|
-
* (default {@link PROCESS_GRACE}); aborting `signal` terminates the same way. The child's
|
|
193
|
-
* `environment` merges over the parent, then `options.environment` on top, and `options.input`
|
|
194
|
-
* overrides `command.input`. Unless `reject` is `false`, a failed run rejects with a
|
|
195
|
-
* {@link createRunError} carrying the {@link RunResult}.
|
|
371
|
+
* A NUL character terminates a string for the host, so it is refused everywhere rather than
|
|
372
|
+
* reaching the spawn as a raw `ERR_INVALID_ARG_VALUE`.
|
|
196
373
|
*
|
|
197
|
-
* @param
|
|
198
|
-
* @param
|
|
199
|
-
* @
|
|
200
|
-
* @
|
|
374
|
+
* @param value - The value to inspect
|
|
375
|
+
* @param subject - The rejected input named as the caller wrote it, such as `command file`
|
|
376
|
+
* @param required - If `true`, an empty string is refused; if `false`, it is accepted
|
|
377
|
+
* @returns Nothing; a successful return means the string is spawn-safe
|
|
378
|
+
* @throws A {@link ProcessError} coded `invalid` when the value is not a string, carries a NUL, or is required and empty
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* validateText('status', 'command argument', false) // returns
|
|
383
|
+
* ```
|
|
384
|
+
*/
|
|
385
|
+
function validateText(value, subject, required) {
|
|
386
|
+
if (!(0, _orkestrel_contract.isString)(value) || value.includes("\0")) throw (0, _src_core.createInvalidError)(subject, value);
|
|
387
|
+
if (required && !(0, _orkestrel_contract.isNonEmptyString)(value)) throw (0, _src_core.createInvalidError)(subject, value);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Validates one timer-valued option in milliseconds.
|
|
391
|
+
*
|
|
392
|
+
* @remarks
|
|
393
|
+
* The host truncates a fractional delay and converts anything above {@link PROCESS_TIMER} to one
|
|
394
|
+
* millisecond, so a requested 25-day timeout would otherwise fire immediately.
|
|
395
|
+
*
|
|
396
|
+
* @param value - The value to inspect, or `undefined` when the option was omitted
|
|
397
|
+
* @param subject - The rejected input named as the caller wrote it, such as `option 'grace'`
|
|
398
|
+
* @returns Nothing; a successful return means the value schedules as written
|
|
399
|
+
* @throws A {@link ProcessError} coded `invalid` when the value is not a non-negative integer at or below `PROCESS_TIMER`
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```ts
|
|
403
|
+
* validateTimer(5_000, "option 'grace'") // returns
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
function validateTimer(value, subject) {
|
|
407
|
+
if (value === void 0) return;
|
|
408
|
+
if (!(0, _orkestrel_contract.isNonNegativeInteger)(value) || !(0, _orkestrel_contract.boundsOf)(0, _src_core.PROCESS_TIMER)(value)) throw (0, _src_core.createInvalidError)(subject, value);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Validates one byte-valued option.
|
|
412
|
+
*
|
|
413
|
+
* @param value - The value to inspect, or `undefined` when the option was omitted
|
|
414
|
+
* @param subject - The rejected input named as the caller wrote it, such as `option 'limit'`
|
|
415
|
+
* @param minimum - The smallest accepted value
|
|
416
|
+
* @returns Nothing; a successful return means the value bounds a real byte count
|
|
417
|
+
* @throws A {@link ProcessError} coded `invalid` when the value is not a safe integer at or above `minimum`
|
|
418
|
+
*
|
|
419
|
+
* @example
|
|
420
|
+
* ```ts
|
|
421
|
+
* validateBytes(1_024, "option 'limit'", 0) // returns
|
|
422
|
+
* ```
|
|
423
|
+
*/
|
|
424
|
+
function validateBytes(value, subject, minimum) {
|
|
425
|
+
if (value === void 0) return;
|
|
426
|
+
if (!(0, _orkestrel_contract.isNonNegativeInteger)(value) || !(0, _orkestrel_contract.boundsOf)(minimum, Number.MAX_SAFE_INTEGER)(value)) throw (0, _src_core.createInvalidError)(subject, value);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Validates every spawn-bound string of one environment override map.
|
|
430
|
+
*
|
|
431
|
+
* @remarks
|
|
432
|
+
* The host refuses a NUL in a variable name or value with a raw `ERR_INVALID_ARG_VALUE`, so the same
|
|
433
|
+
* refusal runs here for a command's own overrides and for a per-invocation override map alike.
|
|
434
|
+
*
|
|
435
|
+
* @param environment - The override map, or `undefined` when none was supplied
|
|
436
|
+
* @returns Nothing; a successful return means every name and value reaches the host intact
|
|
437
|
+
* @throws A {@link ProcessError} coded `invalid` when a name is empty or a name or value carries a NUL
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```ts
|
|
441
|
+
* validateEnvironment({ TOKEN: 'a' }) // returns
|
|
442
|
+
* ```
|
|
443
|
+
*/
|
|
444
|
+
function validateEnvironment(environment) {
|
|
445
|
+
if (environment === void 0) return;
|
|
446
|
+
for (const [key, value] of Object.entries(environment)) {
|
|
447
|
+
validateText(key, "environment key", true);
|
|
448
|
+
if (value !== void 0) validateText(value, "environment value", false);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Validates every spawn-bound string of one command.
|
|
453
|
+
*
|
|
454
|
+
* @param command - The executable, arguments, and optional environment overrides
|
|
455
|
+
* @returns Nothing; a successful return means the command reaches the host intact
|
|
456
|
+
* @throws A {@link ProcessError} coded `invalid` when the file is empty or any string carries a NUL
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```ts
|
|
460
|
+
* validateCommand({ file: 'git', arguments: ['status'] }) // returns
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
function validateCommand(command) {
|
|
464
|
+
validateText(command.file, "command file", true);
|
|
465
|
+
for (const argument of command.arguments) validateText(argument, "command argument", false);
|
|
466
|
+
validateEnvironment(command.environment);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Validates the working directory one child starts in.
|
|
470
|
+
*
|
|
471
|
+
* @param workspace - The directory, or `undefined` when the option was omitted
|
|
472
|
+
* @returns Nothing; a successful return means the directory reaches the host intact
|
|
473
|
+
* @throws A {@link ProcessError} coded `invalid` when the directory is empty or carries a NUL
|
|
474
|
+
*
|
|
475
|
+
* @example
|
|
476
|
+
* ```ts
|
|
477
|
+
* validateWorkspace(process.cwd()) // returns
|
|
478
|
+
* ```
|
|
479
|
+
*/
|
|
480
|
+
function validateWorkspace(workspace) {
|
|
481
|
+
if (workspace === void 0) return;
|
|
482
|
+
validateText(workspace, "workspace", true);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Checks whether a child process has reached its native exit.
|
|
486
|
+
*
|
|
487
|
+
* @remarks
|
|
488
|
+
* The host sets exactly one of these fields when the process ends, so together they are the
|
|
489
|
+
* authoritative liveness answer. Reading them before each signal is what keeps a signal from being
|
|
490
|
+
* initiated against a pid the host may have reused.
|
|
491
|
+
*
|
|
492
|
+
* @param child - The child boundary
|
|
493
|
+
* @returns True when the process has exited; false while it is live
|
|
494
|
+
*
|
|
495
|
+
* @example
|
|
496
|
+
* ```ts
|
|
497
|
+
* isExited({ exitCode: 0, signalCode: null }) // true
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
function isExited(child) {
|
|
501
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Signals one owned child process, or its detached process group on a POSIX host.
|
|
505
|
+
*
|
|
506
|
+
* @remarks
|
|
507
|
+
* On a POSIX host a child spawned `detached` leads its own process group, so signalling the
|
|
508
|
+
* negated pid reaches the whole tree. When no group owns the pid and the host reports `ESRCH`, the
|
|
509
|
+
* signal falls back to the child directly. On Windows, or when no pid is available, the signal also
|
|
510
|
+
* goes to the child directly. Every other throw during signalling is swallowed: the process can
|
|
511
|
+
* exit between the caller's liveness check and this call, and the child's native exit remains the
|
|
512
|
+
* authoritative terminal state.
|
|
513
|
+
*
|
|
514
|
+
* @param child - The child boundary
|
|
515
|
+
* @param signal - The signal to deliver
|
|
516
|
+
* @returns Nothing
|
|
201
517
|
*
|
|
202
518
|
* @example
|
|
203
519
|
* ```ts
|
|
204
|
-
*
|
|
520
|
+
* killProcess(child, 'SIGTERM')
|
|
205
521
|
* ```
|
|
206
522
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
523
|
+
function killProcess(child, signal) {
|
|
524
|
+
try {
|
|
525
|
+
if (process.platform === "win32" || child.pid === void 0) {
|
|
526
|
+
child.kill(signal);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
process.kill(-child.pid, signal);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
532
|
+
(0, _orkestrel_contract.holds)(() => child.kill(signal));
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Kills one Windows process tree through `taskkill`.
|
|
539
|
+
*
|
|
540
|
+
* @remarks
|
|
541
|
+
* Windows has no process group a signal can reach, so the whole tree is ended by the host utility,
|
|
542
|
+
* addressed by its absolute `System32` path so a `PATH` override cannot substitute another program.
|
|
543
|
+
* The call is bounded: it is killed itself when `timeout` elapses, which is what keeps a termination
|
|
544
|
+
* from waiting on the host indefinitely. A tree is discoverable only while its root lives, so a
|
|
545
|
+
* descendant that outlives the root is beyond this mechanism.
|
|
546
|
+
*
|
|
547
|
+
* @param pid - The root process id of the tree to end
|
|
548
|
+
* @param timeout - The milliseconds the utility is given before it is killed
|
|
549
|
+
* @returns True when the utility reported success; false when it failed, was unavailable, or was cut off
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```ts
|
|
553
|
+
* await killTree(child.pid ?? 0, 5_000)
|
|
554
|
+
* ```
|
|
555
|
+
*/
|
|
556
|
+
function killTree(pid, timeout) {
|
|
214
557
|
const settled = Promise.withResolvers();
|
|
215
|
-
const
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
cwd: options?.workspace ?? process.cwd(),
|
|
226
|
-
detached: process.platform !== "win32",
|
|
227
|
-
env: mergeEnvironment(command.environment, options?.environment),
|
|
228
|
-
stdio: [
|
|
229
|
-
"pipe",
|
|
230
|
-
"pipe",
|
|
231
|
-
"pipe"
|
|
232
|
-
],
|
|
233
|
-
shell: requiresShell(command.file),
|
|
234
|
-
windowsHide: true
|
|
235
|
-
});
|
|
236
|
-
terminate.signal.addEventListener("abort", () => {
|
|
237
|
-
killProcess(child, "SIGTERM");
|
|
238
|
-
killTimer = setTimeout(() => killProcess(child, "SIGKILL"), grace);
|
|
239
|
-
}, { once: true });
|
|
240
|
-
child.stdin.on("error", () => void 0);
|
|
241
|
-
child.stdout.on("data", (chunk) => {
|
|
242
|
-
if (node_buffer.Buffer.isBuffer(chunk)) stdout = trimHead(node_buffer.Buffer.concat([stdout, chunk]), limit);
|
|
243
|
-
});
|
|
244
|
-
child.stderr.on("data", (chunk) => {
|
|
245
|
-
if (node_buffer.Buffer.isBuffer(chunk)) stderr = trimHead(node_buffer.Buffer.concat([stderr, chunk]), limit);
|
|
246
|
-
});
|
|
247
|
-
child.once("error", (cause) => {
|
|
248
|
-
if (done) return;
|
|
249
|
-
done = true;
|
|
250
|
-
launchCause = cause;
|
|
251
|
-
cleanup.abort();
|
|
252
|
-
clearTimeout(killTimer);
|
|
253
|
-
clearTimeout(timeoutTimer);
|
|
254
|
-
settled.resolve(buildRunResult(line, stdout, stderr, null, null, false, limit));
|
|
255
|
-
});
|
|
256
|
-
child.once("close", (code, signal) => {
|
|
257
|
-
if (done) return;
|
|
258
|
-
done = true;
|
|
259
|
-
cleanup.abort();
|
|
260
|
-
clearTimeout(killTimer);
|
|
261
|
-
clearTimeout(timeoutTimer);
|
|
262
|
-
settled.resolve(buildRunResult(line, stdout, stderr, code, signal, timedOut, limit));
|
|
558
|
+
const executable = (0, node_path.join)(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe");
|
|
559
|
+
const killer = (0, node_child_process.spawn)(executable, [
|
|
560
|
+
"/F",
|
|
561
|
+
"/T",
|
|
562
|
+
"/PID",
|
|
563
|
+
String(pid)
|
|
564
|
+
], {
|
|
565
|
+
stdio: "ignore",
|
|
566
|
+
windowsHide: true,
|
|
567
|
+
timeout
|
|
263
568
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
569
|
+
killer.once("error", () => settled.resolve(false));
|
|
570
|
+
killer.once("close", (code) => settled.resolve(code === 0));
|
|
571
|
+
return settled.promise;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Waits for one child process's native exit, bounded by a deadline.
|
|
575
|
+
*
|
|
576
|
+
* @param child - The child boundary
|
|
577
|
+
* @param timeout - The milliseconds to wait before giving up
|
|
578
|
+
* @returns A promise that resolves on the native exit or when the deadline elapses, whichever comes first
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```ts
|
|
582
|
+
* await waitForExit(child, 5_000)
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
function waitForExit(child, timeout) {
|
|
586
|
+
if (isExited(child)) return Promise.resolve();
|
|
587
|
+
const settled = Promise.withResolvers();
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
child.off("exit", settled.resolve);
|
|
590
|
+
settled.resolve();
|
|
269
591
|
}, timeout);
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
592
|
+
child.once("exit", settled.resolve);
|
|
593
|
+
return settled.promise.finally(() => clearTimeout(timer));
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Terminates one child process tree and reports whether its native exit was observed.
|
|
597
|
+
*
|
|
598
|
+
* @remarks
|
|
599
|
+
* Windows ends the whole tree at once through {@link killTree}, falling back to a direct kill after
|
|
600
|
+
* the utility fails, because the host has no cooperative termination to offer. A POSIX host signals
|
|
601
|
+
* the process group `SIGTERM`, falls back to the direct child when no group owns its pid, waits
|
|
602
|
+
* `grace`, then sends `SIGKILL` through the same route. No signal is initiated after the native exit
|
|
603
|
+
* is observed; the window between initiating a signal and the host delivering it belongs to the
|
|
604
|
+
* operating system. `confirm` bounds each awaited step rather than the call as a whole: on Windows
|
|
605
|
+
* it bounds the `taskkill` call and then the final wait, and on a POSIX host `grace` bounds the
|
|
606
|
+
* cooperative wait and `confirm` the final one. Never rejects.
|
|
607
|
+
*
|
|
608
|
+
* @param child - The child boundary
|
|
609
|
+
* @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
|
|
610
|
+
* @param confirm - The milliseconds the native exit is awaited after the final kill
|
|
611
|
+
* @returns True when the native exit was observed; false when `confirm` elapsed without it
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* const confirmed = await stopChild(child, 5_000, 5_000)
|
|
616
|
+
* ```
|
|
617
|
+
*/
|
|
618
|
+
async function stopChild(child, grace, confirm) {
|
|
619
|
+
if (isExited(child)) return true;
|
|
620
|
+
if (process.platform === "win32") {
|
|
621
|
+
if (!(child.pid === void 0 ? false : await killTree(child.pid, confirm)) && !isExited(child)) killProcess(child, "SIGKILL");
|
|
622
|
+
} else {
|
|
623
|
+
killProcess(child, "SIGTERM");
|
|
624
|
+
await waitForExit(child, grace);
|
|
625
|
+
if (!isExited(child)) killProcess(child, "SIGKILL");
|
|
277
626
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return
|
|
627
|
+
if (isExited(child)) return true;
|
|
628
|
+
await waitForExit(child, confirm);
|
|
629
|
+
return isExited(child);
|
|
281
630
|
}
|
|
282
631
|
/**
|
|
283
|
-
*
|
|
632
|
+
* Builds one settled {@link ExecuteResult} from a completed run's captured bytes and terminal facts.
|
|
284
633
|
*
|
|
285
634
|
* @remarks
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* as {@link run}. Unless `reject` is `false`, a failed run throws a {@link createRunError} carrying
|
|
291
|
-
* the {@link RunResult}.
|
|
635
|
+
* `failed` is derived: a run failed when it timed out, was aborted, ended on a host fault, was ended
|
|
636
|
+
* by a signal, or exited with a code other than `0` — a `null` code from a spawn fault is therefore
|
|
637
|
+
* a failure. Each byte field is bounded by `limit` on a code-point boundary here, which is the one
|
|
638
|
+
* place the captured bytes are decoded.
|
|
292
639
|
*
|
|
293
|
-
* @param
|
|
294
|
-
* @
|
|
295
|
-
* @returns The run outcome
|
|
296
|
-
* @throws A {@link ProcessError} carrying the {@link RunResult} when the run failed and `reject` is not `false`
|
|
640
|
+
* @param input - The captured bytes, terminal state, and capture limit
|
|
641
|
+
* @returns The frozen run outcome
|
|
297
642
|
*
|
|
298
643
|
* @example
|
|
299
644
|
* ```ts
|
|
300
|
-
*
|
|
645
|
+
* buildExecuteResult({
|
|
646
|
+
* command: 'git status',
|
|
647
|
+
* stdout: Buffer.from('ok'),
|
|
648
|
+
* stderr: Buffer.alloc(0),
|
|
649
|
+
* code: 0,
|
|
650
|
+
* signal: null,
|
|
651
|
+
* expired: false,
|
|
652
|
+
* aborted: false,
|
|
653
|
+
* truncated: false,
|
|
654
|
+
* limit: 1_024,
|
|
655
|
+
* }).failed // false
|
|
301
656
|
* ```
|
|
302
657
|
*/
|
|
303
|
-
function
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
shell: requiresShell(command.file),
|
|
315
|
-
windowsHide: true,
|
|
316
|
-
...input !== void 0 ? { input } : {},
|
|
317
|
-
...options?.signal !== void 0 ? { signal: options.signal } : {},
|
|
318
|
-
...timeout > 0 ? {
|
|
319
|
-
timeout,
|
|
320
|
-
killSignal: "SIGKILL"
|
|
321
|
-
} : {}
|
|
658
|
+
function buildExecuteResult(input) {
|
|
659
|
+
return Object.freeze({
|
|
660
|
+
command: input.command,
|
|
661
|
+
stdout: trimHead(input.stdout, input.limit).toString("utf8"),
|
|
662
|
+
stderr: trimHead(input.stderr, input.limit).toString("utf8"),
|
|
663
|
+
code: input.code,
|
|
664
|
+
signal: input.signal,
|
|
665
|
+
failed: input.expired || input.aborted || input.cause !== void 0 || input.signal !== null || input.code !== 0,
|
|
666
|
+
expired: input.expired,
|
|
667
|
+
aborted: input.aborted,
|
|
668
|
+
truncated: input.truncated
|
|
322
669
|
});
|
|
323
|
-
const error = outcome.error;
|
|
324
|
-
const timedOut = error !== void 0 && "code" in error && error.code === "ETIMEDOUT";
|
|
325
|
-
const result = buildRunResult(line, node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0), node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0), outcome.status, outcome.signal, timedOut, limit);
|
|
326
|
-
if (result.failed && reject) throw (0, _src_core.createRunError)(result, outcome.error);
|
|
327
|
-
return result;
|
|
328
670
|
}
|
|
329
671
|
//#endregion
|
|
330
672
|
//#region src/server/Process.ts
|
|
331
673
|
/**
|
|
332
|
-
* One supervised child process with
|
|
674
|
+
* One supervised child process with framed output, a bounded backlog, and bounded termination.
|
|
333
675
|
*
|
|
334
676
|
* @remarks
|
|
335
|
-
* Standard output is
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
677
|
+
* Standard output is framed through `readline` into a single-consumer stream: each line goes to
|
|
678
|
+
* exactly one waiting iterator, so concurrent iterators split the output rather than each receiving
|
|
679
|
+
* all of it. While no consumer has ever requested an iterator, stdout keeps draining so `exit` still
|
|
680
|
+
* resolves, and retention stops at the `backlog` mark: a
|
|
681
|
+
* consumer attaching later receives the retained head, a gap, then the live stream. After an iterator
|
|
682
|
+
* has been requested, stdout pauses at the mark and resumes at half of it, so that consumer loses
|
|
683
|
+
* nothing before termination and the child feels real backpressure. The mark is soft — the ordinary backlog can
|
|
684
|
+
* overshoot it by the line that crossed it plus the rest of its delivered chunk. Termination never
|
|
685
|
+
* reapplies backpressure, and retained lines are capped at twice `backlog`; later lines are dropped
|
|
686
|
+
* and `truncated` reports the omission. Standard error is decoded and forwarded live as the `stderr`
|
|
687
|
+
* event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries the
|
|
688
|
+
* child `error` cause on a spawn fault and the terminal `exit`, alongside the `exit` promise. `pid`,
|
|
689
|
+
* `code`, and `signal` read the spawned child directly, so the native exit is readable before the
|
|
690
|
+
* `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
|
|
691
|
+
* exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
|
|
692
|
+
* destroys the emitter last. `destroy` resolving does not imply the child's stdio has closed, so
|
|
693
|
+
* `exit` and `lines` can still be outstanding when a descendant holds an inherited pipe.
|
|
342
694
|
*
|
|
343
695
|
* @example
|
|
344
696
|
* ```ts
|
|
@@ -347,7 +699,6 @@ function runSync(command, options) {
|
|
|
347
699
|
* const child = new Process({
|
|
348
700
|
* command: { file: 'node', arguments: ['--version'] },
|
|
349
701
|
* workspace: process.cwd(),
|
|
350
|
-
* grace: 5000,
|
|
351
702
|
* })
|
|
352
703
|
* const exit = await child.exit
|
|
353
704
|
* await child.destroy()
|
|
@@ -358,45 +709,74 @@ var Process = class {
|
|
|
358
709
|
#child;
|
|
359
710
|
#reader;
|
|
360
711
|
#grace;
|
|
361
|
-
#
|
|
712
|
+
#evidence;
|
|
713
|
+
#backlog;
|
|
362
714
|
#decoder = new node_string_decoder.StringDecoder("utf8");
|
|
363
715
|
#exit = Promise.withResolvers();
|
|
364
716
|
#lines;
|
|
365
|
-
#
|
|
717
|
+
#queue = [];
|
|
366
718
|
#waiters = [];
|
|
367
719
|
#signal;
|
|
368
720
|
#abort;
|
|
369
721
|
#tail = node_buffer.Buffer.alloc(0);
|
|
722
|
+
#head = 0;
|
|
723
|
+
#pending = 0;
|
|
724
|
+
#requested = false;
|
|
725
|
+
#paused = false;
|
|
726
|
+
#truncated = false;
|
|
727
|
+
#terminating = false;
|
|
370
728
|
#closed = false;
|
|
371
729
|
#ended = false;
|
|
372
730
|
#stopping;
|
|
373
731
|
#ending;
|
|
374
732
|
/**
|
|
375
|
-
* Spawn one child process and begin
|
|
733
|
+
* Spawn one child process and begin stream capture.
|
|
376
734
|
*
|
|
377
|
-
* @param options - Command, workspace, termination,
|
|
735
|
+
* @param options - Command, workspace, termination, capture, stdin, and observation settings
|
|
736
|
+
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
378
737
|
*/
|
|
379
738
|
constructor(options) {
|
|
739
|
+
const source = options.command;
|
|
740
|
+
const workspace = options.workspace;
|
|
741
|
+
const grace = options.grace;
|
|
742
|
+
const evidence = options.evidence;
|
|
743
|
+
const backlog = options.backlog;
|
|
744
|
+
const writable = options.writable;
|
|
745
|
+
const signal = options.signal;
|
|
380
746
|
const on = options.on;
|
|
381
747
|
const error = options.error;
|
|
748
|
+
const command = snapshotCommand(source);
|
|
749
|
+
const input = command.input;
|
|
750
|
+
validateCommand(command);
|
|
751
|
+
validateWorkspace(workspace);
|
|
752
|
+
validateTimer(grace, "option 'grace'");
|
|
753
|
+
validateBytes(evidence, "option 'evidence'", 0);
|
|
754
|
+
validateBytes(backlog, "option 'backlog'", 1);
|
|
382
755
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
383
756
|
...on === void 0 ? {} : { on },
|
|
384
757
|
...error === void 0 ? {} : { error }
|
|
385
758
|
});
|
|
386
|
-
this.#grace =
|
|
387
|
-
this.#
|
|
388
|
-
this.#
|
|
759
|
+
this.#grace = grace ?? _src_core.PROCESS_GRACE;
|
|
760
|
+
this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
|
|
761
|
+
this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
|
|
762
|
+
this.#signal = signal;
|
|
389
763
|
this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
|
|
390
|
-
|
|
391
|
-
|
|
764
|
+
const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
|
|
765
|
+
const plan = buildSpawn(command, {
|
|
766
|
+
workspace,
|
|
767
|
+
environment: childEnvironment
|
|
768
|
+
});
|
|
769
|
+
this.#child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
770
|
+
cwd: workspace,
|
|
392
771
|
detached: process.platform !== "win32",
|
|
393
|
-
env:
|
|
772
|
+
env: childEnvironment,
|
|
394
773
|
stdio: [
|
|
395
774
|
"pipe",
|
|
396
775
|
"pipe",
|
|
397
776
|
"pipe"
|
|
398
777
|
],
|
|
399
|
-
windowsHide: true
|
|
778
|
+
windowsHide: true,
|
|
779
|
+
windowsVerbatimArguments: plan.verbatim
|
|
400
780
|
});
|
|
401
781
|
this.#reader = (0, node_readline.createInterface)({
|
|
402
782
|
input: this.#child.stdout,
|
|
@@ -408,19 +788,31 @@ var Process = class {
|
|
|
408
788
|
this.#child.once("close", this.#close.bind(this));
|
|
409
789
|
this.#child.stdin.on("error", () => void 0);
|
|
410
790
|
this.#child.stderr.on("data", this.#retain.bind(this));
|
|
411
|
-
if (
|
|
412
|
-
if (
|
|
413
|
-
if (
|
|
791
|
+
if (input !== void 0) this.#child.stdin.write(input);
|
|
792
|
+
if (writable !== true) this.#child.stdin.end();
|
|
793
|
+
if (signal !== void 0) {
|
|
414
794
|
this.#abort = this.#terminate.bind(this);
|
|
415
|
-
|
|
416
|
-
if (
|
|
795
|
+
signal.addEventListener("abort", this.#abort, { once: true });
|
|
796
|
+
if (signal.aborted) this.stop();
|
|
417
797
|
}
|
|
418
798
|
}
|
|
799
|
+
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
800
|
+
get pid() {
|
|
801
|
+
return this.#child.pid;
|
|
802
|
+
}
|
|
803
|
+
/** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
804
|
+
get code() {
|
|
805
|
+
return this.#child.exitCode;
|
|
806
|
+
}
|
|
807
|
+
/** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
808
|
+
get signal() {
|
|
809
|
+
return this.#child.signalCode;
|
|
810
|
+
}
|
|
419
811
|
/** The typed lifecycle observation surface. */
|
|
420
812
|
get emitter() {
|
|
421
813
|
return this.#emitter;
|
|
422
814
|
}
|
|
423
|
-
/** The
|
|
815
|
+
/** The captured stdout lines, in arrival order, ending when the child's stdout closes. */
|
|
424
816
|
get lines() {
|
|
425
817
|
return this.#lines;
|
|
426
818
|
}
|
|
@@ -428,6 +820,10 @@ var Process = class {
|
|
|
428
820
|
get evidence() {
|
|
429
821
|
return this.#tail.toString("utf8");
|
|
430
822
|
}
|
|
823
|
+
/** True when the `lines` stream omitted output after a retention bound was reached. */
|
|
824
|
+
get truncated() {
|
|
825
|
+
return this.#truncated;
|
|
826
|
+
}
|
|
431
827
|
/** The terminal child state, observed once from the close event. */
|
|
432
828
|
get exit() {
|
|
433
829
|
return this.#exit.promise;
|
|
@@ -435,21 +831,35 @@ var Process = class {
|
|
|
435
831
|
/**
|
|
436
832
|
* Write one line to the open standard-input channel.
|
|
437
833
|
*
|
|
834
|
+
* @remarks
|
|
835
|
+
* Never rejects. The promise settles when the host reports the line handled, so a line written to
|
|
836
|
+
* a child that is not reading stays pending until the child drains it or the channel closes. Every
|
|
837
|
+
* terminal teardown path destroys stdin, so a pending write always settles by teardown; a caller
|
|
838
|
+
* that needs its own deadline arms a timer and calls `stop`.
|
|
839
|
+
*
|
|
438
840
|
* @param text - The line text without its trailing newline
|
|
439
|
-
* @returns True when the
|
|
841
|
+
* @returns True when the line reached the host without error; false when the channel was closed, destroyed, ended, or the write failed
|
|
440
842
|
*/
|
|
441
843
|
send(text) {
|
|
442
|
-
|
|
844
|
+
const stdin = this.#child.stdin;
|
|
845
|
+
if (this.#closed || !stdin.writable) return Promise.resolve(false);
|
|
846
|
+
const settled = Promise.withResolvers();
|
|
443
847
|
try {
|
|
444
|
-
|
|
848
|
+
stdin.write(`${text}\n`, (error) => {
|
|
849
|
+
settled.resolve(error === void 0 || error === null);
|
|
850
|
+
});
|
|
445
851
|
} catch {
|
|
446
|
-
return false;
|
|
852
|
+
return Promise.resolve(false);
|
|
447
853
|
}
|
|
854
|
+
return settled.promise;
|
|
448
855
|
}
|
|
449
856
|
/**
|
|
450
857
|
* Terminate the child process tree and await its observed exit.
|
|
451
858
|
*
|
|
452
|
-
* @
|
|
859
|
+
* @remarks
|
|
860
|
+
* Never rejects, and every call shares one termination.
|
|
861
|
+
*
|
|
862
|
+
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
453
863
|
*/
|
|
454
864
|
stop() {
|
|
455
865
|
if (this.#stopping !== void 0) return this.#stopping;
|
|
@@ -457,7 +867,10 @@ var Process = class {
|
|
|
457
867
|
return this.#stopping;
|
|
458
868
|
}
|
|
459
869
|
/**
|
|
460
|
-
* Stop the child and destroy the observation emitter.
|
|
870
|
+
* Stop the child, close its standard-input channel, and destroy the observation emitter.
|
|
871
|
+
*
|
|
872
|
+
* @remarks
|
|
873
|
+
* Always resolves, including when termination was never confirmed.
|
|
461
874
|
*
|
|
462
875
|
* @returns The stable barrier shared by every call
|
|
463
876
|
*/
|
|
@@ -467,10 +880,11 @@ var Process = class {
|
|
|
467
880
|
return this.#ending;
|
|
468
881
|
}
|
|
469
882
|
#iterate() {
|
|
883
|
+
this.#requested = true;
|
|
470
884
|
return Object.freeze({ next: this.#next.bind(this) });
|
|
471
885
|
}
|
|
472
886
|
#next() {
|
|
473
|
-
const line = this.#
|
|
887
|
+
const line = this.#take();
|
|
474
888
|
if (line !== void 0) return Promise.resolve(Object.freeze({
|
|
475
889
|
done: false,
|
|
476
890
|
value: line
|
|
@@ -483,21 +897,56 @@ var Process = class {
|
|
|
483
897
|
this.#waiters.push(waiter);
|
|
484
898
|
return waiter.promise;
|
|
485
899
|
}
|
|
900
|
+
#take() {
|
|
901
|
+
if (this.#head >= this.#queue.length) return void 0;
|
|
902
|
+
const line = this.#queue[this.#head];
|
|
903
|
+
this.#queue[this.#head] = "";
|
|
904
|
+
this.#head += 1;
|
|
905
|
+
if (this.#head >= this.#queue.length) {
|
|
906
|
+
this.#queue.length = 0;
|
|
907
|
+
this.#head = 0;
|
|
908
|
+
}
|
|
909
|
+
if (line === void 0) return void 0;
|
|
910
|
+
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
911
|
+
this.#relieve();
|
|
912
|
+
return line;
|
|
913
|
+
}
|
|
486
914
|
#push(line) {
|
|
487
915
|
if (this.#ended) return;
|
|
488
916
|
const waiter = this.#waiters.shift();
|
|
489
|
-
if (waiter
|
|
490
|
-
|
|
917
|
+
if (waiter !== void 0) {
|
|
918
|
+
waiter.resolve(Object.freeze({
|
|
919
|
+
done: false,
|
|
920
|
+
value: line
|
|
921
|
+
}));
|
|
491
922
|
return;
|
|
492
923
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
924
|
+
const bytes = node_buffer.Buffer.byteLength(line) + 1;
|
|
925
|
+
const limit = this.#terminating ? this.#backlog * 2 : this.#backlog;
|
|
926
|
+
if ((!this.#requested || this.#terminating) && this.#pending + bytes > limit) {
|
|
927
|
+
this.#truncated = true;
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
this.#queue.push(line);
|
|
931
|
+
this.#pending += bytes;
|
|
932
|
+
this.#restrain();
|
|
933
|
+
}
|
|
934
|
+
#restrain() {
|
|
935
|
+
if (this.#paused || this.#terminating || !this.#requested) return;
|
|
936
|
+
if (this.#pending < this.#backlog) return;
|
|
937
|
+
this.#paused = true;
|
|
938
|
+
this.#reader.pause();
|
|
939
|
+
}
|
|
940
|
+
#relieve() {
|
|
941
|
+
if (!this.#paused || this.#ended) return;
|
|
942
|
+
if (!this.#terminating && this.#pending > this.#backlog / 2) return;
|
|
943
|
+
this.#paused = false;
|
|
944
|
+
this.#reader.resume();
|
|
497
945
|
}
|
|
498
946
|
#finish() {
|
|
499
947
|
if (this.#ended) return;
|
|
500
948
|
this.#ended = true;
|
|
949
|
+
this.#paused = false;
|
|
501
950
|
for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
|
|
502
951
|
done: true,
|
|
503
952
|
value: void 0
|
|
@@ -506,7 +955,7 @@ var Process = class {
|
|
|
506
955
|
}
|
|
507
956
|
#retain(chunk) {
|
|
508
957
|
if (!node_buffer.Buffer.isBuffer(chunk) || this.#closed) return;
|
|
509
|
-
this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#
|
|
958
|
+
this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
|
|
510
959
|
const text = this.#decoder.write(chunk);
|
|
511
960
|
if (text.length > 0) this.#emitter.emit("stderr", text);
|
|
512
961
|
}
|
|
@@ -515,7 +964,7 @@ var Process = class {
|
|
|
515
964
|
const suffix = this.#decoder.end();
|
|
516
965
|
if (suffix.length > 0) this.#emitter.emit("stderr", suffix);
|
|
517
966
|
this.#closed = true;
|
|
518
|
-
|
|
967
|
+
this.#removeAbortListener();
|
|
519
968
|
this.#reader.close();
|
|
520
969
|
const exit = Object.freeze({
|
|
521
970
|
code,
|
|
@@ -527,18 +976,29 @@ var Process = class {
|
|
|
527
976
|
#terminate() {
|
|
528
977
|
this.stop();
|
|
529
978
|
}
|
|
979
|
+
#removeAbortListener() {
|
|
980
|
+
if (this.#signal === void 0 || this.#abort === void 0) return;
|
|
981
|
+
this.#signal.removeEventListener("abort", this.#abort);
|
|
982
|
+
}
|
|
983
|
+
#capBacklog() {
|
|
984
|
+
const limit = this.#backlog * 2;
|
|
985
|
+
while (this.#pending > limit && this.#queue.length > this.#head) {
|
|
986
|
+
const line = this.#queue.pop();
|
|
987
|
+
if (line === void 0) break;
|
|
988
|
+
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
989
|
+
this.#truncated = true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
530
992
|
async #kill() {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
if (this.#closed) return;
|
|
538
|
-
killProcess(this.#child, "SIGKILL");
|
|
539
|
-
await this.#exit.promise;
|
|
993
|
+
this.#terminating = true;
|
|
994
|
+
this.#capBacklog();
|
|
995
|
+
this.#relieve();
|
|
996
|
+
const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
|
|
997
|
+
this.#child.stdin.destroy();
|
|
998
|
+
return confirmed;
|
|
540
999
|
}
|
|
541
1000
|
async #teardown() {
|
|
1001
|
+
this.#removeAbortListener();
|
|
542
1002
|
await this.stop();
|
|
543
1003
|
this.#emitter.destroy();
|
|
544
1004
|
}
|
|
@@ -550,9 +1010,15 @@ var Process = class {
|
|
|
550
1010
|
*
|
|
551
1011
|
* @remarks
|
|
552
1012
|
* A child launched under an id joins the registry and emits `launch`; when it settles it removes
|
|
553
|
-
* itself and emits `exit`, so `count` and `processes` reflect only live children.
|
|
554
|
-
*
|
|
555
|
-
*
|
|
1013
|
+
* itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
|
|
1014
|
+
* before the child is constructed and released when construction throws, so a refused launch never
|
|
1015
|
+
* strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
|
|
1016
|
+
* it lands one microtask after the child's public `exit` event: a listener on that event still sees
|
|
1017
|
+
* the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
|
|
1018
|
+
* and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
|
|
1019
|
+
* getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
|
|
1020
|
+
* `exit`, never from `launch`. `destroy` awaits every child's own teardown, which destroys each
|
|
1021
|
+
* child's observation emitter and leaves every subscription on it silently inert, then destroys the
|
|
556
1022
|
* registry emitter last.
|
|
557
1023
|
*
|
|
558
1024
|
* @example
|
|
@@ -563,7 +1029,6 @@ var Process = class {
|
|
|
563
1029
|
* const child = manager.launch('build', {
|
|
564
1030
|
* command: { file: 'node', arguments: ['build.js'] },
|
|
565
1031
|
* workspace: process.cwd(),
|
|
566
|
-
* grace: 5000,
|
|
567
1032
|
* })
|
|
568
1033
|
* await child.exit
|
|
569
1034
|
* await manager.destroy()
|
|
@@ -572,6 +1037,8 @@ var Process = class {
|
|
|
572
1037
|
var ProcessManager = class {
|
|
573
1038
|
#emitter;
|
|
574
1039
|
#children = /* @__PURE__ */ new Map();
|
|
1040
|
+
#ids = /* @__PURE__ */ new Set();
|
|
1041
|
+
#destroying = false;
|
|
575
1042
|
#ending;
|
|
576
1043
|
/**
|
|
577
1044
|
* Construct a process registry.
|
|
@@ -614,16 +1081,32 @@ var ProcessManager = class {
|
|
|
614
1081
|
/**
|
|
615
1082
|
* Spawn and register one child under `id`.
|
|
616
1083
|
*
|
|
1084
|
+
* @remarks
|
|
1085
|
+
* {@link Process} reads every option before it spawns, so a caller's own option getter runs while
|
|
1086
|
+
* nothing has started and a throw from one strands no process. A getter that begins `destroy`
|
|
1087
|
+
* without throwing is the one remaining race, and it leaves a bounded residual: the child is
|
|
1088
|
+
* already spawned, so the launch is refused with a {@link createProtocolError} and that child is
|
|
1089
|
+
* torn down asynchronously, bounded by `grace` plus the confirmation window. The `protocol`
|
|
1090
|
+
* refusal throws synchronously before the `destroy` barrier settles. The barrier does not cover
|
|
1091
|
+
* that child's asynchronous teardown.
|
|
1092
|
+
*
|
|
617
1093
|
* @param id - The registry key, unique among live children
|
|
618
1094
|
* @param options - The child construction options
|
|
619
1095
|
* @returns The launched child
|
|
620
|
-
* @throws A {@link ProcessError} coded `duplicate` when `id` is already live
|
|
1096
|
+
* @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
|
|
621
1097
|
*/
|
|
622
1098
|
launch(id, options) {
|
|
623
|
-
if (this.#
|
|
624
|
-
|
|
1099
|
+
if (this.#destroying) throw (0, _src_core.createProtocolError)(id);
|
|
1100
|
+
if (this.#ids.has(id)) throw (0, _src_core.createDuplicateError)(id);
|
|
1101
|
+
this.#ids.add(id);
|
|
1102
|
+
const child = this.#construct(id, options);
|
|
1103
|
+
if (this.#destroying) {
|
|
1104
|
+
this.#ids.delete(id);
|
|
1105
|
+
child.destroy();
|
|
1106
|
+
throw (0, _src_core.createProtocolError)(id);
|
|
1107
|
+
}
|
|
625
1108
|
this.#children.set(id, child);
|
|
626
|
-
child.
|
|
1109
|
+
child.exit.then((exit) => this.#evict(id, child, exit));
|
|
627
1110
|
this.#emitter.emit("launch", id);
|
|
628
1111
|
return child;
|
|
629
1112
|
}
|
|
@@ -635,32 +1118,48 @@ var ProcessManager = class {
|
|
|
635
1118
|
/**
|
|
636
1119
|
* Stop every child, then destroy the registry emitter last.
|
|
637
1120
|
*
|
|
1121
|
+
* @remarks
|
|
1122
|
+
* Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
|
|
1123
|
+
* destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
|
|
1124
|
+
* subscription on it goes silently inert.
|
|
1125
|
+
*
|
|
638
1126
|
* @returns The stable barrier shared by every call
|
|
639
1127
|
*/
|
|
640
1128
|
destroy() {
|
|
641
1129
|
if (this.#ending !== void 0) return this.#ending;
|
|
1130
|
+
this.#destroying = true;
|
|
642
1131
|
this.#ending = this.#teardown();
|
|
643
1132
|
return this.#ending;
|
|
644
1133
|
}
|
|
1134
|
+
#construct(id, options) {
|
|
1135
|
+
try {
|
|
1136
|
+
return new Process(options);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
this.#ids.delete(id);
|
|
1139
|
+
throw error;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
645
1142
|
#evict(id, child, exit) {
|
|
646
1143
|
if (this.#children.get(id) !== child) return;
|
|
647
1144
|
this.#children.delete(id);
|
|
1145
|
+
this.#ids.delete(id);
|
|
648
1146
|
this.#emitter.emit("exit", id, exit);
|
|
649
1147
|
}
|
|
650
1148
|
async #stopOne(id) {
|
|
651
1149
|
const child = this.#children.get(id);
|
|
652
1150
|
if (child === void 0) return false;
|
|
653
|
-
|
|
654
|
-
return true;
|
|
1151
|
+
return child.stop();
|
|
655
1152
|
}
|
|
656
1153
|
async #stopMany(ids) {
|
|
657
1154
|
return (await Promise.all(ids.map((id) => this.#stopOne(id)))).every((ok) => ok);
|
|
658
1155
|
}
|
|
659
1156
|
async #stopAll() {
|
|
660
|
-
await Promise.
|
|
1157
|
+
await Promise.allSettled([...this.#children.values()].map((child) => child.stop()));
|
|
661
1158
|
}
|
|
662
1159
|
async #teardown() {
|
|
663
|
-
await Promise.
|
|
1160
|
+
await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
|
|
1161
|
+
this.#children.clear();
|
|
1162
|
+
this.#ids.clear();
|
|
664
1163
|
this.#emitter.destroy();
|
|
665
1164
|
}
|
|
666
1165
|
};
|
|
@@ -703,18 +1202,351 @@ function createProcessManager(options) {
|
|
|
703
1202
|
return new ProcessManager(options);
|
|
704
1203
|
}
|
|
705
1204
|
//#endregion
|
|
1205
|
+
//#region src/server/Retention.ts
|
|
1206
|
+
/**
|
|
1207
|
+
* Accumulates byte totals while retaining a bounded stream head.
|
|
1208
|
+
*
|
|
1209
|
+
* @example
|
|
1210
|
+
* ```ts
|
|
1211
|
+
* const retention = new Retention()
|
|
1212
|
+
* retention.retain(Buffer.from('hello'), 3)?.toString('utf8') // 'hel'
|
|
1213
|
+
* retention.retained // 3
|
|
1214
|
+
* ```
|
|
1215
|
+
*/
|
|
1216
|
+
var Retention = class {
|
|
1217
|
+
#delivered = 0;
|
|
1218
|
+
#retained = 0;
|
|
1219
|
+
/** The bytes delivered by the stream. */
|
|
1220
|
+
get delivered() {
|
|
1221
|
+
return this.#delivered;
|
|
1222
|
+
}
|
|
1223
|
+
/** The bytes retained from the stream head. */
|
|
1224
|
+
get retained() {
|
|
1225
|
+
return this.#retained;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Retains a delivered chunk within a byte limit.
|
|
1229
|
+
*
|
|
1230
|
+
* @param chunk - The delivered chunk, ignored when it is not a buffer
|
|
1231
|
+
* @param limit - The maximum retained byte count
|
|
1232
|
+
* @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
|
|
1233
|
+
*/
|
|
1234
|
+
retain(chunk, limit) {
|
|
1235
|
+
if (!node_buffer.Buffer.isBuffer(chunk)) return void 0;
|
|
1236
|
+
this.#delivered += chunk.byteLength;
|
|
1237
|
+
const room = limit - this.#retained;
|
|
1238
|
+
if (room <= 0) return void 0;
|
|
1239
|
+
const slice = chunk.byteLength <= room ? chunk : node_buffer.Buffer.from(chunk.subarray(0, room));
|
|
1240
|
+
this.#retained += slice.byteLength;
|
|
1241
|
+
return slice;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/server/execution/execute.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
1248
|
+
*
|
|
1249
|
+
* @remarks
|
|
1250
|
+
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
1251
|
+
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
1252
|
+
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
1253
|
+
* whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
|
|
1254
|
+
* child through the same bounded stop, and only the earliest is recorded: a timeout reports
|
|
1255
|
+
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
1256
|
+
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
1257
|
+
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
1258
|
+
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
1259
|
+
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
1260
|
+
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
1261
|
+
* overrides `command.input`. Unless `strict` is `false`, a failed run rejects with a
|
|
1262
|
+
* {@link createExecuteError} carrying the {@link ExecuteResult}. An invalid option or command string rejects
|
|
1263
|
+
* before the child is spawned, because an async function cannot throw synchronously.
|
|
1264
|
+
*
|
|
1265
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
1266
|
+
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
1267
|
+
* @returns The settled run outcome
|
|
1268
|
+
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```ts
|
|
1272
|
+
* const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
1273
|
+
* ```
|
|
1274
|
+
*/
|
|
1275
|
+
async function execute(command, options) {
|
|
1276
|
+
const snapshot = snapshotCommand(command);
|
|
1277
|
+
const optionEnvironment = options?.environment;
|
|
1278
|
+
const optionWorkspace = options?.workspace;
|
|
1279
|
+
const optionInput = options?.input;
|
|
1280
|
+
const optionTimeout = options?.timeout;
|
|
1281
|
+
const optionGrace = options?.grace;
|
|
1282
|
+
const optionSignal = options?.signal;
|
|
1283
|
+
const optionStrict = options?.strict;
|
|
1284
|
+
const optionLimit = options?.limit;
|
|
1285
|
+
validateCommand(snapshot);
|
|
1286
|
+
validateEnvironment(optionEnvironment);
|
|
1287
|
+
validateWorkspace(optionWorkspace);
|
|
1288
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
1289
|
+
validateTimer(optionGrace, "option 'grace'");
|
|
1290
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
1291
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
1292
|
+
const grace = optionGrace ?? _src_core.PROCESS_GRACE;
|
|
1293
|
+
const timeout = optionTimeout ?? 0;
|
|
1294
|
+
const strict = optionStrict ?? true;
|
|
1295
|
+
const text = optionInput ?? snapshot.input;
|
|
1296
|
+
const line = formatCommand(snapshot);
|
|
1297
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1298
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
1299
|
+
const plan = buildSpawn(snapshot, {
|
|
1300
|
+
workspace,
|
|
1301
|
+
environment
|
|
1302
|
+
});
|
|
1303
|
+
const settled = Promise.withResolvers();
|
|
1304
|
+
const terminate = new AbortController();
|
|
1305
|
+
const cleanup = new AbortController();
|
|
1306
|
+
const finish = new AbortController();
|
|
1307
|
+
const outChunks = [];
|
|
1308
|
+
const errChunks = [];
|
|
1309
|
+
const outRetention = new Retention();
|
|
1310
|
+
const errRetention = new Retention();
|
|
1311
|
+
let spawned = false;
|
|
1312
|
+
let expired = false;
|
|
1313
|
+
let aborted = false;
|
|
1314
|
+
let cause;
|
|
1315
|
+
let timeoutTimer;
|
|
1316
|
+
let confirmTimer;
|
|
1317
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1318
|
+
cwd: workspace,
|
|
1319
|
+
detached: process.platform !== "win32",
|
|
1320
|
+
env: environment,
|
|
1321
|
+
stdio: [
|
|
1322
|
+
"pipe",
|
|
1323
|
+
"pipe",
|
|
1324
|
+
"pipe"
|
|
1325
|
+
],
|
|
1326
|
+
windowsHide: true,
|
|
1327
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1328
|
+
});
|
|
1329
|
+
finish.signal.addEventListener("abort", () => {
|
|
1330
|
+
clearTimeout(timeoutTimer);
|
|
1331
|
+
clearTimeout(confirmTimer);
|
|
1332
|
+
cleanup.abort();
|
|
1333
|
+
settled.resolve(buildExecuteResult({
|
|
1334
|
+
command: line,
|
|
1335
|
+
stdout: node_buffer.Buffer.concat(outChunks),
|
|
1336
|
+
stderr: node_buffer.Buffer.concat(errChunks),
|
|
1337
|
+
code: child.exitCode,
|
|
1338
|
+
signal: child.signalCode,
|
|
1339
|
+
expired,
|
|
1340
|
+
aborted,
|
|
1341
|
+
truncated: outRetention.delivered > limit || errRetention.delivered > limit,
|
|
1342
|
+
limit,
|
|
1343
|
+
...cause === void 0 ? {} : { cause }
|
|
1344
|
+
}));
|
|
1345
|
+
}, { once: true });
|
|
1346
|
+
terminate.signal.addEventListener("abort", () => {
|
|
1347
|
+
clearTimeout(timeoutTimer);
|
|
1348
|
+
cleanup.abort();
|
|
1349
|
+
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
1350
|
+
if (finish.signal.aborted) return;
|
|
1351
|
+
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
1352
|
+
});
|
|
1353
|
+
}, { once: true });
|
|
1354
|
+
child.stdin.on("error", () => void 0);
|
|
1355
|
+
child.stdout.on("data", (chunk) => {
|
|
1356
|
+
const retained = outRetention.retain(chunk, limit);
|
|
1357
|
+
if (retained !== void 0) outChunks.push(retained);
|
|
1358
|
+
});
|
|
1359
|
+
child.stderr.on("data", (chunk) => {
|
|
1360
|
+
const retained = errRetention.retain(chunk, limit);
|
|
1361
|
+
if (retained !== void 0) errChunks.push(retained);
|
|
1362
|
+
});
|
|
1363
|
+
child.once("spawn", () => {
|
|
1364
|
+
spawned = true;
|
|
1365
|
+
});
|
|
1366
|
+
child.once("error", (error) => {
|
|
1367
|
+
if (spawned) return;
|
|
1368
|
+
cause = error;
|
|
1369
|
+
finish.abort();
|
|
1370
|
+
});
|
|
1371
|
+
child.once("close", () => finish.abort());
|
|
1372
|
+
if (text !== void 0) child.stdin.write(text);
|
|
1373
|
+
child.stdin.end();
|
|
1374
|
+
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
1375
|
+
expired = true;
|
|
1376
|
+
terminate.abort();
|
|
1377
|
+
}, timeout);
|
|
1378
|
+
if (optionSignal !== void 0) {
|
|
1379
|
+
optionSignal.addEventListener("abort", () => {
|
|
1380
|
+
aborted = true;
|
|
1381
|
+
terminate.abort();
|
|
1382
|
+
}, {
|
|
1383
|
+
once: true,
|
|
1384
|
+
signal: cleanup.signal
|
|
1385
|
+
});
|
|
1386
|
+
if (optionSignal.aborted) {
|
|
1387
|
+
aborted = true;
|
|
1388
|
+
terminate.abort();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const result = await settled.promise;
|
|
1392
|
+
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, cause);
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region src/server/execution/executeSync.ts
|
|
1397
|
+
/**
|
|
1398
|
+
* Runs one command to completion synchronously, buffering its output, and returns the outcome.
|
|
1399
|
+
*
|
|
1400
|
+
* @remarks
|
|
1401
|
+
* The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
|
|
1402
|
+
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
1403
|
+
* contract carries neither. A positive `timeout` ends only the root process and can leave
|
|
1404
|
+
* descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
|
|
1405
|
+
* timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
|
|
1406
|
+
* and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
|
|
1407
|
+
* reports `truncated` without failing. The environment and input follow the same merge as
|
|
1408
|
+
* {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
|
|
1409
|
+
* carrying the {@link ExecuteResult}.
|
|
1410
|
+
*
|
|
1411
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
1412
|
+
* @param options - Working directory, timeout, capture limit, and failure delivery
|
|
1413
|
+
* @returns The run outcome
|
|
1414
|
+
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```ts
|
|
1418
|
+
* const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
|
|
1419
|
+
* ```
|
|
1420
|
+
*/
|
|
1421
|
+
function executeSync(command, options) {
|
|
1422
|
+
const snapshot = snapshotCommand(command);
|
|
1423
|
+
const optionEnvironment = options?.environment;
|
|
1424
|
+
const optionWorkspace = options?.workspace;
|
|
1425
|
+
const optionInput = options?.input;
|
|
1426
|
+
const optionTimeout = options?.timeout;
|
|
1427
|
+
const optionStrict = options?.strict;
|
|
1428
|
+
const optionLimit = options?.limit;
|
|
1429
|
+
validateCommand(snapshot);
|
|
1430
|
+
validateEnvironment(optionEnvironment);
|
|
1431
|
+
validateWorkspace(optionWorkspace);
|
|
1432
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
1433
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
1434
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
1435
|
+
const timeout = optionTimeout ?? 0;
|
|
1436
|
+
const strict = optionStrict ?? true;
|
|
1437
|
+
const text = optionInput ?? snapshot.input;
|
|
1438
|
+
const line = formatCommand(snapshot);
|
|
1439
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1440
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
1441
|
+
const plan = buildSpawn(snapshot, {
|
|
1442
|
+
workspace,
|
|
1443
|
+
environment
|
|
1444
|
+
});
|
|
1445
|
+
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
1446
|
+
cwd: workspace,
|
|
1447
|
+
env: environment,
|
|
1448
|
+
encoding: "buffer",
|
|
1449
|
+
maxBuffer: limit,
|
|
1450
|
+
killSignal: "SIGKILL",
|
|
1451
|
+
windowsHide: true,
|
|
1452
|
+
windowsVerbatimArguments: plan.verbatim,
|
|
1453
|
+
...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
|
|
1454
|
+
...timeout > 0 ? { timeout } : {}
|
|
1455
|
+
});
|
|
1456
|
+
const error = outcome.error;
|
|
1457
|
+
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
1458
|
+
const result = buildExecuteResult({
|
|
1459
|
+
command: line,
|
|
1460
|
+
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
1461
|
+
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
1462
|
+
code: outcome.status,
|
|
1463
|
+
signal: outcome.signal,
|
|
1464
|
+
expired: fault === "ETIMEDOUT",
|
|
1465
|
+
aborted: false,
|
|
1466
|
+
truncated: fault === "ENOBUFS",
|
|
1467
|
+
limit,
|
|
1468
|
+
...error === void 0 ? {} : { cause: error }
|
|
1469
|
+
});
|
|
1470
|
+
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
//#endregion
|
|
1474
|
+
//#region src/server/execution/detach.ts
|
|
1475
|
+
/**
|
|
1476
|
+
* Spawns one command as a detached process and returns without waiting for it.
|
|
1477
|
+
*
|
|
1478
|
+
* @remarks
|
|
1479
|
+
* The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
|
|
1480
|
+
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
1481
|
+
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
1482
|
+
* is spawned.
|
|
1483
|
+
*
|
|
1484
|
+
* @param command - The executable, arguments, and optional environment
|
|
1485
|
+
* @param options - The working directory the detached child starts in
|
|
1486
|
+
* @returns Nothing
|
|
1487
|
+
* @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
|
|
1488
|
+
*
|
|
1489
|
+
* @example
|
|
1490
|
+
* ```ts
|
|
1491
|
+
* detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
|
|
1492
|
+
* ```
|
|
1493
|
+
*/
|
|
1494
|
+
function detach(command, options) {
|
|
1495
|
+
const snapshot = snapshotCommand(command);
|
|
1496
|
+
const optionWorkspace = options?.workspace;
|
|
1497
|
+
validateCommand(snapshot);
|
|
1498
|
+
validateWorkspace(optionWorkspace);
|
|
1499
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1500
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
|
|
1501
|
+
const plan = buildSpawn(snapshot, {
|
|
1502
|
+
workspace,
|
|
1503
|
+
environment
|
|
1504
|
+
});
|
|
1505
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1506
|
+
cwd: workspace,
|
|
1507
|
+
detached: true,
|
|
1508
|
+
env: environment,
|
|
1509
|
+
stdio: "ignore",
|
|
1510
|
+
windowsHide: true,
|
|
1511
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1512
|
+
});
|
|
1513
|
+
child.once("error", () => void 0);
|
|
1514
|
+
child.unref();
|
|
1515
|
+
}
|
|
1516
|
+
//#endregion
|
|
706
1517
|
exports.Process = Process;
|
|
707
1518
|
exports.ProcessManager = ProcessManager;
|
|
708
|
-
exports.
|
|
709
|
-
exports.
|
|
1519
|
+
exports.Retention = Retention;
|
|
1520
|
+
exports.buildExecutableCandidates = buildExecutableCandidates;
|
|
1521
|
+
exports.buildExecuteResult = buildExecuteResult;
|
|
1522
|
+
exports.buildPlatformSpawn = buildPlatformSpawn;
|
|
1523
|
+
exports.buildSpawn = buildSpawn;
|
|
710
1524
|
exports.createProcess = createProcess;
|
|
711
1525
|
exports.createProcessManager = createProcessManager;
|
|
1526
|
+
exports.detach = detach;
|
|
1527
|
+
exports.execute = execute;
|
|
1528
|
+
exports.executeSync = executeSync;
|
|
1529
|
+
exports.formatCommand = formatCommand;
|
|
1530
|
+
exports.isExited = isExited;
|
|
1531
|
+
exports.isFile = isFile;
|
|
712
1532
|
exports.killProcess = killProcess;
|
|
1533
|
+
exports.killTree = killTree;
|
|
713
1534
|
exports.mergeEnvironment = mergeEnvironment;
|
|
714
|
-
exports.
|
|
715
|
-
exports.
|
|
716
|
-
exports.
|
|
1535
|
+
exports.mergePlatformEnvironment = mergePlatformEnvironment;
|
|
1536
|
+
exports.quoteArgument = quoteArgument;
|
|
1537
|
+
exports.readPlatformVariable = readPlatformVariable;
|
|
1538
|
+
exports.readVariable = readVariable;
|
|
1539
|
+
exports.resolveExecutable = resolveExecutable;
|
|
1540
|
+
exports.snapshotCommand = snapshotCommand;
|
|
1541
|
+
exports.stopChild = stopChild;
|
|
717
1542
|
exports.trimHead = trimHead;
|
|
718
1543
|
exports.trimTail = trimTail;
|
|
1544
|
+
exports.validateBytes = validateBytes;
|
|
1545
|
+
exports.validateCommand = validateCommand;
|
|
1546
|
+
exports.validateEnvironment = validateEnvironment;
|
|
1547
|
+
exports.validateText = validateText;
|
|
1548
|
+
exports.validateTimer = validateTimer;
|
|
1549
|
+
exports.validateWorkspace = validateWorkspace;
|
|
1550
|
+
exports.waitForExit = waitForExit;
|
|
719
1551
|
|
|
720
1552
|
//# sourceMappingURL=index.cjs.map
|