command-stream 0.3.2 → 0.5.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 +444 -41
- package/package.json +1 -1
- package/src/$.mjs +1535 -220
- package/src/commands/$.cat.mjs +7 -1
- package/src/commands/$.sleep.mjs +63 -6
- package/src/commands/$.yes.mjs +23 -9
package/src/$.mjs
CHANGED
|
@@ -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
|
|
@@ -392,10 +513,25 @@ class StreamEmitter {
|
|
|
392
513
|
return this;
|
|
393
514
|
}
|
|
394
515
|
|
|
516
|
+
once(event, listener) {
|
|
517
|
+
const onceWrapper = (...args) => {
|
|
518
|
+
this.off(event, onceWrapper);
|
|
519
|
+
listener(...args);
|
|
520
|
+
};
|
|
521
|
+
return this.on(event, onceWrapper);
|
|
522
|
+
}
|
|
523
|
+
|
|
395
524
|
emit(event, ...args) {
|
|
396
525
|
const eventListeners = this.listeners.get(event);
|
|
526
|
+
trace('StreamEmitter', () => `Emitting event | ${JSON.stringify({
|
|
527
|
+
event,
|
|
528
|
+
hasListeners: !!eventListeners,
|
|
529
|
+
listenerCount: eventListeners?.length || 0
|
|
530
|
+
})}`);
|
|
397
531
|
if (eventListeners) {
|
|
398
|
-
|
|
532
|
+
// Create a copy to avoid issues if listeners modify the array
|
|
533
|
+
const listenersToCall = [...eventListeners];
|
|
534
|
+
for (const listener of listenersToCall) {
|
|
399
535
|
listener(...args);
|
|
400
536
|
}
|
|
401
537
|
}
|
|
@@ -498,20 +634,355 @@ class ProcessRunner extends StreamEmitter {
|
|
|
498
634
|
this._mode = null; // 'async' or 'sync'
|
|
499
635
|
|
|
500
636
|
this._cancelled = false;
|
|
637
|
+
this._cancellationSignal = null; // Track which signal caused cancellation
|
|
501
638
|
this._virtualGenerator = null;
|
|
502
639
|
this._abortController = new AbortController();
|
|
503
640
|
|
|
504
641
|
activeProcessRunners.add(this);
|
|
642
|
+
|
|
643
|
+
// Ensure parent stream monitoring is set up for all ProcessRunners
|
|
644
|
+
monitorParentStreams();
|
|
645
|
+
|
|
646
|
+
trace('ProcessRunner', () => `Added to activeProcessRunners | ${JSON.stringify({
|
|
647
|
+
command: this.spec?.command || 'unknown',
|
|
648
|
+
totalActive: activeProcessRunners.size
|
|
649
|
+
}, null, 2)}`);
|
|
650
|
+
installSignalHandlers();
|
|
505
651
|
|
|
506
|
-
|
|
507
|
-
this._finished = false;
|
|
652
|
+
this.finished = false;
|
|
508
653
|
}
|
|
509
654
|
|
|
510
|
-
|
|
511
|
-
|
|
655
|
+
// Stream property getters for child process streams (null for virtual commands)
|
|
656
|
+
get stdout() {
|
|
657
|
+
return this.child ? this.child.stdout : null;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
get stderr() {
|
|
661
|
+
return this.child ? this.child.stderr : null;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
get stdin() {
|
|
665
|
+
return this.child ? this.child.stdin : null;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Issue #33: New streaming interfaces
|
|
669
|
+
_autoStartIfNeeded(reason) {
|
|
670
|
+
if (!this.started && !this.finished) {
|
|
671
|
+
trace('ProcessRunner', () => `Auto-starting process due to ${reason}`);
|
|
672
|
+
this.start({ mode: 'async', stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
get streams() {
|
|
677
|
+
const self = this;
|
|
678
|
+
return {
|
|
679
|
+
get stdin() {
|
|
680
|
+
trace('ProcessRunner.streams', () => `stdin access | ${JSON.stringify({
|
|
681
|
+
hasChild: !!self.child,
|
|
682
|
+
hasStdin: !!(self.child && self.child.stdin),
|
|
683
|
+
started: self.started,
|
|
684
|
+
finished: self.finished,
|
|
685
|
+
hasPromise: !!self.promise,
|
|
686
|
+
command: self.spec?.command?.slice(0, 50)
|
|
687
|
+
}, null, 2)}`);
|
|
688
|
+
|
|
689
|
+
self._autoStartIfNeeded('streams.stdin access');
|
|
690
|
+
|
|
691
|
+
// Streams are available immediately after spawn, or null if not piped
|
|
692
|
+
// Return the stream directly if available, otherwise ensure process starts
|
|
693
|
+
if (self.child && self.child.stdin) {
|
|
694
|
+
trace('ProcessRunner.streams', () => 'stdin: returning existing stream');
|
|
695
|
+
return self.child.stdin;
|
|
696
|
+
}
|
|
697
|
+
if (self.finished) {
|
|
698
|
+
trace('ProcessRunner.streams', () => 'stdin: process finished, returning null');
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// For virtual commands, there's no child process
|
|
703
|
+
// Exception: virtual commands with stdin: "pipe" will fallback to real commands
|
|
704
|
+
const isVirtualCommand = self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]));
|
|
705
|
+
const willFallbackToReal = isVirtualCommand && self.options.stdin === 'pipe';
|
|
706
|
+
|
|
707
|
+
if (isVirtualCommand && !willFallbackToReal) {
|
|
708
|
+
trace('ProcessRunner.streams', () => 'stdin: virtual command, returning null');
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// If not started, start it and wait for child to be created (not for completion!)
|
|
713
|
+
if (!self.started) {
|
|
714
|
+
trace('ProcessRunner.streams', () => 'stdin: not started, starting and waiting for child');
|
|
715
|
+
// Start the process
|
|
716
|
+
self._startAsync();
|
|
717
|
+
// Wait for child to be created using async iteration
|
|
718
|
+
return new Promise((resolve) => {
|
|
719
|
+
const checkForChild = () => {
|
|
720
|
+
if (self.child && self.child.stdin) {
|
|
721
|
+
resolve(self.child.stdin);
|
|
722
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
723
|
+
resolve(null);
|
|
724
|
+
} else {
|
|
725
|
+
// Use setImmediate to check again in next event loop iteration
|
|
726
|
+
setImmediate(checkForChild);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
setImmediate(checkForChild);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Process is starting - wait for child to appear
|
|
734
|
+
if (self.promise && !self.child) {
|
|
735
|
+
trace('ProcessRunner.streams', () => 'stdin: process starting, waiting for child');
|
|
736
|
+
return new Promise((resolve) => {
|
|
737
|
+
const checkForChild = () => {
|
|
738
|
+
if (self.child && self.child.stdin) {
|
|
739
|
+
resolve(self.child.stdin);
|
|
740
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
741
|
+
resolve(null);
|
|
742
|
+
} else {
|
|
743
|
+
setImmediate(checkForChild);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
setImmediate(checkForChild);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
trace('ProcessRunner.streams', () => 'stdin: returning null (no conditions met)');
|
|
751
|
+
return null;
|
|
752
|
+
},
|
|
753
|
+
get stdout() {
|
|
754
|
+
trace('ProcessRunner.streams', () => `stdout access | ${JSON.stringify({
|
|
755
|
+
hasChild: !!self.child,
|
|
756
|
+
hasStdout: !!(self.child && self.child.stdout),
|
|
757
|
+
started: self.started,
|
|
758
|
+
finished: self.finished,
|
|
759
|
+
hasPromise: !!self.promise,
|
|
760
|
+
command: self.spec?.command?.slice(0, 50)
|
|
761
|
+
}, null, 2)}`);
|
|
762
|
+
|
|
763
|
+
self._autoStartIfNeeded('streams.stdout access');
|
|
764
|
+
|
|
765
|
+
if (self.child && self.child.stdout) {
|
|
766
|
+
trace('ProcessRunner.streams', () => 'stdout: returning existing stream');
|
|
767
|
+
return self.child.stdout;
|
|
768
|
+
}
|
|
769
|
+
if (self.finished) {
|
|
770
|
+
trace('ProcessRunner.streams', () => 'stdout: process finished, returning null');
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// For virtual commands, there's no child process
|
|
775
|
+
if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
|
|
776
|
+
trace('ProcessRunner.streams', () => 'stdout: virtual command, returning null');
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (!self.started) {
|
|
781
|
+
trace('ProcessRunner.streams', () => 'stdout: not started, starting and waiting for child');
|
|
782
|
+
self._startAsync();
|
|
783
|
+
return new Promise((resolve) => {
|
|
784
|
+
const checkForChild = () => {
|
|
785
|
+
if (self.child && self.child.stdout) {
|
|
786
|
+
resolve(self.child.stdout);
|
|
787
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
788
|
+
resolve(null);
|
|
789
|
+
} else {
|
|
790
|
+
setImmediate(checkForChild);
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
setImmediate(checkForChild);
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (self.promise && !self.child) {
|
|
798
|
+
trace('ProcessRunner.streams', () => 'stdout: process starting, waiting for child');
|
|
799
|
+
return new Promise((resolve) => {
|
|
800
|
+
const checkForChild = () => {
|
|
801
|
+
if (self.child && self.child.stdout) {
|
|
802
|
+
resolve(self.child.stdout);
|
|
803
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
804
|
+
resolve(null);
|
|
805
|
+
} else {
|
|
806
|
+
setImmediate(checkForChild);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
setImmediate(checkForChild);
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
trace('ProcessRunner.streams', () => 'stdout: returning null (no conditions met)');
|
|
814
|
+
return null;
|
|
815
|
+
},
|
|
816
|
+
get stderr() {
|
|
817
|
+
trace('ProcessRunner.streams', () => `stderr access | ${JSON.stringify({
|
|
818
|
+
hasChild: !!self.child,
|
|
819
|
+
hasStderr: !!(self.child && self.child.stderr),
|
|
820
|
+
started: self.started,
|
|
821
|
+
finished: self.finished,
|
|
822
|
+
hasPromise: !!self.promise,
|
|
823
|
+
command: self.spec?.command?.slice(0, 50)
|
|
824
|
+
}, null, 2)}`);
|
|
825
|
+
|
|
826
|
+
self._autoStartIfNeeded('streams.stderr access');
|
|
827
|
+
|
|
828
|
+
if (self.child && self.child.stderr) {
|
|
829
|
+
trace('ProcessRunner.streams', () => 'stderr: returning existing stream');
|
|
830
|
+
return self.child.stderr;
|
|
831
|
+
}
|
|
832
|
+
if (self.finished) {
|
|
833
|
+
trace('ProcessRunner.streams', () => 'stderr: process finished, returning null');
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// For virtual commands, there's no child process
|
|
838
|
+
if (self._virtualGenerator || (self.spec && self.spec.command && virtualCommands.has(self.spec.command.split(' ')[0]))) {
|
|
839
|
+
trace('ProcessRunner.streams', () => 'stderr: virtual command, returning null');
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (!self.started) {
|
|
844
|
+
trace('ProcessRunner.streams', () => 'stderr: not started, starting and waiting for child');
|
|
845
|
+
self._startAsync();
|
|
846
|
+
return new Promise((resolve) => {
|
|
847
|
+
const checkForChild = () => {
|
|
848
|
+
if (self.child && self.child.stderr) {
|
|
849
|
+
resolve(self.child.stderr);
|
|
850
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
851
|
+
resolve(null);
|
|
852
|
+
} else {
|
|
853
|
+
setImmediate(checkForChild);
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
setImmediate(checkForChild);
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (self.promise && !self.child) {
|
|
861
|
+
trace('ProcessRunner.streams', () => 'stderr: process starting, waiting for child');
|
|
862
|
+
return new Promise((resolve) => {
|
|
863
|
+
const checkForChild = () => {
|
|
864
|
+
if (self.child && self.child.stderr) {
|
|
865
|
+
resolve(self.child.stderr);
|
|
866
|
+
} else if (self.finished || self._virtualGenerator) {
|
|
867
|
+
resolve(null);
|
|
868
|
+
} else {
|
|
869
|
+
setImmediate(checkForChild);
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
setImmediate(checkForChild);
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
trace('ProcessRunner.streams', () => 'stderr: returning null (no conditions met)');
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
get buffers() {
|
|
883
|
+
const self = this;
|
|
884
|
+
return {
|
|
885
|
+
get stdin() {
|
|
886
|
+
self._autoStartIfNeeded('buffers.stdin access');
|
|
887
|
+
if (self.finished && self.result) {
|
|
888
|
+
return Buffer.from(self.result.stdin || '', 'utf8');
|
|
889
|
+
}
|
|
890
|
+
// Return promise if not finished
|
|
891
|
+
return self.then ? self.then(result => Buffer.from(result.stdin || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
892
|
+
},
|
|
893
|
+
get stdout() {
|
|
894
|
+
self._autoStartIfNeeded('buffers.stdout access');
|
|
895
|
+
if (self.finished && self.result) {
|
|
896
|
+
return Buffer.from(self.result.stdout || '', 'utf8');
|
|
897
|
+
}
|
|
898
|
+
// Return promise if not finished
|
|
899
|
+
return self.then ? self.then(result => Buffer.from(result.stdout || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
900
|
+
},
|
|
901
|
+
get stderr() {
|
|
902
|
+
self._autoStartIfNeeded('buffers.stderr access');
|
|
903
|
+
if (self.finished && self.result) {
|
|
904
|
+
return Buffer.from(self.result.stderr || '', 'utf8');
|
|
905
|
+
}
|
|
906
|
+
// Return promise if not finished
|
|
907
|
+
return self.then ? self.then(result => Buffer.from(result.stderr || '', 'utf8')) : Promise.resolve(Buffer.alloc(0));
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
get strings() {
|
|
913
|
+
const self = this;
|
|
914
|
+
return {
|
|
915
|
+
get stdin() {
|
|
916
|
+
self._autoStartIfNeeded('strings.stdin access');
|
|
917
|
+
if (self.finished && self.result) {
|
|
918
|
+
return self.result.stdin || '';
|
|
919
|
+
}
|
|
920
|
+
// Return promise if not finished
|
|
921
|
+
return self.then ? self.then(result => result.stdin || '') : Promise.resolve('');
|
|
922
|
+
},
|
|
923
|
+
get stdout() {
|
|
924
|
+
self._autoStartIfNeeded('strings.stdout access');
|
|
925
|
+
if (self.finished && self.result) {
|
|
926
|
+
return self.result.stdout || '';
|
|
927
|
+
}
|
|
928
|
+
// Return promise if not finished
|
|
929
|
+
return self.then ? self.then(result => result.stdout || '') : Promise.resolve('');
|
|
930
|
+
},
|
|
931
|
+
get stderr() {
|
|
932
|
+
self._autoStartIfNeeded('strings.stderr access');
|
|
933
|
+
if (self.finished && self.result) {
|
|
934
|
+
return self.result.stderr || '';
|
|
935
|
+
}
|
|
936
|
+
// Return promise if not finished
|
|
937
|
+
return self.then ? self.then(result => result.stderr || '') : Promise.resolve('');
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
// Centralized method to properly finish a process with correct event emission order
|
|
944
|
+
finish(result) {
|
|
945
|
+
trace('ProcessRunner', () => `finish() called | ${JSON.stringify({
|
|
946
|
+
alreadyFinished: this.finished,
|
|
947
|
+
resultCode: result?.code,
|
|
948
|
+
hasStdout: !!result?.stdout,
|
|
949
|
+
hasStderr: !!result?.stderr,
|
|
950
|
+
command: this.spec?.command?.slice(0, 50)
|
|
951
|
+
}, null, 2)}`);
|
|
952
|
+
|
|
953
|
+
// Make finish() idempotent - safe to call multiple times
|
|
954
|
+
if (this.finished) {
|
|
955
|
+
trace('ProcessRunner', () => `Already finished, returning existing result`);
|
|
956
|
+
return this.result || result;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Store result
|
|
960
|
+
this.result = result;
|
|
961
|
+
trace('ProcessRunner', () => `Result stored, about to emit events`);
|
|
962
|
+
|
|
963
|
+
// Emit completion events BEFORE setting finished to prevent _cleanup() from clearing listeners
|
|
964
|
+
this.emit('end', result);
|
|
965
|
+
trace('ProcessRunner', () => `'end' event emitted`);
|
|
966
|
+
this.emit('exit', result.code);
|
|
967
|
+
trace('ProcessRunner', () => `'exit' event emitted with code ${result.code}`);
|
|
968
|
+
|
|
969
|
+
// Set finished after events are emitted
|
|
970
|
+
this.finished = true;
|
|
971
|
+
trace('ProcessRunner', () => `Marked as finished, calling cleanup`);
|
|
972
|
+
|
|
973
|
+
// Trigger cleanup now that process is finished
|
|
974
|
+
this._cleanup();
|
|
975
|
+
trace('ProcessRunner', () => `Cleanup completed`);
|
|
976
|
+
|
|
977
|
+
return result;
|
|
512
978
|
}
|
|
513
979
|
|
|
514
980
|
_emitProcessedData(type, buf) {
|
|
981
|
+
// Don't emit data if we've been cancelled
|
|
982
|
+
if (this._cancelled) {
|
|
983
|
+
trace('ProcessRunner', () => 'Skipping data emission - process cancelled');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
515
986
|
const processedBuf = processOutput(buf, this.options.ansi);
|
|
516
987
|
this.emit(type, processedBuf);
|
|
517
988
|
this.emit('data', { type, data: processedBuf });
|
|
@@ -531,6 +1002,36 @@ class ProcessRunner extends StreamEmitter {
|
|
|
531
1002
|
|
|
532
1003
|
// Forward stdin data to child process
|
|
533
1004
|
const onData = (chunk) => {
|
|
1005
|
+
// Check for CTRL+C (ASCII code 3)
|
|
1006
|
+
if (chunk[0] === 3) {
|
|
1007
|
+
trace('ProcessRunner', () => 'CTRL+C detected, sending SIGINT to child process');
|
|
1008
|
+
// Send SIGINT to the child process
|
|
1009
|
+
if (this.child && this.child.pid) {
|
|
1010
|
+
try {
|
|
1011
|
+
if (isBun) {
|
|
1012
|
+
this.child.kill('SIGINT');
|
|
1013
|
+
} else {
|
|
1014
|
+
// In Node.js, send SIGINT to the process group if detached
|
|
1015
|
+
// or to the process directly if not
|
|
1016
|
+
if (this.child.pid > 0) {
|
|
1017
|
+
try {
|
|
1018
|
+
// Try process group first if detached
|
|
1019
|
+
process.kill(-this.child.pid, 'SIGINT');
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
// Fall back to direct process
|
|
1022
|
+
process.kill(this.child.pid, 'SIGINT');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
trace('ProcessRunner', () => `Error sending SIGINT: ${err.message}`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
// Don't forward CTRL+C to stdin, just handle the signal
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Forward other input to child stdin
|
|
534
1035
|
if (this.child.stdin) {
|
|
535
1036
|
if (isBun && this.child.stdin.write) {
|
|
536
1037
|
this.child.stdin.write(chunk);
|
|
@@ -564,17 +1065,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
564
1065
|
}
|
|
565
1066
|
}
|
|
566
1067
|
|
|
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
1068
|
|
|
576
1069
|
_handleParentStreamClosure() {
|
|
577
|
-
if (this.finished || this._cancelled)
|
|
1070
|
+
if (this.finished || this._cancelled) {
|
|
1071
|
+
trace('ProcessRunner', () => `Parent stream closure ignored | ${JSON.stringify({
|
|
1072
|
+
finished: this.finished,
|
|
1073
|
+
cancelled: this._cancelled
|
|
1074
|
+
})}`);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
578
1077
|
|
|
579
1078
|
trace('ProcessRunner', () => `Handling parent stream closure | ${JSON.stringify({
|
|
580
1079
|
started: this.started,
|
|
@@ -600,32 +1099,140 @@ class ProcessRunner extends StreamEmitter {
|
|
|
600
1099
|
writer.close().catch(() => { }); // Ignore close errors
|
|
601
1100
|
}
|
|
602
1101
|
|
|
603
|
-
|
|
1102
|
+
// Use setImmediate for deferred termination instead of setTimeout
|
|
1103
|
+
setImmediate(() => {
|
|
604
1104
|
if (this.child && !this.finished) {
|
|
605
1105
|
trace('ProcessRunner', () => 'Terminating child process after parent stream closure');
|
|
606
1106
|
if (typeof this.child.kill === 'function') {
|
|
607
1107
|
this.child.kill('SIGTERM');
|
|
608
1108
|
}
|
|
609
1109
|
}
|
|
610
|
-
}
|
|
1110
|
+
});
|
|
611
1111
|
|
|
612
1112
|
} catch (error) {
|
|
613
1113
|
trace('ProcessRunner', () => `Error during graceful shutdown | ${JSON.stringify({ error: error.message }, null, 2)}`);
|
|
614
1114
|
}
|
|
615
1115
|
}
|
|
616
1116
|
|
|
617
|
-
|
|
1117
|
+
this._cleanup();
|
|
618
1118
|
}
|
|
619
1119
|
|
|
620
1120
|
_cleanup() {
|
|
1121
|
+
trace('ProcessRunner', () => `_cleanup() called | ${JSON.stringify({
|
|
1122
|
+
wasActiveBeforeCleanup: activeProcessRunners.has(this),
|
|
1123
|
+
totalActiveBefore: activeProcessRunners.size,
|
|
1124
|
+
finished: this.finished,
|
|
1125
|
+
hasChild: !!this.child,
|
|
1126
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1127
|
+
}, null, 2)}`);
|
|
1128
|
+
|
|
1129
|
+
const wasActive = activeProcessRunners.has(this);
|
|
621
1130
|
activeProcessRunners.delete(this);
|
|
1131
|
+
|
|
1132
|
+
if (wasActive) {
|
|
1133
|
+
trace('ProcessRunner', () => `Removed from activeProcessRunners | ${JSON.stringify({
|
|
1134
|
+
command: this.spec?.command || 'unknown',
|
|
1135
|
+
totalActiveAfter: activeProcessRunners.size,
|
|
1136
|
+
remainingCommands: Array.from(activeProcessRunners).map(r => r.spec?.command?.slice(0, 30))
|
|
1137
|
+
}, null, 2)}`);
|
|
1138
|
+
} else {
|
|
1139
|
+
trace('ProcessRunner', () => `Was not in activeProcessRunners (already cleaned up)`);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// If this is a pipeline runner, also clean up the source and destination
|
|
1143
|
+
if (this.spec?.mode === 'pipeline') {
|
|
1144
|
+
trace('ProcessRunner', () => 'Cleaning up pipeline components');
|
|
1145
|
+
if (this.spec.source && typeof this.spec.source._cleanup === 'function') {
|
|
1146
|
+
this.spec.source._cleanup();
|
|
1147
|
+
}
|
|
1148
|
+
if (this.spec.destination && typeof this.spec.destination._cleanup === 'function') {
|
|
1149
|
+
this.spec.destination._cleanup();
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// If no more active ProcessRunners, remove the SIGINT handler
|
|
1154
|
+
if (activeProcessRunners.size === 0) {
|
|
1155
|
+
uninstallSignalHandlers();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Clean up event listeners from StreamEmitter
|
|
1159
|
+
if (this.listeners) {
|
|
1160
|
+
this.listeners.clear();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Clean up abort controller
|
|
1164
|
+
if (this._abortController) {
|
|
1165
|
+
trace('ProcessRunner', () => `Cleaning up abort controller during cleanup | ${JSON.stringify({
|
|
1166
|
+
wasAborted: this._abortController?.signal?.aborted
|
|
1167
|
+
}, null, 2)}`);
|
|
1168
|
+
try {
|
|
1169
|
+
this._abortController.abort();
|
|
1170
|
+
trace('ProcessRunner', () => `Abort controller aborted successfully during cleanup`);
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
trace('ProcessRunner', () => `Error aborting controller during cleanup: ${e.message}`);
|
|
1173
|
+
}
|
|
1174
|
+
this._abortController = null;
|
|
1175
|
+
trace('ProcessRunner', () => `Abort controller reference cleared during cleanup`);
|
|
1176
|
+
} else {
|
|
1177
|
+
trace('ProcessRunner', () => `No abort controller to clean up during cleanup`);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Clean up child process reference
|
|
1181
|
+
if (this.child) {
|
|
1182
|
+
trace('ProcessRunner', () => `Cleaning up child process reference | ${JSON.stringify({
|
|
1183
|
+
hasChild: true,
|
|
1184
|
+
childPid: this.child.pid,
|
|
1185
|
+
childKilled: this.child.killed
|
|
1186
|
+
}, null, 2)}`);
|
|
1187
|
+
try {
|
|
1188
|
+
this.child.removeAllListeners?.();
|
|
1189
|
+
trace('ProcessRunner', () => `Child process listeners removed successfully`);
|
|
1190
|
+
} catch (e) {
|
|
1191
|
+
trace('ProcessRunner', () => `Error removing child process listeners: ${e.message}`);
|
|
1192
|
+
}
|
|
1193
|
+
this.child = null;
|
|
1194
|
+
trace('ProcessRunner', () => `Child process reference cleared`);
|
|
1195
|
+
} else {
|
|
1196
|
+
trace('ProcessRunner', () => `No child process reference to clean up`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Clean up virtual generator
|
|
1200
|
+
if (this._virtualGenerator) {
|
|
1201
|
+
trace('ProcessRunner', () => `Cleaning up virtual generator | ${JSON.stringify({
|
|
1202
|
+
hasReturn: !!this._virtualGenerator.return
|
|
1203
|
+
}, null, 2)}`);
|
|
1204
|
+
try {
|
|
1205
|
+
if (this._virtualGenerator.return) {
|
|
1206
|
+
this._virtualGenerator.return();
|
|
1207
|
+
trace('ProcessRunner', () => `Virtual generator return() called successfully`);
|
|
1208
|
+
}
|
|
1209
|
+
} catch (e) {
|
|
1210
|
+
trace('ProcessRunner', () => `Error calling virtual generator return(): ${e.message}`);
|
|
1211
|
+
}
|
|
1212
|
+
this._virtualGenerator = null;
|
|
1213
|
+
trace('ProcessRunner', () => `Virtual generator reference cleared`);
|
|
1214
|
+
} else {
|
|
1215
|
+
trace('ProcessRunner', () => `No virtual generator to clean up`);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
trace('ProcessRunner', () => `_cleanup() completed | ${JSON.stringify({
|
|
1219
|
+
totalActiveAfter: activeProcessRunners.size,
|
|
1220
|
+
sigintListenerCount: process.listeners('SIGINT').length
|
|
1221
|
+
}, null, 2)}`);
|
|
622
1222
|
}
|
|
623
1223
|
|
|
624
1224
|
// Unified start method that can work in both async and sync modes
|
|
625
1225
|
start(options = {}) {
|
|
626
1226
|
const mode = options.mode || 'async';
|
|
627
1227
|
|
|
628
|
-
trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({
|
|
1228
|
+
trace('ProcessRunner', () => `start ENTER | ${JSON.stringify({
|
|
1229
|
+
mode,
|
|
1230
|
+
options,
|
|
1231
|
+
started: this.started,
|
|
1232
|
+
hasPromise: !!this.promise,
|
|
1233
|
+
hasChild: !!this.child,
|
|
1234
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1235
|
+
}, null, 2)}`);
|
|
629
1236
|
|
|
630
1237
|
// Merge new options with existing options before starting
|
|
631
1238
|
if (Object.keys(options).length > 0 && !this.started) {
|
|
@@ -637,6 +1244,79 @@ class ProcessRunner extends StreamEmitter {
|
|
|
637
1244
|
// Create a new options object merging the current ones with the new ones
|
|
638
1245
|
this.options = { ...this.options, ...options };
|
|
639
1246
|
|
|
1247
|
+
// Handle external abort signal
|
|
1248
|
+
if (this.options.signal && typeof this.options.signal.addEventListener === 'function') {
|
|
1249
|
+
trace('ProcessRunner', () => `Setting up external abort signal listener | ${JSON.stringify({
|
|
1250
|
+
hasSignal: !!this.options.signal,
|
|
1251
|
+
signalAborted: this.options.signal.aborted,
|
|
1252
|
+
hasInternalController: !!this._abortController,
|
|
1253
|
+
internalAborted: this._abortController?.signal.aborted
|
|
1254
|
+
}, null, 2)}`);
|
|
1255
|
+
|
|
1256
|
+
this.options.signal.addEventListener('abort', () => {
|
|
1257
|
+
trace('ProcessRunner', () => `External abort signal triggered | ${JSON.stringify({
|
|
1258
|
+
externalSignalAborted: this.options.signal.aborted,
|
|
1259
|
+
hasInternalController: !!this._abortController,
|
|
1260
|
+
internalAborted: this._abortController?.signal.aborted,
|
|
1261
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1262
|
+
}, null, 2)}`);
|
|
1263
|
+
|
|
1264
|
+
// Kill the process when abort signal is triggered
|
|
1265
|
+
trace('ProcessRunner', () => `External abort signal received - killing process | ${JSON.stringify({
|
|
1266
|
+
hasChild: !!this.child,
|
|
1267
|
+
childPid: this.child?.pid,
|
|
1268
|
+
finished: this.finished,
|
|
1269
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1270
|
+
}, null, 2)}`);
|
|
1271
|
+
this.kill('SIGTERM');
|
|
1272
|
+
trace('ProcessRunner', () => 'Process kill initiated due to external abort signal');
|
|
1273
|
+
|
|
1274
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
1275
|
+
trace('ProcessRunner', () => 'Aborting internal controller due to external signal');
|
|
1276
|
+
this._abortController.abort();
|
|
1277
|
+
trace('ProcessRunner', () => `Internal controller aborted | ${JSON.stringify({
|
|
1278
|
+
internalAborted: this._abortController?.signal?.aborted
|
|
1279
|
+
}, null, 2)}`);
|
|
1280
|
+
} else {
|
|
1281
|
+
trace('ProcessRunner', () => `Cannot abort internal controller | ${JSON.stringify({
|
|
1282
|
+
hasInternalController: !!this._abortController,
|
|
1283
|
+
internalAlreadyAborted: this._abortController?.signal?.aborted
|
|
1284
|
+
}, null, 2)}`);
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// If the external signal is already aborted, abort immediately
|
|
1289
|
+
if (this.options.signal.aborted) {
|
|
1290
|
+
trace('ProcessRunner', () => `External signal already aborted, killing process and aborting internal controller | ${JSON.stringify({
|
|
1291
|
+
hasInternalController: !!this._abortController,
|
|
1292
|
+
internalAborted: this._abortController?.signal.aborted
|
|
1293
|
+
}, null, 2)}`);
|
|
1294
|
+
|
|
1295
|
+
// Kill the process immediately since signal is already aborted
|
|
1296
|
+
trace('ProcessRunner', () => `Signal already aborted - killing process immediately | ${JSON.stringify({
|
|
1297
|
+
hasChild: !!this.child,
|
|
1298
|
+
childPid: this.child?.pid,
|
|
1299
|
+
finished: this.finished,
|
|
1300
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1301
|
+
}, null, 2)}`);
|
|
1302
|
+
this.kill('SIGTERM');
|
|
1303
|
+
trace('ProcessRunner', () => 'Process kill initiated due to pre-aborted signal');
|
|
1304
|
+
|
|
1305
|
+
if (this._abortController && !this._abortController.signal.aborted) {
|
|
1306
|
+
this._abortController.abort();
|
|
1307
|
+
trace('ProcessRunner', () => `Internal controller aborted immediately | ${JSON.stringify({
|
|
1308
|
+
internalAborted: this._abortController?.signal?.aborted
|
|
1309
|
+
}, null, 2)}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
trace('ProcessRunner', () => `No external signal to handle | ${JSON.stringify({
|
|
1314
|
+
hasSignal: !!this.options.signal,
|
|
1315
|
+
signalType: typeof this.options.signal,
|
|
1316
|
+
hasAddEventListener: !!(this.options.signal && typeof this.options.signal.addEventListener === 'function')
|
|
1317
|
+
}, null, 2)}`);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
640
1320
|
// Reinitialize chunks based on updated capture option
|
|
641
1321
|
if ('capture' in options) {
|
|
642
1322
|
trace('ProcessRunner', () => `BRANCH: capture => REINIT_CHUNKS | ${JSON.stringify({
|
|
@@ -699,6 +1379,9 @@ class ProcessRunner extends StreamEmitter {
|
|
|
699
1379
|
this.started = true;
|
|
700
1380
|
this._mode = 'async';
|
|
701
1381
|
|
|
1382
|
+
// Ensure cleanup happens even if execution fails
|
|
1383
|
+
try {
|
|
1384
|
+
|
|
702
1385
|
const { cwd, env, stdin } = this.options;
|
|
703
1386
|
|
|
704
1387
|
if (this.spec.mode === 'pipeline') {
|
|
@@ -725,26 +1408,57 @@ class ProcessRunner extends StreamEmitter {
|
|
|
725
1408
|
commandCount: parsed.commands?.length
|
|
726
1409
|
}, null, 2)}`);
|
|
727
1410
|
return await this._runPipeline(parsed.commands);
|
|
728
|
-
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd)) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1411
|
+
} else if (parsed.type === 'simple' && virtualCommandsEnabled && virtualCommands.has(parsed.cmd) && !this.options._bypassVirtual) {
|
|
1412
|
+
// For built-in virtual commands that have real counterparts (like sleep),
|
|
1413
|
+
// skip the virtual version when custom stdin is provided to ensure proper process handling
|
|
1414
|
+
const hasCustomStdin = this.options.stdin &&
|
|
1415
|
+
this.options.stdin !== 'inherit' &&
|
|
1416
|
+
this.options.stdin !== 'ignore';
|
|
1417
|
+
|
|
1418
|
+
// Only bypass for commands that truly need real process behavior with custom stdin
|
|
1419
|
+
// Most commands like 'echo' work fine with virtual implementations even with stdin
|
|
1420
|
+
const commandsThatNeedRealStdin = ['sleep', 'cat']; // Only these really need real processes for stdin
|
|
1421
|
+
const shouldBypassVirtual = hasCustomStdin && commandsThatNeedRealStdin.includes(parsed.cmd);
|
|
1422
|
+
|
|
1423
|
+
if (shouldBypassVirtual) {
|
|
1424
|
+
trace('ProcessRunner', () => `Bypassing built-in virtual command due to custom stdin | ${JSON.stringify({
|
|
1425
|
+
cmd: parsed.cmd,
|
|
1426
|
+
stdin: typeof this.options.stdin
|
|
1427
|
+
}, null, 2)}`);
|
|
1428
|
+
// Fall through to run as real command
|
|
1429
|
+
} else {
|
|
1430
|
+
trace('ProcessRunner', () => `BRANCH: virtualCommand => ${parsed.cmd} | ${JSON.stringify({
|
|
1431
|
+
isVirtual: true,
|
|
1432
|
+
args: parsed.args
|
|
1433
|
+
}, null, 2)}`);
|
|
1434
|
+
trace('ProcessRunner', () => `Executing virtual command | ${JSON.stringify({
|
|
1435
|
+
cmd: parsed.cmd,
|
|
1436
|
+
argsLength: parsed.args.length,
|
|
1437
|
+
command: this.spec.command
|
|
1438
|
+
}, null, 2)}`);
|
|
1439
|
+
return await this._runVirtual(parsed.cmd, parsed.args, this.spec.command);
|
|
1440
|
+
}
|
|
734
1441
|
}
|
|
735
1442
|
}
|
|
736
1443
|
}
|
|
737
1444
|
|
|
738
1445
|
const argv = this.spec.mode === 'shell' ? ['sh', '-lc', this.spec.command] : [this.spec.file, ...this.spec.args];
|
|
1446
|
+
trace('ProcessRunner', () => `Constructed argv | ${JSON.stringify({
|
|
1447
|
+
mode: this.spec.mode,
|
|
1448
|
+
argv: argv,
|
|
1449
|
+
originalCommand: this.spec.command
|
|
1450
|
+
}, null, 2)}`);
|
|
739
1451
|
|
|
740
1452
|
if (globalShellSettings.xtrace) {
|
|
741
1453
|
const traceCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
742
1454
|
console.log(`+ ${traceCmd}`);
|
|
1455
|
+
trace('ProcessRunner', () => `xtrace output displayed: + ${traceCmd}`);
|
|
743
1456
|
}
|
|
744
1457
|
|
|
745
1458
|
if (globalShellSettings.verbose) {
|
|
746
1459
|
const verboseCmd = this.spec.mode === 'shell' ? this.spec.command : argv.join(' ');
|
|
747
1460
|
console.log(verboseCmd);
|
|
1461
|
+
trace('ProcessRunner', () => `verbose output displayed: ${verboseCmd}`);
|
|
748
1462
|
}
|
|
749
1463
|
|
|
750
1464
|
// Detect if this is an interactive command that needs direct TTY access
|
|
@@ -754,28 +1468,154 @@ class ProcessRunner extends StreamEmitter {
|
|
|
754
1468
|
process.stdout.isTTY === true &&
|
|
755
1469
|
process.stderr.isTTY === true &&
|
|
756
1470
|
(this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file));
|
|
1471
|
+
|
|
1472
|
+
trace('ProcessRunner', () => `Interactive command detection | ${JSON.stringify({
|
|
1473
|
+
isInteractive,
|
|
1474
|
+
stdinInherit: stdin === 'inherit',
|
|
1475
|
+
stdinTTY: process.stdin.isTTY,
|
|
1476
|
+
stdoutTTY: process.stdout.isTTY,
|
|
1477
|
+
stderrTTY: process.stderr.isTTY,
|
|
1478
|
+
commandCheck: this.spec.mode === 'shell' ? isInteractiveCommand(this.spec.command) : isInteractiveCommand(this.spec.file)
|
|
1479
|
+
}, null, 2)}`);
|
|
757
1480
|
|
|
758
1481
|
const spawnBun = (argv) => {
|
|
1482
|
+
trace('ProcessRunner', () => `spawnBun: Creating process | ${JSON.stringify({
|
|
1483
|
+
command: argv[0],
|
|
1484
|
+
args: argv.slice(1),
|
|
1485
|
+
isInteractive,
|
|
1486
|
+
cwd,
|
|
1487
|
+
platform: process.platform
|
|
1488
|
+
}, null, 2)}`);
|
|
1489
|
+
|
|
759
1490
|
if (isInteractive) {
|
|
760
1491
|
// For interactive commands, use inherit to provide direct TTY access
|
|
761
|
-
|
|
1492
|
+
trace('ProcessRunner', () => `spawnBun: Using interactive mode with inherited stdio`);
|
|
1493
|
+
const child = Bun.spawn(argv, { cwd, env, stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' });
|
|
1494
|
+
trace('ProcessRunner', () => `spawnBun: Interactive process created | ${JSON.stringify({
|
|
1495
|
+
pid: child.pid,
|
|
1496
|
+
killed: child.killed
|
|
1497
|
+
}, null, 2)}`);
|
|
1498
|
+
return child;
|
|
762
1499
|
}
|
|
763
|
-
|
|
1500
|
+
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1501
|
+
// This allows us to send signals to the entire process group, killing shell and all its children
|
|
1502
|
+
trace('ProcessRunner', () => `spawnBun: Using non-interactive mode with pipes and detached=${process.platform !== 'win32'}`);
|
|
1503
|
+
const child = Bun.spawn(argv, {
|
|
1504
|
+
cwd,
|
|
1505
|
+
env,
|
|
1506
|
+
stdin: 'pipe',
|
|
1507
|
+
stdout: 'pipe',
|
|
1508
|
+
stderr: 'pipe',
|
|
1509
|
+
detached: process.platform !== 'win32' // Create process group on Unix-like systems
|
|
1510
|
+
});
|
|
1511
|
+
trace('ProcessRunner', () => `spawnBun: Non-interactive process created | ${JSON.stringify({
|
|
1512
|
+
pid: child.pid,
|
|
1513
|
+
killed: child.killed,
|
|
1514
|
+
hasStdout: !!child.stdout,
|
|
1515
|
+
hasStderr: !!child.stderr,
|
|
1516
|
+
hasStdin: !!child.stdin
|
|
1517
|
+
}, null, 2)}`);
|
|
1518
|
+
return child;
|
|
764
1519
|
};
|
|
765
1520
|
const spawnNode = async (argv) => {
|
|
1521
|
+
trace('ProcessRunner', () => `spawnNode: Creating process | ${JSON.stringify({
|
|
1522
|
+
command: argv[0],
|
|
1523
|
+
args: argv.slice(1),
|
|
1524
|
+
isInteractive,
|
|
1525
|
+
cwd,
|
|
1526
|
+
platform: process.platform
|
|
1527
|
+
})}`);
|
|
1528
|
+
|
|
766
1529
|
if (isInteractive) {
|
|
767
1530
|
// For interactive commands, use inherit to provide direct TTY access
|
|
768
1531
|
return cp.spawn(argv[0], argv.slice(1), { cwd, env, stdio: 'inherit' });
|
|
769
1532
|
}
|
|
770
|
-
|
|
1533
|
+
// For non-interactive commands, spawn with detached to create process group (for proper signal handling)
|
|
1534
|
+
// This allows us to send signals to the entire process group
|
|
1535
|
+
const child = cp.spawn(argv[0], argv.slice(1), {
|
|
1536
|
+
cwd,
|
|
1537
|
+
env,
|
|
1538
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1539
|
+
detached: process.platform !== 'win32' // Create process group on Unix-like systems
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
trace('ProcessRunner', () => `spawnNode: Process created | ${JSON.stringify({
|
|
1543
|
+
pid: child.pid,
|
|
1544
|
+
killed: child.killed,
|
|
1545
|
+
hasStdout: !!child.stdout,
|
|
1546
|
+
hasStderr: !!child.stderr,
|
|
1547
|
+
hasStdin: !!child.stdin
|
|
1548
|
+
})}`);
|
|
1549
|
+
|
|
1550
|
+
return child;
|
|
771
1551
|
};
|
|
772
1552
|
|
|
773
1553
|
const needsExplicitPipe = stdin !== 'inherit' && stdin !== 'ignore';
|
|
774
1554
|
const preferNodeForInput = isBun && needsExplicitPipe;
|
|
1555
|
+
trace('ProcessRunner', () => `About to spawn process | ${JSON.stringify({
|
|
1556
|
+
needsExplicitPipe,
|
|
1557
|
+
preferNodeForInput,
|
|
1558
|
+
runtime: isBun ? 'Bun' : 'Node',
|
|
1559
|
+
command: argv[0],
|
|
1560
|
+
args: argv.slice(1)
|
|
1561
|
+
}, null, 2)}`);
|
|
775
1562
|
this.child = preferNodeForInput ? await spawnNode(argv) : (isBun ? spawnBun(argv) : await spawnNode(argv));
|
|
1563
|
+
|
|
1564
|
+
// Add detailed logging for CI debugging
|
|
1565
|
+
if (this.child) {
|
|
1566
|
+
trace('ProcessRunner', () => `Child process created | ${JSON.stringify({
|
|
1567
|
+
pid: this.child.pid,
|
|
1568
|
+
detached: this.child.options?.detached,
|
|
1569
|
+
killed: this.child.killed,
|
|
1570
|
+
exitCode: this.child.exitCode,
|
|
1571
|
+
signalCode: this.child.signalCode,
|
|
1572
|
+
hasStdout: !!this.child.stdout,
|
|
1573
|
+
hasStderr: !!this.child.stderr,
|
|
1574
|
+
hasStdin: !!this.child.stdin,
|
|
1575
|
+
platform: process.platform,
|
|
1576
|
+
command: this.spec?.command?.slice(0, 100)
|
|
1577
|
+
}, null, 2)}`);
|
|
1578
|
+
|
|
1579
|
+
// Add event listeners with detailed tracing (only for Node.js child processes)
|
|
1580
|
+
if (this.child && typeof this.child.on === 'function') {
|
|
1581
|
+
this.child.on('spawn', () => {
|
|
1582
|
+
trace('ProcessRunner', () => `Child process spawned successfully | ${JSON.stringify({
|
|
1583
|
+
pid: this.child.pid,
|
|
1584
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1585
|
+
}, null, 2)}`);
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
this.child.on('error', (error) => {
|
|
1589
|
+
trace('ProcessRunner', () => `Child process error event | ${JSON.stringify({
|
|
1590
|
+
pid: this.child?.pid,
|
|
1591
|
+
error: error.message,
|
|
1592
|
+
code: error.code,
|
|
1593
|
+
errno: error.errno,
|
|
1594
|
+
syscall: error.syscall,
|
|
1595
|
+
command: this.spec?.command?.slice(0, 50)
|
|
1596
|
+
}, null, 2)}`);
|
|
1597
|
+
});
|
|
1598
|
+
} else {
|
|
1599
|
+
trace('ProcessRunner', () => `Skipping event listeners - child does not support .on() method (likely Bun process)`);
|
|
1600
|
+
}
|
|
1601
|
+
} else {
|
|
1602
|
+
trace('ProcessRunner', () => `No child process created | ${JSON.stringify({
|
|
1603
|
+
spec: this.spec,
|
|
1604
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
1605
|
+
}, null, 2)}`);
|
|
1606
|
+
}
|
|
776
1607
|
|
|
777
1608
|
// For interactive commands with stdio: 'inherit', stdout/stderr will be null
|
|
1609
|
+
const childPid = this.child?.pid; // Capture PID once at the start
|
|
778
1610
|
const outPump = this.child.stdout ? pumpReadable(this.child.stdout, async (buf) => {
|
|
1611
|
+
trace('ProcessRunner', () => `stdout data received | ${JSON.stringify({
|
|
1612
|
+
pid: childPid,
|
|
1613
|
+
bufferLength: buf.length,
|
|
1614
|
+
capture: this.options.capture,
|
|
1615
|
+
mirror: this.options.mirror,
|
|
1616
|
+
preview: buf.toString().slice(0, 100)
|
|
1617
|
+
})}`);
|
|
1618
|
+
|
|
779
1619
|
if (this.options.capture) this.outChunks.push(buf);
|
|
780
1620
|
if (this.options.mirror) safeWrite(process.stdout, buf);
|
|
781
1621
|
|
|
@@ -784,6 +1624,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
784
1624
|
}) : Promise.resolve();
|
|
785
1625
|
|
|
786
1626
|
const errPump = this.child.stderr ? pumpReadable(this.child.stderr, async (buf) => {
|
|
1627
|
+
trace('ProcessRunner', () => `stderr data received | ${JSON.stringify({
|
|
1628
|
+
pid: childPid,
|
|
1629
|
+
bufferLength: buf.length,
|
|
1630
|
+
capture: this.options.capture,
|
|
1631
|
+
mirror: this.options.mirror,
|
|
1632
|
+
preview: buf.toString().slice(0, 100)
|
|
1633
|
+
})}`);
|
|
1634
|
+
|
|
787
1635
|
if (this.options.capture) this.errChunks.push(buf);
|
|
788
1636
|
if (this.options.mirror) safeWrite(process.stderr, buf);
|
|
789
1637
|
|
|
@@ -792,28 +1640,82 @@ class ProcessRunner extends StreamEmitter {
|
|
|
792
1640
|
}) : Promise.resolve();
|
|
793
1641
|
|
|
794
1642
|
let stdinPumpPromise = Promise.resolve();
|
|
1643
|
+
trace('ProcessRunner', () => `Setting up stdin handling | ${JSON.stringify({
|
|
1644
|
+
stdinType: typeof stdin,
|
|
1645
|
+
stdin: stdin === 'inherit' ? 'inherit' : stdin === 'ignore' ? 'ignore' : (typeof stdin === 'string' ? `string(${stdin.length})` : 'other'),
|
|
1646
|
+
isInteractive,
|
|
1647
|
+
hasChildStdin: !!this.child?.stdin,
|
|
1648
|
+
processTTY: process.stdin.isTTY
|
|
1649
|
+
}, null, 2)}`);
|
|
1650
|
+
|
|
795
1651
|
if (stdin === 'inherit') {
|
|
796
1652
|
if (isInteractive) {
|
|
797
1653
|
// For interactive commands with stdio: 'inherit', stdin is handled automatically
|
|
1654
|
+
trace('ProcessRunner', () => `stdin: Using inherit mode for interactive command`);
|
|
798
1655
|
stdinPumpPromise = Promise.resolve();
|
|
799
1656
|
} else {
|
|
800
1657
|
const isPipedIn = process.stdin && process.stdin.isTTY === false;
|
|
1658
|
+
trace('ProcessRunner', () => `stdin: Non-interactive inherit mode | ${JSON.stringify({
|
|
1659
|
+
isPipedIn,
|
|
1660
|
+
stdinTTY: process.stdin.isTTY
|
|
1661
|
+
}, null, 2)}`);
|
|
801
1662
|
if (isPipedIn) {
|
|
1663
|
+
trace('ProcessRunner', () => `stdin: Pumping piped input to child process`);
|
|
802
1664
|
stdinPumpPromise = this._pumpStdinTo(this.child, this.options.capture ? this.inChunks : null);
|
|
803
1665
|
} else {
|
|
804
1666
|
// For TTY (interactive terminal), forward stdin directly for non-interactive commands
|
|
1667
|
+
trace('ProcessRunner', () => `stdin: Forwarding TTY stdin for non-interactive command`);
|
|
805
1668
|
stdinPumpPromise = this._forwardTTYStdin();
|
|
806
1669
|
}
|
|
807
1670
|
}
|
|
808
1671
|
} else if (stdin === 'ignore') {
|
|
809
|
-
|
|
1672
|
+
trace('ProcessRunner', () => `stdin: Ignoring and closing stdin`);
|
|
1673
|
+
if (this.child.stdin && typeof this.child.stdin.end === 'function') {
|
|
1674
|
+
this.child.stdin.end();
|
|
1675
|
+
trace('ProcessRunner', () => `stdin: Child stdin closed successfully`);
|
|
1676
|
+
}
|
|
1677
|
+
} else if (stdin === 'pipe') {
|
|
1678
|
+
trace('ProcessRunner', () => `stdin: Using pipe mode - leaving stdin open for manual control`);
|
|
1679
|
+
// Leave stdin open for manual writing via streams.stdin
|
|
1680
|
+
stdinPumpPromise = Promise.resolve();
|
|
810
1681
|
} else if (typeof stdin === 'string' || Buffer.isBuffer(stdin)) {
|
|
811
1682
|
const buf = Buffer.isBuffer(stdin) ? stdin : Buffer.from(stdin);
|
|
1683
|
+
trace('ProcessRunner', () => `stdin: Writing buffer to child | ${JSON.stringify({
|
|
1684
|
+
bufferLength: buf.length,
|
|
1685
|
+
willCapture: this.options.capture && !!this.inChunks
|
|
1686
|
+
}, null, 2)}`);
|
|
812
1687
|
if (this.options.capture && this.inChunks) this.inChunks.push(Buffer.from(buf));
|
|
813
1688
|
stdinPumpPromise = this._writeToStdin(buf);
|
|
1689
|
+
} else {
|
|
1690
|
+
trace('ProcessRunner', () => `stdin: Unhandled stdin type: ${typeof stdin}`);
|
|
814
1691
|
}
|
|
815
1692
|
|
|
816
|
-
const exited = isBun ? this.child.exited : new Promise((resolve) =>
|
|
1693
|
+
const exited = isBun ? this.child.exited : new Promise((resolve) => {
|
|
1694
|
+
trace('ProcessRunner', () => `Setting up child process event listeners for PID ${this.child.pid}`);
|
|
1695
|
+
this.child.on('close', (code, signal) => {
|
|
1696
|
+
trace('ProcessRunner', () => `Child process close event | ${JSON.stringify({
|
|
1697
|
+
pid: this.child.pid,
|
|
1698
|
+
code,
|
|
1699
|
+
signal,
|
|
1700
|
+
killed: this.child.killed,
|
|
1701
|
+
exitCode: this.child.exitCode,
|
|
1702
|
+
signalCode: this.child.signalCode,
|
|
1703
|
+
command: this.command
|
|
1704
|
+
}, null, 2)}`);
|
|
1705
|
+
resolve(code);
|
|
1706
|
+
});
|
|
1707
|
+
this.child.on('exit', (code, signal) => {
|
|
1708
|
+
trace('ProcessRunner', () => `Child process exit event | ${JSON.stringify({
|
|
1709
|
+
pid: this.child.pid,
|
|
1710
|
+
code,
|
|
1711
|
+
signal,
|
|
1712
|
+
killed: this.child.killed,
|
|
1713
|
+
exitCode: this.child.exitCode,
|
|
1714
|
+
signalCode: this.child.signalCode,
|
|
1715
|
+
command: this.command
|
|
1716
|
+
}, null, 2)}`);
|
|
1717
|
+
});
|
|
1718
|
+
});
|
|
817
1719
|
const code = await exited;
|
|
818
1720
|
await Promise.all([outPump, errPump, stdinPumpPromise]);
|
|
819
1721
|
|
|
@@ -825,35 +1727,126 @@ class ProcessRunner extends StreamEmitter {
|
|
|
825
1727
|
isBun
|
|
826
1728
|
}, null, 2)}`);
|
|
827
1729
|
|
|
1730
|
+
// When a process is killed, it may not have an exit code
|
|
1731
|
+
// If cancelled and no exit code, assume it was killed with SIGTERM
|
|
1732
|
+
let finalExitCode = code;
|
|
1733
|
+
trace('ProcessRunner', () => `Processing exit code | ${JSON.stringify({
|
|
1734
|
+
rawCode: code,
|
|
1735
|
+
cancelled: this._cancelled,
|
|
1736
|
+
childKilled: this.child?.killed,
|
|
1737
|
+
childExitCode: this.child?.exitCode,
|
|
1738
|
+
childSignalCode: this.child?.signalCode
|
|
1739
|
+
}, null, 2)}`);
|
|
1740
|
+
|
|
1741
|
+
if (finalExitCode === undefined || finalExitCode === null) {
|
|
1742
|
+
if (this._cancelled) {
|
|
1743
|
+
// Process was killed, use SIGTERM exit code
|
|
1744
|
+
finalExitCode = 143; // 128 + 15 (SIGTERM)
|
|
1745
|
+
trace('ProcessRunner', () => `Process was killed, using SIGTERM exit code 143`);
|
|
1746
|
+
} else {
|
|
1747
|
+
// Process exited without a code, default to 0
|
|
1748
|
+
finalExitCode = 0;
|
|
1749
|
+
trace('ProcessRunner', () => `Process exited without code, defaulting to 0`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
828
1753
|
const resultData = {
|
|
829
|
-
code:
|
|
1754
|
+
code: finalExitCode,
|
|
830
1755
|
stdout: this.options.capture ? (this.outChunks && this.outChunks.length > 0 ? Buffer.concat(this.outChunks).toString('utf8') : '') : undefined,
|
|
831
1756
|
stderr: this.options.capture ? (this.errChunks && this.errChunks.length > 0 ? Buffer.concat(this.errChunks).toString('utf8') : '') : undefined,
|
|
832
1757
|
stdin: this.options.capture && this.inChunks ? Buffer.concat(this.inChunks).toString('utf8') : undefined,
|
|
833
1758
|
child: this.child
|
|
834
1759
|
};
|
|
1760
|
+
|
|
1761
|
+
trace('ProcessRunner', () => `Process completed | ${JSON.stringify({
|
|
1762
|
+
command: this.command,
|
|
1763
|
+
finalExitCode,
|
|
1764
|
+
captured: this.options.capture,
|
|
1765
|
+
hasStdout: !!resultData.stdout,
|
|
1766
|
+
hasStderr: !!resultData.stderr,
|
|
1767
|
+
stdoutLength: resultData.stdout?.length || 0,
|
|
1768
|
+
stderrLength: resultData.stderr?.length || 0,
|
|
1769
|
+
stdoutPreview: resultData.stdout?.slice(0, 100),
|
|
1770
|
+
stderrPreview: resultData.stderr?.slice(0, 100),
|
|
1771
|
+
childPid: this.child?.pid,
|
|
1772
|
+
cancelled: this._cancelled,
|
|
1773
|
+
cancellationSignal: this._cancellationSignal,
|
|
1774
|
+
platform: process.platform,
|
|
1775
|
+
runtime: isBun ? 'Bun' : 'Node.js'
|
|
1776
|
+
}, null, 2)}`);
|
|
835
1777
|
|
|
836
|
-
|
|
1778
|
+
const result = {
|
|
837
1779
|
...resultData,
|
|
838
1780
|
async text() {
|
|
839
1781
|
return resultData.stdout || '';
|
|
840
1782
|
}
|
|
841
1783
|
};
|
|
842
1784
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1785
|
+
trace('ProcessRunner', () => `About to finish process with result | ${JSON.stringify({
|
|
1786
|
+
exitCode: result.code,
|
|
1787
|
+
finished: this.finished
|
|
1788
|
+
}, null, 2)}`);
|
|
1789
|
+
|
|
1790
|
+
// Finish the process with proper event emission order
|
|
1791
|
+
this.finish(result);
|
|
1792
|
+
|
|
1793
|
+
trace('ProcessRunner', () => `Process finished, result set | ${JSON.stringify({
|
|
1794
|
+
finished: this.finished,
|
|
1795
|
+
resultCode: this.result?.code
|
|
1796
|
+
}, null, 2)}`);
|
|
846
1797
|
|
|
847
1798
|
if (globalShellSettings.errexit && this.result.code !== 0) {
|
|
1799
|
+
trace('ProcessRunner', () => `Errexit mode: throwing error for non-zero exit code | ${JSON.stringify({
|
|
1800
|
+
exitCode: this.result.code,
|
|
1801
|
+
errexit: globalShellSettings.errexit,
|
|
1802
|
+
hasStdout: !!this.result.stdout,
|
|
1803
|
+
hasStderr: !!this.result.stderr
|
|
1804
|
+
}, null, 2)}`);
|
|
1805
|
+
|
|
848
1806
|
const error = new Error(`Command failed with exit code ${this.result.code}`);
|
|
849
1807
|
error.code = this.result.code;
|
|
850
1808
|
error.stdout = this.result.stdout;
|
|
851
1809
|
error.stderr = this.result.stderr;
|
|
852
1810
|
error.result = this.result;
|
|
1811
|
+
|
|
1812
|
+
trace('ProcessRunner', () => `About to throw errexit error`);
|
|
853
1813
|
throw error;
|
|
854
1814
|
}
|
|
1815
|
+
|
|
1816
|
+
trace('ProcessRunner', () => `Returning result successfully | ${JSON.stringify({
|
|
1817
|
+
exitCode: this.result.code,
|
|
1818
|
+
errexit: globalShellSettings.errexit
|
|
1819
|
+
}, null, 2)}`);
|
|
855
1820
|
|
|
856
1821
|
return this.result;
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
trace('ProcessRunner', () => `Caught error in _doStartAsync | ${JSON.stringify({
|
|
1824
|
+
errorMessage: error.message,
|
|
1825
|
+
errorCode: error.code,
|
|
1826
|
+
isCommandError: error.isCommandError,
|
|
1827
|
+
hasResult: !!error.result,
|
|
1828
|
+
command: this.spec?.command?.slice(0, 100)
|
|
1829
|
+
}, null, 2)}`);
|
|
1830
|
+
|
|
1831
|
+
// Ensure cleanup happens even if execution fails
|
|
1832
|
+
trace('ProcessRunner', () => `_doStartAsync caught error: ${error.message}`);
|
|
1833
|
+
|
|
1834
|
+
if (!this.finished) {
|
|
1835
|
+
// Create a result from the error
|
|
1836
|
+
const errorResult = createResult({
|
|
1837
|
+
code: error.code ?? 1,
|
|
1838
|
+
stdout: error.stdout ?? '',
|
|
1839
|
+
stderr: error.stderr ?? error.message ?? '',
|
|
1840
|
+
stdin: ''
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
// Finish to trigger cleanup
|
|
1844
|
+
this.finish(errorResult);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Re-throw the error after cleanup
|
|
1848
|
+
throw error;
|
|
1849
|
+
}
|
|
857
1850
|
}
|
|
858
1851
|
|
|
859
1852
|
async _pumpStdinTo(child, captureChunks) {
|
|
@@ -980,7 +1973,23 @@ class ProcessRunner extends StreamEmitter {
|
|
|
980
1973
|
try {
|
|
981
1974
|
// Prepare stdin
|
|
982
1975
|
let stdinData = '';
|
|
983
|
-
|
|
1976
|
+
|
|
1977
|
+
// Special handling for streaming mode (stdin: "pipe")
|
|
1978
|
+
if (this.options.stdin === 'pipe') {
|
|
1979
|
+
// For streaming interfaces, virtual commands should fallback to real commands
|
|
1980
|
+
// because virtual commands don't support true streaming
|
|
1981
|
+
trace('ProcessRunner', () => `Virtual command fallback for streaming | ${JSON.stringify({ cmd }, null, 2)}`);
|
|
1982
|
+
|
|
1983
|
+
// Create a new ProcessRunner for the real command with properly merged options
|
|
1984
|
+
// Preserve main options but use appropriate stdin for the real command
|
|
1985
|
+
const modifiedOptions = {
|
|
1986
|
+
...this.options,
|
|
1987
|
+
stdin: 'pipe', // Keep pipe but ensure it doesn't trigger virtual command fallback
|
|
1988
|
+
_bypassVirtual: true // Flag to prevent virtual command recursion
|
|
1989
|
+
};
|
|
1990
|
+
const realRunner = new ProcessRunner({ mode: 'shell', command: originalCommand || cmd }, modifiedOptions);
|
|
1991
|
+
return await realRunner._doStartAsync();
|
|
1992
|
+
} else if (this.options.stdin && typeof this.options.stdin === 'string') {
|
|
984
1993
|
stdinData = this.options.stdin;
|
|
985
1994
|
} else if (this.options.stdin && Buffer.isBuffer(this.options.stdin)) {
|
|
986
1995
|
stdinData = this.options.stdin.toString('utf8');
|
|
@@ -1003,12 +2012,28 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1003
2012
|
const chunks = [];
|
|
1004
2013
|
|
|
1005
2014
|
const commandOptions = {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
2015
|
+
// Commonly used options at top level for convenience
|
|
2016
|
+
cwd: this.options.cwd,
|
|
2017
|
+
env: this.options.env,
|
|
2018
|
+
// All original options (built-in + custom) in options object
|
|
2019
|
+
options: this.options,
|
|
2020
|
+
isCancelled: () => this._cancelled
|
|
1009
2021
|
};
|
|
2022
|
+
|
|
2023
|
+
trace('ProcessRunner', () => `_runVirtual signal details | ${JSON.stringify({
|
|
2024
|
+
cmd,
|
|
2025
|
+
hasAbortController: !!this._abortController,
|
|
2026
|
+
signalAborted: this._abortController?.signal?.aborted,
|
|
2027
|
+
optionsSignalExists: !!this.options.signal,
|
|
2028
|
+
optionsSignalAborted: this.options.signal?.aborted
|
|
2029
|
+
}, null, 2)}`);
|
|
1010
2030
|
|
|
1011
|
-
const generator = handler({
|
|
2031
|
+
const generator = handler({
|
|
2032
|
+
args: argValues,
|
|
2033
|
+
stdin: stdinData,
|
|
2034
|
+
abortSignal: this._abortController?.signal,
|
|
2035
|
+
...commandOptions
|
|
2036
|
+
});
|
|
1012
2037
|
this._virtualGenerator = generator;
|
|
1013
2038
|
|
|
1014
2039
|
const cancelPromise = new Promise(resolve => {
|
|
@@ -1020,12 +2045,27 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1020
2045
|
let done = false;
|
|
1021
2046
|
|
|
1022
2047
|
while (!done && !this._cancelled) {
|
|
2048
|
+
trace('ProcessRunner', () => `Virtual command iteration starting | ${JSON.stringify({
|
|
2049
|
+
cancelled: this._cancelled,
|
|
2050
|
+
streamBreaking: this._streamBreaking
|
|
2051
|
+
}, null, 2)}`);
|
|
2052
|
+
|
|
1023
2053
|
const result = await Promise.race([
|
|
1024
2054
|
iterator.next(),
|
|
1025
2055
|
cancelPromise.then(() => ({ done: true, cancelled: true }))
|
|
1026
2056
|
]);
|
|
1027
2057
|
|
|
2058
|
+
trace('ProcessRunner', () => `Virtual command iteration result | ${JSON.stringify({
|
|
2059
|
+
hasValue: !!result.value,
|
|
2060
|
+
done: result.done,
|
|
2061
|
+
cancelled: result.cancelled || this._cancelled
|
|
2062
|
+
}, null, 2)}`);
|
|
2063
|
+
|
|
1028
2064
|
if (result.cancelled || this._cancelled) {
|
|
2065
|
+
trace('ProcessRunner', () => `Virtual command cancelled - closing generator | ${JSON.stringify({
|
|
2066
|
+
resultCancelled: result.cancelled,
|
|
2067
|
+
thisCancelled: this._cancelled
|
|
2068
|
+
}, null, 2)}`);
|
|
1029
2069
|
// Cancelled - close the generator
|
|
1030
2070
|
if (iterator.return) {
|
|
1031
2071
|
await iterator.return();
|
|
@@ -1036,18 +2076,35 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1036
2076
|
done = result.done;
|
|
1037
2077
|
|
|
1038
2078
|
if (!done) {
|
|
2079
|
+
// Check cancellation again before processing the chunk
|
|
2080
|
+
if (this._cancelled) {
|
|
2081
|
+
trace('ProcessRunner', () => 'Skipping chunk processing - cancelled during iteration');
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1039
2085
|
const chunk = result.value;
|
|
1040
2086
|
const buf = Buffer.from(chunk);
|
|
2087
|
+
|
|
2088
|
+
// Check cancelled flag once more before any output
|
|
2089
|
+
if (this._cancelled || this._streamBreaking) {
|
|
2090
|
+
trace('ProcessRunner', () => `Cancelled or stream breaking before output - skipping | ${JSON.stringify({
|
|
2091
|
+
cancelled: this._cancelled,
|
|
2092
|
+
streamBreaking: this._streamBreaking
|
|
2093
|
+
}, null, 2)}`);
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1041
2097
|
chunks.push(buf);
|
|
1042
2098
|
|
|
1043
|
-
// Only output if not cancelled
|
|
1044
|
-
if (!this._cancelled) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
this._emitProcessedData('stdout', buf);
|
|
2099
|
+
// Only output if not cancelled and stream not breaking
|
|
2100
|
+
if (!this._cancelled && !this._streamBreaking && this.options.mirror) {
|
|
2101
|
+
trace('ProcessRunner', () => `Mirroring virtual command output | ${JSON.stringify({
|
|
2102
|
+
chunkSize: buf.length
|
|
2103
|
+
}, null, 2)}`);
|
|
2104
|
+
safeWrite(process.stdout, buf);
|
|
1050
2105
|
}
|
|
2106
|
+
|
|
2107
|
+
this._emitProcessedData('stdout', buf);
|
|
1051
2108
|
}
|
|
1052
2109
|
}
|
|
1053
2110
|
} finally {
|
|
@@ -1063,8 +2120,59 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1063
2120
|
stdin: this.options.capture ? stdinData : undefined
|
|
1064
2121
|
};
|
|
1065
2122
|
} else {
|
|
1066
|
-
// Regular async function
|
|
1067
|
-
|
|
2123
|
+
// Regular async function - race with abort signal
|
|
2124
|
+
const commandOptions = {
|
|
2125
|
+
// Commonly used options at top level for convenience
|
|
2126
|
+
cwd: this.options.cwd,
|
|
2127
|
+
env: this.options.env,
|
|
2128
|
+
// All original options (built-in + custom) in options object
|
|
2129
|
+
options: this.options,
|
|
2130
|
+
isCancelled: () => this._cancelled
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
trace('ProcessRunner', () => `_runVirtual signal details (non-generator) | ${JSON.stringify({
|
|
2134
|
+
cmd,
|
|
2135
|
+
hasAbortController: !!this._abortController,
|
|
2136
|
+
signalAborted: this._abortController?.signal?.aborted,
|
|
2137
|
+
optionsSignalExists: !!this.options.signal,
|
|
2138
|
+
optionsSignalAborted: this.options.signal?.aborted
|
|
2139
|
+
}, null, 2)}`);
|
|
2140
|
+
|
|
2141
|
+
const handlerPromise = handler({
|
|
2142
|
+
args: argValues,
|
|
2143
|
+
stdin: stdinData,
|
|
2144
|
+
abortSignal: this._abortController?.signal,
|
|
2145
|
+
...commandOptions
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
// Create an abort promise that rejects when cancelled
|
|
2149
|
+
const abortPromise = new Promise((_, reject) => {
|
|
2150
|
+
if (this._abortController && this._abortController.signal.aborted) {
|
|
2151
|
+
reject(new Error('Command cancelled'));
|
|
2152
|
+
}
|
|
2153
|
+
if (this._abortController) {
|
|
2154
|
+
this._abortController.signal.addEventListener('abort', () => {
|
|
2155
|
+
reject(new Error('Command cancelled'));
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
try {
|
|
2161
|
+
result = await Promise.race([handlerPromise, abortPromise]);
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
if (err.message === 'Command cancelled') {
|
|
2164
|
+
// Command was cancelled, return appropriate exit code based on signal
|
|
2165
|
+
const exitCode = this._cancellationSignal === 'SIGINT' ? 130 : 143; // 130 for SIGINT, 143 for SIGTERM
|
|
2166
|
+
trace('ProcessRunner', () => `Virtual command cancelled with signal ${this._cancellationSignal}, exit code: ${exitCode}`);
|
|
2167
|
+
result = {
|
|
2168
|
+
code: exitCode,
|
|
2169
|
+
stdout: '',
|
|
2170
|
+
stderr: ''
|
|
2171
|
+
};
|
|
2172
|
+
} else {
|
|
2173
|
+
throw err;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
1068
2176
|
|
|
1069
2177
|
result = {
|
|
1070
2178
|
...result,
|
|
@@ -1092,13 +2200,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1092
2200
|
}
|
|
1093
2201
|
}
|
|
1094
2202
|
|
|
1095
|
-
//
|
|
1096
|
-
this.result
|
|
1097
|
-
this.finished = true;
|
|
1098
|
-
|
|
1099
|
-
// Emit completion events
|
|
1100
|
-
this.emit('end', result);
|
|
1101
|
-
this.emit('exit', result.code);
|
|
2203
|
+
// Finish the process with proper event emission order
|
|
2204
|
+
this.finish(result);
|
|
1102
2205
|
|
|
1103
2206
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1104
2207
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
@@ -1118,9 +2221,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1118
2221
|
stdin: ''
|
|
1119
2222
|
};
|
|
1120
2223
|
|
|
1121
|
-
this.result = result;
|
|
1122
|
-
this.finished = true;
|
|
1123
|
-
|
|
1124
2224
|
if (result.stderr) {
|
|
1125
2225
|
const buf = Buffer.from(result.stderr);
|
|
1126
2226
|
if (this.options.mirror) {
|
|
@@ -1129,8 +2229,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1129
2229
|
this._emitProcessedData('stderr', buf);
|
|
1130
2230
|
}
|
|
1131
2231
|
|
|
1132
|
-
this.
|
|
1133
|
-
this.emit('exit', result.code);
|
|
2232
|
+
this.finish(result);
|
|
1134
2233
|
|
|
1135
2234
|
if (globalShellSettings.errexit) {
|
|
1136
2235
|
error.result = result;
|
|
@@ -1321,11 +2420,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1321
2420
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1322
2421
|
});
|
|
1323
2422
|
|
|
1324
|
-
|
|
1325
|
-
this.
|
|
1326
|
-
|
|
1327
|
-
this.emit('end', result);
|
|
1328
|
-
this.emit('exit', result.code);
|
|
2423
|
+
// Finish the process with proper event emission order
|
|
2424
|
+
this.finish(result);
|
|
1329
2425
|
|
|
1330
2426
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1331
2427
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
@@ -1499,11 +2595,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1499
2595
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1500
2596
|
});
|
|
1501
2597
|
|
|
1502
|
-
|
|
1503
|
-
this.
|
|
1504
|
-
|
|
1505
|
-
this.emit('end', result);
|
|
1506
|
-
this.emit('exit', result.code);
|
|
2598
|
+
// Finish the process with proper event emission order
|
|
2599
|
+
this.finish(result);
|
|
1507
2600
|
|
|
1508
2601
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
1509
2602
|
const error = new Error(`Pipeline failed with exit code ${result.code}`);
|
|
@@ -1728,11 +2821,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1728
2821
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1729
2822
|
});
|
|
1730
2823
|
|
|
1731
|
-
|
|
1732
|
-
this.
|
|
1733
|
-
|
|
1734
|
-
this.emit('end', result);
|
|
1735
|
-
this.emit('exit', result.code);
|
|
2824
|
+
// Finish the process with proper event emission order
|
|
2825
|
+
this.finish(result);
|
|
1736
2826
|
|
|
1737
2827
|
return result;
|
|
1738
2828
|
}
|
|
@@ -1835,12 +2925,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1835
2925
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1836
2926
|
});
|
|
1837
2927
|
|
|
1838
|
-
|
|
1839
|
-
this.
|
|
1840
|
-
|
|
1841
|
-
// Emit completion events
|
|
1842
|
-
this.emit('end', finalResult);
|
|
1843
|
-
this.emit('exit', finalResult.code);
|
|
2928
|
+
// Finish the process with proper event emission order
|
|
2929
|
+
this.finish(finalResult);
|
|
1844
2930
|
|
|
1845
2931
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
1846
2932
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
@@ -1871,9 +2957,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1871
2957
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
1872
2958
|
});
|
|
1873
2959
|
|
|
1874
|
-
this.result = result;
|
|
1875
|
-
this.finished = true;
|
|
1876
|
-
|
|
1877
2960
|
if (result.stderr) {
|
|
1878
2961
|
const buf = Buffer.from(result.stderr);
|
|
1879
2962
|
if (this.options.mirror) {
|
|
@@ -1882,8 +2965,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1882
2965
|
this._emitProcessedData('stderr', buf);
|
|
1883
2966
|
}
|
|
1884
2967
|
|
|
1885
|
-
this.
|
|
1886
|
-
this.emit('exit', result.code);
|
|
2968
|
+
this.finish(result);
|
|
1887
2969
|
|
|
1888
2970
|
if (globalShellSettings.errexit) {
|
|
1889
2971
|
throw error;
|
|
@@ -1928,17 +3010,47 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1928
3010
|
const spawnNodeAsync = async (argv, stdin, isLastCommand = false) => {
|
|
1929
3011
|
|
|
1930
3012
|
return new Promise((resolve, reject) => {
|
|
3013
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Creating child process | ${JSON.stringify({
|
|
3014
|
+
command: argv[0],
|
|
3015
|
+
args: argv.slice(1),
|
|
3016
|
+
cwd: this.options.cwd,
|
|
3017
|
+
isLastCommand
|
|
3018
|
+
})}`);
|
|
3019
|
+
|
|
1931
3020
|
const proc = cp.spawn(argv[0], argv.slice(1), {
|
|
1932
3021
|
cwd: this.options.cwd,
|
|
1933
3022
|
env: this.options.env,
|
|
1934
3023
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
1935
3024
|
});
|
|
1936
3025
|
|
|
3026
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Child process created | ${JSON.stringify({
|
|
3027
|
+
pid: proc.pid,
|
|
3028
|
+
killed: proc.killed,
|
|
3029
|
+
hasStdout: !!proc.stdout,
|
|
3030
|
+
hasStderr: !!proc.stderr
|
|
3031
|
+
})}`);
|
|
3032
|
+
|
|
1937
3033
|
let stdout = '';
|
|
1938
3034
|
let stderr = '';
|
|
3035
|
+
let stdoutChunks = 0;
|
|
3036
|
+
let stderrChunks = 0;
|
|
1939
3037
|
|
|
3038
|
+
const procPid = proc.pid; // Capture PID once to avoid null reference
|
|
3039
|
+
|
|
1940
3040
|
proc.stdout.on('data', (chunk) => {
|
|
1941
|
-
|
|
3041
|
+
const chunkStr = chunk.toString();
|
|
3042
|
+
stdout += chunkStr;
|
|
3043
|
+
stdoutChunks++;
|
|
3044
|
+
|
|
3045
|
+
trace('ProcessRunner', () => `spawnNodeAsync: stdout chunk received | ${JSON.stringify({
|
|
3046
|
+
pid: procPid,
|
|
3047
|
+
chunkNumber: stdoutChunks,
|
|
3048
|
+
chunkLength: chunk.length,
|
|
3049
|
+
totalStdoutLength: stdout.length,
|
|
3050
|
+
isLastCommand,
|
|
3051
|
+
preview: chunkStr.slice(0, 100)
|
|
3052
|
+
})}`);
|
|
3053
|
+
|
|
1942
3054
|
// If this is the last command, emit streaming data
|
|
1943
3055
|
if (isLastCommand) {
|
|
1944
3056
|
if (this.options.mirror) {
|
|
@@ -1949,7 +3061,19 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1949
3061
|
});
|
|
1950
3062
|
|
|
1951
3063
|
proc.stderr.on('data', (chunk) => {
|
|
1952
|
-
|
|
3064
|
+
const chunkStr = chunk.toString();
|
|
3065
|
+
stderr += chunkStr;
|
|
3066
|
+
stderrChunks++;
|
|
3067
|
+
|
|
3068
|
+
trace('ProcessRunner', () => `spawnNodeAsync: stderr chunk received | ${JSON.stringify({
|
|
3069
|
+
pid: procPid,
|
|
3070
|
+
chunkNumber: stderrChunks,
|
|
3071
|
+
chunkLength: chunk.length,
|
|
3072
|
+
totalStderrLength: stderr.length,
|
|
3073
|
+
isLastCommand,
|
|
3074
|
+
preview: chunkStr.slice(0, 100)
|
|
3075
|
+
})}`);
|
|
3076
|
+
|
|
1953
3077
|
// If this is the last command, emit streaming data
|
|
1954
3078
|
if (isLastCommand) {
|
|
1955
3079
|
if (this.options.mirror) {
|
|
@@ -1960,6 +3084,15 @@ class ProcessRunner extends StreamEmitter {
|
|
|
1960
3084
|
});
|
|
1961
3085
|
|
|
1962
3086
|
proc.on('close', (code) => {
|
|
3087
|
+
trace('ProcessRunner', () => `spawnNodeAsync: Process closed | ${JSON.stringify({
|
|
3088
|
+
pid: procPid,
|
|
3089
|
+
code,
|
|
3090
|
+
stdoutLength: stdout.length,
|
|
3091
|
+
stderrLength: stderr.length,
|
|
3092
|
+
stdoutChunks,
|
|
3093
|
+
stderrChunks
|
|
3094
|
+
})}`);
|
|
3095
|
+
|
|
1963
3096
|
resolve({
|
|
1964
3097
|
status: code,
|
|
1965
3098
|
stdout,
|
|
@@ -2040,12 +3173,8 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2040
3173
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2041
3174
|
});
|
|
2042
3175
|
|
|
2043
|
-
|
|
2044
|
-
this.
|
|
2045
|
-
|
|
2046
|
-
// Emit completion events
|
|
2047
|
-
this.emit('end', finalResult);
|
|
2048
|
-
this.emit('exit', finalResult.code);
|
|
3176
|
+
// Finish the process with proper event emission order
|
|
3177
|
+
this.finish(finalResult);
|
|
2049
3178
|
|
|
2050
3179
|
if (globalShellSettings.errexit && finalResult.code !== 0) {
|
|
2051
3180
|
const error = new Error(`Pipeline failed with exit code ${finalResult.code}`);
|
|
@@ -2068,9 +3197,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2068
3197
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2069
3198
|
});
|
|
2070
3199
|
|
|
2071
|
-
this.result = result;
|
|
2072
|
-
this.finished = true;
|
|
2073
|
-
|
|
2074
3200
|
if (result.stderr) {
|
|
2075
3201
|
const buf = Buffer.from(result.stderr);
|
|
2076
3202
|
if (this.options.mirror) {
|
|
@@ -2079,8 +3205,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2079
3205
|
this._emitProcessedData('stderr', buf);
|
|
2080
3206
|
}
|
|
2081
3207
|
|
|
2082
|
-
this.
|
|
2083
|
-
this.emit('exit', result.code);
|
|
3208
|
+
this.finish(result);
|
|
2084
3209
|
|
|
2085
3210
|
if (globalShellSettings.errexit) {
|
|
2086
3211
|
throw error;
|
|
@@ -2163,17 +3288,13 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2163
3288
|
this.options.stdin && Buffer.isBuffer(this.options.stdin) ? this.options.stdin.toString('utf8') : ''
|
|
2164
3289
|
});
|
|
2165
3290
|
|
|
2166
|
-
this.result = result;
|
|
2167
|
-
this.finished = true;
|
|
2168
|
-
|
|
2169
3291
|
const buf = Buffer.from(result.stderr);
|
|
2170
3292
|
if (this.options.mirror) {
|
|
2171
3293
|
safeWrite(process.stderr, buf);
|
|
2172
3294
|
}
|
|
2173
3295
|
this._emitProcessedData('stderr', buf);
|
|
2174
3296
|
|
|
2175
|
-
this.
|
|
2176
|
-
this.emit('exit', result.code);
|
|
3297
|
+
this.finish(result);
|
|
2177
3298
|
|
|
2178
3299
|
return result;
|
|
2179
3300
|
}
|
|
@@ -2194,12 +3315,16 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2194
3315
|
let resolve, reject;
|
|
2195
3316
|
let ended = false;
|
|
2196
3317
|
let cleanedUp = false;
|
|
3318
|
+
let killed = false;
|
|
2197
3319
|
|
|
2198
3320
|
const onData = (chunk) => {
|
|
2199
|
-
buffer
|
|
2200
|
-
if (
|
|
2201
|
-
|
|
2202
|
-
resolve
|
|
3321
|
+
// Don't buffer more data if we're being killed
|
|
3322
|
+
if (!killed) {
|
|
3323
|
+
buffer.push(chunk);
|
|
3324
|
+
if (resolve) {
|
|
3325
|
+
resolve();
|
|
3326
|
+
resolve = reject = null;
|
|
3327
|
+
}
|
|
2203
3328
|
}
|
|
2204
3329
|
};
|
|
2205
3330
|
|
|
@@ -2216,8 +3341,18 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2216
3341
|
|
|
2217
3342
|
try {
|
|
2218
3343
|
while (!ended || buffer.length > 0) {
|
|
3344
|
+
// Check if we've been killed and should stop immediately
|
|
3345
|
+
if (killed) {
|
|
3346
|
+
trace('ProcessRunner', () => 'Stream killed, stopping iteration');
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
2219
3349
|
if (buffer.length > 0) {
|
|
2220
|
-
|
|
3350
|
+
const chunk = buffer.shift();
|
|
3351
|
+
// Set a flag that we're about to yield - if the consumer breaks,
|
|
3352
|
+
// we'll know not to process any more data
|
|
3353
|
+
this._streamYielding = true;
|
|
3354
|
+
yield chunk;
|
|
3355
|
+
this._streamYielding = false;
|
|
2221
3356
|
} else if (!ended) {
|
|
2222
3357
|
await new Promise((res, rej) => {
|
|
2223
3358
|
resolve = res;
|
|
@@ -2232,41 +3367,97 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2232
3367
|
|
|
2233
3368
|
// This happens when breaking from a for-await loop
|
|
2234
3369
|
if (!this.finished) {
|
|
3370
|
+
killed = true;
|
|
3371
|
+
buffer = []; // Clear any buffered data
|
|
3372
|
+
this._streamBreaking = true; // Signal that stream is breaking
|
|
2235
3373
|
this.kill();
|
|
2236
3374
|
}
|
|
2237
3375
|
}
|
|
2238
3376
|
}
|
|
2239
3377
|
|
|
2240
|
-
kill() {
|
|
3378
|
+
kill(signal = 'SIGTERM') {
|
|
2241
3379
|
trace('ProcessRunner', () => `kill ENTER | ${JSON.stringify({
|
|
3380
|
+
signal,
|
|
2242
3381
|
cancelled: this._cancelled,
|
|
2243
3382
|
finished: this.finished,
|
|
2244
3383
|
hasChild: !!this.child,
|
|
2245
|
-
hasVirtualGenerator: !!this._virtualGenerator
|
|
3384
|
+
hasVirtualGenerator: !!this._virtualGenerator,
|
|
3385
|
+
command: this.spec?.command?.slice(0, 50) || 'unknown'
|
|
2246
3386
|
}, null, 2)}`);
|
|
2247
3387
|
|
|
2248
|
-
|
|
3388
|
+
if (this.finished) {
|
|
3389
|
+
trace('ProcessRunner', () => 'Already finished, skipping kill');
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// Mark as cancelled for virtual commands and store the signal
|
|
3394
|
+
trace('ProcessRunner', () => `Marking as cancelled | ${JSON.stringify({
|
|
3395
|
+
signal,
|
|
3396
|
+
previouslyCancelled: this._cancelled,
|
|
3397
|
+
previousSignal: this._cancellationSignal
|
|
3398
|
+
}, null, 2)}`);
|
|
2249
3399
|
this._cancelled = true;
|
|
3400
|
+
this._cancellationSignal = signal;
|
|
3401
|
+
|
|
3402
|
+
// If this is a pipeline runner, also kill the source and destination
|
|
3403
|
+
if (this.spec?.mode === 'pipeline') {
|
|
3404
|
+
trace('ProcessRunner', () => 'Killing pipeline components');
|
|
3405
|
+
if (this.spec.source && typeof this.spec.source.kill === 'function') {
|
|
3406
|
+
this.spec.source.kill(signal);
|
|
3407
|
+
}
|
|
3408
|
+
if (this.spec.destination && typeof this.spec.destination.kill === 'function') {
|
|
3409
|
+
this.spec.destination.kill(signal);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
2250
3412
|
|
|
2251
3413
|
if (this._cancelResolve) {
|
|
2252
3414
|
trace('ProcessRunner', () => 'Resolving cancel promise');
|
|
2253
3415
|
this._cancelResolve();
|
|
3416
|
+
trace('ProcessRunner', () => 'Cancel promise resolved');
|
|
3417
|
+
} else {
|
|
3418
|
+
trace('ProcessRunner', () => 'No cancel promise to resolve');
|
|
2254
3419
|
}
|
|
2255
3420
|
|
|
2256
3421
|
// Abort any async operations
|
|
2257
3422
|
if (this._abortController) {
|
|
2258
|
-
trace('ProcessRunner', () =>
|
|
3423
|
+
trace('ProcessRunner', () => `Aborting internal controller | ${JSON.stringify({
|
|
3424
|
+
wasAborted: this._abortController?.signal?.aborted
|
|
3425
|
+
}, null, 2)}`);
|
|
2259
3426
|
this._abortController.abort();
|
|
3427
|
+
trace('ProcessRunner', () => `Internal controller aborted | ${JSON.stringify({
|
|
3428
|
+
nowAborted: this._abortController?.signal?.aborted
|
|
3429
|
+
}, null, 2)}`);
|
|
3430
|
+
} else {
|
|
3431
|
+
trace('ProcessRunner', () => 'No abort controller to abort');
|
|
2260
3432
|
}
|
|
2261
3433
|
|
|
2262
3434
|
// If it's a virtual generator, try to close it
|
|
2263
|
-
if (this._virtualGenerator
|
|
2264
|
-
trace('ProcessRunner', () =>
|
|
2265
|
-
|
|
2266
|
-
this._virtualGenerator.
|
|
2267
|
-
|
|
2268
|
-
|
|
3435
|
+
if (this._virtualGenerator) {
|
|
3436
|
+
trace('ProcessRunner', () => `Virtual generator found for cleanup | ${JSON.stringify({
|
|
3437
|
+
hasReturn: typeof this._virtualGenerator.return === 'function',
|
|
3438
|
+
hasThrow: typeof this._virtualGenerator.throw === 'function',
|
|
3439
|
+
cancelled: this._cancelled,
|
|
3440
|
+
signal
|
|
3441
|
+
}, null, 2)}`);
|
|
3442
|
+
|
|
3443
|
+
if (this._virtualGenerator.return) {
|
|
3444
|
+
trace('ProcessRunner', () => 'Closing virtual generator with return()');
|
|
3445
|
+
try {
|
|
3446
|
+
this._virtualGenerator.return();
|
|
3447
|
+
trace('ProcessRunner', () => 'Virtual generator closed successfully');
|
|
3448
|
+
} catch (err) {
|
|
3449
|
+
trace('ProcessRunner', () => `Error closing generator | ${JSON.stringify({
|
|
3450
|
+
error: err.message,
|
|
3451
|
+
stack: err.stack?.slice(0, 200)
|
|
3452
|
+
}, null, 2)}`);
|
|
3453
|
+
}
|
|
3454
|
+
} else {
|
|
3455
|
+
trace('ProcessRunner', () => 'Virtual generator has no return() method');
|
|
2269
3456
|
}
|
|
3457
|
+
} else {
|
|
3458
|
+
trace('ProcessRunner', () => `No virtual generator to cleanup | ${JSON.stringify({
|
|
3459
|
+
hasVirtualGenerator: !!this._virtualGenerator
|
|
3460
|
+
}, null, 2)}`);
|
|
2270
3461
|
}
|
|
2271
3462
|
|
|
2272
3463
|
// Kill child process if it exists
|
|
@@ -2276,14 +3467,112 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2276
3467
|
if (this.child.pid) {
|
|
2277
3468
|
if (isBun) {
|
|
2278
3469
|
trace('ProcessRunner', () => `Killing Bun process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
2279
|
-
|
|
3470
|
+
|
|
3471
|
+
// For Bun, use the same enhanced kill logic as Node.js for CI reliability
|
|
3472
|
+
const killOperations = [];
|
|
3473
|
+
|
|
3474
|
+
// Try SIGTERM first
|
|
3475
|
+
try {
|
|
3476
|
+
process.kill(this.child.pid, 'SIGTERM');
|
|
3477
|
+
trace('ProcessRunner', () => `Sent SIGTERM to Bun process ${this.child.pid}`);
|
|
3478
|
+
killOperations.push('SIGTERM to process');
|
|
3479
|
+
} catch (err) {
|
|
3480
|
+
trace('ProcessRunner', () => `Error sending SIGTERM to Bun process: ${err.message}`);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
// Try process group SIGTERM
|
|
3484
|
+
try {
|
|
3485
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
3486
|
+
trace('ProcessRunner', () => `Sent SIGTERM to Bun process group -${this.child.pid}`);
|
|
3487
|
+
killOperations.push('SIGTERM to group');
|
|
3488
|
+
} catch (err) {
|
|
3489
|
+
trace('ProcessRunner', () => `Bun process group SIGTERM failed: ${err.message}`);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
// Immediately follow with SIGKILL for both process and group
|
|
3493
|
+
try {
|
|
3494
|
+
process.kill(this.child.pid, 'SIGKILL');
|
|
3495
|
+
trace('ProcessRunner', () => `Sent SIGKILL to Bun process ${this.child.pid}`);
|
|
3496
|
+
killOperations.push('SIGKILL to process');
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
trace('ProcessRunner', () => `Error sending SIGKILL to Bun process: ${err.message}`);
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
try {
|
|
3502
|
+
process.kill(-this.child.pid, 'SIGKILL');
|
|
3503
|
+
trace('ProcessRunner', () => `Sent SIGKILL to Bun process group -${this.child.pid}`);
|
|
3504
|
+
killOperations.push('SIGKILL to group');
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
trace('ProcessRunner', () => `Bun process group SIGKILL failed: ${err.message}`);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
trace('ProcessRunner', () => `Bun kill operations attempted: ${killOperations.join(', ')}`);
|
|
3510
|
+
|
|
3511
|
+
// Also call the original Bun kill method as backup
|
|
3512
|
+
try {
|
|
3513
|
+
this.child.kill();
|
|
3514
|
+
trace('ProcessRunner', () => `Called child.kill() for Bun process ${this.child.pid}`);
|
|
3515
|
+
} catch (err) {
|
|
3516
|
+
trace('ProcessRunner', () => `Error calling child.kill(): ${err.message}`);
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// Force cleanup of child reference
|
|
3520
|
+
if (this.child) {
|
|
3521
|
+
this.child.removeAllListeners?.();
|
|
3522
|
+
this.child = null;
|
|
3523
|
+
}
|
|
2280
3524
|
} else {
|
|
2281
|
-
// In Node.js,
|
|
2282
|
-
trace('ProcessRunner', () => `Killing Node process
|
|
2283
|
-
|
|
3525
|
+
// In Node.js, use a more robust approach for CI environments
|
|
3526
|
+
trace('ProcessRunner', () => `Killing Node process | ${JSON.stringify({ pid: this.child.pid }, null, 2)}`);
|
|
3527
|
+
|
|
3528
|
+
// Use immediate and aggressive termination for CI environments
|
|
3529
|
+
const killOperations = [];
|
|
3530
|
+
|
|
3531
|
+
// Try SIGTERM to the process directly
|
|
3532
|
+
try {
|
|
3533
|
+
process.kill(this.child.pid, 'SIGTERM');
|
|
3534
|
+
trace('ProcessRunner', () => `Sent SIGTERM to process ${this.child.pid}`);
|
|
3535
|
+
killOperations.push('SIGTERM to process');
|
|
3536
|
+
} catch (err) {
|
|
3537
|
+
trace('ProcessRunner', () => `Error sending SIGTERM to process: ${err.message}`);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
// Try process group if detached (negative PID)
|
|
3541
|
+
try {
|
|
3542
|
+
process.kill(-this.child.pid, 'SIGTERM');
|
|
3543
|
+
trace('ProcessRunner', () => `Sent SIGTERM to process group -${this.child.pid}`);
|
|
3544
|
+
killOperations.push('SIGTERM to group');
|
|
3545
|
+
} catch (err) {
|
|
3546
|
+
trace('ProcessRunner', () => `Process group SIGTERM failed: ${err.message}`);
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// Immediately follow up with SIGKILL for CI reliability
|
|
3550
|
+
try {
|
|
3551
|
+
process.kill(this.child.pid, 'SIGKILL');
|
|
3552
|
+
trace('ProcessRunner', () => `Sent SIGKILL to process ${this.child.pid}`);
|
|
3553
|
+
killOperations.push('SIGKILL to process');
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
trace('ProcessRunner', () => `Error sending SIGKILL to process: ${err.message}`);
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
try {
|
|
3559
|
+
process.kill(-this.child.pid, 'SIGKILL');
|
|
3560
|
+
trace('ProcessRunner', () => `Sent SIGKILL to process group -${this.child.pid}`);
|
|
3561
|
+
killOperations.push('SIGKILL to group');
|
|
3562
|
+
} catch (err) {
|
|
3563
|
+
trace('ProcessRunner', () => `Process group SIGKILL failed: ${err.message}`);
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
trace('ProcessRunner', () => `Kill operations attempted: ${killOperations.join(', ')}`);
|
|
3567
|
+
|
|
3568
|
+
// Force cleanup of child reference to prevent hanging awaits
|
|
3569
|
+
if (this.child) {
|
|
3570
|
+
this.child.removeAllListeners?.();
|
|
3571
|
+
this.child = null;
|
|
3572
|
+
}
|
|
2284
3573
|
}
|
|
2285
3574
|
}
|
|
2286
|
-
|
|
3575
|
+
// finished will be set by the main cleanup below
|
|
2287
3576
|
} catch (err) {
|
|
2288
3577
|
// Process might already be dead
|
|
2289
3578
|
trace('ProcessRunner', () => `Error killing process | ${JSON.stringify({ error: err.message }, null, 2)}`);
|
|
@@ -2291,8 +3580,14 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2291
3580
|
}
|
|
2292
3581
|
}
|
|
2293
3582
|
|
|
2294
|
-
// Mark as finished
|
|
2295
|
-
|
|
3583
|
+
// Mark as finished and emit completion events
|
|
3584
|
+
const result = createResult({
|
|
3585
|
+
code: signal === 'SIGKILL' ? 137 : signal === 'SIGTERM' ? 143 : 130,
|
|
3586
|
+
stdout: '',
|
|
3587
|
+
stderr: `Process killed with ${signal}`,
|
|
3588
|
+
stdin: ''
|
|
3589
|
+
});
|
|
3590
|
+
this.finish(result);
|
|
2296
3591
|
|
|
2297
3592
|
trace('ProcessRunner', () => `kill EXIT | ${JSON.stringify({
|
|
2298
3593
|
cancelled: this._cancelled,
|
|
@@ -2353,7 +3648,20 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2353
3648
|
if (!this.promise) {
|
|
2354
3649
|
this.promise = this._startAsync();
|
|
2355
3650
|
}
|
|
2356
|
-
return this.promise.finally(
|
|
3651
|
+
return this.promise.finally(() => {
|
|
3652
|
+
// Ensure cleanup happened
|
|
3653
|
+
if (!this.finished) {
|
|
3654
|
+
trace('ProcessRunner', () => 'Finally handler ensuring cleanup');
|
|
3655
|
+
const fallbackResult = createResult({
|
|
3656
|
+
code: 1,
|
|
3657
|
+
stdout: '',
|
|
3658
|
+
stderr: 'Process terminated unexpectedly',
|
|
3659
|
+
stdin: ''
|
|
3660
|
+
});
|
|
3661
|
+
this.finish(fallbackResult);
|
|
3662
|
+
}
|
|
3663
|
+
if (onFinally) onFinally();
|
|
3664
|
+
});
|
|
2357
3665
|
}
|
|
2358
3666
|
|
|
2359
3667
|
// Internal sync execution
|
|
@@ -2438,9 +3746,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2438
3746
|
this.outChunks = result.stdout ? [Buffer.from(result.stdout)] : [];
|
|
2439
3747
|
this.errChunks = result.stderr ? [Buffer.from(result.stderr)] : [];
|
|
2440
3748
|
|
|
2441
|
-
this.result = result;
|
|
2442
|
-
this.finished = true;
|
|
2443
|
-
|
|
2444
3749
|
// Emit batched events after completion
|
|
2445
3750
|
if (result.stdout) {
|
|
2446
3751
|
const stdoutBuf = Buffer.from(result.stdout);
|
|
@@ -2452,8 +3757,7 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2452
3757
|
this._emitProcessedData('stderr', stderrBuf);
|
|
2453
3758
|
}
|
|
2454
3759
|
|
|
2455
|
-
this.
|
|
2456
|
-
this.emit('exit', result.code);
|
|
3760
|
+
this.finish(result);
|
|
2457
3761
|
|
|
2458
3762
|
if (globalShellSettings.errexit && result.code !== 0) {
|
|
2459
3763
|
const error = new Error(`Command failed with exit code ${result.code}`);
|
|
@@ -2467,18 +3771,6 @@ class ProcessRunner extends StreamEmitter {
|
|
|
2467
3771
|
return result;
|
|
2468
3772
|
}
|
|
2469
3773
|
|
|
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
3774
|
}
|
|
2483
3775
|
|
|
2484
3776
|
// Public APIs
|
|
@@ -2526,6 +3818,28 @@ async function run(commandOrTokens, options = {}) {
|
|
|
2526
3818
|
}
|
|
2527
3819
|
|
|
2528
3820
|
function $tagged(strings, ...values) {
|
|
3821
|
+
// Check if called as a function with options object: $({ options })
|
|
3822
|
+
if (!Array.isArray(strings) && typeof strings === 'object' && strings !== null) {
|
|
3823
|
+
const options = strings;
|
|
3824
|
+
trace('API', () => `$tagged called with options | ${JSON.stringify({ options }, null, 2)}`);
|
|
3825
|
+
|
|
3826
|
+
// Return a new tagged template function with those options
|
|
3827
|
+
return (innerStrings, ...innerValues) => {
|
|
3828
|
+
trace('API', () => `$tagged.withOptions ENTER | ${JSON.stringify({
|
|
3829
|
+
stringsLength: innerStrings.length,
|
|
3830
|
+
valuesLength: innerValues.length,
|
|
3831
|
+
options
|
|
3832
|
+
}, null, 2)}`);
|
|
3833
|
+
|
|
3834
|
+
const cmd = buildShellCommand(innerStrings, innerValues);
|
|
3835
|
+
const runner = new ProcessRunner({ mode: 'shell', command: cmd }, { mirror: true, capture: true, ...options });
|
|
3836
|
+
|
|
3837
|
+
trace('API', () => `$tagged.withOptions EXIT | ${JSON.stringify({ command: cmd }, null, 2)}`);
|
|
3838
|
+
return runner;
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
// Normal tagged template literal usage
|
|
2529
3843
|
trace('API', () => `$tagged ENTER | ${JSON.stringify({
|
|
2530
3844
|
stringsLength: strings.length,
|
|
2531
3845
|
valuesLength: values.length
|
|
@@ -2779,6 +4093,7 @@ export {
|
|
|
2779
4093
|
AnsiUtils,
|
|
2780
4094
|
configureAnsi,
|
|
2781
4095
|
getAnsiConfig,
|
|
2782
|
-
processOutput
|
|
4096
|
+
processOutput,
|
|
4097
|
+
forceCleanupAll
|
|
2783
4098
|
};
|
|
2784
4099
|
export default $tagged;
|