command-stream 0.9.0 → 0.9.2
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/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,1497 @@
|
|
|
1
|
+
// ProcessRunner execution methods - start, sync, async, and related methods
|
|
2
|
+
// Part of the modular ProcessRunner architecture
|
|
3
|
+
|
|
4
|
+
import cp from 'child_process';
|
|
5
|
+
import { trace } from './$.trace.mjs';
|
|
6
|
+
import { findAvailableShell } from './$.shell.mjs';
|
|
7
|
+
import { StreamUtils, safeWrite, asBuffer } from './$.stream-utils.mjs';
|
|
8
|
+
import { pumpReadable } from './$.quote.mjs';
|
|
9
|
+
import { createResult } from './$.result.mjs';
|
|
10
|
+
import { parseShellCommand, needsRealShell } from './shell-parser.mjs';
|
|
11
|
+
|
|
12
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check for shell operators in command
|
|
16
|
+
* @param {string} command - Command to check
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function hasShellOperators(command) {
|
|
20
|
+
return (
|
|
21
|
+
command.includes('&&') ||
|
|
22
|
+
command.includes('||') ||
|
|
23
|
+
command.includes('(') ||
|
|
24
|
+
command.includes(';') ||
|
|
25
|
+
(command.includes('cd ') && command.includes('&&'))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if command is a streaming pattern
|
|
31
|
+
* @param {string} command - Command to check
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isStreamingPattern(command) {
|
|
35
|
+
return (
|
|
36
|
+
command.includes('sleep') &&
|
|
37
|
+
command.includes(';') &&
|
|
38
|
+
(command.includes('echo') || command.includes('printf'))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Determine if shell operators should be used
|
|
44
|
+
* @param {object} runner - ProcessRunner instance
|
|
45
|
+
* @param {string} command - Command to check
|
|
46
|
+
* @returns {boolean}
|
|
47
|
+
*/
|
|
48
|
+
function shouldUseShellOperators(runner, command) {
|
|
49
|
+
const hasOps = hasShellOperators(command);
|
|
50
|
+
const isStreaming = isStreamingPattern(command);
|
|
51
|
+
return (
|
|
52
|
+
runner.options.shellOperators &&
|
|
53
|
+
hasOps &&
|
|
54
|
+
!isStreaming &&
|
|
55
|
+
!runner._isStreaming
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if stdin is interactive
|
|
61
|
+
* @param {string} stdin - Stdin option
|
|
62
|
+
* @param {object} options - Runner options
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isInteractiveMode(stdin, options) {
|
|
66
|
+
return (
|
|
67
|
+
stdin === 'inherit' &&
|
|
68
|
+
process.stdin.isTTY === true &&
|
|
69
|
+
process.stdout.isTTY === true &&
|
|
70
|
+
process.stderr.isTTY === true &&
|
|
71
|
+
options.interactive === true
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Spawn process using Bun
|
|
77
|
+
* @param {Array} argv - Command arguments
|
|
78
|
+
* @param {object} config - Spawn configuration
|
|
79
|
+
* @returns {object} Child process
|
|
80
|
+
*/
|
|
81
|
+
function spawnWithBun(argv, config) {
|
|
82
|
+
const { cwd, env, isInteractive } = config;
|
|
83
|
+
|
|
84
|
+
trace(
|
|
85
|
+
'ProcessRunner',
|
|
86
|
+
() =>
|
|
87
|
+
`spawnBun: Creating process | ${JSON.stringify({
|
|
88
|
+
command: argv[0],
|
|
89
|
+
args: argv.slice(1),
|
|
90
|
+
isInteractive,
|
|
91
|
+
cwd,
|
|
92
|
+
platform: process.platform,
|
|
93
|
+
})}`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (isInteractive) {
|
|
97
|
+
trace(
|
|
98
|
+
'ProcessRunner',
|
|
99
|
+
() => `spawnBun: Using interactive mode with inherited stdio`
|
|
100
|
+
);
|
|
101
|
+
return Bun.spawn(argv, {
|
|
102
|
+
cwd,
|
|
103
|
+
env,
|
|
104
|
+
stdin: 'inherit',
|
|
105
|
+
stdout: 'inherit',
|
|
106
|
+
stderr: 'inherit',
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
trace(
|
|
111
|
+
'ProcessRunner',
|
|
112
|
+
() =>
|
|
113
|
+
`spawnBun: Using non-interactive mode with pipes and detached=${process.platform !== 'win32'}`
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return Bun.spawn(argv, {
|
|
117
|
+
cwd,
|
|
118
|
+
env,
|
|
119
|
+
stdin: 'pipe',
|
|
120
|
+
stdout: 'pipe',
|
|
121
|
+
stderr: 'pipe',
|
|
122
|
+
detached: process.platform !== 'win32',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spawn process using Node
|
|
128
|
+
* @param {Array} argv - Command arguments
|
|
129
|
+
* @param {object} config - Spawn configuration
|
|
130
|
+
* @returns {object} Child process
|
|
131
|
+
*/
|
|
132
|
+
function spawnWithNode(argv, config) {
|
|
133
|
+
const { cwd, env, isInteractive } = config;
|
|
134
|
+
|
|
135
|
+
trace(
|
|
136
|
+
'ProcessRunner',
|
|
137
|
+
() =>
|
|
138
|
+
`spawnNode: Creating process | ${JSON.stringify({
|
|
139
|
+
command: argv[0],
|
|
140
|
+
args: argv.slice(1),
|
|
141
|
+
isInteractive,
|
|
142
|
+
cwd,
|
|
143
|
+
platform: process.platform,
|
|
144
|
+
})}`
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (isInteractive) {
|
|
148
|
+
return cp.spawn(argv[0], argv.slice(1), {
|
|
149
|
+
cwd,
|
|
150
|
+
env,
|
|
151
|
+
stdio: 'inherit',
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const child = cp.spawn(argv[0], argv.slice(1), {
|
|
156
|
+
cwd,
|
|
157
|
+
env,
|
|
158
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
159
|
+
detached: process.platform !== 'win32',
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
trace(
|
|
163
|
+
'ProcessRunner',
|
|
164
|
+
() =>
|
|
165
|
+
`spawnNode: Process created | ${JSON.stringify({
|
|
166
|
+
pid: child.pid,
|
|
167
|
+
killed: child.killed,
|
|
168
|
+
hasStdout: !!child.stdout,
|
|
169
|
+
hasStderr: !!child.stderr,
|
|
170
|
+
hasStdin: !!child.stdin,
|
|
171
|
+
})}`
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return child;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Spawn child process with appropriate runtime
|
|
179
|
+
* @param {Array} argv - Command arguments
|
|
180
|
+
* @param {object} config - Spawn configuration
|
|
181
|
+
* @returns {object} Child process
|
|
182
|
+
*/
|
|
183
|
+
function spawnChild(argv, config) {
|
|
184
|
+
const { stdin } = config;
|
|
185
|
+
const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
|
|
186
|
+
const preferNodeForInput = isBun && needsExplicitPipe;
|
|
187
|
+
|
|
188
|
+
trace(
|
|
189
|
+
'ProcessRunner',
|
|
190
|
+
() =>
|
|
191
|
+
`About to spawn process | ${JSON.stringify({
|
|
192
|
+
needsExplicitPipe,
|
|
193
|
+
preferNodeForInput,
|
|
194
|
+
runtime: isBun ? 'Bun' : 'Node',
|
|
195
|
+
command: argv[0],
|
|
196
|
+
args: argv.slice(1),
|
|
197
|
+
})}`
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (preferNodeForInput) {
|
|
201
|
+
return spawnWithNode(argv, config);
|
|
202
|
+
}
|
|
203
|
+
return isBun ? spawnWithBun(argv, config) : spawnWithNode(argv, config);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Setup child process event listeners
|
|
208
|
+
* @param {object} runner - ProcessRunner instance
|
|
209
|
+
*/
|
|
210
|
+
function setupChildEventListeners(runner) {
|
|
211
|
+
if (!runner.child || typeof runner.child.on !== 'function') {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
runner.child.on('spawn', () => {
|
|
216
|
+
trace(
|
|
217
|
+
'ProcessRunner',
|
|
218
|
+
() =>
|
|
219
|
+
`Child process spawned successfully | ${JSON.stringify({
|
|
220
|
+
pid: runner.child.pid,
|
|
221
|
+
command: runner.spec?.command?.slice(0, 50),
|
|
222
|
+
})}`
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
runner.child.on('error', (error) => {
|
|
227
|
+
trace(
|
|
228
|
+
'ProcessRunner',
|
|
229
|
+
() =>
|
|
230
|
+
`Child process error event | ${JSON.stringify({
|
|
231
|
+
pid: runner.child?.pid,
|
|
232
|
+
error: error.message,
|
|
233
|
+
code: error.code,
|
|
234
|
+
errno: error.errno,
|
|
235
|
+
syscall: error.syscall,
|
|
236
|
+
command: runner.spec?.command?.slice(0, 50),
|
|
237
|
+
})}`
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Create stdout pump
|
|
244
|
+
* @param {object} runner - ProcessRunner instance
|
|
245
|
+
* @param {number} childPid - Child process PID
|
|
246
|
+
* @returns {Promise}
|
|
247
|
+
*/
|
|
248
|
+
function createStdoutPump(runner, childPid) {
|
|
249
|
+
if (!runner.child.stdout) {
|
|
250
|
+
return Promise.resolve();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return pumpReadable(runner.child.stdout, (buf) => {
|
|
254
|
+
trace(
|
|
255
|
+
'ProcessRunner',
|
|
256
|
+
() =>
|
|
257
|
+
`stdout data received | ${JSON.stringify({
|
|
258
|
+
pid: childPid,
|
|
259
|
+
bufferLength: buf.length,
|
|
260
|
+
capture: runner.options.capture,
|
|
261
|
+
mirror: runner.options.mirror,
|
|
262
|
+
preview: buf.toString().slice(0, 100),
|
|
263
|
+
})}`
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (runner.options.capture) {
|
|
267
|
+
runner.outChunks.push(buf);
|
|
268
|
+
}
|
|
269
|
+
if (runner.options.mirror) {
|
|
270
|
+
safeWrite(process.stdout, buf);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
runner._emitProcessedData('stdout', buf);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Create stderr pump
|
|
279
|
+
* @param {object} runner - ProcessRunner instance
|
|
280
|
+
* @param {number} childPid - Child process PID
|
|
281
|
+
* @returns {Promise}
|
|
282
|
+
*/
|
|
283
|
+
function createStderrPump(runner, childPid) {
|
|
284
|
+
if (!runner.child.stderr) {
|
|
285
|
+
return Promise.resolve();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return pumpReadable(runner.child.stderr, (buf) => {
|
|
289
|
+
trace(
|
|
290
|
+
'ProcessRunner',
|
|
291
|
+
() =>
|
|
292
|
+
`stderr data received | ${JSON.stringify({
|
|
293
|
+
pid: childPid,
|
|
294
|
+
bufferLength: buf.length,
|
|
295
|
+
capture: runner.options.capture,
|
|
296
|
+
mirror: runner.options.mirror,
|
|
297
|
+
preview: buf.toString().slice(0, 100),
|
|
298
|
+
})}`
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (runner.options.capture) {
|
|
302
|
+
runner.errChunks.push(buf);
|
|
303
|
+
}
|
|
304
|
+
if (runner.options.mirror) {
|
|
305
|
+
safeWrite(process.stderr, buf);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
runner._emitProcessedData('stderr', buf);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle stdin for inherit mode
|
|
314
|
+
* @param {object} runner - ProcessRunner instance
|
|
315
|
+
* @param {boolean} isInteractive - Is interactive mode
|
|
316
|
+
* @returns {Promise}
|
|
317
|
+
*/
|
|
318
|
+
function handleInheritStdin(runner, isInteractive) {
|
|
319
|
+
if (isInteractive) {
|
|
320
|
+
trace(
|
|
321
|
+
'ProcessRunner',
|
|
322
|
+
() => `stdin: Using inherit mode for interactive command`
|
|
323
|
+
);
|
|
324
|
+
return Promise.resolve();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const isPipedIn = process.stdin && process.stdin.isTTY === false;
|
|
328
|
+
trace(
|
|
329
|
+
'ProcessRunner',
|
|
330
|
+
() =>
|
|
331
|
+
`stdin: Non-interactive inherit mode | ${JSON.stringify({
|
|
332
|
+
isPipedIn,
|
|
333
|
+
stdinTTY: process.stdin.isTTY,
|
|
334
|
+
})}`
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (isPipedIn) {
|
|
338
|
+
trace('ProcessRunner', () => `stdin: Pumping piped input to child process`);
|
|
339
|
+
return runner._pumpStdinTo(
|
|
340
|
+
runner.child,
|
|
341
|
+
runner.options.capture ? runner.inChunks : null
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
trace(
|
|
346
|
+
'ProcessRunner',
|
|
347
|
+
() => `stdin: Forwarding TTY stdin for non-interactive command`
|
|
348
|
+
);
|
|
349
|
+
return runner._forwardTTYStdin();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Handle stdin based on configuration
|
|
354
|
+
* @param {object} runner - ProcessRunner instance
|
|
355
|
+
* @param {string|Buffer} stdin - Stdin configuration
|
|
356
|
+
* @param {boolean} isInteractive - Is interactive mode
|
|
357
|
+
* @returns {Promise}
|
|
358
|
+
*/
|
|
359
|
+
function handleStdin(runner, stdin, isInteractive) {
|
|
360
|
+
trace(
|
|
361
|
+
'ProcessRunner',
|
|
362
|
+
() =>
|
|
363
|
+
`Setting up stdin handling | ${JSON.stringify({
|
|
364
|
+
stdinType: typeof stdin,
|
|
365
|
+
stdin:
|
|
366
|
+
stdin === 'inherit'
|
|
367
|
+
? 'inherit'
|
|
368
|
+
: stdin === 'ignore'
|
|
369
|
+
? 'ignore'
|
|
370
|
+
: typeof stdin === 'string'
|
|
371
|
+
? `string(${stdin.length})`
|
|
372
|
+
: 'other',
|
|
373
|
+
isInteractive,
|
|
374
|
+
hasChildStdin: !!runner.child?.stdin,
|
|
375
|
+
processTTY: process.stdin.isTTY,
|
|
376
|
+
})}`
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (stdin === 'inherit') {
|
|
380
|
+
return handleInheritStdin(runner, isInteractive);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (stdin === 'ignore') {
|
|
384
|
+
trace('ProcessRunner', () => `stdin: Ignoring and closing stdin`);
|
|
385
|
+
if (runner.child.stdin && typeof runner.child.stdin.end === 'function') {
|
|
386
|
+
runner.child.stdin.end();
|
|
387
|
+
}
|
|
388
|
+
return Promise.resolve();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (stdin === 'pipe') {
|
|
392
|
+
trace(
|
|
393
|
+
'ProcessRunner',
|
|
394
|
+
() => `stdin: Using pipe mode - leaving stdin open for manual control`
|
|
395
|
+
);
|
|
396
|
+
return Promise.resolve();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
|
|
400
|
+
const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
|
|
401
|
+
trace(
|
|
402
|
+
'ProcessRunner',
|
|
403
|
+
() =>
|
|
404
|
+
`stdin: Writing buffer to child | ${JSON.stringify({
|
|
405
|
+
bufferLength: buf.length,
|
|
406
|
+
willCapture: runner.options.capture && !!runner.inChunks,
|
|
407
|
+
})}`
|
|
408
|
+
);
|
|
409
|
+
if (runner.options.capture && runner.inChunks) {
|
|
410
|
+
runner.inChunks.push(Buffer.from(buf));
|
|
411
|
+
}
|
|
412
|
+
return runner._writeToStdin(buf);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return Promise.resolve();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create promise for child exit
|
|
420
|
+
* @param {object} child - Child process
|
|
421
|
+
* @returns {Promise}
|
|
422
|
+
*/
|
|
423
|
+
function createExitPromise(child) {
|
|
424
|
+
if (isBun) {
|
|
425
|
+
return child.exited;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
trace(
|
|
430
|
+
'ProcessRunner',
|
|
431
|
+
() => `Setting up child process event listeners for PID ${child.pid}`
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
child.on('close', (code, signal) => {
|
|
435
|
+
trace(
|
|
436
|
+
'ProcessRunner',
|
|
437
|
+
() =>
|
|
438
|
+
`Child process close event | ${JSON.stringify({
|
|
439
|
+
pid: child.pid,
|
|
440
|
+
code,
|
|
441
|
+
signal,
|
|
442
|
+
killed: child.killed,
|
|
443
|
+
exitCode: child.exitCode,
|
|
444
|
+
signalCode: child.signalCode,
|
|
445
|
+
})}`
|
|
446
|
+
);
|
|
447
|
+
resolve(code);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
child.on('exit', (code, signal) => {
|
|
451
|
+
trace(
|
|
452
|
+
'ProcessRunner',
|
|
453
|
+
() =>
|
|
454
|
+
`Child process exit event | ${JSON.stringify({
|
|
455
|
+
pid: child.pid,
|
|
456
|
+
code,
|
|
457
|
+
signal,
|
|
458
|
+
killed: child.killed,
|
|
459
|
+
exitCode: child.exitCode,
|
|
460
|
+
signalCode: child.signalCode,
|
|
461
|
+
})}`
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Determine final exit code
|
|
469
|
+
* @param {number|null|undefined} code - Raw exit code
|
|
470
|
+
* @param {boolean} cancelled - Was process cancelled
|
|
471
|
+
* @returns {number}
|
|
472
|
+
*/
|
|
473
|
+
function determineFinalExitCode(code, cancelled) {
|
|
474
|
+
trace(
|
|
475
|
+
'ProcessRunner',
|
|
476
|
+
() =>
|
|
477
|
+
`Raw exit code from child | ${JSON.stringify({
|
|
478
|
+
code,
|
|
479
|
+
codeType: typeof code,
|
|
480
|
+
cancelled,
|
|
481
|
+
isBun,
|
|
482
|
+
})}`
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (code !== undefined && code !== null) {
|
|
486
|
+
return code;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (cancelled) {
|
|
490
|
+
trace(
|
|
491
|
+
'ProcessRunner',
|
|
492
|
+
() => `Process was killed, using SIGTERM exit code 143`
|
|
493
|
+
);
|
|
494
|
+
return 143;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
trace('ProcessRunner', () => `Process exited without code, defaulting to 0`);
|
|
498
|
+
return 0;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Build result data from runner state
|
|
503
|
+
* @param {object} runner - ProcessRunner instance
|
|
504
|
+
* @param {number} exitCode - Exit code
|
|
505
|
+
* @returns {object}
|
|
506
|
+
*/
|
|
507
|
+
function buildResultData(runner, exitCode) {
|
|
508
|
+
return {
|
|
509
|
+
code: exitCode,
|
|
510
|
+
stdout: runner.options.capture
|
|
511
|
+
? runner.outChunks && runner.outChunks.length > 0
|
|
512
|
+
? Buffer.concat(runner.outChunks).toString('utf8')
|
|
513
|
+
: ''
|
|
514
|
+
: undefined,
|
|
515
|
+
stderr: runner.options.capture
|
|
516
|
+
? runner.errChunks && runner.errChunks.length > 0
|
|
517
|
+
? Buffer.concat(runner.errChunks).toString('utf8')
|
|
518
|
+
: ''
|
|
519
|
+
: undefined,
|
|
520
|
+
stdin:
|
|
521
|
+
runner.options.capture && runner.inChunks
|
|
522
|
+
? Buffer.concat(runner.inChunks).toString('utf8')
|
|
523
|
+
: undefined,
|
|
524
|
+
child: runner.child,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Throw errexit error if needed
|
|
530
|
+
* @param {object} runner - ProcessRunner instance
|
|
531
|
+
* @param {object} globalShellSettings - Shell settings
|
|
532
|
+
*/
|
|
533
|
+
function throwErrexitIfNeeded(runner, globalShellSettings) {
|
|
534
|
+
if (!globalShellSettings.errexit || runner.result.code === 0) {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
trace('ProcessRunner', () => `Errexit mode: throwing error`);
|
|
539
|
+
|
|
540
|
+
const error = new Error(
|
|
541
|
+
`Command failed with exit code ${runner.result.code}`
|
|
542
|
+
);
|
|
543
|
+
error.code = runner.result.code;
|
|
544
|
+
error.stdout = runner.result.stdout;
|
|
545
|
+
error.stderr = runner.result.stderr;
|
|
546
|
+
error.result = runner.result;
|
|
547
|
+
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Get stdin input for sync spawn
|
|
553
|
+
* @param {string|Buffer} stdin - Stdin option
|
|
554
|
+
* @returns {Buffer|undefined}
|
|
555
|
+
*/
|
|
556
|
+
function getSyncStdinInput(stdin) {
|
|
557
|
+
if (typeof stdin === 'string') {
|
|
558
|
+
return Buffer.from(stdin);
|
|
559
|
+
}
|
|
560
|
+
if (Buffer.isBuffer(stdin)) {
|
|
561
|
+
return stdin;
|
|
562
|
+
}
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get stdin string for result
|
|
568
|
+
* @param {string|Buffer} stdin - Stdin option
|
|
569
|
+
* @returns {string}
|
|
570
|
+
*/
|
|
571
|
+
function getStdinString(stdin) {
|
|
572
|
+
if (typeof stdin === 'string') {
|
|
573
|
+
return stdin;
|
|
574
|
+
}
|
|
575
|
+
if (Buffer.isBuffer(stdin)) {
|
|
576
|
+
return stdin.toString('utf8');
|
|
577
|
+
}
|
|
578
|
+
return '';
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Execute sync process using Bun
|
|
583
|
+
* @param {Array} argv - Command arguments
|
|
584
|
+
* @param {object} options - Spawn options
|
|
585
|
+
* @returns {object} Result object
|
|
586
|
+
*/
|
|
587
|
+
function executeSyncBun(argv, options) {
|
|
588
|
+
const { cwd, env, stdin } = options;
|
|
589
|
+
const proc = Bun.spawnSync(argv, {
|
|
590
|
+
cwd,
|
|
591
|
+
env,
|
|
592
|
+
stdin: getSyncStdinInput(stdin),
|
|
593
|
+
stdout: 'pipe',
|
|
594
|
+
stderr: 'pipe',
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
const result = createResult({
|
|
598
|
+
code: proc.exitCode || 0,
|
|
599
|
+
stdout: proc.stdout?.toString('utf8') || '',
|
|
600
|
+
stderr: proc.stderr?.toString('utf8') || '',
|
|
601
|
+
stdin: getStdinString(stdin),
|
|
602
|
+
});
|
|
603
|
+
result.child = proc;
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Execute sync process using Node
|
|
609
|
+
* @param {Array} argv - Command arguments
|
|
610
|
+
* @param {object} options - Spawn options
|
|
611
|
+
* @returns {object} Result object
|
|
612
|
+
*/
|
|
613
|
+
function executeSyncNode(argv, options) {
|
|
614
|
+
const { cwd, env, stdin } = options;
|
|
615
|
+
const proc = cp.spawnSync(argv[0], argv.slice(1), {
|
|
616
|
+
cwd,
|
|
617
|
+
env,
|
|
618
|
+
input: getSyncStdinInput(stdin),
|
|
619
|
+
encoding: 'utf8',
|
|
620
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const result = createResult({
|
|
624
|
+
code: proc.status || 0,
|
|
625
|
+
stdout: proc.stdout || '',
|
|
626
|
+
stderr: proc.stderr || '',
|
|
627
|
+
stdin: getStdinString(stdin),
|
|
628
|
+
});
|
|
629
|
+
result.child = proc;
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Execute sync process with appropriate runtime
|
|
635
|
+
* @param {Array} argv - Command arguments
|
|
636
|
+
* @param {object} options - Spawn options
|
|
637
|
+
* @returns {object} Result object
|
|
638
|
+
*/
|
|
639
|
+
function executeSyncProcess(argv, options) {
|
|
640
|
+
return isBun ? executeSyncBun(argv, options) : executeSyncNode(argv, options);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Handle sync result processing
|
|
645
|
+
* @param {object} runner - ProcessRunner instance
|
|
646
|
+
* @param {object} result - Result object
|
|
647
|
+
* @param {object} globalShellSettings - Shell settings
|
|
648
|
+
* @returns {object} Result
|
|
649
|
+
*/
|
|
650
|
+
function processSyncResult(runner, result, globalShellSettings) {
|
|
651
|
+
if (runner.options.mirror) {
|
|
652
|
+
if (result.stdout) {
|
|
653
|
+
safeWrite(process.stdout, result.stdout);
|
|
654
|
+
}
|
|
655
|
+
if (result.stderr) {
|
|
656
|
+
safeWrite(process.stderr, result.stderr);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
runner.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
|
|
661
|
+
runner.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
|
|
662
|
+
|
|
663
|
+
if (result.stdout) {
|
|
664
|
+
runner._emitProcessedData('stdout', Buffer.from(result.stdout));
|
|
665
|
+
}
|
|
666
|
+
if (result.stderr) {
|
|
667
|
+
runner._emitProcessedData('stderr', Buffer.from(result.stderr));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
runner.finish(result);
|
|
671
|
+
|
|
672
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
673
|
+
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
674
|
+
error.code = result.code;
|
|
675
|
+
error.stdout = result.stdout;
|
|
676
|
+
error.stderr = result.stderr;
|
|
677
|
+
error.result = result;
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return result;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Setup external abort signal listener
|
|
686
|
+
* @param {object} runner - ProcessRunner instance
|
|
687
|
+
*/
|
|
688
|
+
function setupExternalAbortSignal(runner) {
|
|
689
|
+
const signal = runner.options.signal;
|
|
690
|
+
if (!signal || typeof signal.addEventListener !== 'function') {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
trace(
|
|
695
|
+
'ProcessRunner',
|
|
696
|
+
() =>
|
|
697
|
+
`Setting up external abort signal listener | ${JSON.stringify({
|
|
698
|
+
hasSignal: !!signal,
|
|
699
|
+
signalAborted: signal.aborted,
|
|
700
|
+
hasInternalController: !!runner._abortController,
|
|
701
|
+
internalAborted: runner._abortController?.signal.aborted,
|
|
702
|
+
})}`
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
signal.addEventListener('abort', () => {
|
|
706
|
+
trace(
|
|
707
|
+
'ProcessRunner',
|
|
708
|
+
() =>
|
|
709
|
+
`External abort signal triggered | ${JSON.stringify({
|
|
710
|
+
externalSignalAborted: signal.aborted,
|
|
711
|
+
hasInternalController: !!runner._abortController,
|
|
712
|
+
internalAborted: runner._abortController?.signal.aborted,
|
|
713
|
+
command: runner.spec?.command?.slice(0, 50),
|
|
714
|
+
})}`
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
runner.kill('SIGTERM');
|
|
718
|
+
trace(
|
|
719
|
+
'ProcessRunner',
|
|
720
|
+
() => 'Process kill initiated due to external abort signal'
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
if (runner._abortController && !runner._abortController.signal.aborted) {
|
|
724
|
+
trace(
|
|
725
|
+
'ProcessRunner',
|
|
726
|
+
() => 'Aborting internal controller due to external signal'
|
|
727
|
+
);
|
|
728
|
+
runner._abortController.abort();
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
if (signal.aborted) {
|
|
733
|
+
trace(
|
|
734
|
+
'ProcessRunner',
|
|
735
|
+
() =>
|
|
736
|
+
`External signal already aborted, killing process and aborting internal controller`
|
|
737
|
+
);
|
|
738
|
+
runner.kill('SIGTERM');
|
|
739
|
+
if (runner._abortController && !runner._abortController.signal.aborted) {
|
|
740
|
+
runner._abortController.abort();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Reinitialize capture chunks when capture option changes
|
|
747
|
+
* @param {object} runner - ProcessRunner instance
|
|
748
|
+
*/
|
|
749
|
+
function reinitCaptureChunks(runner) {
|
|
750
|
+
trace(
|
|
751
|
+
'ProcessRunner',
|
|
752
|
+
() =>
|
|
753
|
+
`BRANCH: capture => REINIT_CHUNKS | ${JSON.stringify({
|
|
754
|
+
capture: runner.options.capture,
|
|
755
|
+
})}`
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
runner.outChunks = runner.options.capture ? [] : null;
|
|
759
|
+
runner.errChunks = runner.options.capture ? [] : null;
|
|
760
|
+
runner.inChunks =
|
|
761
|
+
runner.options.capture && runner.options.stdin === 'inherit'
|
|
762
|
+
? []
|
|
763
|
+
: runner.options.capture &&
|
|
764
|
+
(typeof runner.options.stdin === 'string' ||
|
|
765
|
+
Buffer.isBuffer(runner.options.stdin))
|
|
766
|
+
? [Buffer.from(runner.options.stdin)]
|
|
767
|
+
: [];
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Try running command via enhanced shell parser
|
|
772
|
+
* @param {object} runner - ProcessRunner instance
|
|
773
|
+
* @param {string} command - Command to parse
|
|
774
|
+
* @returns {Promise<object>|null} Result if handled, null if not
|
|
775
|
+
*/
|
|
776
|
+
async function tryEnhancedShellParser(runner, command) {
|
|
777
|
+
const enhancedParsed = parseShellCommand(command);
|
|
778
|
+
if (!enhancedParsed || enhancedParsed.type === 'simple') {
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
trace(
|
|
783
|
+
'ProcessRunner',
|
|
784
|
+
() =>
|
|
785
|
+
`Using enhanced parser for shell operators | ${JSON.stringify({
|
|
786
|
+
type: enhancedParsed.type,
|
|
787
|
+
command: command.slice(0, 50),
|
|
788
|
+
})}`
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
if (enhancedParsed.type === 'sequence') {
|
|
792
|
+
return await runner._runSequence(enhancedParsed);
|
|
793
|
+
}
|
|
794
|
+
if (enhancedParsed.type === 'subshell') {
|
|
795
|
+
return await runner._runSubshell(enhancedParsed);
|
|
796
|
+
}
|
|
797
|
+
if (enhancedParsed.type === 'pipeline') {
|
|
798
|
+
return await runner._runPipeline(enhancedParsed.commands);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Try running command as virtual command
|
|
806
|
+
* @param {object} runner - ProcessRunner instance
|
|
807
|
+
* @param {object} parsed - Parsed command
|
|
808
|
+
* @param {object} deps - Dependencies
|
|
809
|
+
* @returns {Promise<object>|null} Result if handled, null if not
|
|
810
|
+
*/
|
|
811
|
+
async function tryVirtualCommand(runner, parsed, deps) {
|
|
812
|
+
const { virtualCommands, isVirtualCommandsEnabled } = deps;
|
|
813
|
+
|
|
814
|
+
if (
|
|
815
|
+
parsed.type !== 'simple' ||
|
|
816
|
+
!isVirtualCommandsEnabled() ||
|
|
817
|
+
!virtualCommands.has(parsed.cmd) ||
|
|
818
|
+
runner.options._bypassVirtual
|
|
819
|
+
) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const hasCustomStdin =
|
|
824
|
+
runner.options.stdin &&
|
|
825
|
+
runner.options.stdin !== 'inherit' &&
|
|
826
|
+
runner.options.stdin !== 'ignore';
|
|
827
|
+
|
|
828
|
+
const commandsThatNeedRealStdin = ['sleep', 'cat'];
|
|
829
|
+
const shouldBypassVirtual =
|
|
830
|
+
hasCustomStdin && commandsThatNeedRealStdin.includes(parsed.cmd);
|
|
831
|
+
|
|
832
|
+
if (shouldBypassVirtual) {
|
|
833
|
+
trace(
|
|
834
|
+
'ProcessRunner',
|
|
835
|
+
() =>
|
|
836
|
+
`Bypassing built-in virtual command due to custom stdin | ${JSON.stringify(
|
|
837
|
+
{
|
|
838
|
+
cmd: parsed.cmd,
|
|
839
|
+
stdin: typeof runner.options.stdin,
|
|
840
|
+
}
|
|
841
|
+
)}`
|
|
842
|
+
);
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
trace(
|
|
847
|
+
'ProcessRunner',
|
|
848
|
+
() =>
|
|
849
|
+
`BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
|
|
850
|
+
isVirtual: true,
|
|
851
|
+
args: parsed.args,
|
|
852
|
+
})}`
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
return await runner._runVirtual(parsed.cmd, parsed.args, runner.spec.command);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Log xtrace/verbose if enabled
|
|
860
|
+
* @param {object} globalShellSettings - Shell settings
|
|
861
|
+
* @param {string} command - Command or argv
|
|
862
|
+
*/
|
|
863
|
+
function logShellTrace(globalShellSettings, command) {
|
|
864
|
+
if (globalShellSettings.xtrace) {
|
|
865
|
+
console.log(`+ ${command}`);
|
|
866
|
+
}
|
|
867
|
+
if (globalShellSettings.verbose) {
|
|
868
|
+
console.log(command);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Handle shell mode execution
|
|
874
|
+
* @param {object} runner - ProcessRunner instance
|
|
875
|
+
* @param {object} deps - Dependencies
|
|
876
|
+
* @returns {Promise<object>|null} Result if handled by special cases
|
|
877
|
+
*/
|
|
878
|
+
async function handleShellMode(runner, deps) {
|
|
879
|
+
const { virtualCommands, isVirtualCommandsEnabled } = deps;
|
|
880
|
+
const command = runner.spec.command;
|
|
881
|
+
|
|
882
|
+
trace(
|
|
883
|
+
'ProcessRunner',
|
|
884
|
+
() => `BRANCH: spec.mode => shell | ${JSON.stringify({})}`
|
|
885
|
+
);
|
|
886
|
+
|
|
887
|
+
const useShellOps = shouldUseShellOperators(runner, command);
|
|
888
|
+
|
|
889
|
+
trace(
|
|
890
|
+
'ProcessRunner',
|
|
891
|
+
() =>
|
|
892
|
+
`Shell operator detection | ${JSON.stringify({
|
|
893
|
+
hasShellOperators: hasShellOperators(command),
|
|
894
|
+
shellOperatorsEnabled: runner.options.shellOperators,
|
|
895
|
+
isStreamingPattern: isStreamingPattern(command),
|
|
896
|
+
isStreaming: runner._isStreaming,
|
|
897
|
+
shouldUseShellOperators: useShellOps,
|
|
898
|
+
command: command.slice(0, 100),
|
|
899
|
+
})}`
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
if (
|
|
903
|
+
!runner.options._bypassVirtual &&
|
|
904
|
+
useShellOps &&
|
|
905
|
+
!needsRealShell(command)
|
|
906
|
+
) {
|
|
907
|
+
const result = await tryEnhancedShellParser(runner, command);
|
|
908
|
+
if (result) {
|
|
909
|
+
return result;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const parsed = runner._parseCommand(command);
|
|
914
|
+
trace(
|
|
915
|
+
'ProcessRunner',
|
|
916
|
+
() =>
|
|
917
|
+
`Parsed command | ${JSON.stringify({
|
|
918
|
+
type: parsed?.type,
|
|
919
|
+
cmd: parsed?.cmd,
|
|
920
|
+
argsCount: parsed?.args?.length,
|
|
921
|
+
})}`
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
if (parsed) {
|
|
925
|
+
if (parsed.type === 'pipeline') {
|
|
926
|
+
trace(
|
|
927
|
+
'ProcessRunner',
|
|
928
|
+
() =>
|
|
929
|
+
`BRANCH: parsed.type => pipeline | ${JSON.stringify({
|
|
930
|
+
commandCount: parsed.commands?.length,
|
|
931
|
+
})}`
|
|
932
|
+
);
|
|
933
|
+
return await runner._runPipeline(parsed.commands);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const virtualResult = await tryVirtualCommand(runner, parsed, {
|
|
937
|
+
virtualCommands,
|
|
938
|
+
isVirtualCommandsEnabled,
|
|
939
|
+
});
|
|
940
|
+
if (virtualResult) {
|
|
941
|
+
return virtualResult;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Execute child process and collect results
|
|
950
|
+
* @param {object} runner - ProcessRunner instance
|
|
951
|
+
* @param {Array} argv - Command arguments
|
|
952
|
+
* @param {object} config - Spawn configuration
|
|
953
|
+
* @returns {Promise<object>} Result
|
|
954
|
+
*/
|
|
955
|
+
async function executeChildProcess(runner, argv, config) {
|
|
956
|
+
const { stdin, isInteractive } = config;
|
|
957
|
+
|
|
958
|
+
runner.child = spawnChild(argv, config);
|
|
959
|
+
|
|
960
|
+
if (runner.child) {
|
|
961
|
+
trace(
|
|
962
|
+
'ProcessRunner',
|
|
963
|
+
() =>
|
|
964
|
+
`Child process created | ${JSON.stringify({
|
|
965
|
+
pid: runner.child.pid,
|
|
966
|
+
detached: runner.child.options?.detached,
|
|
967
|
+
killed: runner.child.killed,
|
|
968
|
+
hasStdout: !!runner.child.stdout,
|
|
969
|
+
hasStderr: !!runner.child.stderr,
|
|
970
|
+
hasStdin: !!runner.child.stdin,
|
|
971
|
+
platform: process.platform,
|
|
972
|
+
command: runner.spec?.command?.slice(0, 100),
|
|
973
|
+
})}`
|
|
974
|
+
);
|
|
975
|
+
setupChildEventListeners(runner);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const childPid = runner.child?.pid;
|
|
979
|
+
const outPump = createStdoutPump(runner, childPid);
|
|
980
|
+
const errPump = createStderrPump(runner, childPid);
|
|
981
|
+
const stdinPumpPromise = handleStdin(runner, stdin, isInteractive);
|
|
982
|
+
const exited = createExitPromise(runner.child);
|
|
983
|
+
|
|
984
|
+
const code = await exited;
|
|
985
|
+
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
986
|
+
|
|
987
|
+
const finalExitCode = determineFinalExitCode(code, runner._cancelled);
|
|
988
|
+
const resultData = buildResultData(runner, finalExitCode);
|
|
989
|
+
|
|
990
|
+
trace(
|
|
991
|
+
'ProcessRunner',
|
|
992
|
+
() =>
|
|
993
|
+
`Process completed | ${JSON.stringify({
|
|
994
|
+
command: runner.command,
|
|
995
|
+
finalExitCode,
|
|
996
|
+
captured: runner.options.capture,
|
|
997
|
+
hasStdout: !!resultData.stdout,
|
|
998
|
+
hasStderr: !!resultData.stderr,
|
|
999
|
+
stdoutLength: resultData.stdout?.length || 0,
|
|
1000
|
+
stderrLength: resultData.stderr?.length || 0,
|
|
1001
|
+
stdoutPreview: resultData.stdout?.slice(0, 100),
|
|
1002
|
+
stderrPreview: resultData.stderr?.slice(0, 100),
|
|
1003
|
+
childPid: runner.child?.pid,
|
|
1004
|
+
cancelled: runner._cancelled,
|
|
1005
|
+
cancellationSignal: runner._cancellationSignal,
|
|
1006
|
+
platform: process.platform,
|
|
1007
|
+
runtime: isBun ? 'Bun' : 'Node.js',
|
|
1008
|
+
})}`
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
return {
|
|
1012
|
+
...resultData,
|
|
1013
|
+
text() {
|
|
1014
|
+
return Promise.resolve(resultData.stdout || '');
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Attach execution methods to ProcessRunner prototype
|
|
1021
|
+
* @param {Function} ProcessRunner - The ProcessRunner class
|
|
1022
|
+
* @param {Object} deps - Dependencies (virtualCommands, globalShellSettings, isVirtualCommandsEnabled)
|
|
1023
|
+
*/
|
|
1024
|
+
export function attachExecutionMethods(ProcessRunner, deps) {
|
|
1025
|
+
const { globalShellSettings } = deps;
|
|
1026
|
+
|
|
1027
|
+
// Unified start method
|
|
1028
|
+
ProcessRunner.prototype.start = function (options = {}) {
|
|
1029
|
+
const mode = options.mode || 'async';
|
|
1030
|
+
|
|
1031
|
+
trace(
|
|
1032
|
+
'ProcessRunner',
|
|
1033
|
+
() =>
|
|
1034
|
+
`start ENTER | ${JSON.stringify({
|
|
1035
|
+
mode,
|
|
1036
|
+
options,
|
|
1037
|
+
started: this.started,
|
|
1038
|
+
hasPromise: !!this.promise,
|
|
1039
|
+
hasChild: !!this.child,
|
|
1040
|
+
command: this.spec?.command?.slice(0, 50),
|
|
1041
|
+
})}`
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
if (Object.keys(options).length > 0 && !this.started) {
|
|
1045
|
+
trace(
|
|
1046
|
+
'ProcessRunner',
|
|
1047
|
+
() =>
|
|
1048
|
+
`BRANCH: options => MERGE | ${JSON.stringify({
|
|
1049
|
+
oldOptions: this.options,
|
|
1050
|
+
newOptions: options,
|
|
1051
|
+
})}`
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
this.options = { ...this.options, ...options };
|
|
1055
|
+
setupExternalAbortSignal(this);
|
|
1056
|
+
|
|
1057
|
+
if ('capture' in options) {
|
|
1058
|
+
reinitCaptureChunks(this);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
trace(
|
|
1062
|
+
'ProcessRunner',
|
|
1063
|
+
() =>
|
|
1064
|
+
`OPTIONS_MERGED | ${JSON.stringify({ finalOptions: this.options })}`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (mode === 'sync') {
|
|
1069
|
+
trace('ProcessRunner', () => `BRANCH: mode => sync`);
|
|
1070
|
+
return this._startSync();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
trace('ProcessRunner', () => `BRANCH: mode => async`);
|
|
1074
|
+
return this._startAsync();
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
ProcessRunner.prototype.sync = function () {
|
|
1078
|
+
return this.start({ mode: 'sync' });
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
ProcessRunner.prototype.async = function () {
|
|
1082
|
+
return this.start({ mode: 'async' });
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
ProcessRunner.prototype.run = function (options = {}) {
|
|
1086
|
+
trace(
|
|
1087
|
+
'ProcessRunner',
|
|
1088
|
+
() => `run ENTER | ${JSON.stringify({ options }, null, 2)}`
|
|
1089
|
+
);
|
|
1090
|
+
return this.start(options);
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
ProcessRunner.prototype._startAsync = function () {
|
|
1094
|
+
if (this.started) {
|
|
1095
|
+
return this.promise;
|
|
1096
|
+
}
|
|
1097
|
+
if (this.promise) {
|
|
1098
|
+
return this.promise;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
this.promise = this._doStartAsync();
|
|
1102
|
+
return this.promise;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
ProcessRunner.prototype._doStartAsync = async function () {
|
|
1106
|
+
trace(
|
|
1107
|
+
'ProcessRunner',
|
|
1108
|
+
() =>
|
|
1109
|
+
`_doStartAsync ENTER | ${JSON.stringify({
|
|
1110
|
+
mode: this.spec.mode,
|
|
1111
|
+
command: this.spec.command?.slice(0, 100),
|
|
1112
|
+
})}`
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
this.started = true;
|
|
1116
|
+
this._mode = 'async';
|
|
1117
|
+
|
|
1118
|
+
try {
|
|
1119
|
+
const { cwd, env, stdin } = this.options;
|
|
1120
|
+
|
|
1121
|
+
// Handle pipeline mode
|
|
1122
|
+
if (this.spec.mode === 'pipeline') {
|
|
1123
|
+
trace(
|
|
1124
|
+
'ProcessRunner',
|
|
1125
|
+
() =>
|
|
1126
|
+
`BRANCH: spec.mode => pipeline | ${JSON.stringify({
|
|
1127
|
+
hasSource: !!this.spec.source,
|
|
1128
|
+
hasDestination: !!this.spec.destination,
|
|
1129
|
+
})}`
|
|
1130
|
+
);
|
|
1131
|
+
return await this._runProgrammaticPipeline(
|
|
1132
|
+
this.spec.source,
|
|
1133
|
+
this.spec.destination
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Handle shell mode special cases
|
|
1138
|
+
if (this.spec.mode === 'shell') {
|
|
1139
|
+
const shellResult = await handleShellMode(this, deps);
|
|
1140
|
+
if (shellResult) {
|
|
1141
|
+
return shellResult;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Build command arguments
|
|
1146
|
+
const shell = findAvailableShell();
|
|
1147
|
+
const argv =
|
|
1148
|
+
this.spec.mode === 'shell'
|
|
1149
|
+
? [shell.cmd, ...shell.args, this.spec.command]
|
|
1150
|
+
: [this.spec.file, ...this.spec.args];
|
|
1151
|
+
|
|
1152
|
+
trace(
|
|
1153
|
+
'ProcessRunner',
|
|
1154
|
+
() =>
|
|
1155
|
+
`Constructed argv | ${JSON.stringify({
|
|
1156
|
+
mode: this.spec.mode,
|
|
1157
|
+
argv,
|
|
1158
|
+
originalCommand: this.spec.command,
|
|
1159
|
+
})}`
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
// Log command if tracing enabled
|
|
1163
|
+
const traceCmd =
|
|
1164
|
+
this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
1165
|
+
logShellTrace(globalShellSettings, traceCmd);
|
|
1166
|
+
|
|
1167
|
+
// Detect interactive mode
|
|
1168
|
+
const isInteractive = isInteractiveMode(stdin, this.options);
|
|
1169
|
+
|
|
1170
|
+
trace(
|
|
1171
|
+
'ProcessRunner',
|
|
1172
|
+
() =>
|
|
1173
|
+
`Interactive command detection | ${JSON.stringify({
|
|
1174
|
+
isInteractive,
|
|
1175
|
+
stdinInherit: stdin === 'inherit',
|
|
1176
|
+
stdinTTY: process.stdin.isTTY,
|
|
1177
|
+
stdoutTTY: process.stdout.isTTY,
|
|
1178
|
+
stderrTTY: process.stderr.isTTY,
|
|
1179
|
+
interactiveOption: this.options.interactive,
|
|
1180
|
+
})}`
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
// Execute child process
|
|
1184
|
+
const result = await executeChildProcess(this, argv, {
|
|
1185
|
+
cwd,
|
|
1186
|
+
env,
|
|
1187
|
+
stdin,
|
|
1188
|
+
isInteractive,
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
this.finish(result);
|
|
1192
|
+
|
|
1193
|
+
trace(
|
|
1194
|
+
'ProcessRunner',
|
|
1195
|
+
() =>
|
|
1196
|
+
`Process finished, result set | ${JSON.stringify({
|
|
1197
|
+
finished: this.finished,
|
|
1198
|
+
resultCode: this.result?.code,
|
|
1199
|
+
})}`
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
throwErrexitIfNeeded(this, globalShellSettings);
|
|
1203
|
+
|
|
1204
|
+
return this.result;
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
trace(
|
|
1207
|
+
'ProcessRunner',
|
|
1208
|
+
() =>
|
|
1209
|
+
`Caught error in _doStartAsync | ${JSON.stringify({
|
|
1210
|
+
errorMessage: error.message,
|
|
1211
|
+
errorCode: error.code,
|
|
1212
|
+
isCommandError: error.isCommandError,
|
|
1213
|
+
hasResult: !!error.result,
|
|
1214
|
+
command: this.spec?.command?.slice(0, 100),
|
|
1215
|
+
})}`
|
|
1216
|
+
);
|
|
1217
|
+
|
|
1218
|
+
if (!this.finished) {
|
|
1219
|
+
const errorResult = createResult({
|
|
1220
|
+
code: error.code ?? 1,
|
|
1221
|
+
stdout: error.stdout ?? '',
|
|
1222
|
+
stderr: error.stderr ?? error.message ?? '',
|
|
1223
|
+
stdin: '',
|
|
1224
|
+
});
|
|
1225
|
+
this.finish(errorResult);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
throw error;
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
ProcessRunner.prototype._pumpStdinTo = async function (child, captureChunks) {
|
|
1233
|
+
trace('ProcessRunner', () => `_pumpStdinTo ENTER`);
|
|
1234
|
+
if (!child.stdin) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
const bunWriter =
|
|
1239
|
+
isBun && child.stdin && typeof child.stdin.getWriter === 'function'
|
|
1240
|
+
? child.stdin.getWriter()
|
|
1241
|
+
: null;
|
|
1242
|
+
|
|
1243
|
+
for await (const chunk of process.stdin) {
|
|
1244
|
+
const buf = asBuffer(chunk);
|
|
1245
|
+
captureChunks && captureChunks.push(buf);
|
|
1246
|
+
if (bunWriter) {
|
|
1247
|
+
await bunWriter.write(buf);
|
|
1248
|
+
} else if (typeof child.stdin.write === 'function') {
|
|
1249
|
+
StreamUtils.addStdinErrorHandler(child.stdin, 'child stdin buffer');
|
|
1250
|
+
StreamUtils.safeStreamWrite(child.stdin, buf, 'child stdin buffer');
|
|
1251
|
+
} else if (isBun && typeof Bun.write === 'function') {
|
|
1252
|
+
await Bun.write(child.stdin, buf);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (bunWriter) {
|
|
1257
|
+
await bunWriter.close();
|
|
1258
|
+
} else if (typeof child.stdin.end === 'function') {
|
|
1259
|
+
child.stdin.end();
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
ProcessRunner.prototype._writeToStdin = async function (buf) {
|
|
1264
|
+
trace('ProcessRunner', () => `_writeToStdin | len=${buf?.length || 0}`);
|
|
1265
|
+
const bytes =
|
|
1266
|
+
buf instanceof Uint8Array
|
|
1267
|
+
? buf
|
|
1268
|
+
: new Uint8Array(buf.buffer, buf.byteOffset ?? 0, buf.byteLength);
|
|
1269
|
+
|
|
1270
|
+
if (await StreamUtils.writeToStream(this.child.stdin, bytes, 'stdin')) {
|
|
1271
|
+
if (StreamUtils.isBunStream(this.child.stdin)) {
|
|
1272
|
+
// Stream was already closed by writeToStream utility - no action needed
|
|
1273
|
+
} else if (StreamUtils.isNodeStream(this.child.stdin)) {
|
|
1274
|
+
try {
|
|
1275
|
+
this.child.stdin.end();
|
|
1276
|
+
} catch (_endError) {
|
|
1277
|
+
/* Expected when stream is already closed */
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
} else if (isBun && typeof Bun.write === 'function') {
|
|
1281
|
+
await Bun.write(this.child.stdin, buf);
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
ProcessRunner.prototype._forwardTTYStdin = function () {
|
|
1286
|
+
trace('ProcessRunner', () => `_forwardTTYStdin ENTER`);
|
|
1287
|
+
if (!process.stdin.isTTY || !this.child.stdin) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
try {
|
|
1292
|
+
if (process.stdin.setRawMode) {
|
|
1293
|
+
process.stdin.setRawMode(true);
|
|
1294
|
+
}
|
|
1295
|
+
process.stdin.resume();
|
|
1296
|
+
|
|
1297
|
+
const onData = (chunk) => {
|
|
1298
|
+
if (chunk[0] === 3) {
|
|
1299
|
+
this._sendSigintToChild();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (this.child.stdin?.write) {
|
|
1303
|
+
this.child.stdin.write(chunk);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
const cleanup = () => {
|
|
1308
|
+
process.stdin.removeListener('data', onData);
|
|
1309
|
+
if (process.stdin.setRawMode) {
|
|
1310
|
+
process.stdin.setRawMode(false);
|
|
1311
|
+
}
|
|
1312
|
+
process.stdin.pause();
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
process.stdin.on('data', onData);
|
|
1316
|
+
|
|
1317
|
+
const childExit = isBun
|
|
1318
|
+
? this.child.exited
|
|
1319
|
+
: new Promise((resolve) => {
|
|
1320
|
+
this.child.once('close', resolve);
|
|
1321
|
+
this.child.once('exit', resolve);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
childExit.then(cleanup).catch(cleanup);
|
|
1325
|
+
|
|
1326
|
+
return childExit;
|
|
1327
|
+
} catch (_error) {
|
|
1328
|
+
// TTY forwarding error - ignore
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
ProcessRunner.prototype._sendSigintToChild = function () {
|
|
1333
|
+
if (!this.child?.pid) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
if (isBun) {
|
|
1338
|
+
this.child.kill('SIGINT');
|
|
1339
|
+
} else {
|
|
1340
|
+
try {
|
|
1341
|
+
process.kill(-this.child.pid, 'SIGINT');
|
|
1342
|
+
} catch (_e) {
|
|
1343
|
+
process.kill(this.child.pid, 'SIGINT');
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} catch (_err) {
|
|
1347
|
+
// Error sending SIGINT - ignore
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
ProcessRunner.prototype._parseCommand = function (command) {
|
|
1352
|
+
const trimmed = command.trim();
|
|
1353
|
+
if (!trimmed) {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (trimmed.includes('|')) {
|
|
1358
|
+
return this._parsePipeline(trimmed);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const parts = trimmed.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
1362
|
+
if (parts.length === 0) {
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const cmd = parts[0];
|
|
1367
|
+
const args = parts.slice(1).map((arg) => {
|
|
1368
|
+
if (
|
|
1369
|
+
(arg.startsWith('"') && arg.endsWith('"')) ||
|
|
1370
|
+
(arg.startsWith("'") && arg.endsWith("'"))
|
|
1371
|
+
) {
|
|
1372
|
+
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
1373
|
+
}
|
|
1374
|
+
return { value: arg, quoted: false };
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
return { cmd, args, type: 'simple' };
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
ProcessRunner.prototype._parsePipeline = function (command) {
|
|
1381
|
+
const segments = [];
|
|
1382
|
+
let current = '';
|
|
1383
|
+
let inQuotes = false;
|
|
1384
|
+
let quoteChar = '';
|
|
1385
|
+
|
|
1386
|
+
for (let i = 0; i < command.length; i++) {
|
|
1387
|
+
const char = command[i];
|
|
1388
|
+
|
|
1389
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
1390
|
+
inQuotes = true;
|
|
1391
|
+
quoteChar = char;
|
|
1392
|
+
current += char;
|
|
1393
|
+
} else if (inQuotes && char === quoteChar) {
|
|
1394
|
+
inQuotes = false;
|
|
1395
|
+
quoteChar = '';
|
|
1396
|
+
current += char;
|
|
1397
|
+
} else if (!inQuotes && char === '|') {
|
|
1398
|
+
segments.push(current.trim());
|
|
1399
|
+
current = '';
|
|
1400
|
+
} else {
|
|
1401
|
+
current += char;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (current.trim()) {
|
|
1406
|
+
segments.push(current.trim());
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const commands = segments
|
|
1410
|
+
.map((segment) => {
|
|
1411
|
+
const parts = segment.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || [];
|
|
1412
|
+
if (parts.length === 0) {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const cmd = parts[0];
|
|
1417
|
+
const args = parts.slice(1).map((arg) => {
|
|
1418
|
+
if (
|
|
1419
|
+
(arg.startsWith('"') && arg.endsWith('"')) ||
|
|
1420
|
+
(arg.startsWith("'") && arg.endsWith("'"))
|
|
1421
|
+
) {
|
|
1422
|
+
return { value: arg.slice(1, -1), quoted: true, quoteChar: arg[0] };
|
|
1423
|
+
}
|
|
1424
|
+
return { value: arg, quoted: false };
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
return { cmd, args };
|
|
1428
|
+
})
|
|
1429
|
+
.filter(Boolean);
|
|
1430
|
+
|
|
1431
|
+
return { type: 'pipeline', commands };
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// Sync execution
|
|
1435
|
+
ProcessRunner.prototype._startSync = function () {
|
|
1436
|
+
trace('ProcessRunner', () => `_startSync ENTER`);
|
|
1437
|
+
|
|
1438
|
+
if (this.started) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
'Command already started - cannot run sync after async start'
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
this.started = true;
|
|
1445
|
+
this._mode = 'sync';
|
|
1446
|
+
|
|
1447
|
+
const { cwd, env, stdin } = this.options;
|
|
1448
|
+
const shell = findAvailableShell();
|
|
1449
|
+
const argv =
|
|
1450
|
+
this.spec.mode === 'shell'
|
|
1451
|
+
? [shell.cmd, ...shell.args, this.spec.command]
|
|
1452
|
+
: [this.spec.file, ...this.spec.args];
|
|
1453
|
+
|
|
1454
|
+
const traceCmd =
|
|
1455
|
+
this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
1456
|
+
logShellTrace(globalShellSettings, traceCmd);
|
|
1457
|
+
|
|
1458
|
+
const result = executeSyncProcess(argv, { cwd, env, stdin });
|
|
1459
|
+
return processSyncResult(this, result, globalShellSettings);
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
// Promise interface
|
|
1463
|
+
ProcessRunner.prototype.then = function (onFulfilled, onRejected) {
|
|
1464
|
+
if (!this.promise) {
|
|
1465
|
+
this.promise = this._startAsync();
|
|
1466
|
+
}
|
|
1467
|
+
return this.promise.then(onFulfilled, onRejected);
|
|
1468
|
+
};
|
|
1469
|
+
|
|
1470
|
+
ProcessRunner.prototype.catch = function (onRejected) {
|
|
1471
|
+
if (!this.promise) {
|
|
1472
|
+
this.promise = this._startAsync();
|
|
1473
|
+
}
|
|
1474
|
+
return this.promise.catch(onRejected);
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
ProcessRunner.prototype.finally = function (onFinally) {
|
|
1478
|
+
if (!this.promise) {
|
|
1479
|
+
this.promise = this._startAsync();
|
|
1480
|
+
}
|
|
1481
|
+
return this.promise.finally(() => {
|
|
1482
|
+
if (!this.finished) {
|
|
1483
|
+
this.finish(
|
|
1484
|
+
createResult({
|
|
1485
|
+
code: 1,
|
|
1486
|
+
stdout: '',
|
|
1487
|
+
stderr: 'Process terminated unexpectedly',
|
|
1488
|
+
stdin: '',
|
|
1489
|
+
})
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
if (onFinally) {
|
|
1493
|
+
onFinally();
|
|
1494
|
+
}
|
|
1495
|
+
});
|
|
1496
|
+
};
|
|
1497
|
+
}
|