iterate-skill-installer 3.4.4 → 3.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/lib/installer.js +58 -8
- package/package.json +1 -1
package/lib/installer.js
CHANGED
|
@@ -492,40 +492,90 @@ class TailBuffer {
|
|
|
492
492
|
// take minutes on slow networks, but a wedged child must never hang the
|
|
493
493
|
// installer forever.
|
|
494
494
|
const DEFAULT_COMMAND_TIMEOUT_MS = 10 * 60 * 1000;
|
|
495
|
+
// Grace period between the SIGKILL and giving up on 'close'. A grandchild that
|
|
496
|
+
// inherited our stdio pipe keeps that pipe open, so Node withholds 'close'
|
|
497
|
+
// even though the leader is already dead — without this the promise would
|
|
498
|
+
// never settle and the installer would hang despite the timeout having fired.
|
|
499
|
+
const POST_KILL_SETTLE_MS = 5 * 1000;
|
|
495
500
|
// Hard cap on retained stdout/stderr per command (memory bound).
|
|
496
501
|
const MAX_CAPTURED_OUTPUT_CHARS = 1024 * 1024;
|
|
497
502
|
|
|
503
|
+
// True only on POSIX: Windows has no process groups keyed by negative pid and
|
|
504
|
+
// `detached` there means "new console window", not "new process group".
|
|
505
|
+
const IS_POSIX = process.platform !== 'win32';
|
|
506
|
+
|
|
507
|
+
function killProcessTree(child) {
|
|
508
|
+
// The child was spawned detached (POSIX), so it is its own process-group
|
|
509
|
+
// leader and its negative pid addresses the WHOLE group. A bare
|
|
510
|
+
// `child.kill()` only signals the immediate process, so a `pip` that had
|
|
511
|
+
// already spawned a compiler/download helper outlived the kill and kept
|
|
512
|
+
// running (and holding our stdio pipe) after the installer "timed out".
|
|
513
|
+
if (IS_POSIX && child.pid) {
|
|
514
|
+
try {
|
|
515
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
516
|
+
return;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
// ESRCH: the group is already gone. EPERM: fall through to the leader.
|
|
519
|
+
if (err && err.code === 'ESRCH') return;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
child.kill('SIGKILL');
|
|
524
|
+
} catch {
|
|
525
|
+
// Already exited — nothing left to signal.
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
498
529
|
function runCommand(bin, args, options = {}) {
|
|
499
530
|
const { timeout = DEFAULT_COMMAND_TIMEOUT_MS, ...spawnOptions } = options;
|
|
500
531
|
return new Promise((resolve, reject) => {
|
|
501
|
-
|
|
532
|
+
// `detached` on POSIX puts the child in its own process group so the
|
|
533
|
+
// timeout can kill the entire tree, not just the leader. It does not
|
|
534
|
+
// change how we await the child (we never unref it).
|
|
535
|
+
const child = spawn(bin, args, { stdio: 'pipe', detached: IS_POSIX, ...spawnOptions });
|
|
502
536
|
const stdoutTail = new TailBuffer(MAX_CAPTURED_OUTPUT_CHARS);
|
|
503
537
|
const stderrTail = new TailBuffer(MAX_CAPTURED_OUTPUT_CHARS);
|
|
504
538
|
child.stdout?.on('data', (data) => stdoutTail.push(data.toString()));
|
|
505
539
|
child.stderr?.on('data', (data) => stderrTail.push(data.toString()));
|
|
506
540
|
let killed = false;
|
|
541
|
+
let settled = false;
|
|
542
|
+
let killFallback = null;
|
|
543
|
+
const settle = (fn, value) => {
|
|
544
|
+
if (settled) return;
|
|
545
|
+
settled = true;
|
|
546
|
+
fn(value);
|
|
547
|
+
};
|
|
507
548
|
const timer =
|
|
508
549
|
timeout > 0
|
|
509
550
|
? setTimeout(() => {
|
|
510
551
|
killed = true;
|
|
511
|
-
child
|
|
552
|
+
killProcessTree(child);
|
|
553
|
+
// The leader may be dead while a grandchild still holds our stdio
|
|
554
|
+
// pipe open, in which case 'close' never arrives. Guarantee the
|
|
555
|
+
// promise settles either way.
|
|
556
|
+
killFallback = setTimeout(() => {
|
|
557
|
+
settle(reject, new InstallerError(`${bin} timed out after ${timeout} ms`));
|
|
558
|
+
}, POST_KILL_SETTLE_MS);
|
|
512
559
|
}, timeout)
|
|
513
560
|
: null;
|
|
514
|
-
|
|
561
|
+
const finish = (code) => {
|
|
515
562
|
if (timer) clearTimeout(timer);
|
|
563
|
+
if (killFallback) clearTimeout(killFallback);
|
|
516
564
|
if (killed) {
|
|
517
|
-
reject
|
|
565
|
+
settle(reject, new InstallerError(`${bin} timed out after ${timeout} ms`));
|
|
518
566
|
return;
|
|
519
567
|
}
|
|
520
568
|
if (code === 0) {
|
|
521
|
-
resolve
|
|
569
|
+
settle(resolve, stdoutTail.toString());
|
|
522
570
|
} else {
|
|
523
|
-
reject
|
|
571
|
+
settle(reject, new InstallerError(`${bin} exited with ${code}: ${stderrTail.toString() || stdoutTail.toString()}`));
|
|
524
572
|
}
|
|
525
|
-
}
|
|
573
|
+
};
|
|
574
|
+
child.on('close', finish);
|
|
526
575
|
child.on('error', (err) => {
|
|
527
576
|
if (timer) clearTimeout(timer);
|
|
528
|
-
|
|
577
|
+
if (killFallback) clearTimeout(killFallback);
|
|
578
|
+
settle(reject, err);
|
|
529
579
|
});
|
|
530
580
|
});
|
|
531
581
|
}
|