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,312 @@
|
|
|
1
|
+
// ProcessRunner stream and kill methods - streaming and process termination
|
|
2
|
+
// Part of the modular ProcessRunner architecture
|
|
3
|
+
|
|
4
|
+
import { trace } from './$.trace.mjs';
|
|
5
|
+
import { createResult } from './$.result.mjs';
|
|
6
|
+
|
|
7
|
+
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Send a signal to a process and its group
|
|
11
|
+
* @param {number} pid - Process ID
|
|
12
|
+
* @param {string} sig - Signal name (e.g., 'SIGTERM', 'SIGKILL')
|
|
13
|
+
* @param {string} runtime - Runtime identifier for logging
|
|
14
|
+
* @returns {string[]} List of successful operations
|
|
15
|
+
*/
|
|
16
|
+
function sendSignalToProcess(pid, sig, runtime) {
|
|
17
|
+
const operations = [];
|
|
18
|
+
const prefix = runtime === 'Bun' ? 'Bun ' : '';
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
process.kill(pid, sig);
|
|
22
|
+
trace('ProcessRunner', () => `Sent ${sig} to ${prefix}process ${pid}`);
|
|
23
|
+
operations.push(`${sig} to process`);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
trace(
|
|
26
|
+
'ProcessRunner',
|
|
27
|
+
() => `Error sending ${sig} to ${prefix}process: ${err.message}`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
process.kill(-pid, sig);
|
|
33
|
+
trace(
|
|
34
|
+
'ProcessRunner',
|
|
35
|
+
() => `Sent ${sig} to ${prefix}process group -${pid}`
|
|
36
|
+
);
|
|
37
|
+
operations.push(`${sig} to group`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
trace(
|
|
40
|
+
'ProcessRunner',
|
|
41
|
+
() => `${prefix}process group ${sig} failed: ${err.message}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return operations;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Kill a child process with escalating signals
|
|
50
|
+
* @param {object} child - Child process object
|
|
51
|
+
*/
|
|
52
|
+
function killChildProcess(child) {
|
|
53
|
+
if (!child || !child.pid) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const runtime = isBun ? 'Bun' : 'Node';
|
|
58
|
+
trace(
|
|
59
|
+
'ProcessRunner',
|
|
60
|
+
() =>
|
|
61
|
+
`Killing ${runtime} process | ${JSON.stringify({ pid: child.pid }, null, 2)}`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const killOperations = [];
|
|
65
|
+
killOperations.push(...sendSignalToProcess(child.pid, 'SIGTERM', runtime));
|
|
66
|
+
killOperations.push(...sendSignalToProcess(child.pid, 'SIGKILL', runtime));
|
|
67
|
+
|
|
68
|
+
trace(
|
|
69
|
+
'ProcessRunner',
|
|
70
|
+
() => `${runtime} kill operations attempted: ${killOperations.join(', ')}`
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (isBun) {
|
|
74
|
+
try {
|
|
75
|
+
child.kill();
|
|
76
|
+
trace(
|
|
77
|
+
'ProcessRunner',
|
|
78
|
+
() => `Called child.kill() for Bun process ${child.pid}`
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
trace(
|
|
82
|
+
'ProcessRunner',
|
|
83
|
+
() => `Error calling child.kill(): ${err.message}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
child.removeAllListeners?.();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Kill pipeline components (source and destination)
|
|
93
|
+
* @param {object} spec - Process runner spec
|
|
94
|
+
* @param {string} signal - Kill signal
|
|
95
|
+
*/
|
|
96
|
+
function killPipelineComponents(spec, signal) {
|
|
97
|
+
if (spec?.mode !== 'pipeline') {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
trace('ProcessRunner', () => 'Killing pipeline components');
|
|
101
|
+
if (spec.source && typeof spec.source.kill === 'function') {
|
|
102
|
+
spec.source.kill(signal);
|
|
103
|
+
}
|
|
104
|
+
if (spec.destination && typeof spec.destination.kill === 'function') {
|
|
105
|
+
spec.destination.kill(signal);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Handle abort controller during kill
|
|
111
|
+
* @param {AbortController} controller - The abort controller
|
|
112
|
+
*/
|
|
113
|
+
function abortController(controller) {
|
|
114
|
+
if (!controller) {
|
|
115
|
+
trace('ProcessRunner', () => 'No abort controller to abort');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
trace(
|
|
119
|
+
'ProcessRunner',
|
|
120
|
+
() =>
|
|
121
|
+
`Aborting internal controller | ${JSON.stringify({ wasAborted: controller?.signal?.aborted }, null, 2)}`
|
|
122
|
+
);
|
|
123
|
+
controller.abort();
|
|
124
|
+
trace(
|
|
125
|
+
'ProcessRunner',
|
|
126
|
+
() =>
|
|
127
|
+
`Internal controller aborted | ${JSON.stringify({ nowAborted: controller?.signal?.aborted }, null, 2)}`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle virtual generator cleanup during kill
|
|
133
|
+
* @param {object} generator - The virtual generator
|
|
134
|
+
* @param {string} signal - Kill signal
|
|
135
|
+
*/
|
|
136
|
+
function cleanupVirtualGenerator(generator, signal) {
|
|
137
|
+
if (!generator) {
|
|
138
|
+
trace(
|
|
139
|
+
'ProcessRunner',
|
|
140
|
+
() =>
|
|
141
|
+
`No virtual generator to cleanup | ${JSON.stringify({ hasVirtualGenerator: false }, null, 2)}`
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
trace(
|
|
147
|
+
'ProcessRunner',
|
|
148
|
+
() =>
|
|
149
|
+
`Virtual generator found for cleanup | ${JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
hasReturn: typeof generator.return === 'function',
|
|
152
|
+
hasThrow: typeof generator.throw === 'function',
|
|
153
|
+
signal,
|
|
154
|
+
},
|
|
155
|
+
null,
|
|
156
|
+
2
|
|
157
|
+
)}`
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (generator.return) {
|
|
161
|
+
trace('ProcessRunner', () => 'Closing virtual generator with return()');
|
|
162
|
+
try {
|
|
163
|
+
generator.return();
|
|
164
|
+
trace('ProcessRunner', () => 'Virtual generator closed successfully');
|
|
165
|
+
} catch (err) {
|
|
166
|
+
trace(
|
|
167
|
+
'ProcessRunner',
|
|
168
|
+
() =>
|
|
169
|
+
`Error closing generator | ${JSON.stringify({ error: err.message, stack: err.stack?.slice(0, 200) }, null, 2)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
trace('ProcessRunner', () => 'Virtual generator has no return() method');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get exit code for signal
|
|
179
|
+
* @param {string} signal - Signal name
|
|
180
|
+
* @returns {number} Exit code
|
|
181
|
+
*/
|
|
182
|
+
function getSignalExitCode(signal) {
|
|
183
|
+
if (signal === 'SIGKILL') {
|
|
184
|
+
return 137;
|
|
185
|
+
}
|
|
186
|
+
if (signal === 'SIGTERM') {
|
|
187
|
+
return 143;
|
|
188
|
+
}
|
|
189
|
+
return 130;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Kill the runner and create result
|
|
194
|
+
* @param {object} runner - ProcessRunner instance
|
|
195
|
+
* @param {string} signal - Kill signal
|
|
196
|
+
*/
|
|
197
|
+
function killRunner(runner, signal) {
|
|
198
|
+
runner._cancelled = true;
|
|
199
|
+
runner._cancellationSignal = signal;
|
|
200
|
+
killPipelineComponents(runner.spec, signal);
|
|
201
|
+
|
|
202
|
+
if (runner._cancelResolve) {
|
|
203
|
+
trace('ProcessRunner', () => 'Resolving cancel promise');
|
|
204
|
+
runner._cancelResolve();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
abortController(runner._abortController);
|
|
208
|
+
cleanupVirtualGenerator(runner._virtualGenerator, signal);
|
|
209
|
+
|
|
210
|
+
if (runner.child && !runner.finished) {
|
|
211
|
+
trace('ProcessRunner', () => `Killing child process ${runner.child.pid}`);
|
|
212
|
+
try {
|
|
213
|
+
killChildProcess(runner.child);
|
|
214
|
+
runner.child = null;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
trace('ProcessRunner', () => `Error killing process: ${err.message}`);
|
|
217
|
+
console.error('Error killing process:', err.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const result = createResult({
|
|
222
|
+
code: getSignalExitCode(signal),
|
|
223
|
+
stdout: '',
|
|
224
|
+
stderr: `Process killed with ${signal}`,
|
|
225
|
+
stdin: '',
|
|
226
|
+
});
|
|
227
|
+
runner.finish(result);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Attach stream and kill methods to ProcessRunner prototype
|
|
232
|
+
* @param {Function} ProcessRunner - The ProcessRunner class
|
|
233
|
+
* @param {Object} deps - Dependencies (not used but kept for consistency)
|
|
234
|
+
*/
|
|
235
|
+
export function attachStreamKillMethods(ProcessRunner) {
|
|
236
|
+
ProcessRunner.prototype[Symbol.asyncIterator] = async function* () {
|
|
237
|
+
yield* this.stream();
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
ProcessRunner.prototype.stream = async function* () {
|
|
241
|
+
trace('ProcessRunner', () => `stream ENTER | started=${this.started}`);
|
|
242
|
+
this._isStreaming = true;
|
|
243
|
+
if (!this.started) {
|
|
244
|
+
this._startAsync();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let buffer = [];
|
|
248
|
+
let resolve, _reject;
|
|
249
|
+
let ended = false;
|
|
250
|
+
let killed = false;
|
|
251
|
+
|
|
252
|
+
const onData = (chunk) => {
|
|
253
|
+
if (!killed) {
|
|
254
|
+
buffer.push(chunk);
|
|
255
|
+
if (resolve) {
|
|
256
|
+
resolve();
|
|
257
|
+
resolve = _reject = null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const onEnd = () => {
|
|
263
|
+
ended = true;
|
|
264
|
+
if (resolve) {
|
|
265
|
+
resolve();
|
|
266
|
+
resolve = _reject = null;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
this.on('data', onData);
|
|
271
|
+
this.on('end', onEnd);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
while (!ended || buffer.length > 0) {
|
|
275
|
+
if (killed) {
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (buffer.length > 0) {
|
|
279
|
+
const chunk = buffer.shift();
|
|
280
|
+
this._streamYielding = true;
|
|
281
|
+
yield chunk;
|
|
282
|
+
this._streamYielding = false;
|
|
283
|
+
} else if (!ended) {
|
|
284
|
+
await new Promise((res, rej) => {
|
|
285
|
+
resolve = res;
|
|
286
|
+
_reject = rej;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} finally {
|
|
291
|
+
this.off('data', onData);
|
|
292
|
+
this.off('end', onEnd);
|
|
293
|
+
if (!this.finished) {
|
|
294
|
+
killed = true;
|
|
295
|
+
buffer = [];
|
|
296
|
+
this._streamBreaking = true;
|
|
297
|
+
this.kill();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
ProcessRunner.prototype.kill = function (signal = 'SIGTERM') {
|
|
303
|
+
trace(
|
|
304
|
+
'ProcessRunner',
|
|
305
|
+
() => `kill | signal=${signal} finished=${this.finished}`
|
|
306
|
+
);
|
|
307
|
+
if (this.finished) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
killRunner(this, signal);
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// ProcessRunner virtual command methods - virtual command execution
|
|
2
|
+
// Part of the modular ProcessRunner architecture
|
|
3
|
+
|
|
4
|
+
import { trace } from './$.trace.mjs';
|
|
5
|
+
import { safeWrite } from './$.stream-utils.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get stdin data from options
|
|
9
|
+
* @param {object} options - Runner options
|
|
10
|
+
* @returns {string} Stdin data
|
|
11
|
+
*/
|
|
12
|
+
function getStdinData(options) {
|
|
13
|
+
if (options.stdin && typeof options.stdin === 'string') {
|
|
14
|
+
return options.stdin;
|
|
15
|
+
}
|
|
16
|
+
if (options.stdin && Buffer.isBuffer(options.stdin)) {
|
|
17
|
+
return options.stdin.toString('utf8');
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get exit code for cancellation signal
|
|
24
|
+
* @param {string} signal - Cancellation signal
|
|
25
|
+
* @returns {number} Exit code
|
|
26
|
+
*/
|
|
27
|
+
function getCancellationExitCode(signal) {
|
|
28
|
+
if (signal === 'SIGINT') {
|
|
29
|
+
return 130;
|
|
30
|
+
}
|
|
31
|
+
if (signal === 'SIGTERM') {
|
|
32
|
+
return 143;
|
|
33
|
+
}
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create abort promise for non-generator handlers
|
|
39
|
+
* @param {AbortController} abortController - Abort controller
|
|
40
|
+
* @returns {Promise} Promise that rejects on abort
|
|
41
|
+
*/
|
|
42
|
+
function createAbortPromise(abortController) {
|
|
43
|
+
return new Promise((_, reject) => {
|
|
44
|
+
if (abortController && abortController.signal.aborted) {
|
|
45
|
+
reject(new Error('Command cancelled'));
|
|
46
|
+
}
|
|
47
|
+
if (abortController) {
|
|
48
|
+
abortController.signal.addEventListener('abort', () => {
|
|
49
|
+
reject(new Error('Command cancelled'));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Emit output and mirror if needed
|
|
57
|
+
* @param {object} runner - ProcessRunner instance
|
|
58
|
+
* @param {string} type - Output type (stdout/stderr)
|
|
59
|
+
* @param {string} data - Output data
|
|
60
|
+
*/
|
|
61
|
+
function emitOutput(runner, type, data) {
|
|
62
|
+
if (!data) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const buf = Buffer.from(data);
|
|
66
|
+
const stream = type === 'stdout' ? process.stdout : process.stderr;
|
|
67
|
+
if (runner.options.mirror) {
|
|
68
|
+
safeWrite(stream, buf);
|
|
69
|
+
}
|
|
70
|
+
runner._emitProcessedData(type, buf);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle error in virtual command
|
|
75
|
+
* @param {object} runner - ProcessRunner instance
|
|
76
|
+
* @param {Error} error - Error that occurred
|
|
77
|
+
* @param {object} shellSettings - Global shell settings
|
|
78
|
+
* @returns {object} Result object
|
|
79
|
+
*/
|
|
80
|
+
function handleVirtualError(runner, error, shellSettings) {
|
|
81
|
+
let exitCode = error.code ?? 1;
|
|
82
|
+
if (runner._cancelled && runner._cancellationSignal) {
|
|
83
|
+
exitCode = getCancellationExitCode(runner._cancellationSignal);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = {
|
|
87
|
+
code: exitCode,
|
|
88
|
+
stdout: error.stdout ?? '',
|
|
89
|
+
stderr: error.stderr ?? error.message,
|
|
90
|
+
stdin: '',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
emitOutput(runner, 'stderr', result.stderr);
|
|
94
|
+
runner.finish(result);
|
|
95
|
+
|
|
96
|
+
if (shellSettings.errexit) {
|
|
97
|
+
error.result = result;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run async generator handler
|
|
106
|
+
* @param {object} runner - ProcessRunner instance
|
|
107
|
+
* @param {Function} handler - Generator handler
|
|
108
|
+
* @param {Array} argValues - Argument values
|
|
109
|
+
* @param {string} stdinData - Stdin data
|
|
110
|
+
* @returns {Promise<object>} Result object
|
|
111
|
+
*/
|
|
112
|
+
async function runGeneratorHandler(runner, handler, argValues, stdinData) {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
const commandOptions = {
|
|
115
|
+
cwd: runner.options.cwd,
|
|
116
|
+
env: runner.options.env,
|
|
117
|
+
options: runner.options,
|
|
118
|
+
isCancelled: () => runner._cancelled,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const generator = handler({
|
|
122
|
+
args: argValues,
|
|
123
|
+
stdin: stdinData,
|
|
124
|
+
abortSignal: runner._abortController?.signal,
|
|
125
|
+
...commandOptions,
|
|
126
|
+
});
|
|
127
|
+
runner._virtualGenerator = generator;
|
|
128
|
+
|
|
129
|
+
const cancelPromise = new Promise((resolve) => {
|
|
130
|
+
runner._cancelResolve = resolve;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const iterator = generator[Symbol.asyncIterator]();
|
|
135
|
+
let done = false;
|
|
136
|
+
|
|
137
|
+
while (!done && !runner._cancelled) {
|
|
138
|
+
const result = await Promise.race([
|
|
139
|
+
iterator.next(),
|
|
140
|
+
cancelPromise.then(() => ({ done: true, cancelled: true })),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
if (result.cancelled || runner._cancelled) {
|
|
144
|
+
if (iterator.return) {
|
|
145
|
+
await iterator.return();
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
done = result.done;
|
|
151
|
+
|
|
152
|
+
if (!done && !runner._cancelled && !runner._streamBreaking) {
|
|
153
|
+
const buf = Buffer.from(result.value);
|
|
154
|
+
chunks.push(buf);
|
|
155
|
+
|
|
156
|
+
if (runner.options.mirror) {
|
|
157
|
+
safeWrite(process.stdout, buf);
|
|
158
|
+
}
|
|
159
|
+
runner._emitProcessedData('stdout', buf);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
runner._virtualGenerator = null;
|
|
164
|
+
runner._cancelResolve = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
code: 0,
|
|
169
|
+
stdout: runner.options.capture
|
|
170
|
+
? Buffer.concat(chunks).toString('utf8')
|
|
171
|
+
: undefined,
|
|
172
|
+
stderr: runner.options.capture ? '' : undefined,
|
|
173
|
+
stdin: runner.options.capture ? stdinData : undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Run regular (non-generator) handler
|
|
179
|
+
* @param {object} runner - ProcessRunner instance
|
|
180
|
+
* @param {Function} handler - Handler function
|
|
181
|
+
* @param {Array} argValues - Argument values
|
|
182
|
+
* @param {string} stdinData - Stdin data
|
|
183
|
+
* @returns {Promise<object>} Result object
|
|
184
|
+
*/
|
|
185
|
+
async function runRegularHandler(runner, handler, argValues, stdinData) {
|
|
186
|
+
const commandOptions = {
|
|
187
|
+
cwd: runner.options.cwd,
|
|
188
|
+
env: runner.options.env,
|
|
189
|
+
options: runner.options,
|
|
190
|
+
isCancelled: () => runner._cancelled,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handlerPromise = handler({
|
|
194
|
+
args: argValues,
|
|
195
|
+
stdin: stdinData,
|
|
196
|
+
abortSignal: runner._abortController?.signal,
|
|
197
|
+
...commandOptions,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const abortPromise = createAbortPromise(runner._abortController);
|
|
201
|
+
let result;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
result = await Promise.race([handlerPromise, abortPromise]);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err.message === 'Command cancelled') {
|
|
207
|
+
const exitCode = getCancellationExitCode(runner._cancellationSignal);
|
|
208
|
+
result = { code: exitCode, stdout: '', stderr: '' };
|
|
209
|
+
} else {
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
result = {
|
|
215
|
+
...result,
|
|
216
|
+
code: result.code ?? 0,
|
|
217
|
+
stdout: runner.options.capture ? (result.stdout ?? '') : undefined,
|
|
218
|
+
stderr: runner.options.capture ? (result.stderr ?? '') : undefined,
|
|
219
|
+
stdin: runner.options.capture ? stdinData : undefined,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
emitOutput(runner, 'stdout', result.stdout);
|
|
223
|
+
emitOutput(runner, 'stderr', result.stderr);
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Attach virtual command methods to ProcessRunner prototype
|
|
230
|
+
* @param {Function} ProcessRunner - The ProcessRunner class
|
|
231
|
+
* @param {Object} deps - Dependencies
|
|
232
|
+
*/
|
|
233
|
+
export function attachVirtualCommandMethods(ProcessRunner, deps) {
|
|
234
|
+
const { virtualCommands, globalShellSettings } = deps;
|
|
235
|
+
|
|
236
|
+
ProcessRunner.prototype._runVirtual = async function (
|
|
237
|
+
cmd,
|
|
238
|
+
args,
|
|
239
|
+
originalCommand = null
|
|
240
|
+
) {
|
|
241
|
+
trace('ProcessRunner', () => `_runVirtual | cmd=${cmd}`);
|
|
242
|
+
|
|
243
|
+
const handler = virtualCommands.get(cmd);
|
|
244
|
+
if (!handler) {
|
|
245
|
+
throw new Error(`Virtual command not found: ${cmd}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Special handling for streaming mode (stdin: "pipe")
|
|
250
|
+
if (this.options.stdin === 'pipe') {
|
|
251
|
+
const modifiedOptions = {
|
|
252
|
+
...this.options,
|
|
253
|
+
stdin: 'pipe',
|
|
254
|
+
_bypassVirtual: true,
|
|
255
|
+
};
|
|
256
|
+
const ProcessRunnerRef = this.constructor;
|
|
257
|
+
const realRunner = new ProcessRunnerRef(
|
|
258
|
+
{ mode: 'shell', command: originalCommand || cmd },
|
|
259
|
+
modifiedOptions
|
|
260
|
+
);
|
|
261
|
+
return await realRunner._doStartAsync();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const stdinData = getStdinData(this.options);
|
|
265
|
+
const argValues = args.map((arg) =>
|
|
266
|
+
arg.value !== undefined ? arg.value : arg
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (globalShellSettings.xtrace) {
|
|
270
|
+
console.log(`+ ${originalCommand || `${cmd} ${argValues.join(' ')}`}`);
|
|
271
|
+
}
|
|
272
|
+
if (globalShellSettings.verbose) {
|
|
273
|
+
console.log(`${originalCommand || `${cmd} ${argValues.join(' ')}`}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const isGenerator = handler.constructor.name === 'AsyncGeneratorFunction';
|
|
277
|
+
const result = isGenerator
|
|
278
|
+
? await runGeneratorHandler(this, handler, argValues, stdinData)
|
|
279
|
+
: await runRegularHandler(this, handler, argValues, stdinData);
|
|
280
|
+
|
|
281
|
+
this.finish(result);
|
|
282
|
+
|
|
283
|
+
if (globalShellSettings.errexit && result.code !== 0) {
|
|
284
|
+
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
285
|
+
error.code = result.code;
|
|
286
|
+
error.stdout = result.stdout;
|
|
287
|
+
error.stderr = result.stderr;
|
|
288
|
+
error.result = result;
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return result;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return handleVirtualError(this, error, globalShellSettings);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|