@orkestrel/process 0.0.3 → 0.0.5
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 +19 -17
- package/dist/src/core/index.cjs +32 -6
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +228 -160
- package/dist/src/core/index.d.ts +228 -160
- package/dist/src/core/index.js +32 -7
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +660 -383
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +287 -136
- package/dist/src/server/index.d.ts +287 -136
- package/dist/src/server/index.js +654 -382
- package/dist/src/server/index.js.map +1 -1
- package/package.json +25 -13
|
@@ -3,6 +3,7 @@ let node_buffer = require("node:buffer");
|
|
|
3
3
|
let node_child_process = require("node:child_process");
|
|
4
4
|
let node_fs = require("node:fs");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
|
+
let node_process = require("node:process");
|
|
6
7
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
7
8
|
let _src_core = require("../core/index.cjs");
|
|
8
9
|
let node_readline = require("node:readline");
|
|
@@ -66,10 +67,44 @@ function trimHead(bytes, limit) {
|
|
|
66
67
|
return node_buffer.Buffer.from(buffer.subarray(0, end));
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
70
|
+
* Takes one owned frozen snapshot of a caller's command.
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
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`.
|
|
79
|
+
*
|
|
80
|
+
* @param command - The caller's command, whose properties may be getters
|
|
81
|
+
* @returns A frozen command carrying the values read at this instant
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
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
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
69
104
|
* Renders one command into its diagnostic command line.
|
|
70
105
|
*
|
|
71
106
|
* @param command - The executable and its argument vector
|
|
72
|
-
* @returns The space-joined command line, for
|
|
107
|
+
* @returns The space-joined command line, for an {@link ExecuteResult} and error messages
|
|
73
108
|
*
|
|
74
109
|
* @example
|
|
75
110
|
* ```ts
|
|
@@ -80,36 +115,54 @@ function formatCommand(command) {
|
|
|
80
115
|
return [command.file, ...command.arguments].join(" ");
|
|
81
116
|
}
|
|
82
117
|
/**
|
|
83
|
-
* Reads one environment variable
|
|
118
|
+
* Reads one environment variable under an explicit platform's key rules.
|
|
84
119
|
*
|
|
85
120
|
* @remarks
|
|
86
121
|
* Windows environment keys are case-insensitive, so a merged record can hold `Path` where the
|
|
87
|
-
* caller asks for `PATH`. The exact key is tried first and a folded scan runs only
|
|
122
|
+
* caller asks for `PATH`. The exact key is tried first and a folded scan runs only for `win32`.
|
|
88
123
|
*
|
|
89
124
|
* @param environment - The environment record to read
|
|
90
125
|
* @param name - The variable name
|
|
126
|
+
* @param platform - The platform whose key rules apply
|
|
91
127
|
* @returns The value, or `undefined` when the environment declares no such variable
|
|
92
128
|
*
|
|
93
129
|
* @example
|
|
94
130
|
* ```ts
|
|
95
|
-
*
|
|
131
|
+
* readPlatformVariable({ Path: 'C:\\Windows' }, 'PATH', 'win32') // 'C:\\Windows'
|
|
96
132
|
* ```
|
|
97
133
|
*/
|
|
98
|
-
function
|
|
134
|
+
function readPlatformVariable(environment, name, platform) {
|
|
99
135
|
const direct = environment[name];
|
|
100
136
|
if (direct !== void 0) return direct;
|
|
101
|
-
if (
|
|
137
|
+
if (platform !== "win32") return void 0;
|
|
102
138
|
const target = name.toUpperCase();
|
|
103
139
|
for (const [key, value] of Object.entries(environment)) if (key.toUpperCase() === target) return value;
|
|
104
140
|
}
|
|
105
141
|
/**
|
|
106
|
-
*
|
|
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.
|
|
107
158
|
*
|
|
108
159
|
* @remarks
|
|
109
|
-
* Later maps override earlier ones and an `undefined` value unsets a key.
|
|
160
|
+
* Later maps override earlier ones and an `undefined` value unsets a key. For `win32` the keys fold
|
|
110
161
|
* case-insensitively and the last writer wins, so `PATH` followed by `Path` yields one variable
|
|
111
162
|
* rather than two the host would resolve unpredictably.
|
|
112
163
|
*
|
|
164
|
+
* @param platform - The platform whose key rules apply
|
|
165
|
+
* @param parent - The parent environment layer
|
|
113
166
|
* @param isolated - If `true`, the parent environment is excluded; if `false`, the overrides layer over it
|
|
114
167
|
* @param base - The command's own environment overrides
|
|
115
168
|
* @param override - Per-invocation overrides applied last
|
|
@@ -117,13 +170,13 @@ function readVariable(environment, name) {
|
|
|
117
170
|
*
|
|
118
171
|
* @example
|
|
119
172
|
* ```ts
|
|
120
|
-
*
|
|
173
|
+
* mergePlatformEnvironment('linux', {}, false, { TOKEN: 'a' }, { TOKEN: undefined }) // TOKEN unset
|
|
121
174
|
* ```
|
|
122
175
|
*/
|
|
123
|
-
function
|
|
124
|
-
const folded =
|
|
176
|
+
function mergePlatformEnvironment(platform, parent, isolated, base, override) {
|
|
177
|
+
const folded = platform === "win32";
|
|
125
178
|
const layers = [
|
|
126
|
-
isolated ? {} :
|
|
179
|
+
isolated ? {} : parent,
|
|
127
180
|
base ?? {},
|
|
128
181
|
override ?? {}
|
|
129
182
|
];
|
|
@@ -141,6 +194,22 @@ function mergeEnvironment(isolated, base, override) {
|
|
|
141
194
|
return merged;
|
|
142
195
|
}
|
|
143
196
|
/**
|
|
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
|
+
/**
|
|
144
213
|
* Checks whether a path names a regular file.
|
|
145
214
|
*
|
|
146
215
|
* @remarks
|
|
@@ -159,18 +228,44 @@ function isFile(target) {
|
|
|
159
228
|
return (0, _orkestrel_contract.holds)(() => (0, node_fs.statSync)(target, { throwIfNoEntry: false })?.isFile() === true);
|
|
160
229
|
}
|
|
161
230
|
/**
|
|
162
|
-
*
|
|
231
|
+
* Builds the executable candidates an explicit platform would search.
|
|
163
232
|
*
|
|
164
233
|
* @remarks
|
|
165
|
-
* Windows alone needs this: the host searches the working directory before `PATH` and applies
|
|
234
|
+
* Windows alone needs this list: the host searches the working directory before `PATH` and applies
|
|
166
235
|
* `PATHEXT`, and Node reproduces neither for a direct spawn. Within each searched directory the
|
|
167
236
|
* literal name is tried first and each `PATHEXT` candidate after it, whether or not the name already
|
|
168
237
|
* carries an extension — so `report.txt` resolves to a `report.txt` file where one exists and to
|
|
169
238
|
* `report.txt.cmd` where none does. The lookup reads the child's effective environment, so an
|
|
170
239
|
* overridden `PATH` selects the executable the child would have found. A POSIX host resolves the
|
|
171
|
-
* file itself, so the
|
|
240
|
+
* file itself, so the candidate list there is empty. An appended extension is spelled the way
|
|
172
241
|
* `PATHEXT` spells it rather than the way the directory entry does, which the case-insensitive host
|
|
173
|
-
* treats as the same file.
|
|
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.
|
|
174
269
|
*
|
|
175
270
|
* @param file - The command executable name or path
|
|
176
271
|
* @param options - The directory searched first and the child's effective environment
|
|
@@ -182,18 +277,9 @@ function isFile(target) {
|
|
|
182
277
|
* ```
|
|
183
278
|
*/
|
|
184
279
|
function resolveExecutable(file, options) {
|
|
185
|
-
if (process.platform !== "win32") return void 0;
|
|
186
280
|
const environment = options?.environment ?? process.env;
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const directories = file.includes("/") || file.includes("\\") ? [workspace] : [workspace, ...(readVariable(environment, "PATH") ?? "").split(node_path.delimiter)];
|
|
190
|
-
for (const directory of directories) {
|
|
191
|
-
if (directory.length === 0) continue;
|
|
192
|
-
for (const candidate of candidates) {
|
|
193
|
-
const target = (0, node_path.resolve)(directory, candidate);
|
|
194
|
-
if (isFile(target)) return target;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
281
|
+
const candidates = buildExecutableCandidates(file, options?.workspace ?? process.cwd(), environment, node_process.platform);
|
|
282
|
+
for (const target of candidates) if (isFile(target)) return target;
|
|
197
283
|
}
|
|
198
284
|
/**
|
|
199
285
|
* Quotes one command-line token for a `cmd.exe` command line.
|
|
@@ -201,8 +287,9 @@ function resolveExecutable(file, options) {
|
|
|
201
287
|
* @remarks
|
|
202
288
|
* A token carrying whitespace or a shell metacharacter is wrapped in double quotes so `cmd.exe`
|
|
203
289
|
* passes it as one literal argument; an embedded double quote is doubled, which is how `cmd.exe`
|
|
204
|
-
* reads a literal quote inside a quoted token.
|
|
205
|
-
*
|
|
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.
|
|
206
293
|
*
|
|
207
294
|
* @param value - The token to quote
|
|
208
295
|
* @returns The token, quoted only when `cmd.exe` would otherwise split or interpret it
|
|
@@ -217,41 +304,41 @@ function quoteArgument(value) {
|
|
|
217
304
|
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
218
305
|
}
|
|
219
306
|
/**
|
|
220
|
-
* Builds
|
|
307
|
+
* Builds a spawn form from a resolved file and an explicit platform.
|
|
221
308
|
*
|
|
222
309
|
* @remarks
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
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.
|
|
231
318
|
*
|
|
232
319
|
* @param command - The executable and its argument vector
|
|
233
|
-
* @param
|
|
320
|
+
* @param file - The resolved executable file
|
|
321
|
+
* @param environment - The child's effective environment
|
|
322
|
+
* @param platform - The platform whose batch rules apply
|
|
234
323
|
* @returns The file, argument vector, and verbatim flag to spawn with
|
|
235
|
-
* @throws A {@link ProcessError} coded `invalid` when
|
|
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
|
|
236
325
|
*
|
|
237
326
|
* @example
|
|
238
327
|
* ```ts
|
|
239
|
-
*
|
|
328
|
+
* buildPlatformSpawn({ file: 'node', arguments: ['--version'] }, 'node', {}, 'linux').verbatim // false
|
|
240
329
|
* ```
|
|
241
330
|
*/
|
|
242
|
-
function
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
if (!(process.platform === "win32" && (extension === ".cmd" || extension === ".bat"))) return Object.freeze({
|
|
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({
|
|
246
334
|
file,
|
|
247
335
|
arguments: Object.freeze([...command.arguments]),
|
|
248
336
|
verbatim: false
|
|
249
337
|
});
|
|
250
338
|
for (const argument of command.arguments) if (argument.includes("%")) throw (0, _src_core.createInvalidError)("command argument", argument);
|
|
251
339
|
const line = [`"${file}"`, ...command.arguments.map(quoteArgument)].join(" ");
|
|
252
|
-
const environment = options?.environment ?? process.env;
|
|
253
340
|
return Object.freeze({
|
|
254
|
-
file:
|
|
341
|
+
file: readPlatformVariable(environment, "ComSpec", platform) ?? "cmd.exe",
|
|
255
342
|
arguments: Object.freeze([
|
|
256
343
|
"/d",
|
|
257
344
|
"/s",
|
|
@@ -262,6 +349,22 @@ function buildSpawn(command, options) {
|
|
|
262
349
|
});
|
|
263
350
|
}
|
|
264
351
|
/**
|
|
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
|
+
/**
|
|
265
368
|
* Validates one spawn-bound string.
|
|
266
369
|
*
|
|
267
370
|
* @remarks
|
|
@@ -379,40 +482,6 @@ function validateWorkspace(workspace) {
|
|
|
379
482
|
validateText(workspace, "workspace", true);
|
|
380
483
|
}
|
|
381
484
|
/**
|
|
382
|
-
* Retains one delivered chunk up to a byte limit and records what the stream delivered.
|
|
383
|
-
*
|
|
384
|
-
* @remarks
|
|
385
|
-
* The retained head is kept as chunk slices and joined once, so a long stream never repeatedly
|
|
386
|
-
* concatenates a growing buffer. `counts` is a two-slot tally: slot `0` counts every delivered byte
|
|
387
|
-
* and slot `1` counts the retained bytes, so a caller compares slot `0` against `limit` to learn
|
|
388
|
-
* whether the capture was truncated.
|
|
389
|
-
*
|
|
390
|
-
* @param chunk - The delivered chunk, ignored when it is not a buffer
|
|
391
|
-
* @param chunks - The retained head, appended to in place
|
|
392
|
-
* @param counts - The `[delivered, retained]` tally, updated in place
|
|
393
|
-
* @param limit - The maximum retained byte count
|
|
394
|
-
* @returns Nothing
|
|
395
|
-
*
|
|
396
|
-
* @example
|
|
397
|
-
* ```ts
|
|
398
|
-
* const chunks: Buffer[] = []
|
|
399
|
-
* const counts = [0, 0]
|
|
400
|
-
* retainChunk(Buffer.from('hello'), chunks, counts, 3)
|
|
401
|
-
* counts[1] // 3
|
|
402
|
-
* ```
|
|
403
|
-
*/
|
|
404
|
-
function retainChunk(chunk, chunks, counts, limit) {
|
|
405
|
-
if (!node_buffer.Buffer.isBuffer(chunk)) return;
|
|
406
|
-
const delivered = counts[0] ?? 0;
|
|
407
|
-
const retained = counts[1] ?? 0;
|
|
408
|
-
counts[0] = delivered + chunk.byteLength;
|
|
409
|
-
const room = limit - retained;
|
|
410
|
-
if (room <= 0) return;
|
|
411
|
-
const slice = chunk.byteLength <= room ? chunk : node_buffer.Buffer.from(chunk.subarray(0, room));
|
|
412
|
-
chunks.push(slice);
|
|
413
|
-
counts[1] = retained + slice.byteLength;
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
485
|
* Checks whether a child process has reached its native exit.
|
|
417
486
|
*
|
|
418
487
|
* @remarks
|
|
@@ -436,10 +505,11 @@ function isExited(child) {
|
|
|
436
505
|
*
|
|
437
506
|
* @remarks
|
|
438
507
|
* On a POSIX host a child spawned `detached` leads its own process group, so signalling the
|
|
439
|
-
* negated pid reaches the whole tree.
|
|
440
|
-
* the child directly.
|
|
441
|
-
*
|
|
442
|
-
*
|
|
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.
|
|
443
513
|
*
|
|
444
514
|
* @param child - The child boundary
|
|
445
515
|
* @param signal - The signal to deliver
|
|
@@ -457,7 +527,12 @@ function killProcess(child, signal) {
|
|
|
457
527
|
return;
|
|
458
528
|
}
|
|
459
529
|
process.kill(-child.pid, signal);
|
|
460
|
-
} catch {
|
|
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
|
+
}
|
|
461
536
|
}
|
|
462
537
|
/**
|
|
463
538
|
* Kills one Windows process tree through `taskkill`.
|
|
@@ -510,24 +585,25 @@ function killTree(pid, timeout) {
|
|
|
510
585
|
function waitForExit(child, timeout) {
|
|
511
586
|
if (isExited(child)) return Promise.resolve();
|
|
512
587
|
const settled = Promise.withResolvers();
|
|
513
|
-
const timer = setTimeout(
|
|
514
|
-
|
|
515
|
-
clearTimeout(timer);
|
|
588
|
+
const timer = setTimeout(() => {
|
|
589
|
+
child.off("exit", settled.resolve);
|
|
516
590
|
settled.resolve();
|
|
517
|
-
});
|
|
518
|
-
|
|
591
|
+
}, timeout);
|
|
592
|
+
child.once("exit", settled.resolve);
|
|
593
|
+
return settled.promise.finally(() => clearTimeout(timer));
|
|
519
594
|
}
|
|
520
595
|
/**
|
|
521
596
|
* Terminates one child process tree and reports whether its native exit was observed.
|
|
522
597
|
*
|
|
523
598
|
* @remarks
|
|
524
|
-
* Windows ends the tree at once through {@link killTree}, falling back to a direct kill
|
|
525
|
-
* utility fails, because the host has no cooperative termination to offer. A POSIX host signals
|
|
526
|
-
* process group `SIGTERM`,
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
* the
|
|
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.
|
|
531
607
|
*
|
|
532
608
|
* @param child - The child boundary
|
|
533
609
|
* @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
|
|
@@ -553,12 +629,12 @@ async function stopChild(child, grace, confirm) {
|
|
|
553
629
|
return isExited(child);
|
|
554
630
|
}
|
|
555
631
|
/**
|
|
556
|
-
* Builds one settled {@link
|
|
632
|
+
* Builds one settled {@link ExecuteResult} from a completed run's captured bytes and terminal facts.
|
|
557
633
|
*
|
|
558
634
|
* @remarks
|
|
559
635
|
* `failed` is derived: a run failed when it timed out, was aborted, ended on a host fault, was ended
|
|
560
636
|
* by a signal, or exited with a code other than `0` — a `null` code from a spawn fault is therefore
|
|
561
|
-
* a failure.
|
|
637
|
+
* a failure. Each byte field is bounded by `limit` on a code-point boundary here, which is the one
|
|
562
638
|
* place the captured bytes are decoded.
|
|
563
639
|
*
|
|
564
640
|
* @param input - The captured bytes, terminal state, and capture limit
|
|
@@ -566,7 +642,7 @@ async function stopChild(child, grace, confirm) {
|
|
|
566
642
|
*
|
|
567
643
|
* @example
|
|
568
644
|
* ```ts
|
|
569
|
-
*
|
|
645
|
+
* buildExecuteResult({
|
|
570
646
|
* command: 'git status',
|
|
571
647
|
* stdout: Buffer.from('ok'),
|
|
572
648
|
* stderr: Buffer.alloc(0),
|
|
@@ -579,7 +655,7 @@ async function stopChild(child, grace, confirm) {
|
|
|
579
655
|
* }).failed // false
|
|
580
656
|
* ```
|
|
581
657
|
*/
|
|
582
|
-
function
|
|
658
|
+
function buildExecuteResult(input) {
|
|
583
659
|
return Object.freeze({
|
|
584
660
|
command: input.command,
|
|
585
661
|
stdout: trimHead(input.stdout, input.limit).toString("utf8"),
|
|
@@ -592,247 +668,6 @@ function buildRunResult(input) {
|
|
|
592
668
|
truncated: input.truncated
|
|
593
669
|
});
|
|
594
670
|
}
|
|
595
|
-
/**
|
|
596
|
-
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
597
|
-
*
|
|
598
|
-
* @remarks
|
|
599
|
-
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
600
|
-
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
601
|
-
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
602
|
-
* whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
|
|
603
|
-
* child through the same bounded stop, and only the first of them is recorded: a timeout reports
|
|
604
|
-
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
605
|
-
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
606
|
-
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
607
|
-
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
608
|
-
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
609
|
-
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
610
|
-
* overrides `command.input`. Unless `strict` is `false`, a failed run rejects with a
|
|
611
|
-
* {@link createRunError} carrying the {@link RunResult}. An invalid option or command string rejects
|
|
612
|
-
* before the child is spawned, because an async function cannot throw synchronously.
|
|
613
|
-
*
|
|
614
|
-
* @param command - The executable, arguments, and optional environment and input
|
|
615
|
-
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
616
|
-
* @returns The settled run outcome
|
|
617
|
-
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link RunResult} when the run failed and `strict` is not `false`
|
|
618
|
-
*
|
|
619
|
-
* @example
|
|
620
|
-
* ```ts
|
|
621
|
-
* const result = await run({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
622
|
-
* ```
|
|
623
|
-
*/
|
|
624
|
-
async function run(command, options) {
|
|
625
|
-
validateCommand(command);
|
|
626
|
-
validateEnvironment(options?.environment);
|
|
627
|
-
validateWorkspace(options?.workspace);
|
|
628
|
-
validateTimer(options?.timeout, "option 'timeout'");
|
|
629
|
-
validateTimer(options?.grace, "option 'grace'");
|
|
630
|
-
validateBytes(options?.limit, "option 'limit'", 0);
|
|
631
|
-
const limit = options?.limit ?? _src_core.PROCESS_OUTPUT;
|
|
632
|
-
const grace = options?.grace ?? _src_core.PROCESS_GRACE;
|
|
633
|
-
const timeout = options?.timeout ?? 0;
|
|
634
|
-
const strict = options?.strict ?? true;
|
|
635
|
-
const text = options?.input ?? command.input;
|
|
636
|
-
const line = formatCommand(command);
|
|
637
|
-
const workspace = options?.workspace ?? process.cwd();
|
|
638
|
-
const environment = mergeEnvironment(command.isolated === true, command.environment, options?.environment);
|
|
639
|
-
const plan = buildSpawn(command, {
|
|
640
|
-
workspace,
|
|
641
|
-
environment
|
|
642
|
-
});
|
|
643
|
-
const settled = Promise.withResolvers();
|
|
644
|
-
const terminate = new AbortController();
|
|
645
|
-
const cleanup = new AbortController();
|
|
646
|
-
const finish = new AbortController();
|
|
647
|
-
const outChunks = [];
|
|
648
|
-
const errChunks = [];
|
|
649
|
-
const outCounts = [0, 0];
|
|
650
|
-
const errCounts = [0, 0];
|
|
651
|
-
let spawned = false;
|
|
652
|
-
let expired = false;
|
|
653
|
-
let aborted = false;
|
|
654
|
-
let cause;
|
|
655
|
-
let timeoutTimer;
|
|
656
|
-
let confirmTimer;
|
|
657
|
-
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
658
|
-
cwd: workspace,
|
|
659
|
-
detached: process.platform !== "win32",
|
|
660
|
-
env: environment,
|
|
661
|
-
stdio: [
|
|
662
|
-
"pipe",
|
|
663
|
-
"pipe",
|
|
664
|
-
"pipe"
|
|
665
|
-
],
|
|
666
|
-
windowsHide: true,
|
|
667
|
-
windowsVerbatimArguments: plan.verbatim
|
|
668
|
-
});
|
|
669
|
-
finish.signal.addEventListener("abort", () => {
|
|
670
|
-
clearTimeout(timeoutTimer);
|
|
671
|
-
clearTimeout(confirmTimer);
|
|
672
|
-
cleanup.abort();
|
|
673
|
-
settled.resolve(buildRunResult({
|
|
674
|
-
command: line,
|
|
675
|
-
stdout: node_buffer.Buffer.concat(outChunks),
|
|
676
|
-
stderr: node_buffer.Buffer.concat(errChunks),
|
|
677
|
-
code: child.exitCode,
|
|
678
|
-
signal: child.signalCode,
|
|
679
|
-
expired,
|
|
680
|
-
aborted,
|
|
681
|
-
truncated: (outCounts[0] ?? 0) > limit || (errCounts[0] ?? 0) > limit,
|
|
682
|
-
limit,
|
|
683
|
-
...cause === void 0 ? {} : { cause }
|
|
684
|
-
}));
|
|
685
|
-
}, { once: true });
|
|
686
|
-
terminate.signal.addEventListener("abort", () => {
|
|
687
|
-
clearTimeout(timeoutTimer);
|
|
688
|
-
cleanup.abort();
|
|
689
|
-
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
690
|
-
if (finish.signal.aborted) return;
|
|
691
|
-
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
692
|
-
});
|
|
693
|
-
}, { once: true });
|
|
694
|
-
child.stdin.on("error", () => void 0);
|
|
695
|
-
child.stdout.on("data", (chunk) => retainChunk(chunk, outChunks, outCounts, limit));
|
|
696
|
-
child.stderr.on("data", (chunk) => retainChunk(chunk, errChunks, errCounts, limit));
|
|
697
|
-
child.once("spawn", () => {
|
|
698
|
-
spawned = true;
|
|
699
|
-
});
|
|
700
|
-
child.once("error", (error) => {
|
|
701
|
-
if (spawned) return;
|
|
702
|
-
cause = error;
|
|
703
|
-
finish.abort();
|
|
704
|
-
});
|
|
705
|
-
child.once("close", () => finish.abort());
|
|
706
|
-
if (text !== void 0) child.stdin.write(text);
|
|
707
|
-
child.stdin.end();
|
|
708
|
-
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
709
|
-
expired = true;
|
|
710
|
-
terminate.abort();
|
|
711
|
-
}, timeout);
|
|
712
|
-
if (options?.signal !== void 0) {
|
|
713
|
-
const signal = options.signal;
|
|
714
|
-
signal.addEventListener("abort", () => {
|
|
715
|
-
aborted = true;
|
|
716
|
-
terminate.abort();
|
|
717
|
-
}, {
|
|
718
|
-
once: true,
|
|
719
|
-
signal: cleanup.signal
|
|
720
|
-
});
|
|
721
|
-
if (signal.aborted) {
|
|
722
|
-
aborted = true;
|
|
723
|
-
terminate.abort();
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const result = await settled.promise;
|
|
727
|
-
if (result.failed && strict) throw (0, _src_core.createRunError)(result, cause);
|
|
728
|
-
return result;
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Runs one command to completion synchronously, buffering its output, and returns the outcome.
|
|
732
|
-
*
|
|
733
|
-
* @remarks
|
|
734
|
-
* The synchronous counterpart of {@link run}, spawned through the same resolver and never through a
|
|
735
|
-
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
736
|
-
* contract carries neither. A positive `timeout` and an output overflow both end the child with
|
|
737
|
-
* `SIGKILL`: an overflow reports `truncated` and `failed` together and trims the partial output to
|
|
738
|
-
* `limit`, where {@link run} keeps reading and reports `truncated` without failing. The environment
|
|
739
|
-
* and input follow the same merge as {@link run}. Unless `strict` is `false`, a failed run throws a
|
|
740
|
-
* {@link createRunError} carrying the {@link RunResult}.
|
|
741
|
-
*
|
|
742
|
-
* @param command - The executable, arguments, and optional environment and input
|
|
743
|
-
* @param options - Working directory, timeout, capture limit, and failure delivery
|
|
744
|
-
* @returns The run outcome
|
|
745
|
-
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link RunResult} when the run failed and `strict` is not `false`
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* ```ts
|
|
749
|
-
* const result = runSync({ file: 'git', arguments: ['--version'] }, { strict: false })
|
|
750
|
-
* ```
|
|
751
|
-
*/
|
|
752
|
-
function runSync(command, options) {
|
|
753
|
-
validateCommand(command);
|
|
754
|
-
validateEnvironment(options?.environment);
|
|
755
|
-
validateWorkspace(options?.workspace);
|
|
756
|
-
validateTimer(options?.timeout, "option 'timeout'");
|
|
757
|
-
validateBytes(options?.limit, "option 'limit'", 0);
|
|
758
|
-
const limit = options?.limit ?? _src_core.PROCESS_OUTPUT;
|
|
759
|
-
const timeout = options?.timeout ?? 0;
|
|
760
|
-
const strict = options?.strict ?? true;
|
|
761
|
-
const text = options?.input ?? command.input;
|
|
762
|
-
const line = formatCommand(command);
|
|
763
|
-
const workspace = options?.workspace ?? process.cwd();
|
|
764
|
-
const environment = mergeEnvironment(command.isolated === true, command.environment, options?.environment);
|
|
765
|
-
const plan = buildSpawn(command, {
|
|
766
|
-
workspace,
|
|
767
|
-
environment
|
|
768
|
-
});
|
|
769
|
-
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
770
|
-
cwd: workspace,
|
|
771
|
-
env: environment,
|
|
772
|
-
encoding: "buffer",
|
|
773
|
-
maxBuffer: limit,
|
|
774
|
-
killSignal: "SIGKILL",
|
|
775
|
-
windowsHide: true,
|
|
776
|
-
windowsVerbatimArguments: plan.verbatim,
|
|
777
|
-
...text !== void 0 ? { input: text } : {},
|
|
778
|
-
...timeout > 0 ? { timeout } : {}
|
|
779
|
-
});
|
|
780
|
-
const error = outcome.error;
|
|
781
|
-
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
782
|
-
const result = buildRunResult({
|
|
783
|
-
command: line,
|
|
784
|
-
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
785
|
-
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
786
|
-
code: outcome.status,
|
|
787
|
-
signal: outcome.signal,
|
|
788
|
-
expired: fault === "ETIMEDOUT",
|
|
789
|
-
aborted: false,
|
|
790
|
-
truncated: fault === "ENOBUFS",
|
|
791
|
-
limit,
|
|
792
|
-
...error === void 0 ? {} : { cause: error }
|
|
793
|
-
});
|
|
794
|
-
if (result.failed && strict) throw (0, _src_core.createRunError)(result, error);
|
|
795
|
-
return result;
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Spawns one command as a detached process and returns without waiting for it.
|
|
799
|
-
*
|
|
800
|
-
* @remarks
|
|
801
|
-
* The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
|
|
802
|
-
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
803
|
-
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
804
|
-
* is spawned.
|
|
805
|
-
*
|
|
806
|
-
* @param command - The executable, arguments, and optional environment
|
|
807
|
-
* @param options - The working directory the detached child starts in
|
|
808
|
-
* @returns Nothing
|
|
809
|
-
* @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
|
|
810
|
-
*
|
|
811
|
-
* @example
|
|
812
|
-
* ```ts
|
|
813
|
-
* detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
|
|
814
|
-
* ```
|
|
815
|
-
*/
|
|
816
|
-
function detach(command, options) {
|
|
817
|
-
validateCommand(command);
|
|
818
|
-
validateWorkspace(options?.workspace);
|
|
819
|
-
const workspace = options?.workspace ?? process.cwd();
|
|
820
|
-
const environment = mergeEnvironment(command.isolated === true, command.environment);
|
|
821
|
-
const plan = buildSpawn(command, {
|
|
822
|
-
workspace,
|
|
823
|
-
environment
|
|
824
|
-
});
|
|
825
|
-
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
826
|
-
cwd: workspace,
|
|
827
|
-
detached: true,
|
|
828
|
-
env: environment,
|
|
829
|
-
stdio: "ignore",
|
|
830
|
-
windowsHide: true,
|
|
831
|
-
windowsVerbatimArguments: plan.verbatim
|
|
832
|
-
});
|
|
833
|
-
child.once("error", () => void 0);
|
|
834
|
-
child.unref();
|
|
835
|
-
}
|
|
836
671
|
//#endregion
|
|
837
672
|
//#region src/server/Process.ts
|
|
838
673
|
/**
|
|
@@ -840,20 +675,25 @@ function detach(command, options) {
|
|
|
840
675
|
*
|
|
841
676
|
* @remarks
|
|
842
677
|
* Standard output is framed through `readline` into a single-consumer stream: each line goes to
|
|
843
|
-
* exactly one waiting iterator, so
|
|
844
|
-
* it. While no consumer has ever requested an iterator,
|
|
845
|
-
*
|
|
846
|
-
* consumer attaching later receives the retained head, a gap, then the live stream.
|
|
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
|
|
847
682
|
* has been requested, stdout pauses at the mark and resumes at half of it, so that consumer loses
|
|
848
|
-
* nothing and the child feels real backpressure. The mark is soft — the
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
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 a
|
|
688
|
+
* child `error` cause on a spawn fault, a `protocol` {@link ProcessError} on a host-reported stdin
|
|
689
|
+
* fault after the constructor input phase, and the terminal `exit`, alongside the `exit` promise.
|
|
690
|
+
* A fault arising from the constructor-supplied `input` write or its closing `end` stays quiet
|
|
691
|
+
* because the package initiated that sequence. `pid`,
|
|
692
|
+
* `code`, and `signal` read the spawned child directly, so the native exit is readable before the
|
|
693
|
+
* `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
|
|
694
|
+
* exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
|
|
695
|
+
* destroys the emitter last. `destroy` resolving does not imply the child's stdio has closed, so
|
|
696
|
+
* `exit` and `lines` can still be outstanding when a descendant holds an inherited pipe.
|
|
857
697
|
*
|
|
858
698
|
* @example
|
|
859
699
|
* ```ts
|
|
@@ -874,6 +714,7 @@ var Process = class {
|
|
|
874
714
|
#grace;
|
|
875
715
|
#evidence;
|
|
876
716
|
#backlog;
|
|
717
|
+
#delivery;
|
|
877
718
|
#decoder = new node_string_decoder.StringDecoder("utf8");
|
|
878
719
|
#exit = Promise.withResolvers();
|
|
879
720
|
#lines;
|
|
@@ -881,14 +722,18 @@ var Process = class {
|
|
|
881
722
|
#waiters = [];
|
|
882
723
|
#signal;
|
|
883
724
|
#abort;
|
|
725
|
+
#writes = /* @__PURE__ */ new Map();
|
|
884
726
|
#tail = node_buffer.Buffer.alloc(0);
|
|
885
727
|
#head = 0;
|
|
886
728
|
#pending = 0;
|
|
887
729
|
#requested = false;
|
|
888
730
|
#paused = false;
|
|
731
|
+
#truncated = false;
|
|
889
732
|
#terminating = false;
|
|
890
733
|
#closed = false;
|
|
891
734
|
#ended = false;
|
|
735
|
+
#input = 0;
|
|
736
|
+
#failure;
|
|
892
737
|
#stopping;
|
|
893
738
|
#ending;
|
|
894
739
|
/**
|
|
@@ -898,21 +743,22 @@ var Process = class {
|
|
|
898
743
|
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
899
744
|
*/
|
|
900
745
|
constructor(options) {
|
|
901
|
-
const
|
|
746
|
+
const source = options.command;
|
|
902
747
|
const workspace = options.workspace;
|
|
903
748
|
const grace = options.grace;
|
|
904
749
|
const evidence = options.evidence;
|
|
905
750
|
const backlog = options.backlog;
|
|
751
|
+
const delivery = options.delivery;
|
|
906
752
|
const writable = options.writable;
|
|
907
753
|
const signal = options.signal;
|
|
908
754
|
const on = options.on;
|
|
909
755
|
const error = options.error;
|
|
910
|
-
const
|
|
911
|
-
const overrides = command.environment;
|
|
756
|
+
const command = snapshotCommand(source);
|
|
912
757
|
const input = command.input;
|
|
913
758
|
validateCommand(command);
|
|
914
759
|
validateWorkspace(workspace);
|
|
915
760
|
validateTimer(grace, "option 'grace'");
|
|
761
|
+
validateTimer(delivery, "option 'delivery'");
|
|
916
762
|
validateBytes(evidence, "option 'evidence'", 0);
|
|
917
763
|
validateBytes(backlog, "option 'backlog'", 1);
|
|
918
764
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
@@ -922,17 +768,18 @@ var Process = class {
|
|
|
922
768
|
this.#grace = grace ?? _src_core.PROCESS_GRACE;
|
|
923
769
|
this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
|
|
924
770
|
this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
|
|
771
|
+
this.#delivery = delivery ?? 0;
|
|
925
772
|
this.#signal = signal;
|
|
926
773
|
this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
|
|
927
|
-
const
|
|
774
|
+
const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
|
|
928
775
|
const plan = buildSpawn(command, {
|
|
929
776
|
workspace,
|
|
930
|
-
environment
|
|
777
|
+
environment: childEnvironment
|
|
931
778
|
});
|
|
932
779
|
this.#child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
933
780
|
cwd: workspace,
|
|
934
781
|
detached: process.platform !== "win32",
|
|
935
|
-
env:
|
|
782
|
+
env: childEnvironment,
|
|
936
783
|
stdio: [
|
|
937
784
|
"pipe",
|
|
938
785
|
"pipe",
|
|
@@ -949,16 +796,34 @@ var Process = class {
|
|
|
949
796
|
this.#reader.once("close", this.#finish.bind(this));
|
|
950
797
|
this.#child.once("error", (cause) => this.#emitter.emit("error", cause));
|
|
951
798
|
this.#child.once("close", this.#close.bind(this));
|
|
952
|
-
this.#child.stdin.on("error", () =>
|
|
799
|
+
this.#child.stdin.on("error", (cause) => this.#failInputStream(cause));
|
|
953
800
|
this.#child.stderr.on("data", this.#retain.bind(this));
|
|
954
|
-
if (input !== void 0)
|
|
955
|
-
|
|
801
|
+
if (input !== void 0) {
|
|
802
|
+
this.#input += 1;
|
|
803
|
+
this.#child.stdin.write(input, this.#completeInput.bind(this));
|
|
804
|
+
}
|
|
805
|
+
if (writable !== true) {
|
|
806
|
+
this.#input += 1;
|
|
807
|
+
this.#child.stdin.end(this.#completeInput.bind(this));
|
|
808
|
+
}
|
|
956
809
|
if (signal !== void 0) {
|
|
957
810
|
this.#abort = this.#terminate.bind(this);
|
|
958
811
|
signal.addEventListener("abort", this.#abort, { once: true });
|
|
959
812
|
if (signal.aborted) this.stop();
|
|
960
813
|
}
|
|
961
814
|
}
|
|
815
|
+
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
816
|
+
get pid() {
|
|
817
|
+
return this.#child.pid;
|
|
818
|
+
}
|
|
819
|
+
/** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
820
|
+
get code() {
|
|
821
|
+
return this.#child.exitCode;
|
|
822
|
+
}
|
|
823
|
+
/** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
824
|
+
get signal() {
|
|
825
|
+
return this.#child.signalCode;
|
|
826
|
+
}
|
|
962
827
|
/** The typed lifecycle observation surface. */
|
|
963
828
|
get emitter() {
|
|
964
829
|
return this.#emitter;
|
|
@@ -971,6 +836,10 @@ var Process = class {
|
|
|
971
836
|
get evidence() {
|
|
972
837
|
return this.#tail.toString("utf8");
|
|
973
838
|
}
|
|
839
|
+
/** True when the `lines` stream omitted output after a retention bound was reached. */
|
|
840
|
+
get truncated() {
|
|
841
|
+
return this.#truncated;
|
|
842
|
+
}
|
|
974
843
|
/** The terminal child state, observed once from the close event. */
|
|
975
844
|
get exit() {
|
|
976
845
|
return this.#exit.promise;
|
|
@@ -979,24 +848,34 @@ var Process = class {
|
|
|
979
848
|
* Write one line to the open standard-input channel.
|
|
980
849
|
*
|
|
981
850
|
* @remarks
|
|
982
|
-
* Never rejects.
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
851
|
+
* Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
|
|
852
|
+
* prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
|
|
853
|
+
* full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
|
|
854
|
+
* terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
|
|
855
|
+
* 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
|
|
856
|
+
* the write can settle `true` without a callback error or a stream error while the child remains
|
|
857
|
+
* alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
|
|
858
|
+
* that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
|
|
859
|
+
* for bytes the package is about to discard.
|
|
986
860
|
*
|
|
987
861
|
* @param text - The line text without its trailing newline
|
|
988
|
-
* @returns True when the
|
|
862
|
+
* @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
|
|
989
863
|
*/
|
|
990
864
|
send(text) {
|
|
991
865
|
const stdin = this.#child.stdin;
|
|
992
|
-
if (this.#closed || !stdin.writable) return Promise.resolve(false);
|
|
866
|
+
if (this.#closed || this.#terminating || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
|
|
993
867
|
const settled = Promise.withResolvers();
|
|
868
|
+
this.#writes.set(settled, void 0);
|
|
994
869
|
try {
|
|
995
|
-
stdin.write(`${text}\n`, (
|
|
996
|
-
settled.resolve(error === void 0 || error === null);
|
|
997
|
-
});
|
|
870
|
+
stdin.write(`${text}\n`, this.#confirmWrite.bind(this, settled));
|
|
998
871
|
} catch {
|
|
999
|
-
|
|
872
|
+
this.#settleWrite(settled, false);
|
|
873
|
+
return settled.promise;
|
|
874
|
+
}
|
|
875
|
+
if (this.#delivery > 0 && this.#writes.has(settled)) {
|
|
876
|
+
const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
|
|
877
|
+
timer.unref();
|
|
878
|
+
this.#writes.set(settled, timer);
|
|
1000
879
|
}
|
|
1001
880
|
return settled.promise;
|
|
1002
881
|
}
|
|
@@ -1069,7 +948,11 @@ var Process = class {
|
|
|
1069
948
|
return;
|
|
1070
949
|
}
|
|
1071
950
|
const bytes = node_buffer.Buffer.byteLength(line) + 1;
|
|
1072
|
-
|
|
951
|
+
const limit = this.#terminating ? this.#backlog * 2 : this.#backlog;
|
|
952
|
+
if ((!this.#requested || this.#terminating) && this.#pending + bytes > limit) {
|
|
953
|
+
this.#truncated = true;
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
1073
956
|
this.#queue.push(line);
|
|
1074
957
|
this.#pending += bytes;
|
|
1075
958
|
this.#restrain();
|
|
@@ -1107,7 +990,7 @@ var Process = class {
|
|
|
1107
990
|
const suffix = this.#decoder.end();
|
|
1108
991
|
if (suffix.length > 0) this.#emitter.emit("stderr", suffix);
|
|
1109
992
|
this.#closed = true;
|
|
1110
|
-
|
|
993
|
+
this.#removeAbortListener();
|
|
1111
994
|
this.#reader.close();
|
|
1112
995
|
const exit = Object.freeze({
|
|
1113
996
|
code,
|
|
@@ -1119,14 +1002,66 @@ var Process = class {
|
|
|
1119
1002
|
#terminate() {
|
|
1120
1003
|
this.stop();
|
|
1121
1004
|
}
|
|
1005
|
+
#removeAbortListener() {
|
|
1006
|
+
if (this.#signal === void 0 || this.#abort === void 0) return;
|
|
1007
|
+
this.#signal.removeEventListener("abort", this.#abort);
|
|
1008
|
+
}
|
|
1009
|
+
#confirmWrite(settled, error) {
|
|
1010
|
+
if (error === void 0 || error === null) {
|
|
1011
|
+
this.#settleWrite(settled, true);
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
this.#failInputCallback(error);
|
|
1015
|
+
}
|
|
1016
|
+
#completeInput() {
|
|
1017
|
+
this.#input -= 1;
|
|
1018
|
+
}
|
|
1019
|
+
#settleWrite(settled, accepted) {
|
|
1020
|
+
if (!this.#writes.has(settled)) return;
|
|
1021
|
+
const timer = this.#writes.get(settled);
|
|
1022
|
+
this.#writes.delete(settled);
|
|
1023
|
+
clearTimeout(timer);
|
|
1024
|
+
settled.resolve(accepted);
|
|
1025
|
+
}
|
|
1026
|
+
#settleWrites() {
|
|
1027
|
+
for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
|
|
1028
|
+
}
|
|
1029
|
+
#failInputStream(cause) {
|
|
1030
|
+
if (this.#child.stdin.writableEnded || this.#input > 0) {
|
|
1031
|
+
this.#settleWrites();
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
this.#failInputCallback(cause);
|
|
1035
|
+
}
|
|
1036
|
+
#failInputCallback(cause) {
|
|
1037
|
+
if (this.#failure !== void 0 || this.#terminating) return;
|
|
1038
|
+
this.#failure = cause;
|
|
1039
|
+
this.#settleWrites();
|
|
1040
|
+
this.#emitter.emit("error", new _src_core.ProcessError("The standard-input channel failed", {
|
|
1041
|
+
code: "protocol",
|
|
1042
|
+
cause
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
#capBacklog() {
|
|
1046
|
+
const limit = this.#backlog * 2;
|
|
1047
|
+
while (this.#pending > limit && this.#queue.length > this.#head) {
|
|
1048
|
+
const line = this.#queue.pop();
|
|
1049
|
+
if (line === void 0) break;
|
|
1050
|
+
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
1051
|
+
this.#truncated = true;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1122
1054
|
async #kill() {
|
|
1123
1055
|
this.#terminating = true;
|
|
1056
|
+
this.#capBacklog();
|
|
1124
1057
|
this.#relieve();
|
|
1125
1058
|
const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
|
|
1059
|
+
this.#settleWrites();
|
|
1126
1060
|
this.#child.stdin.destroy();
|
|
1127
1061
|
return confirmed;
|
|
1128
1062
|
}
|
|
1129
1063
|
async #teardown() {
|
|
1064
|
+
this.#removeAbortListener();
|
|
1130
1065
|
await this.stop();
|
|
1131
1066
|
this.#emitter.destroy();
|
|
1132
1067
|
}
|
|
@@ -1214,8 +1149,9 @@ var ProcessManager = class {
|
|
|
1214
1149
|
* nothing has started and a throw from one strands no process. A getter that begins `destroy`
|
|
1215
1150
|
* without throwing is the one remaining race, and it leaves a bounded residual: the child is
|
|
1216
1151
|
* already spawned, so the launch is refused with a {@link createProtocolError} and that child is
|
|
1217
|
-
* torn down asynchronously, bounded by `grace` plus the confirmation window. The `
|
|
1218
|
-
*
|
|
1152
|
+
* torn down asynchronously, bounded by `grace` plus the confirmation window. The `protocol`
|
|
1153
|
+
* refusal throws synchronously before the `destroy` barrier settles. The barrier does not cover
|
|
1154
|
+
* that child's asynchronous teardown.
|
|
1219
1155
|
*
|
|
1220
1156
|
* @param id - The registry key, unique among live children
|
|
1221
1157
|
* @param options - The child construction options
|
|
@@ -1329,25 +1265,366 @@ function createProcessManager(options) {
|
|
|
1329
1265
|
return new ProcessManager(options);
|
|
1330
1266
|
}
|
|
1331
1267
|
//#endregion
|
|
1268
|
+
//#region src/server/Retention.ts
|
|
1269
|
+
/**
|
|
1270
|
+
* Accumulates byte totals while retaining a bounded stream head.
|
|
1271
|
+
*
|
|
1272
|
+
* @example
|
|
1273
|
+
* ```ts
|
|
1274
|
+
* const retention = new Retention()
|
|
1275
|
+
* retention.retain(Buffer.from('hello'), 3)?.toString('utf8') // 'hel'
|
|
1276
|
+
* retention.retained // 3
|
|
1277
|
+
* ```
|
|
1278
|
+
*/
|
|
1279
|
+
var Retention = class {
|
|
1280
|
+
#delivered = 0;
|
|
1281
|
+
#retained = 0;
|
|
1282
|
+
/** The bytes delivered by the stream. */
|
|
1283
|
+
get delivered() {
|
|
1284
|
+
return this.#delivered;
|
|
1285
|
+
}
|
|
1286
|
+
/** The bytes retained from the stream head. */
|
|
1287
|
+
get retained() {
|
|
1288
|
+
return this.#retained;
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Retains a delivered chunk within a byte limit.
|
|
1292
|
+
*
|
|
1293
|
+
* @param chunk - The delivered chunk, ignored when it is not a buffer
|
|
1294
|
+
* @param limit - The maximum retained byte count
|
|
1295
|
+
* @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
|
|
1296
|
+
*/
|
|
1297
|
+
retain(chunk, limit) {
|
|
1298
|
+
if (!node_buffer.Buffer.isBuffer(chunk)) return void 0;
|
|
1299
|
+
this.#delivered += chunk.byteLength;
|
|
1300
|
+
const room = limit - this.#retained;
|
|
1301
|
+
if (room <= 0) return void 0;
|
|
1302
|
+
const slice = chunk.byteLength <= room ? chunk : node_buffer.Buffer.from(chunk.subarray(0, room));
|
|
1303
|
+
this.#retained += slice.byteLength;
|
|
1304
|
+
return slice;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/server/execution/execute.ts
|
|
1309
|
+
/**
|
|
1310
|
+
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
1311
|
+
*
|
|
1312
|
+
* @remarks
|
|
1313
|
+
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
1314
|
+
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
1315
|
+
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
1316
|
+
* whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
|
|
1317
|
+
* child through the same bounded stop, and only the earliest is recorded: a timeout reports
|
|
1318
|
+
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
1319
|
+
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
1320
|
+
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
1321
|
+
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
1322
|
+
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
1323
|
+
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
1324
|
+
* overrides `command.input`. A host fault while writing that input terminates the run by design and
|
|
1325
|
+
* marks its result failed; a strict rejection carries the host fault as its cause. This differs
|
|
1326
|
+
* from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
|
|
1327
|
+
* is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
|
|
1328
|
+
* {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
|
|
1329
|
+
* option or command string rejects before the child is spawned, because an async function cannot
|
|
1330
|
+
* throw synchronously.
|
|
1331
|
+
*
|
|
1332
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
1333
|
+
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
1334
|
+
* @returns The settled run outcome
|
|
1335
|
+
* @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`
|
|
1336
|
+
*
|
|
1337
|
+
* @example
|
|
1338
|
+
* ```ts
|
|
1339
|
+
* const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
1340
|
+
* ```
|
|
1341
|
+
*/
|
|
1342
|
+
async function execute(command, options) {
|
|
1343
|
+
const snapshot = snapshotCommand(command);
|
|
1344
|
+
const optionEnvironment = options?.environment;
|
|
1345
|
+
const optionWorkspace = options?.workspace;
|
|
1346
|
+
const optionInput = options?.input;
|
|
1347
|
+
const optionTimeout = options?.timeout;
|
|
1348
|
+
const optionGrace = options?.grace;
|
|
1349
|
+
const optionSignal = options?.signal;
|
|
1350
|
+
const optionStrict = options?.strict;
|
|
1351
|
+
const optionLimit = options?.limit;
|
|
1352
|
+
validateCommand(snapshot);
|
|
1353
|
+
validateEnvironment(optionEnvironment);
|
|
1354
|
+
validateWorkspace(optionWorkspace);
|
|
1355
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
1356
|
+
validateTimer(optionGrace, "option 'grace'");
|
|
1357
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
1358
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
1359
|
+
const grace = optionGrace ?? _src_core.PROCESS_GRACE;
|
|
1360
|
+
const timeout = optionTimeout ?? 0;
|
|
1361
|
+
const strict = optionStrict ?? true;
|
|
1362
|
+
const text = optionInput ?? snapshot.input;
|
|
1363
|
+
const line = formatCommand(snapshot);
|
|
1364
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1365
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
1366
|
+
const plan = buildSpawn(snapshot, {
|
|
1367
|
+
workspace,
|
|
1368
|
+
environment
|
|
1369
|
+
});
|
|
1370
|
+
const settled = Promise.withResolvers();
|
|
1371
|
+
const terminate = new AbortController();
|
|
1372
|
+
const cleanup = new AbortController();
|
|
1373
|
+
const finish = new AbortController();
|
|
1374
|
+
const inputFailure = new AbortController();
|
|
1375
|
+
const outChunks = [];
|
|
1376
|
+
const errChunks = [];
|
|
1377
|
+
const outRetention = new Retention();
|
|
1378
|
+
const errRetention = new Retention();
|
|
1379
|
+
let spawned = false;
|
|
1380
|
+
let expired = false;
|
|
1381
|
+
let aborted = false;
|
|
1382
|
+
let cause;
|
|
1383
|
+
let timeoutTimer;
|
|
1384
|
+
let confirmTimer;
|
|
1385
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1386
|
+
cwd: workspace,
|
|
1387
|
+
detached: process.platform !== "win32",
|
|
1388
|
+
env: environment,
|
|
1389
|
+
stdio: [
|
|
1390
|
+
"pipe",
|
|
1391
|
+
"pipe",
|
|
1392
|
+
"pipe"
|
|
1393
|
+
],
|
|
1394
|
+
windowsHide: true,
|
|
1395
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1396
|
+
});
|
|
1397
|
+
finish.signal.addEventListener("abort", () => {
|
|
1398
|
+
clearTimeout(timeoutTimer);
|
|
1399
|
+
clearTimeout(confirmTimer);
|
|
1400
|
+
cleanup.abort();
|
|
1401
|
+
settled.resolve(buildExecuteResult({
|
|
1402
|
+
command: line,
|
|
1403
|
+
stdout: node_buffer.Buffer.concat(outChunks),
|
|
1404
|
+
stderr: node_buffer.Buffer.concat(errChunks),
|
|
1405
|
+
code: child.exitCode,
|
|
1406
|
+
signal: child.signalCode,
|
|
1407
|
+
expired,
|
|
1408
|
+
aborted,
|
|
1409
|
+
truncated: outRetention.delivered > limit || errRetention.delivered > limit,
|
|
1410
|
+
limit,
|
|
1411
|
+
...cause === void 0 ? {} : { cause }
|
|
1412
|
+
}));
|
|
1413
|
+
}, { once: true });
|
|
1414
|
+
terminate.signal.addEventListener("abort", () => {
|
|
1415
|
+
clearTimeout(timeoutTimer);
|
|
1416
|
+
cleanup.abort();
|
|
1417
|
+
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
1418
|
+
if (finish.signal.aborted) return;
|
|
1419
|
+
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
1420
|
+
});
|
|
1421
|
+
}, { once: true });
|
|
1422
|
+
inputFailure.signal.addEventListener("abort", () => {
|
|
1423
|
+
if (finish.signal.aborted) return;
|
|
1424
|
+
cause = inputFailure.signal.reason;
|
|
1425
|
+
terminate.abort();
|
|
1426
|
+
}, { once: true });
|
|
1427
|
+
child.stdin.on("error", (error) => inputFailure.abort(error));
|
|
1428
|
+
child.stdout.on("data", (chunk) => {
|
|
1429
|
+
const retained = outRetention.retain(chunk, limit);
|
|
1430
|
+
if (retained !== void 0) outChunks.push(retained);
|
|
1431
|
+
});
|
|
1432
|
+
child.stderr.on("data", (chunk) => {
|
|
1433
|
+
const retained = errRetention.retain(chunk, limit);
|
|
1434
|
+
if (retained !== void 0) errChunks.push(retained);
|
|
1435
|
+
});
|
|
1436
|
+
child.once("spawn", () => {
|
|
1437
|
+
spawned = true;
|
|
1438
|
+
});
|
|
1439
|
+
child.once("error", (error) => {
|
|
1440
|
+
if (spawned) return;
|
|
1441
|
+
cause = error;
|
|
1442
|
+
finish.abort();
|
|
1443
|
+
});
|
|
1444
|
+
child.once("close", () => finish.abort());
|
|
1445
|
+
if (text !== void 0) child.stdin.write(text, (error) => {
|
|
1446
|
+
if (error !== void 0 && error !== null) inputFailure.abort(error);
|
|
1447
|
+
});
|
|
1448
|
+
child.stdin.end();
|
|
1449
|
+
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
1450
|
+
expired = true;
|
|
1451
|
+
terminate.abort();
|
|
1452
|
+
}, timeout);
|
|
1453
|
+
if (optionSignal !== void 0) {
|
|
1454
|
+
optionSignal.addEventListener("abort", () => {
|
|
1455
|
+
aborted = true;
|
|
1456
|
+
terminate.abort();
|
|
1457
|
+
}, {
|
|
1458
|
+
once: true,
|
|
1459
|
+
signal: cleanup.signal
|
|
1460
|
+
});
|
|
1461
|
+
if (optionSignal.aborted) {
|
|
1462
|
+
aborted = true;
|
|
1463
|
+
terminate.abort();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
const result = await settled.promise;
|
|
1467
|
+
if (result.failed && strict) {
|
|
1468
|
+
if (cause !== void 0 && cause === inputFailure.signal.reason) throw new _src_core.ProcessError(`Command '${result.command}' failed while writing standard input`, {
|
|
1469
|
+
code: "input",
|
|
1470
|
+
context: {
|
|
1471
|
+
command: result.command,
|
|
1472
|
+
code: result.code,
|
|
1473
|
+
signal: result.signal
|
|
1474
|
+
},
|
|
1475
|
+
cause: inputFailure.signal.reason,
|
|
1476
|
+
result
|
|
1477
|
+
});
|
|
1478
|
+
throw (0, _src_core.createExecuteError)(result, cause);
|
|
1479
|
+
}
|
|
1480
|
+
return result;
|
|
1481
|
+
}
|
|
1482
|
+
//#endregion
|
|
1483
|
+
//#region src/server/execution/executeSync.ts
|
|
1484
|
+
/**
|
|
1485
|
+
* Runs one command to completion synchronously, buffering its output, and returns the outcome.
|
|
1486
|
+
*
|
|
1487
|
+
* @remarks
|
|
1488
|
+
* The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
|
|
1489
|
+
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
1490
|
+
* contract carries neither. A positive `timeout` ends only the root process and can leave
|
|
1491
|
+
* descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
|
|
1492
|
+
* timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
|
|
1493
|
+
* and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
|
|
1494
|
+
* reports `truncated` without failing. The environment and input follow the same merge as
|
|
1495
|
+
* {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
|
|
1496
|
+
* carrying the {@link ExecuteResult}.
|
|
1497
|
+
*
|
|
1498
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
1499
|
+
* @param options - Working directory, timeout, capture limit, and failure delivery
|
|
1500
|
+
* @returns The run outcome
|
|
1501
|
+
* @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`
|
|
1502
|
+
*
|
|
1503
|
+
* @example
|
|
1504
|
+
* ```ts
|
|
1505
|
+
* const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
|
|
1506
|
+
* ```
|
|
1507
|
+
*/
|
|
1508
|
+
function executeSync(command, options) {
|
|
1509
|
+
const snapshot = snapshotCommand(command);
|
|
1510
|
+
const optionEnvironment = options?.environment;
|
|
1511
|
+
const optionWorkspace = options?.workspace;
|
|
1512
|
+
const optionInput = options?.input;
|
|
1513
|
+
const optionTimeout = options?.timeout;
|
|
1514
|
+
const optionStrict = options?.strict;
|
|
1515
|
+
const optionLimit = options?.limit;
|
|
1516
|
+
validateCommand(snapshot);
|
|
1517
|
+
validateEnvironment(optionEnvironment);
|
|
1518
|
+
validateWorkspace(optionWorkspace);
|
|
1519
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
1520
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
1521
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
1522
|
+
const timeout = optionTimeout ?? 0;
|
|
1523
|
+
const strict = optionStrict ?? true;
|
|
1524
|
+
const text = optionInput ?? snapshot.input;
|
|
1525
|
+
const line = formatCommand(snapshot);
|
|
1526
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1527
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
1528
|
+
const plan = buildSpawn(snapshot, {
|
|
1529
|
+
workspace,
|
|
1530
|
+
environment
|
|
1531
|
+
});
|
|
1532
|
+
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
1533
|
+
cwd: workspace,
|
|
1534
|
+
env: environment,
|
|
1535
|
+
encoding: "buffer",
|
|
1536
|
+
maxBuffer: limit,
|
|
1537
|
+
killSignal: "SIGKILL",
|
|
1538
|
+
windowsHide: true,
|
|
1539
|
+
windowsVerbatimArguments: plan.verbatim,
|
|
1540
|
+
...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
|
|
1541
|
+
...timeout > 0 ? { timeout } : {}
|
|
1542
|
+
});
|
|
1543
|
+
const error = outcome.error;
|
|
1544
|
+
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
1545
|
+
const result = buildExecuteResult({
|
|
1546
|
+
command: line,
|
|
1547
|
+
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
1548
|
+
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
1549
|
+
code: outcome.status,
|
|
1550
|
+
signal: outcome.signal,
|
|
1551
|
+
expired: fault === "ETIMEDOUT",
|
|
1552
|
+
aborted: false,
|
|
1553
|
+
truncated: fault === "ENOBUFS",
|
|
1554
|
+
limit,
|
|
1555
|
+
...error === void 0 ? {} : { cause: error }
|
|
1556
|
+
});
|
|
1557
|
+
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
|
|
1558
|
+
return result;
|
|
1559
|
+
}
|
|
1560
|
+
//#endregion
|
|
1561
|
+
//#region src/server/execution/detach.ts
|
|
1562
|
+
/**
|
|
1563
|
+
* Spawns one command as a detached process and returns without waiting for it.
|
|
1564
|
+
*
|
|
1565
|
+
* @remarks
|
|
1566
|
+
* The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
|
|
1567
|
+
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
1568
|
+
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
1569
|
+
* is spawned.
|
|
1570
|
+
*
|
|
1571
|
+
* @param command - The executable, arguments, and optional environment
|
|
1572
|
+
* @param options - The working directory the detached child starts in
|
|
1573
|
+
* @returns Nothing
|
|
1574
|
+
* @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
|
|
1575
|
+
*
|
|
1576
|
+
* @example
|
|
1577
|
+
* ```ts
|
|
1578
|
+
* detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
|
|
1579
|
+
* ```
|
|
1580
|
+
*/
|
|
1581
|
+
function detach(command, options) {
|
|
1582
|
+
const snapshot = snapshotCommand(command);
|
|
1583
|
+
const optionWorkspace = options?.workspace;
|
|
1584
|
+
validateCommand(snapshot);
|
|
1585
|
+
validateWorkspace(optionWorkspace);
|
|
1586
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1587
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
|
|
1588
|
+
const plan = buildSpawn(snapshot, {
|
|
1589
|
+
workspace,
|
|
1590
|
+
environment
|
|
1591
|
+
});
|
|
1592
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1593
|
+
cwd: workspace,
|
|
1594
|
+
detached: true,
|
|
1595
|
+
env: environment,
|
|
1596
|
+
stdio: "ignore",
|
|
1597
|
+
windowsHide: true,
|
|
1598
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1599
|
+
});
|
|
1600
|
+
child.once("error", () => void 0);
|
|
1601
|
+
child.unref();
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1332
1604
|
exports.Process = Process;
|
|
1333
1605
|
exports.ProcessManager = ProcessManager;
|
|
1334
|
-
exports.
|
|
1606
|
+
exports.Retention = Retention;
|
|
1607
|
+
exports.buildExecutableCandidates = buildExecutableCandidates;
|
|
1608
|
+
exports.buildExecuteResult = buildExecuteResult;
|
|
1609
|
+
exports.buildPlatformSpawn = buildPlatformSpawn;
|
|
1335
1610
|
exports.buildSpawn = buildSpawn;
|
|
1336
1611
|
exports.createProcess = createProcess;
|
|
1337
1612
|
exports.createProcessManager = createProcessManager;
|
|
1338
1613
|
exports.detach = detach;
|
|
1614
|
+
exports.execute = execute;
|
|
1615
|
+
exports.executeSync = executeSync;
|
|
1339
1616
|
exports.formatCommand = formatCommand;
|
|
1340
1617
|
exports.isExited = isExited;
|
|
1341
1618
|
exports.isFile = isFile;
|
|
1342
1619
|
exports.killProcess = killProcess;
|
|
1343
1620
|
exports.killTree = killTree;
|
|
1344
1621
|
exports.mergeEnvironment = mergeEnvironment;
|
|
1622
|
+
exports.mergePlatformEnvironment = mergePlatformEnvironment;
|
|
1345
1623
|
exports.quoteArgument = quoteArgument;
|
|
1624
|
+
exports.readPlatformVariable = readPlatformVariable;
|
|
1346
1625
|
exports.readVariable = readVariable;
|
|
1347
1626
|
exports.resolveExecutable = resolveExecutable;
|
|
1348
|
-
exports.
|
|
1349
|
-
exports.run = run;
|
|
1350
|
-
exports.runSync = runSync;
|
|
1627
|
+
exports.snapshotCommand = snapshotCommand;
|
|
1351
1628
|
exports.stopChild = stopChild;
|
|
1352
1629
|
exports.trimHead = trimHead;
|
|
1353
1630
|
exports.trimTail = trimTail;
|