command-stream 0.3.1 → 0.4.0
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 +322 -29
- package/package.json +4 -4
- package/{$.mjs → src/$.mjs} +1202 -213
- package/src/$.utils.mjs +81 -0
- package/src/commands/$.basename.mjs +19 -0
- package/src/commands/$.cat.mjs +44 -0
- package/src/commands/$.cd.mjs +16 -0
- package/src/commands/$.cp.mjs +112 -0
- package/src/commands/$.dirname.mjs +12 -0
- package/src/commands/$.echo.mjs +15 -0
- package/src/commands/$.env.mjs +14 -0
- package/src/commands/$.exit.mjs +9 -0
- package/src/commands/$.false.mjs +3 -0
- package/src/commands/$.ls.mjs +79 -0
- package/src/commands/$.mkdir.mjs +45 -0
- package/src/commands/$.mv.mjs +89 -0
- package/src/commands/$.pwd.mjs +8 -0
- package/src/commands/$.rm.mjs +60 -0
- package/src/commands/$.seq.mjs +48 -0
- package/src/commands/$.sleep.mjs +72 -0
- package/src/commands/$.test.mjs +59 -0
- package/src/commands/$.touch.mjs +36 -0
- package/src/commands/$.true.mjs +5 -0
- package/src/commands/$.which.mjs +32 -0
- package/src/commands/$.yes.mjs +48 -0
package/{$.mjs → src/$.mjs}
RENAMED
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
// 3. EventEmitter: $`command`.on('data', chunk => ...).on('end', result => ...)
|
|
6
6
|
// 4. Stream access: $`command`.stdout, $`command`.stderr
|
|
7
7
|
|
|
8
|
-
import { createRequire } from 'module';
|
|
9
8
|
import cp from 'child_process';
|
|
10
|
-
import fs from 'fs';
|
|
11
9
|
import path from 'path';
|
|
12
10
|
|
|
13
11
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
@@ -54,8 +52,203 @@ function isInteractiveCommand(command) {
|
|
|
54
52
|
let parentStreamsMonitored = false;
|
|
55
53
|
const activeProcessRunners = new Set();
|
|
56
54
|
|
|
55
|
+
// Track if SIGINT handler has been installed
|
|
56
|
+
let sigintHandlerInstalled = false;
|
|
57
|
+
let sigintHandler = null; // Store reference to remove it later
|
|
58
|
+
|
|
59
|
+
function installSignalHandlers() {
|
|
60
|
+
// Check if our handler is actually installed (not just the flag)
|
|
61
|
+
// This is more robust against test cleanup that manually removes listeners
|
|
62
|
+
const currentListeners = process.listeners('SIGINT');
|
|
63
|
+
const hasOurHandler = currentListeners.some(l => {
|
|
64
|
+
const str = l.toString();
|
|
65
|
+
return str.includes('activeProcessRunners') &&
|
|
66
|
+
str.includes('ProcessRunner') &&
|
|
67
|
+
str.includes('activeChildren');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (sigintHandlerInstalled && hasOurHandler) {
|
|
71
|
+
trace('SignalHandler', () => 'SIGINT handler already installed, skipping');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Reset flag if handler was removed externally
|
|
76
|
+
if (sigintHandlerInstalled && !hasOurHandler) {
|
|
77
|
+
trace('SignalHandler', () => 'SIGINT handler flag was set but handler missing, resetting');
|
|
78
|
+
sigintHandlerInstalled = false;
|
|
79
|
+
sigintHandler = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
trace('SignalHandler', () => `Installing SIGINT handler | ${JSON.stringify({ activeRunners: activeProcessRunners.size })}`);
|
|
83
|
+
sigintHandlerInstalled = true;
|
|
84
|
+
|
|
85
|
+
// Forward SIGINT to all active child processes
|
|
86
|
+
// The parent process continues running - it's up to the parent to decide what to do
|
|
87
|
+
sigintHandler = () => {
|
|
88
|
+
// Check for other handlers immediately at the start, before doing any processing
|
|
89
|
+
const currentListeners = process.listeners('SIGINT');
|
|
90
|
+
const hasOtherHandlers = currentListeners.length > 1;
|
|
91
|
+
|
|
92
|
+
trace('ProcessRunner', () => `SIGINT handler triggered - checking active processes`);
|
|
93
|
+
|
|
94
|
+
// Count active processes (both child processes and virtual commands)
|
|
95
|
+
const activeChildren = [];
|
|
96
|
+
for (const runner of activeProcessRunners) {
|
|
97
|
+
if (!runner.finished) {
|
|
98
|
+
// Real child process
|
|
99
|
+
if (runner.child && runner.child.pid) {
|
|
100
|
+
activeChildren.push(runner);
|
|
101
|
+
trace('ProcessRunner', () => `Found active child: PID ${runner.child.pid}, command: ${runner.spec?.command || 'unknown'}`);
|
|
102
|
+
}
|
|
103
|
+
// Virtual command (no child process but still active)
|
|
104
|
+
else if (!runner.child) {
|
|
105
|
+
activeChildren.push(runner);
|
|
106
|
+
trace('ProcessRunner', () => `Found active virtual command: ${runner.spec?.command || 'unknown'}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
trace('ProcessRunner', () => `Parent received SIGINT | ${JSON.stringify({
|
|
112
|
+
activeChildrenCount: activeChildren.length,
|
|
113
|
+
hasOtherHandlers,
|
|
114
|
+
platform: process.platform,
|
|
115
|
+
pid: process.pid,
|
|
116
|
+
ppid: process.ppid,
|
|
117
|
+
activeCommands: activeChildren.map(r => ({
|
|
118
|
+
hasChild: !!r.child,
|
|
119
|
+
childPid: r.child?.pid,
|
|
120
|
+
hasVirtualGenerator: !!r._virtualGenerator,
|
|
121
|
+
finished: r.finished,
|
|
122
|
+
command: r.spec?.command?.slice(0, 30)
|
|
123
|
+
}))
|
|
124
|
+
}, null, 2)}`);
|
|
125
|
+
|
|
126
|
+
// Only handle SIGINT if we have active child processes
|
|
127
|
+
// Otherwise, let other handlers or default behavior handle it
|
|
128
|
+
if (activeChildren.length === 0) {
|
|
129
|
+
trace('ProcessRunner', () => `No active children - skipping SIGINT forwarding, letting other handlers handle it`);
|
|
130
|
+
return; // Let other handlers or default behavior handle it
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
trace('ProcessRunner', () => `Beginning SIGINT forwarding to ${activeChildren.length} active processes`);
|
|
134
|
+
|
|
135
|
+
// Forward signal to all active processes (child processes and virtual commands)
|
|
136
|
+
for (const runner of activeChildren) {
|
|
137
|
+
try {
|
|
138
|
+
if (runner.child && runner.child.pid) {
|
|
139
|
+
// Real child process - send SIGINT to it
|
|
140
|
+
trace('ProcessRunner', () => `Sending SIGINT to child process | ${JSON.stringify({
|
|
141
|
+
pid: runner.child.pid,
|
|
142
|
+
killed: runner.child.killed,
|
|
143
|
+
runtime: isBun ? 'Bun' : 'Node.js',
|
|
144
|
+
command: runner.spec?.command?.slice(0, 50)
|
|
145
|
+
}, null, 2)}`);
|
|
146
|
+
|
|
147
|
+
if (isBun) {
|
|
148
|
+
runner.child.kill('SIGINT');
|
|
149
|
+
trace('ProcessRunner', () => `Bun: SIGINT sent to PID ${runner.child.pid}`);
|
|
150
|
+
} else {
|
|
151
|
+
// Send to process group if detached, otherwise to process directly
|
|
152
|
+
try {
|
|
153
|
+
process.kill(-runner.child.pid, 'SIGINT');
|
|
154
|
+
trace('ProcessRunner', () => `Node.js: SIGINT sent to process group -${runner.child.pid}`);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
trace('ProcessRunner', () => `Node.js: Process group kill failed, trying direct: ${err.message}`);
|
|
157
|
+
process.kill(runner.child.pid, 'SIGINT');
|
|
158
|
+
trace('ProcessRunner', () => `Node.js: SIGINT sent directly to PID ${runner.child.pid}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
// Virtual command - cancel it using the runner's kill method
|
|
163
|
+
trace('ProcessRunner', () => `Cancelling virtual command | ${JSON.stringify({
|
|
164
|
+
hasChild: !!runner.child,
|
|
165
|
+
hasVirtualGenerator: !!runner._virtualGenerator,
|
|
166
|
+
finished: runner.finished,
|
|
167
|
+
cancelled: runner._cancelled,
|
|
168
|
+
command: runner.spec?.command?.slice(0, 50)
|
|
169
|
+
}, null, 2)}`);
|
|
170
|
+
runner.kill('SIGINT');
|
|
171
|
+
trace('ProcessRunner', () => `Virtual command kill() called`);
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
trace('ProcessRunner', () => `Error in SIGINT handler for runner | ${JSON.stringify({
|
|
175
|
+
error: err.message,
|
|
176
|
+
stack: err.stack?.slice(0, 300),
|
|
177
|
+
hasPid: !!(runner.child && runner.child.pid),
|
|
178
|
+
pid: runner.child?.pid,
|
|
179
|
+
command: runner.spec?.command?.slice(0, 50)
|
|
180
|
+
}, null, 2)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// We've forwarded SIGINT to all active processes/commands
|
|
185
|
+
// Use the hasOtherHandlers flag we calculated at the start (before any processing)
|
|
186
|
+
trace('ProcessRunner', () => `SIGINT forwarded to ${activeChildren.length} active processes, other handlers: ${hasOtherHandlers}`);
|
|
187
|
+
|
|
188
|
+
if (!hasOtherHandlers) {
|
|
189
|
+
// No other handlers - we should exit like a proper shell
|
|
190
|
+
trace('ProcessRunner', () => `No other SIGINT handlers, exiting with code 130`);
|
|
191
|
+
// Ensure stdout/stderr are flushed before exiting
|
|
192
|
+
if (process.stdout && typeof process.stdout.write === 'function') {
|
|
193
|
+
process.stdout.write('', () => {
|
|
194
|
+
process.exit(130); // 128 + 2 (SIGINT)
|
|
195
|
+
});
|
|
196
|
+
} else {
|
|
197
|
+
process.exit(130); // 128 + 2 (SIGINT)
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Other handlers exist - let them handle the exit completely
|
|
201
|
+
// Do NOT call process.exit() ourselves when other handlers are present
|
|
202
|
+
trace('ProcessRunner', () => `Other SIGINT handlers present, letting them handle the exit completely`);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
process.on('SIGINT', sigintHandler);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function uninstallSignalHandlers() {
|
|
210
|
+
if (!sigintHandlerInstalled || !sigintHandler) {
|
|
211
|
+
trace('SignalHandler', () => 'SIGINT handler not installed or missing, skipping removal');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
trace('SignalHandler', () => `Removing SIGINT handler | ${JSON.stringify({ activeRunners: activeProcessRunners.size })}`);
|
|
216
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
217
|
+
sigintHandlerInstalled = false;
|
|
218
|
+
sigintHandler = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Force cleanup of all command-stream SIGINT handlers and state - for testing
|
|
222
|
+
function forceCleanupAll() {
|
|
223
|
+
// Remove all command-stream SIGINT handlers
|
|
224
|
+
const sigintListeners = process.listeners('SIGINT');
|
|
225
|
+
const commandStreamListeners = sigintListeners.filter(l => {
|
|
226
|
+
const str = l.toString();
|
|
227
|
+
return str.includes('activeProcessRunners') ||
|
|
228
|
+
str.includes('ProcessRunner') ||
|
|
229
|
+
str.includes('activeChildren');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
commandStreamListeners.forEach(listener => {
|
|
233
|
+
process.removeListener('SIGINT', listener);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Clear activeProcessRunners
|
|
237
|
+
activeProcessRunners.clear();
|
|
238
|
+
|
|
239
|
+
// Reset flags
|
|
240
|
+
sigintHandlerInstalled = false;
|
|
241
|
+
sigintHandler = null;
|
|
242
|
+
|
|
243
|
+
trace('SignalHandler', () => `Force cleanup completed - removed ${commandStreamListeners.length} handlers`);
|
|
244
|
+
}
|
|
245
|
+
|
|
57
246
|
function monitorParentStreams() {
|
|
58
|
-
if (parentStreamsMonitored)
|
|
247
|
+
if (parentStreamsMonitored) {
|
|
248
|
+
trace('StreamMonitor', () => 'Parent streams already monitored, skipping');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
trace('StreamMonitor', () => 'Setting up parent stream monitoring');
|
|
59
252
|
parentStreamsMonitored = true;
|
|
60
253
|
|
|
61
254
|
const checkParentStream = (stream, name) => {
|
|
@@ -279,78 +472,6 @@ const StreamUtils = {
|
|
|
279
472
|
}
|
|
280
473
|
};
|
|
281
474
|
|
|
282
|
-
// Virtual command utility functions for consistent behavior and error handling
|
|
283
|
-
const VirtualUtils = {
|
|
284
|
-
/**
|
|
285
|
-
* Create standardized error response for missing operands
|
|
286
|
-
*/
|
|
287
|
-
missingOperandError(commandName, customMessage = null) {
|
|
288
|
-
const message = customMessage || `${commandName}: missing operand`;
|
|
289
|
-
return { stderr: message, code: 1 };
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Create standardized error response for invalid arguments
|
|
294
|
-
*/
|
|
295
|
-
invalidArgumentError(commandName, message) {
|
|
296
|
-
return { stderr: `${commandName}: ${message}`, code: 1 };
|
|
297
|
-
},
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Create standardized success response
|
|
301
|
-
*/
|
|
302
|
-
success(stdout = '', code = 0) {
|
|
303
|
-
return { stdout, stderr: '', code };
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Create standardized error response
|
|
308
|
-
*/
|
|
309
|
-
error(stderr = '', code = 1) {
|
|
310
|
-
return { stdout: '', stderr, code };
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Validate that command has required number of arguments
|
|
315
|
-
*/
|
|
316
|
-
validateArgs(args, minCount, commandName) {
|
|
317
|
-
if (args.length < minCount) {
|
|
318
|
-
if (minCount === 1) {
|
|
319
|
-
return this.missingOperandError(commandName);
|
|
320
|
-
} else {
|
|
321
|
-
return this.invalidArgumentError(commandName, `requires at least ${minCount} arguments`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return null; // No error
|
|
325
|
-
},
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Resolve file path with optional cwd parameter
|
|
329
|
-
*/
|
|
330
|
-
resolvePath(filePath, cwd = null) {
|
|
331
|
-
const basePath = cwd || process.cwd();
|
|
332
|
-
return path.isAbsolute(filePath) ? filePath : path.resolve(basePath, filePath);
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Safe file system operation wrapper
|
|
337
|
-
*/
|
|
338
|
-
async safeFsOperation(operation, errorPrefix) {
|
|
339
|
-
try {
|
|
340
|
-
return await operation();
|
|
341
|
-
} catch (error) {
|
|
342
|
-
return this.error(`${errorPrefix}: ${error.message}`);
|
|
343
|
-
}
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Create async wrapper for Promise-based operations
|
|
348
|
-
*/
|
|
349
|
-
createAsyncWrapper(promiseFactory) {
|
|
350
|
-
return new Promise(promiseFactory);
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
|
|
354
475
|
let globalShellSettings = {
|
|
355
476
|
errexit: false, // set -e equivalent: exit on error
|
|
356
477
|
verbose: false, // set -v equivalent: print commands
|
|
@@ -394,6 +515,11 @@ class StreamEmitter {
|
|
|
394
515
|
|
|
395
516
|
emit(event, ...args) {
|
|
396
517
|
const eventListeners = this.listeners.get(event);
|
|
518
|
+
trace('StreamEmitter', () => `Emitting event | ${JSON.stringify({
|
|
519
|
+
event,
|
|
520
|
+
hasListeners: !!eventListeners,
|
|
521
|
+
listenerCount: eventListeners?.length || 0
|
|
522
|
+
})}`);
|
|
397
523
|
if (eventListeners) {
|
|
398
524
|
for (const listener of eventListeners) {
|
|
399
525
|
listener(...args);
|
|
@@ -498,20 +624,80 @@ class ProcessRunner extends StreamEmitter {
|
|
|
498
624
|
this._mode = null; // 'async' or 'sync'
|
|
499
625
|
|
|
500
626
|
this._cancelled = false;
|
|
627
|
+
this._cancellationSignal = null; // Track which signal caused cancellation
|
|
501
628
|
this._virtualGenerator = null;
|
|
502
629
|
this._abortController = new AbortController();
|
|
503
630
|
|
|
504
631
|
activeProcessRunners.add(this);
|
|
632
|
+
|
|
633
|
+
// Ensure parent stream monitoring is set up for all ProcessRunners
|
|
634
|
+
monitorParentStreams();
|
|
635
|
+
|
|
636
|
+
trace('ProcessRunner', () => `Added to activeProcessRunners | ${JSON.stringify({
|
|
637
|
+
command: this.spec?.command || 'unknown',
|
|
638
|
+
totalActive: activeProcessRunners.size
|
|
639
|
+
}, null, 2)}`);
|
|
640
|
+
installSignalHandlers();
|
|
505
641
|
|
|
506
|
-
|
|
507
|
-
this._finished = false;
|
|
642
|
+
this.finished = false;
|
|
508
643
|
}
|
|
509
644
|
|
|
510
|
-
|
|
511
|
-
|
|
645
|
+
// Stream property getters for child process streams (null for virtual commands)
|
|
646
|
+
get stdout() {
|
|
647
|
+
return this.child ? this.child.stdout : null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
get stderr() {
|
|
651
|
+
return this.child ? this.child.stderr : null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
get stdin() {
|
|
655
|
+
return this.child ? this.child.stdin : null;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Centralized method to properly finish a process with correct event emission order
|
|
659
|
+
finish(result) {
|
|
660
|
+
trace('ProcessRunner', () => `finish() called | ${JSON.stringify({
|
|
661
|
+
alreadyFinished: this.finished,
|
|
662
|
+
resultCode: result?.code,
|
|
663
|
+
hasStdout: !!result?.stdout,
|
|
664
|
+
hasStderr: !!result?.stderr,
|
|
665
|
+
command: this.spec?.command?.slice(0, 50)
|
|
666
|
+
}, null, 2)}`);
|
|
667
|
+
|
|
668
|
+
// Make finish() idempotent - safe to call multiple times
|
|
669
|
+
if (this.finished) {
|
|
670
|
+
trace('ProcessRunner', () => `Already finished, returning existing result`);
|
|
671
|
+
return this.result || result;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Store result
|
|
675
|
+
this.result = result;
|
|
676
|
+
trace('ProcessRunner', () => `Result stored, about to emit events`);
|
|
677
|
+
|
|
678
|
+
// Emit completion events BEFORE setting finished to prevent _cleanup() from clearing listeners
|
|
679
|
+
this.emit('end', result);
|
|
680
|
+
trace('ProcessRunner', () => `'end' event emitted`);
|
|
681
|
+
this.emit('exit', result.code);
|
|
682
|
+
trace('ProcessRunner', () => `'exit' event emitted with code ${result.code}`);
|
|
683
|
+
|
|
684
|
+
// Set finished after events are emitted
|
|
685
|
+
this.finished = true;
|
|
686
|
+
trace('ProcessRunner', () => `Marked as finished, calling cleanup`);
|
|
687
|
+
|
|
688
|
+
// Trigger cleanup now that process is finished
|
|
689
|
+
this._cleanup();
|
|
690
|
+
trace('ProcessRunner', () => `Cleanup completed`);
|
|
691
|
+
|
|
692
|
+
return result;
|
|
512
693
|
}
|
|
513
694
|
|
|
514
695
|
_emitProcessedData(type, buf) {
|
|
696
|
+
// Don't emit data if we've been cancelled
|
|
697
|
+
if (this._cancelled) {
|
|
698
|
+
trace('ProcessRunner', () => 'Skipping data emission - process cancelled');
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
515
701
|
const processedBuf = processOutput(buf, this.options.ansi);
|
|
516
702
|
this.emit(type, processedBuf);
|
|
517
703
|
this.emit('data', { type, data: processedBuf });
|
|
@@ -531,6 +717,36 @@ class ProcessRunner extends StreamEmitter {
|
|
|
531
717
|
|
|
532
718
|
// Forward stdin data to child process
|
|
533
719
|
const onData = (chunk) => {
|
|
720
|
+
// Check for CTRL+C (ASCII code 3)
|
|
721
|
+
if (chunk[0] === 3) {
|
|
722
|
+
trace('ProcessRunner', () => 'CTRL+C detected, sending SIGINT to child process');
|
|
723
|
+
// Send SIGINT to the child process
|
|
724
|
+
if (this.child && this.child.pid) {
|
|
725
|
+
try {
|
|
726
|
+
if (isBun) {
|
|
727
|
+
this.child.kill('SIGINT');
|
|
728
|
+
} else {
|
|
729
|
+
// In Node.js, send SIGINT to the process group if detached
|
|
730
|
+
// or to the process directly if not
|
|
731
|
+
if (this.child.pid > 0) {
|
|
732
|
+
try {
|
|
733
|
+
// Try process group first if detached
|
|
734
|
+
process.kill(-this.child.pid, 'SIGINT');
|
|
735
|
+
} catch (err) {
|
|
736
|
+
// Fall back to direct process
|
|
737
|
+
process.kill(this.child.pid, 'SIGINT');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} catch (err) {
|
|
742
|
+
trace('ProcessRunner', () => `Error sending SIGINT: ${err.message}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
// Don't forward CTRL+C to stdin, just handle the signal
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Forward other input to child stdin
|
|
534
750
|
if (this.child.stdin) {
|
|
535
751
|
if (isBun && this.child.stdin.write) {
|
|
536
752
|
this.child.stdin.write(chunk);
|
|
@@ -564,17 +780,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
564
780
|
}
|
|
565
781
|
}
|
|
566
782
|
|
|
567
|
-
set finished(value) {
|
|
568
|
-
if (value === true && this._finished === false) {
|
|
569
|
-
this._finished = true;
|
|
570
|
-
this._cleanup(); // Trigger cleanup when process finishes
|
|
571
|
-
} else {
|
|
572
|
-
this._finished = value;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
783
|
|
|
576
784
|
_handleParentStreamClosure() {
|
|
577
|
-
if (this.finished || this._cancelled)
|
|
785
|
+
if (this.finished || this._cancelled) {
|
|
786
|
+
trace('ProcessRunner', () => `Parent stream closure ignored | ${JSON.stringify({
|
|
787
|
+
finished: this.finished,
|
|
788
|
+
cancelled: this._cancelled
|
|
789
|
+
})}`);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
578
792
|
|
|
579
793
|
trace('ProcessRunner', () => `Handling parent stream closure | ${JSON.stringify({
|
|
580
794
|
started: this.started,
|
|
@@ -614,11 +828,111 @@ class ProcessRunner extends StreamEmitter {
|
|
|
614
828
|
}
|
|
615
829
|
}
|
|
616
830
|
|
|
617
|
-
|
|
831
|
+
this._cleanup();
|
|
618
832
|
}
|
|
619
833
|
|
|
620
834
|
_cleanup() {
|
|
835
|
+
trace('ProcessRunner', () => `_cleanup() called | ${JSON.stringify({
|
|
836
|
+
wasActiveBeforeCleanup: activeProcessRunners.has(this),
|
|
837
|
+
totalActiveBefore: activeProcessRunners.size,
|
|
838
|
+
finished: this.finished,
|
|
839
|
+
hasChild: !!this.child,
|
|
840
|
+
command: this.spec?.command?.slice(0, 50)
|
|
841
|
+
}, null, 2)}`);
|
|
842
|
+
|
|
843
|
+
const wasActive = activeProcessRunners.has(this);
|
|
621
844
|
activeProcessRunners.delete(this);
|
|
845
|
+
|
|
846
|
+
if (wasActive) {
|
|
847
|
+
trace('ProcessRunner', () => `Removed from activeProcessRunners | ${JSON.stringify({
|
|
848
|
+
command: this.spec?.command || 'unknown',
|
|
849
|
+
totalActiveAfter: activeProcessRunners.size,
|
|
850
|
+
remainingCommands: Array.from(activeProcessRunners).map(r => r.spec?.command?.slice(0, 30))
|
|
851
|
+
}, null, 2)}`);
|
|
852
|
+
} else {
|
|
853
|
+
trace('ProcessRunner', () => `Was not in activeProcessRunners (already cleaned up)`);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// If this is a pipeline runner, also clean up the source and destination
|
|
857
|
+
if (this.spec?.mode === 'pipeline') {
|
|
858
|
+
trace('ProcessRunner', () => 'Cleaning up pipeline components');
|
|
859
|
+
if (this.spec.source && typeof this.spec.source._cleanup === 'function') {
|
|
860
|
+
this.spec.source._cleanup();
|
|
861
|
+
}
|
|
862
|
+
if (this.spec.destination && typeof this.spec.destination._cleanup === 'function') {
|
|
863
|
+
this.spec.destination._cleanup();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// If no more active ProcessRunners, remove the SIGINT handler
|
|
868
|
+
if (activeProcessRunners.size === 0) {
|
|
869
|
+
uninstallSignalHandlers();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Clean up event listeners from StreamEmitter
|
|
873
|
+
if (this.listeners) {
|
|
874
|
+
this.listeners.clear();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Clean up abort controller
|
|
878
|
+
if (this._abortController) {
|
|
879
|
+
trace('ProcessRunner', () => `Cleaning up abort controller during cleanup | ${JSON.stringify({
|
|
880
|
+
wasAborted: this._abortController?.signal?.aborted
|
|
881
|
+
}, null, 2)}`);
|
|
882
|
+
try {
|
|
883
|
+
this._abortController.abort();
|
|
884
|
+
trace('ProcessRunner', () => `Abort controller aborted successfully during cleanup`);
|
|
885
|
+
} catch (e) {
|
|
886
|
+
trace('ProcessRunner', () => `Error aborting controller during cleanup: ${e.message}`);
|
|
887
|
+
}
|
|
888
|
+
this._abortController = null;
|
|
889
|
+
trace('ProcessRunner', () => `Abort controller reference cleared during cleanup`);
|
|
890
|
+
} else {
|
|
891
|
+
trace('ProcessRunner', () => `No abort controller to clean up during cleanup`);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Clean up child process reference
|
|
895
|
+
if (this.child) {
|
|
896
|
+
trace('ProcessRunner', () => `Cleaning up child process reference | ${JSON.stringify({
|
|
897
|
+
hasChild: true,
|
|
898
|
+
childPid: this.child.pid,
|
|
899
|
+
childKilled: this.child.killed
|
|
900
|
+
}, null, 2)}`);
|
|
901
|
+
try {
|
|
902
|
+
this.child.removeAllListeners?.();
|
|
903
|
+
trace('ProcessRunner', () => `Child process listeners removed successfully`);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
trace('ProcessRunner', () => `Error removing child process listeners: ${e.message}`);
|
|
906
|
+
}
|
|
907
|
+
this.child = null;
|
|
908
|
+
trace('ProcessRunner', () => `Child process reference cleared`);
|
|
909
|
+
} else {
|
|
910
|
+
trace('ProcessRunner', () => `No child process reference to clean up`);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Clean up virtual generator
|
|
914
|
+
if (this._virtualGenerator) {
|
|
915
|
+
trace('ProcessRunner', () => `Cleaning up virtual generator | ${JSON.stringify({
|
|
916
|
+
hasReturn: !!this._virtualGenerator.return
|
|
917
|
+
}, null, 2)}`);
|
|
918
|
+
try {
|
|
919
|
+
if (this._virtualGenerator.return) {
|
|
920
|
+
this._virtualGenerator.return();
|
|
921
|
+
trace('ProcessRunner', () => `Virtual generator return() called successfully`);
|
|
922
|
+
}
|
|
923
|
+
} catch (e) {
|
|
924
|
+
trace('ProcessRunner', () => `Error calling virtual generator return(): ${e.message}`);
|
|
925
|
+
}
|
|
926
|
+
this._virtualGenerator = null;
|
|
927
|
+
trace('ProcessRunner', () => `Virtual generator reference cleared`);
|
|
928
|
+
} else {
|
|
929
|
+
trace('ProcessRunner', () => `No virtual generator to clean up`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
trace('ProcessRunner', () => `_cleanup() completed | ${JSON.stringify({
|
|
933
|
+
totalActiveAfter: activeProcessRunners.size,
|
|
934
|
+
sigintListenerCount: process.listeners('SIGINT').length
|
|
935
|
+
}, null, 2)}`);
|
|
622
936
|
}
|
|
623
937
|
|
|
624
938
|
// Unified start method that can work in both async and sync modes
|
|
@@ -637,6 +951,79 @@ class ProcessRunner extends StreamEmitter {
|
|
|
637
951
|
// Create a new options object merging the current ones with the new ones
|
|
638
952
|
this.options = { ...this.options, ...options };
|
|
639
953
|
|
|
954
|
+
// Handle external abort signal
|
|
955
|
+
if (this.options.signal && typeof this.options.signal.addEventListener === 'function') {
|
|
956
|
+
trace('ProcessRunner', () => `Setting up external abort signal listener | ${JSON.stringify({
|
|
957
|
+
hasSignal: !!this.options.signal,
|
|
958
|
+
signalAborted: this.options.signal.aborted,
|
|
959
|
+
hasInternalController: !!this._abortController,
|
|
960
|
+
internalAborted: this._abortController?.signal.aborted
|
|
961
|
+
}, null, 2)}`);
|
|
962
|
+
|
|
963
|
+
this.options.signal.addEventListener('abort', () => {
|
|
964
|
+
trace('ProcessRunner', () => `External abort signal triggered | ${JSON.stringify({
|
|
965
|
+
externalSignalAborted: this.options.signal.aborted,
|
|
966
|
+
hasInternalController: !!this._abortController,
|
|
967
|
+
internalAborted: this._abortController?.signal.aborted,
|
|
968
|
+
command: this.spec?.command?.slice(0, 50)
|
|
969
|
+
}, null, 2)}`);
|
|
970
|
+
|
|
971
|
+
// Kill the process when abort signal is triggered
|
|
972
|
+
trace('ProcessRunner', () => `External abort signal received - killing process | ${JSON.stringify({
|
|
973
|
+
hasChild: !!this.child,
|
|
974
|
+
childPid: this.child?.pid,
|
|
975
|
+
finished: this.finished,
|
|
976
|
+
command: this.spec?.command?.slice(0, 50)
|
|
977
|
+
}, null, 2)}`);
|
|
978
|
+
this.kill('SIGTERM');
|
|
979
|
+
trace('ProcessRunner', () => 'Process kill initiated due to external abort signal');
|
|
980
|
+
|
|
981
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
982
|
+
trace('ProcessRunner', () => 'Aborting internal controller due to external signal');
|
|
983
|
+
this._abortController.abort();
|
|
984
|
+
trace('ProcessRunner', () => `Internal controller aborted | ${JSON.stringify({
|
|
985
|
+
internalAborted: this._abortController?.signal?.aborted
|
|
986
|
+
}, null, 2)}`);
|
|
987
|
+
} else {
|
|
988
|
+
trace('ProcessRunner', () => `Cannot abort internal controller | ${JSON.stringify({
|
|
989
|
+
hasInternalController: !!this._abortController,
|
|
990
|
+
internalAlreadyAborted: this._abortController?.signal?.aborted
|
|
991
|
+
}, null, 2)}`);
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// If the external signal is already aborted, abort immediately
|
|
996
|
+
if (this.options.signal.aborted) {
|
|
997
|
+
trace('ProcessRunner', () => `External signal already aborted, killing process and aborting internal controller | ${JSON.stringify({
|
|
998
|
+
hasInternalController: !!this._abortController,
|
|
999
|
+
internalAborted: this._abortController?.signal.aborted
|
|
1000
|
+
}, null, 2)}`);
|
|
1001
|
+
|
|
1002
|
+
// Kill the process immediately since signal is already aborted
|
|
1003
|
+
trace('ProcessRunner', () => `Signal already aborted - killing process immediately | ${JSON.stringify({
|
|
1004
|
+
hasChild: !!this.child,
|
|
1005
|
+
childPid: this.child?.pid,
|
|
1006
|
+
finished: this.finished,
|
|
1007
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1008
|
+
}, null, 2)}`);
|
|
1009
|
+
this.kill('SIGTERM');
|
|
1010
|
+
trace('ProcessRunner', () => 'Process kill initiated due to pre-aborted signal');
|
|
1011
|
+
|
|
1012
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
1013
|
+
this._abortController.abort();
|
|
1014
|
+
trace('ProcessRunner', () => `Internal controller aborted immediately | ${JSON.stringify({
|
|
1015
|
+
internalAborted: this._abortController?.signal?.aborted
|
|
1016
|
+
}, null, 2)}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
} else {
|
|
1020
|
+
trace('ProcessRunner', () => `No external signal to handle | ${JSON.stringify({
|
|
1021
|
+
hasSignal: !!this.options.signal,
|
|
1022
|
+
signalType: typeof this.options.signal,
|
|
1023
|
+
hasAddEventListener: !!(this.options.signal && typeof this.options.signal.addEventListener === 'function')
|
|
1024
|
+
}, null, 2)}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
640
1027
|
// Reinitialize chunks based on updated capture option
|
|
641
1028
|
if ('capture' in options) {
|
|
642
1029
|
trace('ProcessRunner', () => `BRANCH: capture => REINIT_CHUNKS | ${JSON.stringify({
|
|
@@ -699,6 +1086,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
699
1086
|
this.started = true;
|
|
700
1087
|
this._mode = 'async';
|
|
701
1088
|
|
|
1089
|
+
// Ensure cleanup happens even if execution fails
|
|
1090
|
+
try {
|
|
1091
|
+
|
|
702
1092
|
const { cwd, env, stdin } = this.options;
|
|
703
1093
|
|
|
704
1094
|
if (this.spec.mode === 'pipeline') {
|
|
@@ -726,25 +1116,55 @@ class ProcessRunner extends StreamEmitter {
|
|
|
726
1116
|
}, null, 2)}`);
|
|
727
1117
|
return await this._runPipeline(parsed.commands);
|
|
728
1118
|
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1119
|
+
// For built-in virtual commands that have real counterparts (like sleep),
|
|
1120
|
+
// skip the virtual version when custom stdin is provided to ensure proper process handling
|
|
1121
|
+
const hasCustomStdin = this.options.stdin &&
|
|
1122
|
+
this.options.stdin !== 'inherit' &&
|
|
1123
|
+
this.options.stdin !== 'ignore';
|
|
1124
|
+
|
|
1125
|
+
// List of built-in virtual commands that should fallback to real commands with custom stdin
|
|
1126
|
+
const builtinCommands = ['sleep', 'echo', 'pwd', 'true', 'false', 'yes', 'cat', 'ls', 'which'];
|
|
1127
|
+
const shouldBypassVirtual = hasCustomStdin && builtinCommands.includes(parsed.cmd);
|
|
1128
|
+
|
|
1129
|
+
if (shouldBypassVirtual) {
|
|
1130
|
+
trace('ProcessRunner', () => `Bypassing built-in virtual command due to custom stdin | ${JSON.stringify({
|
|
1131
|
+
cmd: parsed.cmd,
|
|
1132
|
+
stdin: typeof this.options.stdin
|
|
1133
|
+
}, null, 2)}`);
|
|
1134
|
+
// Fall through to run as real command
|
|
1135
|
+
} else {
|
|
1136
|
+
trace('ProcessRunner', () => `BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
|
|
1137
|
+
isVirtual: true,
|
|
1138
|
+
args: parsed.args
|
|
1139
|
+
}, null, 2)}`);
|
|
1140
|
+
trace('ProcessRunner', () => `Executing virtual command | ${JSON.stringify({
|
|
1141
|
+
cmd: parsed.cmd,
|
|
1142
|
+
argsLength: parsed.args.length,
|
|
1143
|
+
command: this.spec.command
|
|
1144
|
+
}, null, 2)}`);
|
|
1145
|
+
return await this._runVirtual(parsed.cmd, parsed.args, this.spec.command);
|
|
1146
|
+
}
|
|
734
1147
|
}
|
|
735
1148
|
}
|
|
736
1149
|
}
|
|
737
1150
|
|
|
738
1151
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1152
|
+
trace('ProcessRunner', () => `Constructed argv | ${JSON.stringify({
|
|
1153
|
+
mode: this.spec.mode,
|
|
1154
|
+
argv: argv,
|
|
1155
|
+
originalCommand: this.spec.command
|
|
1156
|
+
}, null, 2)}`);
|
|
739
1157
|
|
|
740
1158
|
if (globalShellSettings.xtrace) {
|
|
741
1159
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
742
1160
|
console.log(`+ ${traceCmd}`);
|
|
1161
|
+
trace('ProcessRunner', () => `xtrace output displayed: + ${traceCmd}`);
|
|
743
1162
|
}
|
|
744
1163
|
|
|
745
1164
|
if (globalShellSettings.verbose) {
|
|
746
1165
|
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
747
1166
|
console.log(verboseCmd);
|
|
1167
|
+
trace('ProcessRunner', () => `verbose output displayed: ${verboseCmd}`);
|
|
748
1168
|
}
|
|
749
1169
|
|
|
750
1170
|
// Detect if this is an interactive command that needs direct TTY access
|
|
@@ -754,28 +1174,154 @@ class ProcessRunner extends StreamEmitter {
|
|
|
754
1174
|
process.stdout.isTTY === true &&
|
|
755
1175
|
process.stderr.isTTY === true &&
|
|
756
1176
|
(this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file));
|
|
1177
|
+
|
|
1178
|
+
trace('ProcessRunner', () => `Interactive command detection | ${JSON.stringify({
|
|
1179
|
+
isInteractive,
|
|
1180
|
+
stdinInherit: stdin === 'inherit',
|
|
1181
|
+
stdinTTY: process.stdin.isTTY,
|
|
1182
|
+
stdoutTTY: process.stdout.isTTY,
|
|
1183
|
+
stderrTTY: process.stderr.isTTY,
|
|
1184
|
+
commandCheck: this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file)
|
|
1185
|
+
}, null, 2)}`);
|
|
757
1186
|
|
|
758
1187
|
const spawnBun = (argv) => {
|
|
1188
|
+
trace('ProcessRunner', () => `spawnBun: Creating process | ${JSON.stringify({
|
|
1189
|
+
command: argv[0],
|
|
1190
|
+
args: argv.slice(1),
|
|
1191
|
+
isInteractive,
|
|
1192
|
+
cwd,
|
|
1193
|
+
platform: process.platform
|
|
1194
|
+
}, null, 2)}`);
|
|
1195
|
+
|
|
759
1196
|
if (isInteractive) {
|
|
760
1197
|
// For interactive commands, use inherit to provide direct TTY access
|
|
761
|
-
|
|
1198
|
+
trace('ProcessRunner', () => `spawnBun: Using interactive mode with inherited stdio`);
|
|
1199
|
+
const child = Bun.spawn(argv, { cwd, env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' });
|
|
1200
|
+
trace('ProcessRunner', () => `spawnBun: Interactive process created | ${JSON.stringify({
|
|
1201
|
+
pid: child.pid,
|
|
1202
|
+
killed: child.killed
|
|
1203
|
+
}, null, 2)}`);
|
|
1204
|
+
return child;
|
|
762
1205
|
}
|
|
763
|
-
|
|
1206
|
+
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1207
|
+
// This allows us to send signals to the entire process group, killing shell and all its children
|
|
1208
|
+
trace('ProcessRunner', () => `spawnBun: Using non-interactive mode with pipes and detached=${process.platform !== 'win32'}`);
|
|
1209
|
+
const child = Bun.spawn(argv, {
|
|
1210
|
+
cwd,
|
|
1211
|
+
env,
|
|
1212
|
+
stdin: 'pipe',
|
|
1213
|
+
stdout: 'pipe',
|
|
1214
|
+
stderr: 'pipe',
|
|
1215
|
+
detached: process.platform !== 'win32' // Create process group on Unix-like systems
|
|
1216
|
+
});
|
|
1217
|
+
trace('ProcessRunner', () => `spawnBun: Non-interactive process created | ${JSON.stringify({
|
|
1218
|
+
pid: child.pid,
|
|
1219
|
+
killed: child.killed,
|
|
1220
|
+
hasStdout: !!child.stdout,
|
|
1221
|
+
hasStderr: !!child.stderr,
|
|
1222
|
+
hasStdin: !!child.stdin
|
|
1223
|
+
}, null, 2)}`);
|
|
1224
|
+
return child;
|
|
764
1225
|
};
|
|
765
1226
|
const spawnNode = async (argv) => {
|
|
1227
|
+
trace('ProcessRunner', () => `spawnNode: Creating process | ${JSON.stringify({
|
|
1228
|
+
command: argv[0],
|
|
1229
|
+
args: argv.slice(1),
|
|
1230
|
+
isInteractive,
|
|
1231
|
+
cwd,
|
|
1232
|
+
platform: process.platform
|
|
1233
|
+
})}`);
|
|
1234
|
+
|
|
766
1235
|
if (isInteractive) {
|
|
767
1236
|
// For interactive commands, use inherit to provide direct TTY access
|
|
768
1237
|
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: 'inherit' });
|
|
769
1238
|
}
|
|
770
|
-
|
|
1239
|
+
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1240
|
+
// This allows us to send signals to the entire process group
|
|
1241
|
+
const child = cp.spawn(argv[0], argv.slice(1), {
|
|
1242
|
+
cwd,
|
|
1243
|
+
env,
|
|
1244
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1245
|
+
detached: process.platform !== 'win32' // Create process group on Unix-like systems
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
trace('ProcessRunner', () => `spawnNode: Process created | ${JSON.stringify({
|
|
1249
|
+
pid: child.pid,
|
|
1250
|
+
killed: child.killed,
|
|
1251
|
+
hasStdout: !!child.stdout,
|
|
1252
|
+
hasStderr: !!child.stderr,
|
|
1253
|
+
hasStdin: !!child.stdin
|
|
1254
|
+
})}`);
|
|
1255
|
+
|
|
1256
|
+
return child;
|
|
771
1257
|
};
|
|
772
1258
|
|
|
773
1259
|
const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
|
|
774
1260
|
const preferNodeForInput = isBun && needsExplicitPipe;
|
|
1261
|
+
trace('ProcessRunner', () => `About to spawn process | ${JSON.stringify({
|
|
1262
|
+
needsExplicitPipe,
|
|
1263
|
+
preferNodeForInput,
|
|
1264
|
+
runtime: isBun ? 'Bun' : 'Node',
|
|
1265
|
+
command: argv[0],
|
|
1266
|
+
args: argv.slice(1)
|
|
1267
|
+
}, null, 2)}`);
|
|
775
1268
|
this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
|
|
1269
|
+
|
|
1270
|
+
// Add detailed logging for CI debugging
|
|
1271
|
+
if (this.child) {
|
|
1272
|
+
trace('ProcessRunner', () => `Child process created | ${JSON.stringify({
|
|
1273
|
+
pid: this.child.pid,
|
|
1274
|
+
detached: this.child.options?.detached,
|
|
1275
|
+
killed: this.child.killed,
|
|
1276
|
+
exitCode: this.child.exitCode,
|
|
1277
|
+
signalCode: this.child.signalCode,
|
|
1278
|
+
hasStdout: !!this.child.stdout,
|
|
1279
|
+
hasStderr: !!this.child.stderr,
|
|
1280
|
+
hasStdin: !!this.child.stdin,
|
|
1281
|
+
platform: process.platform,
|
|
1282
|
+
command: this.spec?.command?.slice(0, 100)
|
|
1283
|
+
}, null, 2)}`);
|
|
1284
|
+
|
|
1285
|
+
// Add event listeners with detailed tracing (only for Node.js child processes)
|
|
1286
|
+
if (this.child && typeof this.child.on === 'function') {
|
|
1287
|
+
this.child.on('spawn', () => {
|
|
1288
|
+
trace('ProcessRunner', () => `Child process spawned successfully | ${JSON.stringify({
|
|
1289
|
+
pid: this.child.pid,
|
|
1290
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1291
|
+
}, null, 2)}`);
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
this.child.on('error', (error) => {
|
|
1295
|
+
trace('ProcessRunner', () => `Child process error event | ${JSON.stringify({
|
|
1296
|
+
pid: this.child?.pid,
|
|
1297
|
+
error: error.message,
|
|
1298
|
+
code: error.code,
|
|
1299
|
+
errno: error.errno,
|
|
1300
|
+
syscall: error.syscall,
|
|
1301
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1302
|
+
}, null, 2)}`);
|
|
1303
|
+
});
|
|
1304
|
+
} else {
|
|
1305
|
+
trace('ProcessRunner', () => `Skipping event listeners - child does not support .on() method (likely Bun process)`);
|
|
1306
|
+
}
|
|
1307
|
+
} else {
|
|
1308
|
+
trace('ProcessRunner', () => `No child process created | ${JSON.stringify({
|
|
1309
|
+
spec: this.spec,
|
|
1310
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
1311
|
+
}, null, 2)}`);
|
|
1312
|
+
}
|
|
776
1313
|
|
|
777
1314
|
// For interactive commands with stdio: 'inherit', stdout/stderr will be null
|
|
1315
|
+
const childPid = this.child?.pid; // Capture PID once at the start
|
|
778
1316
|
const outPump = this.child.stdout ? pumpReadable(this.child.stdout, async (buf) => {
|
|
1317
|
+
trace('ProcessRunner', () => `stdout data received | ${JSON.stringify({
|
|
1318
|
+
pid: childPid,
|
|
1319
|
+
bufferLength: buf.length,
|
|
1320
|
+
capture: this.options.capture,
|
|
1321
|
+
mirror: this.options.mirror,
|
|
1322
|
+
preview: buf.toString().slice(0, 100)
|
|
1323
|
+
})}`);
|
|
1324
|
+
|
|
779
1325
|
if (this.options.capture) this.outChunks.push(buf);
|
|
780
1326
|
if (this.options.mirror) safeWrite(process.stdout, buf);
|
|
781
1327
|
|
|
@@ -784,6 +1330,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
784
1330
|
}) : Promise.resolve();
|
|
785
1331
|
|
|
786
1332
|
const errPump = this.child.stderr ? pumpReadable(this.child.stderr, async (buf) => {
|
|
1333
|
+
trace('ProcessRunner', () => `stderr data received | ${JSON.stringify({
|
|
1334
|
+
pid: childPid,
|
|
1335
|
+
bufferLength: buf.length,
|
|
1336
|
+
capture: this.options.capture,
|
|
1337
|
+
mirror: this.options.mirror,
|
|
1338
|
+
preview: buf.toString().slice(0, 100)
|
|
1339
|
+
})}`);
|
|
1340
|
+
|
|
787
1341
|
if (this.options.capture) this.errChunks.push(buf);
|
|
788
1342
|
if (this.options.mirror) safeWrite(process.stderr, buf);
|
|
789
1343
|
|
|
@@ -792,28 +1346,78 @@ class ProcessRunner extends StreamEmitter {
|
|
|
792
1346
|
}) : Promise.resolve();
|
|
793
1347
|
|
|
794
1348
|
let stdinPumpPromise = Promise.resolve();
|
|
1349
|
+
trace('ProcessRunner', () => `Setting up stdin handling | ${JSON.stringify({
|
|
1350
|
+
stdinType: typeof stdin,
|
|
1351
|
+
stdin: stdin === 'inherit' ? 'inherit' : stdin === 'ignore' ? 'ignore' : (typeof stdin === 'string' ? `string(${stdin.length})` : 'other'),
|
|
1352
|
+
isInteractive,
|
|
1353
|
+
hasChildStdin: !!this.child?.stdin,
|
|
1354
|
+
processTTY: process.stdin.isTTY
|
|
1355
|
+
}, null, 2)}`);
|
|
1356
|
+
|
|
795
1357
|
if (stdin === 'inherit') {
|
|
796
1358
|
if (isInteractive) {
|
|
797
1359
|
// For interactive commands with stdio: 'inherit', stdin is handled automatically
|
|
1360
|
+
trace('ProcessRunner', () => `stdin: Using inherit mode for interactive command`);
|
|
798
1361
|
stdinPumpPromise = Promise.resolve();
|
|
799
1362
|
} else {
|
|
800
1363
|
const isPipedIn = process.stdin && process.stdin.isTTY === false;
|
|
1364
|
+
trace('ProcessRunner', () => `stdin: Non-interactive inherit mode | ${JSON.stringify({
|
|
1365
|
+
isPipedIn,
|
|
1366
|
+
stdinTTY: process.stdin.isTTY
|
|
1367
|
+
}, null, 2)}`);
|
|
801
1368
|
if (isPipedIn) {
|
|
1369
|
+
trace('ProcessRunner', () => `stdin: Pumping piped input to child process`);
|
|
802
1370
|
stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
|
|
803
1371
|
} else {
|
|
804
1372
|
// For TTY (interactive terminal), forward stdin directly for non-interactive commands
|
|
1373
|
+
trace('ProcessRunner', () => `stdin: Forwarding TTY stdin for non-interactive command`);
|
|
805
1374
|
stdinPumpPromise = this._forwardTTYStdin();
|
|
806
1375
|
}
|
|
807
1376
|
}
|
|
808
1377
|
} else if (stdin === 'ignore') {
|
|
809
|
-
|
|
1378
|
+
trace('ProcessRunner', () => `stdin: Ignoring and closing stdin`);
|
|
1379
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') {
|
|
1380
|
+
this.child.stdin.end();
|
|
1381
|
+
trace('ProcessRunner', () => `stdin: Child stdin closed successfully`);
|
|
1382
|
+
}
|
|
810
1383
|
} else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
|
|
811
1384
|
const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
|
|
1385
|
+
trace('ProcessRunner', () => `stdin: Writing buffer to child | ${JSON.stringify({
|
|
1386
|
+
bufferLength: buf.length,
|
|
1387
|
+
willCapture: this.options.capture && !!this.inChunks
|
|
1388
|
+
}, null, 2)}`);
|
|
812
1389
|
if (this.options.capture && this.inChunks) this.inChunks.push(Buffer.from(buf));
|
|
813
1390
|
stdinPumpPromise = this._writeToStdin(buf);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
|
|
1391
|
+
} else {
|
|
1392
|
+
trace('ProcessRunner', () => `stdin: Unhandled stdin type: ${typeof stdin}`);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const exited = isBun ? this.child.exited : new Promise((resolve) => {
|
|
1396
|
+
trace('ProcessRunner', () => `Setting up child process event listeners for PID ${this.child.pid}`);
|
|
1397
|
+
this.child.on('close', (code, signal) => {
|
|
1398
|
+
trace('ProcessRunner', () => `Child process close event | ${JSON.stringify({
|
|
1399
|
+
pid: this.child.pid,
|
|
1400
|
+
code,
|
|
1401
|
+
signal,
|
|
1402
|
+
killed: this.child.killed,
|
|
1403
|
+
exitCode: this.child.exitCode,
|
|
1404
|
+
signalCode: this.child.signalCode,
|
|
1405
|
+
command: this.command
|
|
1406
|
+
}, null, 2)}`);
|
|
1407
|
+
resolve(code);
|
|
1408
|
+
});
|
|
1409
|
+
this.child.on('exit', (code, signal) => {
|
|
1410
|
+
trace('ProcessRunner', () => `Child process exit event | ${JSON.stringify({
|
|
1411
|
+
pid: this.child.pid,
|
|
1412
|
+
code,
|
|
1413
|
+
signal,
|
|
1414
|
+
killed: this.child.killed,
|
|
1415
|
+
exitCode: this.child.exitCode,
|
|
1416
|
+
signalCode: this.child.signalCode,
|
|
1417
|
+
command: this.command
|
|
1418
|
+
}, null, 2)}`);
|
|
1419
|
+
});
|
|
1420
|
+
});
|
|
817
1421
|
const code = await exited;
|
|
818
1422
|
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
819
1423
|
|
|
@@ -825,35 +1429,126 @@ class ProcessRunner extends StreamEmitter {
|
|
|
825
1429
|
isBun
|
|
826
1430
|
}, null, 2)}`);
|
|
827
1431
|
|
|
1432
|
+
// When a process is killed, it may not have an exit code
|
|
1433
|
+
// If cancelled and no exit code, assume it was killed with SIGTERM
|
|
1434
|
+
let finalExitCode = code;
|
|
1435
|
+
trace('ProcessRunner', () => `Processing exit code | ${JSON.stringify({
|
|
1436
|
+
rawCode: code,
|
|
1437
|
+
cancelled: this._cancelled,
|
|
1438
|
+
childKilled: this.child?.killed,
|
|
1439
|
+
childExitCode: this.child?.exitCode,
|
|
1440
|
+
childSignalCode: this.child?.signalCode
|
|
1441
|
+
}, null, 2)}`);
|
|
1442
|
+
|
|
1443
|
+
if (finalExitCode === undefined || finalExitCode === null) {
|
|
1444
|
+
if (this._cancelled) {
|
|
1445
|
+
// Process was killed, use SIGTERM exit code
|
|
1446
|
+
finalExitCode = 143; // 128 + 15 (SIGTERM)
|
|
1447
|
+
trace('ProcessRunner', () => `Process was killed, using SIGTERM exit code 143`);
|
|
1448
|
+
} else {
|
|
1449
|
+
// Process exited without a code, default to 0
|
|
1450
|
+
finalExitCode = 0;
|
|
1451
|
+
trace('ProcessRunner', () => `Process exited without code, defaulting to 0`);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
828
1455
|
const resultData = {
|
|
829
|
-
code:
|
|
1456
|
+
code: finalExitCode,
|
|
830
1457
|
stdout: this.options.capture ? (this.outChunks && this.outChunks.length > 0 ? Buffer.concat(this.outChunks).toString('utf8') : '') : undefined,
|
|
831
1458
|
stderr: this.options.capture ? (this.errChunks && this.errChunks.length > 0 ? Buffer.concat(this.errChunks).toString('utf8') : '') : undefined,
|
|
832
1459
|
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
833
1460
|
child: this.child
|
|
834
1461
|
};
|
|
1462
|
+
|
|
1463
|
+
trace('ProcessRunner', () => `Process completed | ${JSON.stringify({
|
|
1464
|
+
command: this.command,
|
|
1465
|
+
finalExitCode,
|
|
1466
|
+
captured: this.options.capture,
|
|
1467
|
+
hasStdout: !!resultData.stdout,
|
|
1468
|
+
hasStderr: !!resultData.stderr,
|
|
1469
|
+
stdoutLength: resultData.stdout?.length || 0,
|
|
1470
|
+
stderrLength: resultData.stderr?.length || 0,
|
|
1471
|
+
stdoutPreview: resultData.stdout?.slice(0, 100),
|
|
1472
|
+
stderrPreview: resultData.stderr?.slice(0, 100),
|
|
1473
|
+
childPid: this.child?.pid,
|
|
1474
|
+
cancelled: this._cancelled,
|
|
1475
|
+
cancellationSignal: this._cancellationSignal,
|
|
1476
|
+
platform: process.platform,
|
|
1477
|
+
runtime: isBun ? 'Bun' : 'Node.js'
|
|
1478
|
+
}, null, 2)}`);
|
|
835
1479
|
|
|
836
|
-
|
|
1480
|
+
const result = {
|
|
837
1481
|
...resultData,
|
|
838
1482
|
async text() {
|
|
839
1483
|
return resultData.stdout || '';
|
|
840
1484
|
}
|
|
841
1485
|
};
|
|
842
1486
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1487
|
+
trace('ProcessRunner', () => `About to finish process with result | ${JSON.stringify({
|
|
1488
|
+
exitCode: result.code,
|
|
1489
|
+
finished: this.finished
|
|
1490
|
+
}, null, 2)}`);
|
|
1491
|
+
|
|
1492
|
+
// Finish the process with proper event emission order
|
|
1493
|
+
this.finish(result);
|
|
1494
|
+
|
|
1495
|
+
trace('ProcessRunner', () => `Process finished, result set | ${JSON.stringify({
|
|
1496
|
+
finished: this.finished,
|
|
1497
|
+
resultCode: this.result?.code
|
|
1498
|
+
}, null, 2)}`);
|
|
846
1499
|
|
|
847
1500
|
if (globalShellSettings.errexit && this.result.code !== 0) {
|
|
1501
|
+
trace('ProcessRunner', () => `Errexit mode: throwing error for non-zero exit code | ${JSON.stringify({
|
|
1502
|
+
exitCode: this.result.code,
|
|
1503
|
+
errexit: globalShellSettings.errexit,
|
|
1504
|
+
hasStdout: !!this.result.stdout,
|
|
1505
|
+
hasStderr: !!this.result.stderr
|
|
1506
|
+
}, null, 2)}`);
|
|
1507
|
+
|
|
848
1508
|
const error = new Error(`Command failed with exit code ${this.result.code}`);
|
|
849
1509
|
error.code = this.result.code;
|
|
850
1510
|
error.stdout = this.result.stdout;
|
|
851
1511
|
error.stderr = this.result.stderr;
|
|
852
1512
|
error.result = this.result;
|
|
1513
|
+
|
|
1514
|
+
trace('ProcessRunner', () => `About to throw errexit error`);
|
|
853
1515
|
throw error;
|
|
854
1516
|
}
|
|
1517
|
+
|
|
1518
|
+
trace('ProcessRunner', () => `Returning result successfully | ${JSON.stringify({
|
|
1519
|
+
exitCode: this.result.code,
|
|
1520
|
+
errexit: globalShellSettings.errexit
|
|
1521
|
+
}, null, 2)}`);
|
|
855
1522
|
|
|
856
1523
|
return this.result;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
trace('ProcessRunner', () => `Caught error in _doStartAsync | ${JSON.stringify({
|
|
1526
|
+
errorMessage: error.message,
|
|
1527
|
+
errorCode: error.code,
|
|
1528
|
+
isCommandError: error.isCommandError,
|
|
1529
|
+
hasResult: !!error.result,
|
|
1530
|
+
command: this.spec?.command?.slice(0, 100)
|
|
1531
|
+
}, null, 2)}`);
|
|
1532
|
+
|
|
1533
|
+
// Ensure cleanup happens even if execution fails
|
|
1534
|
+
trace('ProcessRunner', () => `_doStartAsync caught error: ${error.message}`);
|
|
1535
|
+
|
|
1536
|
+
if (!this.finished) {
|
|
1537
|
+
// Create a result from the error
|
|
1538
|
+
const errorResult = createResult({
|
|
1539
|
+
code: error.code ?? 1,
|
|
1540
|
+
stdout: error.stdout ?? '',
|
|
1541
|
+
stderr: error.stderr ?? error.message ?? '',
|
|
1542
|
+
stdin: ''
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
// Finish to trigger cleanup
|
|
1546
|
+
this.finish(errorResult);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Re-throw the error after cleanup
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
857
1552
|
}
|
|
858
1553
|
|
|
859
1554
|
async _pumpStdinTo(child, captureChunks) {
|
|
@@ -1005,8 +1700,18 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1005
1700
|
const commandOptions = {
|
|
1006
1701
|
...this.options,
|
|
1007
1702
|
isCancelled: () => this._cancelled,
|
|
1008
|
-
signal: this._abortController
|
|
1703
|
+
signal: this._abortController?.signal
|
|
1009
1704
|
};
|
|
1705
|
+
|
|
1706
|
+
trace('ProcessRunner', () => `_runVirtual signal details | ${JSON.stringify({
|
|
1707
|
+
cmd,
|
|
1708
|
+
hasAbortController: !!this._abortController,
|
|
1709
|
+
signalAborted: this._abortController?.signal?.aborted,
|
|
1710
|
+
signalExists: !!commandOptions.signal,
|
|
1711
|
+
commandOptionsSignalAborted: commandOptions.signal?.aborted,
|
|
1712
|
+
optionsSignalExists: !!this.options.signal,
|
|
1713
|
+
optionsSignalAborted: this.options.signal?.aborted
|
|
1714
|
+
}, null, 2)}`);
|
|
1010
1715
|
|
|
1011
1716
|
const generator = handler({ args: argValues, stdin: stdinData, ...commandOptions });
|
|
1012
1717
|
this._virtualGenerator = generator;
|
|
@@ -1020,12 +1725,27 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1020
1725
|
let done = false;
|
|
1021
1726
|
|
|
1022
1727
|
while (!done && !this._cancelled) {
|
|
1728
|
+
trace('ProcessRunner', () => `Virtual command iteration starting | ${JSON.stringify({
|
|
1729
|
+
cancelled: this._cancelled,
|
|
1730
|
+
streamBreaking: this._streamBreaking
|
|
1731
|
+
}, null, 2)}`);
|
|
1732
|
+
|
|
1023
1733
|
const result = await Promise.race([
|
|
1024
1734
|
iterator.next(),
|
|
1025
1735
|
cancelPromise.then(() => ({ done: true, cancelled: true }))
|
|
1026
1736
|
]);
|
|
1027
1737
|
|
|
1738
|
+
trace('ProcessRunner', () => `Virtual command iteration result | ${JSON.stringify({
|
|
1739
|
+
hasValue: !!result.value,
|
|
1740
|
+
done: result.done,
|
|
1741
|
+
cancelled: result.cancelled || this._cancelled
|
|
1742
|
+
}, null, 2)}`);
|
|
1743
|
+
|
|
1028
1744
|
if (result.cancelled || this._cancelled) {
|
|
1745
|
+
trace('ProcessRunner', () => `Virtual command cancelled - closing generator | ${JSON.stringify({
|
|
1746
|
+
resultCancelled: result.cancelled,
|
|
1747
|
+
thisCancelled: this._cancelled
|
|
1748
|
+
}, null, 2)}`);
|
|
1029
1749
|
// Cancelled - close the generator
|
|
1030
1750
|
if (iterator.return) {
|
|
1031
1751
|
await iterator.return();
|
|
@@ -1036,18 +1756,35 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1036
1756
|
done = result.done;
|
|
1037
1757
|
|
|
1038
1758
|
if (!done) {
|
|
1759
|
+
// Check cancellation again before processing the chunk
|
|
1760
|
+
if (this._cancelled) {
|
|
1761
|
+
trace('ProcessRunner', () => 'Skipping chunk processing - cancelled during iteration');
|
|
1762
|
+
break;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1039
1765
|
const chunk = result.value;
|
|
1040
1766
|
const buf = Buffer.from(chunk);
|
|
1767
|
+
|
|
1768
|
+
// Check cancelled flag once more before any output
|
|
1769
|
+
if (this._cancelled || this._streamBreaking) {
|
|
1770
|
+
trace('ProcessRunner', () => `Cancelled or stream breaking before output - skipping | ${JSON.stringify({
|
|
1771
|
+
cancelled: this._cancelled,
|
|
1772
|
+
streamBreaking: this._streamBreaking
|
|
1773
|
+
}, null, 2)}`);
|
|
1774
|
+
break;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1041
1777
|
chunks.push(buf);
|
|
1042
1778
|
|
|
1043
|
-
// Only output if not cancelled
|
|
1044
|
-
if (!this._cancelled) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
this._emitProcessedData('stdout', buf);
|
|
1779
|
+
// Only output if not cancelled and stream not breaking
|
|
1780
|
+
if (!this._cancelled && !this._streamBreaking && this.options.mirror) {
|
|
1781
|
+
trace('ProcessRunner', () => `Mirroring virtual command output | ${JSON.stringify({
|
|
1782
|
+
chunkSize: buf.length
|
|
1783
|
+
}, null, 2)}`);
|
|
1784
|
+
safeWrite(process.stdout, buf);
|
|
1050
1785
|
}
|
|
1786
|
+
|
|
1787
|
+
this._emitProcessedData('stdout', buf);
|
|
1051
1788
|
}
|
|
1052
1789
|
}
|
|
1053
1790
|
} finally {
|
|
@@ -1063,8 +1800,53 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1063
1800
|
stdin: this.options.capture ? stdinData : undefined
|
|
1064
1801
|
};
|
|
1065
1802
|
} else {
|
|
1066
|
-
// Regular async function
|
|
1067
|
-
|
|
1803
|
+
// Regular async function - race with abort signal
|
|
1804
|
+
const commandOptions = {
|
|
1805
|
+
...this.options,
|
|
1806
|
+
isCancelled: () => this._cancelled,
|
|
1807
|
+
signal: this._abortController?.signal
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
trace('ProcessRunner', () => `_runVirtual signal details (non-generator) | ${JSON.stringify({
|
|
1811
|
+
cmd,
|
|
1812
|
+
hasAbortController: !!this._abortController,
|
|
1813
|
+
signalAborted: this._abortController?.signal?.aborted,
|
|
1814
|
+
signalExists: !!commandOptions.signal,
|
|
1815
|
+
commandOptionsSignalAborted: commandOptions.signal?.aborted,
|
|
1816
|
+
optionsSignalExists: !!this.options.signal,
|
|
1817
|
+
optionsSignalAborted: this.options.signal?.aborted
|
|
1818
|
+
}, null, 2)}`);
|
|
1819
|
+
|
|
1820
|
+
const handlerPromise = handler({ args: argValues, stdin: stdinData, ...commandOptions });
|
|
1821
|
+
|
|
1822
|
+
// Create an abort promise that rejects when cancelled
|
|
1823
|
+
const abortPromise = new Promise((_, reject) => {
|
|
1824
|
+
if (this._abortController && this._abortController.signal.aborted) {
|
|
1825
|
+
reject(new Error('Command cancelled'));
|
|
1826
|
+
}
|
|
1827
|
+
if (this._abortController) {
|
|
1828
|
+
this._abortController.signal.addEventListener('abort', () => {
|
|
1829
|
+
reject(new Error('Command cancelled'));
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
|
|
1834
|
+
try {
|
|
1835
|
+
result = await Promise.race([handlerPromise, abortPromise]);
|
|
1836
|
+
} catch (err) {
|
|
1837
|
+
if (err.message === 'Command cancelled') {
|
|
1838
|
+
// Command was cancelled, return appropriate exit code based on signal
|
|
1839
|
+
const exitCode = this._cancellationSignal === 'SIGINT' ? 130 : 143; // 130 for SIGINT, 143 for SIGTERM
|
|
1840
|
+
trace('ProcessRunner', () => `Virtual command cancelled with signal ${this._cancellationSignal}, exit code: ${exitCode}`);
|
|
1841
|
+
result = {
|
|
1842
|
+
code: exitCode,
|
|
1843
|
+
stdout: '',
|
|
1844
|
+
stderr: ''
|
|
1845
|
+
};
|
|
1846
|
+
} else {
|
|
1847
|
+
throw err;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1068
1850
|
|
|
1069
1851
|
result = {
|
|
1070
1852
|
...result,
|
|
@@ -1092,13 +1874,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1092
1874
|
}
|
|
1093
1875
|
}
|
|
1094
1876
|
|
|
1095
|
-
//
|
|
1096
|
-
this.result
|
|
1097
|
-
this.finished = true;
|
|
1098
|
-
|
|
1099
|
-
// Emit completion events
|
|
1100
|
-
this.emit('end', result);
|
|
1101
|
-
this.emit('exit', result.code);
|
|
1877
|
+
// Finish the process with proper event emission order
|
|
1878
|
+
this.finish(result);
|
|
1102
1879
|
|
|
1103
1880
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1104
1881
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
@@ -1118,9 +1895,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1118
1895
|
stdin: ''
|
|
1119
1896
|
};
|
|
1120
1897
|
|
|
1121
|
-
this.result = result;
|
|
1122
|
-
this.finished = true;
|
|
1123
|
-
|
|
1124
1898
|
if (result.stderr) {
|
|
1125
1899
|
const buf = Buffer.from(result.stderr);
|
|
1126
1900
|
if (this.options.mirror) {
|
|
@@ -1129,8 +1903,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1129
1903
|
this._emitProcessedData('stderr', buf);
|
|
1130
1904
|
}
|
|
1131
1905
|
|
|
1132
|
-
this.
|
|
1133
|
-
this.emit('exit', result.code);
|
|
1906
|
+
this.finish(result);
|
|
1134
1907
|
|
|
1135
1908
|
if (globalShellSettings.errexit) {
|
|
1136
1909
|
error.result = result;
|
|
@@ -1321,11 +2094,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1321
2094
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1322
2095
|
});
|
|
1323
2096
|
|
|
1324
|
-
|
|
1325
|
-
this.
|
|
1326
|
-
|
|
1327
|
-
this.emit('end', result);
|
|
1328
|
-
this.emit('exit', result.code);
|
|
2097
|
+
// Finish the process with proper event emission order
|
|
2098
|
+
this.finish(result);
|
|
1329
2099
|
|
|
1330
2100
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1331
2101
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
@@ -1499,11 +2269,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1499
2269
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1500
2270
|
});
|
|
1501
2271
|
|
|
1502
|
-
|
|
1503
|
-
this.
|
|
1504
|
-
|
|
1505
|
-
this.emit('end', result);
|
|
1506
|
-
this.emit('exit', result.code);
|
|
2272
|
+
// Finish the process with proper event emission order
|
|
2273
|
+
this.finish(result);
|
|
1507
2274
|
|
|
1508
2275
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1509
2276
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
@@ -1728,11 +2495,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1728
2495
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1729
2496
|
});
|
|
1730
2497
|
|
|
1731
|
-
|
|
1732
|
-
this.
|
|
1733
|
-
|
|
1734
|
-
this.emit('end', result);
|
|
1735
|
-
this.emit('exit', result.code);
|
|
2498
|
+
// Finish the process with proper event emission order
|
|
2499
|
+
this.finish(result);
|
|
1736
2500
|
|
|
1737
2501
|
return result;
|
|
1738
2502
|
}
|
|
@@ -1835,12 +2599,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1835
2599
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1836
2600
|
});
|
|
1837
2601
|
|
|
1838
|
-
|
|
1839
|
-
this.
|
|
1840
|
-
|
|
1841
|
-
// Emit completion events
|
|
1842
|
-
this.emit('end', finalResult);
|
|
1843
|
-
this.emit('exit', finalResult.code);
|
|
2602
|
+
// Finish the process with proper event emission order
|
|
2603
|
+
this.finish(finalResult);
|
|
1844
2604
|
|
|
1845
2605
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
1846
2606
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
@@ -1871,9 +2631,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1871
2631
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1872
2632
|
});
|
|
1873
2633
|
|
|
1874
|
-
this.result = result;
|
|
1875
|
-
this.finished = true;
|
|
1876
|
-
|
|
1877
2634
|
if (result.stderr) {
|
|
1878
2635
|
const buf = Buffer.from(result.stderr);
|
|
1879
2636
|
if (this.options.mirror) {
|
|
@@ -1882,8 +2639,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1882
2639
|
this._emitProcessedData('stderr', buf);
|
|
1883
2640
|
}
|
|
1884
2641
|
|
|
1885
|
-
this.
|
|
1886
|
-
this.emit('exit', result.code);
|
|
2642
|
+
this.finish(result);
|
|
1887
2643
|
|
|
1888
2644
|
if (globalShellSettings.errexit) {
|
|
1889
2645
|
throw error;
|
|
@@ -1928,17 +2684,47 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1928
2684
|
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
1929
2685
|
|
|
1930
2686
|
return new Promise((resolve, reject) => {
|
|
2687
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Creating child process | ${JSON.stringify({
|
|
2688
|
+
command: argv[0],
|
|
2689
|
+
args: argv.slice(1),
|
|
2690
|
+
cwd: this.options.cwd,
|
|
2691
|
+
isLastCommand
|
|
2692
|
+
})}`);
|
|
2693
|
+
|
|
1931
2694
|
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
1932
2695
|
cwd: this.options.cwd,
|
|
1933
2696
|
env: this.options.env,
|
|
1934
2697
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1935
2698
|
});
|
|
1936
2699
|
|
|
2700
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Child process created | ${JSON.stringify({
|
|
2701
|
+
pid: proc.pid,
|
|
2702
|
+
killed: proc.killed,
|
|
2703
|
+
hasStdout: !!proc.stdout,
|
|
2704
|
+
hasStderr: !!proc.stderr
|
|
2705
|
+
})}`);
|
|
2706
|
+
|
|
1937
2707
|
let stdout = '';
|
|
1938
2708
|
let stderr = '';
|
|
2709
|
+
let stdoutChunks = 0;
|
|
2710
|
+
let stderrChunks = 0;
|
|
1939
2711
|
|
|
2712
|
+
const procPid = proc.pid; // Capture PID once to avoid null reference
|
|
2713
|
+
|
|
1940
2714
|
proc.stdout.on('data', (chunk) => {
|
|
1941
|
-
|
|
2715
|
+
const chunkStr = chunk.toString();
|
|
2716
|
+
stdout += chunkStr;
|
|
2717
|
+
stdoutChunks++;
|
|
2718
|
+
|
|
2719
|
+
trace('ProcessRunner', () => `spawnNodeAsync: stdout chunk received | ${JSON.stringify({
|
|
2720
|
+
pid: procPid,
|
|
2721
|
+
chunkNumber: stdoutChunks,
|
|
2722
|
+
chunkLength: chunk.length,
|
|
2723
|
+
totalStdoutLength: stdout.length,
|
|
2724
|
+
isLastCommand,
|
|
2725
|
+
preview: chunkStr.slice(0, 100)
|
|
2726
|
+
})}`);
|
|
2727
|
+
|
|
1942
2728
|
// If this is the last command, emit streaming data
|
|
1943
2729
|
if (isLastCommand) {
|
|
1944
2730
|
if (this.options.mirror) {
|
|
@@ -1949,7 +2735,19 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1949
2735
|
});
|
|
1950
2736
|
|
|
1951
2737
|
proc.stderr.on('data', (chunk) => {
|
|
1952
|
-
|
|
2738
|
+
const chunkStr = chunk.toString();
|
|
2739
|
+
stderr += chunkStr;
|
|
2740
|
+
stderrChunks++;
|
|
2741
|
+
|
|
2742
|
+
trace('ProcessRunner', () => `spawnNodeAsync: stderr chunk received | ${JSON.stringify({
|
|
2743
|
+
pid: procPid,
|
|
2744
|
+
chunkNumber: stderrChunks,
|
|
2745
|
+
chunkLength: chunk.length,
|
|
2746
|
+
totalStderrLength: stderr.length,
|
|
2747
|
+
isLastCommand,
|
|
2748
|
+
preview: chunkStr.slice(0, 100)
|
|
2749
|
+
})}`);
|
|
2750
|
+
|
|
1953
2751
|
// If this is the last command, emit streaming data
|
|
1954
2752
|
if (isLastCommand) {
|
|
1955
2753
|
if (this.options.mirror) {
|
|
@@ -1960,6 +2758,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1960
2758
|
});
|
|
1961
2759
|
|
|
1962
2760
|
proc.on('close', (code) => {
|
|
2761
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Process closed | ${JSON.stringify({
|
|
2762
|
+
pid: procPid,
|
|
2763
|
+
code,
|
|
2764
|
+
stdoutLength: stdout.length,
|
|
2765
|
+
stderrLength: stderr.length,
|
|
2766
|
+
stdoutChunks,
|
|
2767
|
+
stderrChunks
|
|
2768
|
+
})}`);
|
|
2769
|
+
|
|
1963
2770
|
resolve({
|
|
1964
2771
|
status: code,
|
|
1965
2772
|
stdout,
|
|
@@ -2040,12 +2847,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2040
2847
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2041
2848
|
});
|
|
2042
2849
|
|
|
2043
|
-
|
|
2044
|
-
this.
|
|
2045
|
-
|
|
2046
|
-
// Emit completion events
|
|
2047
|
-
this.emit('end', finalResult);
|
|
2048
|
-
this.emit('exit', finalResult.code);
|
|
2850
|
+
// Finish the process with proper event emission order
|
|
2851
|
+
this.finish(finalResult);
|
|
2049
2852
|
|
|
2050
2853
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
2051
2854
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
@@ -2068,9 +2871,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2068
2871
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2069
2872
|
});
|
|
2070
2873
|
|
|
2071
|
-
this.result = result;
|
|
2072
|
-
this.finished = true;
|
|
2073
|
-
|
|
2074
2874
|
if (result.stderr) {
|
|
2075
2875
|
const buf = Buffer.from(result.stderr);
|
|
2076
2876
|
if (this.options.mirror) {
|
|
@@ -2079,8 +2879,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2079
2879
|
this._emitProcessedData('stderr', buf);
|
|
2080
2880
|
}
|
|
2081
2881
|
|
|
2082
|
-
this.
|
|
2083
|
-
this.emit('exit', result.code);
|
|
2882
|
+
this.finish(result);
|
|
2084
2883
|
|
|
2085
2884
|
if (globalShellSettings.errexit) {
|
|
2086
2885
|
throw error;
|
|
@@ -2163,17 +2962,13 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2163
2962
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2164
2963
|
});
|
|
2165
2964
|
|
|
2166
|
-
this.result = result;
|
|
2167
|
-
this.finished = true;
|
|
2168
|
-
|
|
2169
2965
|
const buf = Buffer.from(result.stderr);
|
|
2170
2966
|
if (this.options.mirror) {
|
|
2171
2967
|
safeWrite(process.stderr, buf);
|
|
2172
2968
|
}
|
|
2173
2969
|
this._emitProcessedData('stderr', buf);
|
|
2174
2970
|
|
|
2175
|
-
this.
|
|
2176
|
-
this.emit('exit', result.code);
|
|
2971
|
+
this.finish(result);
|
|
2177
2972
|
|
|
2178
2973
|
return result;
|
|
2179
2974
|
}
|
|
@@ -2194,12 +2989,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2194
2989
|
let resolve, reject;
|
|
2195
2990
|
let ended = false;
|
|
2196
2991
|
let cleanedUp = false;
|
|
2992
|
+
let killed = false;
|
|
2197
2993
|
|
|
2198
2994
|
const onData = (chunk) => {
|
|
2199
|
-
buffer
|
|
2200
|
-
if (
|
|
2201
|
-
|
|
2202
|
-
resolve
|
|
2995
|
+
// Don't buffer more data if we're being killed
|
|
2996
|
+
if (!killed) {
|
|
2997
|
+
buffer.push(chunk);
|
|
2998
|
+
if (resolve) {
|
|
2999
|
+
resolve();
|
|
3000
|
+
resolve = reject = null;
|
|
3001
|
+
}
|
|
2203
3002
|
}
|
|
2204
3003
|
};
|
|
2205
3004
|
|
|
@@ -2216,8 +3015,18 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2216
3015
|
|
|
2217
3016
|
try {
|
|
2218
3017
|
while (!ended || buffer.length > 0) {
|
|
3018
|
+
// Check if we've been killed and should stop immediately
|
|
3019
|
+
if (killed) {
|
|
3020
|
+
trace('ProcessRunner', () => 'Stream killed, stopping iteration');
|
|
3021
|
+
break;
|
|
3022
|
+
}
|
|
2219
3023
|
if (buffer.length > 0) {
|
|
2220
|
-
|
|
3024
|
+
const chunk = buffer.shift();
|
|
3025
|
+
// Set a flag that we're about to yield - if the consumer breaks,
|
|
3026
|
+
// we'll know not to process any more data
|
|
3027
|
+
this._streamYielding = true;
|
|
3028
|
+
yield chunk;
|
|
3029
|
+
this._streamYielding = false;
|
|
2221
3030
|
} else if (!ended) {
|
|
2222
3031
|
await new Promise((res, rej) => {
|
|
2223
3032
|
resolve = res;
|
|
@@ -2232,41 +3041,97 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2232
3041
|
|
|
2233
3042
|
// This happens when breaking from a for-await loop
|
|
2234
3043
|
if (!this.finished) {
|
|
3044
|
+
killed = true;
|
|
3045
|
+
buffer = []; // Clear any buffered data
|
|
3046
|
+
this._streamBreaking = true; // Signal that stream is breaking
|
|
2235
3047
|
this.kill();
|
|
2236
3048
|
}
|
|
2237
3049
|
}
|
|
2238
3050
|
}
|
|
2239
3051
|
|
|
2240
|
-
kill() {
|
|
3052
|
+
kill(signal = 'SIGTERM') {
|
|
2241
3053
|
trace('ProcessRunner', () => `kill ENTER | ${JSON.stringify({
|
|
3054
|
+
signal,
|
|
2242
3055
|
cancelled: this._cancelled,
|
|
2243
3056
|
finished: this.finished,
|
|
2244
3057
|
hasChild: !!this.child,
|
|
2245
|
-
hasVirtualGenerator: !!this._virtualGenerator
|
|
3058
|
+
hasVirtualGenerator: !!this._virtualGenerator,
|
|
3059
|
+
command: this.spec?.command?.slice(0, 50) || 'unknown'
|
|
2246
3060
|
}, null, 2)}`);
|
|
2247
3061
|
|
|
2248
|
-
|
|
3062
|
+
if (this.finished) {
|
|
3063
|
+
trace('ProcessRunner', () => 'Already finished, skipping kill');
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Mark as cancelled for virtual commands and store the signal
|
|
3068
|
+
trace('ProcessRunner', () => `Marking as cancelled | ${JSON.stringify({
|
|
3069
|
+
signal,
|
|
3070
|
+
previouslyCancelled: this._cancelled,
|
|
3071
|
+
previousSignal: this._cancellationSignal
|
|
3072
|
+
}, null, 2)}`);
|
|
2249
3073
|
this._cancelled = true;
|
|
3074
|
+
this._cancellationSignal = signal;
|
|
3075
|
+
|
|
3076
|
+
// If this is a pipeline runner, also kill the source and destination
|
|
3077
|
+
if (this.spec?.mode === 'pipeline') {
|
|
3078
|
+
trace('ProcessRunner', () => 'Killing pipeline components');
|
|
3079
|
+
if (this.spec.source && typeof this.spec.source.kill === 'function') {
|
|
3080
|
+
this.spec.source.kill(signal);
|
|
3081
|
+
}
|
|
3082
|
+
if (this.spec.destination && typeof this.spec.destination.kill === 'function') {
|
|
3083
|
+
this.spec.destination.kill(signal);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
2250
3086
|
|
|
2251
3087
|
if (this._cancelResolve) {
|
|
2252
3088
|
trace('ProcessRunner', () => 'Resolving cancel promise');
|
|
2253
3089
|
this._cancelResolve();
|
|
3090
|
+
trace('ProcessRunner', () => 'Cancel promise resolved');
|
|
3091
|
+
} else {
|
|
3092
|
+
trace('ProcessRunner', () => 'No cancel promise to resolve');
|
|
2254
3093
|
}
|
|
2255
3094
|
|
|
2256
3095
|
// Abort any async operations
|
|
2257
3096
|
if (this._abortController) {
|
|
2258
|
-
trace('ProcessRunner', () =>
|
|
3097
|
+
trace('ProcessRunner', () => `Aborting internal controller | ${JSON.stringify({
|
|
3098
|
+
wasAborted: this._abortController?.signal?.aborted
|
|
3099
|
+
}, null, 2)}`);
|
|
2259
3100
|
this._abortController.abort();
|
|
3101
|
+
trace('ProcessRunner', () => `Internal controller aborted | ${JSON.stringify({
|
|
3102
|
+
nowAborted: this._abortController?.signal?.aborted
|
|
3103
|
+
}, null, 2)}`);
|
|
3104
|
+
} else {
|
|
3105
|
+
trace('ProcessRunner', () => 'No abort controller to abort');
|
|
2260
3106
|
}
|
|
2261
3107
|
|
|
2262
3108
|
// If it's a virtual generator, try to close it
|
|
2263
|
-
if (this._virtualGenerator
|
|
2264
|
-
trace('ProcessRunner', () =>
|
|
2265
|
-
|
|
2266
|
-
this._virtualGenerator.
|
|
2267
|
-
|
|
2268
|
-
|
|
3109
|
+
if (this._virtualGenerator) {
|
|
3110
|
+
trace('ProcessRunner', () => `Virtual generator found for cleanup | ${JSON.stringify({
|
|
3111
|
+
hasReturn: typeof this._virtualGenerator.return === 'function',
|
|
3112
|
+
hasThrow: typeof this._virtualGenerator.throw === 'function',
|
|
3113
|
+
cancelled: this._cancelled,
|
|
3114
|
+
signal
|
|
3115
|
+
}, null, 2)}`);
|
|
3116
|
+
|
|
3117
|
+
if (this._virtualGenerator.return) {
|
|
3118
|
+
trace('ProcessRunner', () => 'Closing virtual generator with return()');
|
|
3119
|
+
try {
|
|
3120
|
+
this._virtualGenerator.return();
|
|
3121
|
+
trace('ProcessRunner', () => 'Virtual generator closed successfully');
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
trace('ProcessRunner', () => `Error closing generator | ${JSON.stringify({
|
|
3124
|
+
error: err.message,
|
|
3125
|
+
stack: err.stack?.slice(0, 200)
|
|
3126
|
+
}, null, 2)}`);
|
|
3127
|
+
}
|
|
3128
|
+
} else {
|
|
3129
|
+
trace('ProcessRunner', () => 'Virtual generator has no return() method');
|
|
2269
3130
|
}
|
|
3131
|
+
} else {
|
|
3132
|
+
trace('ProcessRunner', () => `No virtual generator to cleanup | ${JSON.stringify({
|
|
3133
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
3134
|
+
}, null, 2)}`);
|
|
2270
3135
|
}
|
|
2271
3136
|
|
|
2272
3137
|
// Kill child process if it exists
|
|
@@ -2276,14 +3141,112 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2276
3141
|
if (this.child.pid) {
|
|
2277
3142
|
if (isBun) {
|
|
2278
3143
|
trace('ProcessRunner', () => `Killing Bun process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
2279
|
-
|
|
3144
|
+
|
|
3145
|
+
// For Bun, use the same enhanced kill logic as Node.js for CI reliability
|
|
3146
|
+
const killOperations = [];
|
|
3147
|
+
|
|
3148
|
+
// Try SIGTERM first
|
|
3149
|
+
try {
|
|
3150
|
+
process.kill(this.child.pid, 'SIGTERM');
|
|
3151
|
+
trace('ProcessRunner', () => `Sent SIGTERM to Bun process ${this.child.pid}`);
|
|
3152
|
+
killOperations.push('SIGTERM to process');
|
|
3153
|
+
} catch (err) {
|
|
3154
|
+
trace('ProcessRunner', () => `Error sending SIGTERM to Bun process: ${err.message}`);
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
// Try process group SIGTERM
|
|
3158
|
+
try {
|
|
3159
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
3160
|
+
trace('ProcessRunner', () => `Sent SIGTERM to Bun process group -${this.child.pid}`);
|
|
3161
|
+
killOperations.push('SIGTERM to group');
|
|
3162
|
+
} catch (err) {
|
|
3163
|
+
trace('ProcessRunner', () => `Bun process group SIGTERM failed: ${err.message}`);
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// Immediately follow with SIGKILL for both process and group
|
|
3167
|
+
try {
|
|
3168
|
+
process.kill(this.child.pid, 'SIGKILL');
|
|
3169
|
+
trace('ProcessRunner', () => `Sent SIGKILL to Bun process ${this.child.pid}`);
|
|
3170
|
+
killOperations.push('SIGKILL to process');
|
|
3171
|
+
} catch (err) {
|
|
3172
|
+
trace('ProcessRunner', () => `Error sending SIGKILL to Bun process: ${err.message}`);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
try {
|
|
3176
|
+
process.kill(-this.child.pid, 'SIGKILL');
|
|
3177
|
+
trace('ProcessRunner', () => `Sent SIGKILL to Bun process group -${this.child.pid}`);
|
|
3178
|
+
killOperations.push('SIGKILL to group');
|
|
3179
|
+
} catch (err) {
|
|
3180
|
+
trace('ProcessRunner', () => `Bun process group SIGKILL failed: ${err.message}`);
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
trace('ProcessRunner', () => `Bun kill operations attempted: ${killOperations.join(', ')}`);
|
|
3184
|
+
|
|
3185
|
+
// Also call the original Bun kill method as backup
|
|
3186
|
+
try {
|
|
3187
|
+
this.child.kill();
|
|
3188
|
+
trace('ProcessRunner', () => `Called child.kill() for Bun process ${this.child.pid}`);
|
|
3189
|
+
} catch (err) {
|
|
3190
|
+
trace('ProcessRunner', () => `Error calling child.kill(): ${err.message}`);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
// Force cleanup of child reference
|
|
3194
|
+
if (this.child) {
|
|
3195
|
+
this.child.removeAllListeners?.();
|
|
3196
|
+
this.child = null;
|
|
3197
|
+
}
|
|
2280
3198
|
} else {
|
|
2281
|
-
// In Node.js,
|
|
2282
|
-
trace('ProcessRunner', () => `Killing Node process
|
|
2283
|
-
|
|
3199
|
+
// In Node.js, use a more robust approach for CI environments
|
|
3200
|
+
trace('ProcessRunner', () => `Killing Node process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
3201
|
+
|
|
3202
|
+
// Use immediate and aggressive termination for CI environments
|
|
3203
|
+
const killOperations = [];
|
|
3204
|
+
|
|
3205
|
+
// Try SIGTERM to the process directly
|
|
3206
|
+
try {
|
|
3207
|
+
process.kill(this.child.pid, 'SIGTERM');
|
|
3208
|
+
trace('ProcessRunner', () => `Sent SIGTERM to process ${this.child.pid}`);
|
|
3209
|
+
killOperations.push('SIGTERM to process');
|
|
3210
|
+
} catch (err) {
|
|
3211
|
+
trace('ProcessRunner', () => `Error sending SIGTERM to process: ${err.message}`);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// Try process group if detached (negative PID)
|
|
3215
|
+
try {
|
|
3216
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
3217
|
+
trace('ProcessRunner', () => `Sent SIGTERM to process group -${this.child.pid}`);
|
|
3218
|
+
killOperations.push('SIGTERM to group');
|
|
3219
|
+
} catch (err) {
|
|
3220
|
+
trace('ProcessRunner', () => `Process group SIGTERM failed: ${err.message}`);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// Immediately follow up with SIGKILL for CI reliability
|
|
3224
|
+
try {
|
|
3225
|
+
process.kill(this.child.pid, 'SIGKILL');
|
|
3226
|
+
trace('ProcessRunner', () => `Sent SIGKILL to process ${this.child.pid}`);
|
|
3227
|
+
killOperations.push('SIGKILL to process');
|
|
3228
|
+
} catch (err) {
|
|
3229
|
+
trace('ProcessRunner', () => `Error sending SIGKILL to process: ${err.message}`);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
try {
|
|
3233
|
+
process.kill(-this.child.pid, 'SIGKILL');
|
|
3234
|
+
trace('ProcessRunner', () => `Sent SIGKILL to process group -${this.child.pid}`);
|
|
3235
|
+
killOperations.push('SIGKILL to group');
|
|
3236
|
+
} catch (err) {
|
|
3237
|
+
trace('ProcessRunner', () => `Process group SIGKILL failed: ${err.message}`);
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
trace('ProcessRunner', () => `Kill operations attempted: ${killOperations.join(', ')}`);
|
|
3241
|
+
|
|
3242
|
+
// Force cleanup of child reference to prevent hanging awaits
|
|
3243
|
+
if (this.child) {
|
|
3244
|
+
this.child.removeAllListeners?.();
|
|
3245
|
+
this.child = null;
|
|
3246
|
+
}
|
|
2284
3247
|
}
|
|
2285
3248
|
}
|
|
2286
|
-
|
|
3249
|
+
// finished will be set by the main cleanup below
|
|
2287
3250
|
} catch (err) {
|
|
2288
3251
|
// Process might already be dead
|
|
2289
3252
|
trace('ProcessRunner', () => `Error killing process | ${JSON.stringify({ error: err.message }, null, 2)}`);
|
|
@@ -2291,8 +3254,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2291
3254
|
}
|
|
2292
3255
|
}
|
|
2293
3256
|
|
|
2294
|
-
// Mark as finished
|
|
2295
|
-
|
|
3257
|
+
// Mark as finished and emit completion events
|
|
3258
|
+
const result = createResult({
|
|
3259
|
+
code: signal === 'SIGKILL' ? 137 : signal === 'SIGTERM' ? 143 : 130,
|
|
3260
|
+
stdout: '',
|
|
3261
|
+
stderr: `Process killed with ${signal}`,
|
|
3262
|
+
stdin: ''
|
|
3263
|
+
});
|
|
3264
|
+
this.finish(result);
|
|
2296
3265
|
|
|
2297
3266
|
trace('ProcessRunner', () => `kill EXIT | ${JSON.stringify({
|
|
2298
3267
|
cancelled: this._cancelled,
|
|
@@ -2353,7 +3322,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2353
3322
|
if (!this.promise) {
|
|
2354
3323
|
this.promise = this._startAsync();
|
|
2355
3324
|
}
|
|
2356
|
-
return this.promise.finally(
|
|
3325
|
+
return this.promise.finally(() => {
|
|
3326
|
+
// Ensure cleanup happened
|
|
3327
|
+
if (!this.finished) {
|
|
3328
|
+
trace('ProcessRunner', () => 'Finally handler ensuring cleanup');
|
|
3329
|
+
const fallbackResult = createResult({
|
|
3330
|
+
code: 1,
|
|
3331
|
+
stdout: '',
|
|
3332
|
+
stderr: 'Process terminated unexpectedly',
|
|
3333
|
+
stdin: ''
|
|
3334
|
+
});
|
|
3335
|
+
this.finish(fallbackResult);
|
|
3336
|
+
}
|
|
3337
|
+
if (onFinally) onFinally();
|
|
3338
|
+
});
|
|
2357
3339
|
}
|
|
2358
3340
|
|
|
2359
3341
|
// Internal sync execution
|
|
@@ -2438,9 +3420,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2438
3420
|
this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
|
|
2439
3421
|
this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
|
|
2440
3422
|
|
|
2441
|
-
this.result = result;
|
|
2442
|
-
this.finished = true;
|
|
2443
|
-
|
|
2444
3423
|
// Emit batched events after completion
|
|
2445
3424
|
if (result.stdout) {
|
|
2446
3425
|
const stdoutBuf = Buffer.from(result.stdout);
|
|
@@ -2452,8 +3431,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2452
3431
|
this._emitProcessedData('stderr', stderrBuf);
|
|
2453
3432
|
}
|
|
2454
3433
|
|
|
2455
|
-
this.
|
|
2456
|
-
this.emit('exit', result.code);
|
|
3434
|
+
this.finish(result);
|
|
2457
3435
|
|
|
2458
3436
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
2459
3437
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
@@ -2467,18 +3445,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2467
3445
|
return result;
|
|
2468
3446
|
}
|
|
2469
3447
|
|
|
2470
|
-
// Stream properties
|
|
2471
|
-
get stdout() {
|
|
2472
|
-
return this.child?.stdout;
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
get stderr() {
|
|
2476
|
-
return this.child?.stderr;
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
get stdin() {
|
|
2480
|
-
return this.child?.stdin;
|
|
2481
|
-
}
|
|
2482
3448
|
}
|
|
2483
3449
|
|
|
2484
3450
|
// Public APIs
|
|
@@ -2526,6 +3492,28 @@ async function run(commandOrTokens, options = {}) {
|
|
|
2526
3492
|
}
|
|
2527
3493
|
|
|
2528
3494
|
function $tagged(strings, ...values) {
|
|
3495
|
+
// Check if called as a function with options object: $({ options })
|
|
3496
|
+
if (!Array.isArray(strings) && typeof strings === 'object' && strings !== null) {
|
|
3497
|
+
const options = strings;
|
|
3498
|
+
trace('API', () => `$tagged called with options | ${JSON.stringify({ options }, null, 2)}`);
|
|
3499
|
+
|
|
3500
|
+
// Return a new tagged template function with those options
|
|
3501
|
+
return (innerStrings, ...innerValues) => {
|
|
3502
|
+
trace('API', () => `$tagged.withOptions ENTER | ${JSON.stringify({
|
|
3503
|
+
stringsLength: innerStrings.length,
|
|
3504
|
+
valuesLength: innerValues.length,
|
|
3505
|
+
options
|
|
3506
|
+
}, null, 2)}`);
|
|
3507
|
+
|
|
3508
|
+
const cmd = buildShellCommand(innerStrings, innerValues);
|
|
3509
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...options });
|
|
3510
|
+
|
|
3511
|
+
trace('API', () => `$tagged.withOptions EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
|
|
3512
|
+
return runner;
|
|
3513
|
+
};
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
// Normal tagged template literal usage
|
|
2529
3517
|
trace('API', () => `$tagged ENTER | ${JSON.stringify({
|
|
2530
3518
|
stringsLength: strings.length,
|
|
2531
3519
|
valuesLength: values.length
|
|
@@ -2779,6 +3767,7 @@ export {
|
|
|
2779
3767
|
AnsiUtils,
|
|
2780
3768
|
configureAnsi,
|
|
2781
3769
|
getAnsiConfig,
|
|
2782
|
-
processOutput
|
|
3770
|
+
processOutput,
|
|
3771
|
+
forceCleanupAll
|
|
2783
3772
|
};
|
|
2784
3773
|
export default $tagged;
|