@mjasnikovs/pi-task 0.40.44 → 0.40.45
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.
|
@@ -67,7 +67,10 @@ export type OwnGroupSpawnOptions = {
|
|
|
67
67
|
* backgrounded. Best-effort: a group already gone is not an error. Kept beside
|
|
68
68
|
* the spawn shape so a platform change edits one file.
|
|
69
69
|
*/
|
|
70
|
-
export declare function reapProcessGroup(pid: number, sig: NodeJS.Signals, platform?:
|
|
70
|
+
export declare function reapProcessGroup(pid: number, sig: NodeJS.Signals, { platform, leaderExited }?: {
|
|
71
|
+
platform?: NodeJS.Platform;
|
|
72
|
+
leaderExited?: boolean;
|
|
73
|
+
}): void;
|
|
71
74
|
/**
|
|
72
75
|
* Why runChild killed the child. Five sources converge on one kill path, and
|
|
73
76
|
* each names itself here rather than in its own flag — so a consumer reads ONE
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn as defaultSpawn, spawnSync as spawnSyncDefault } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { realStreamTimerDeps, StreamWatchdog } from './stream-watchdog.js';
|
|
3
4
|
import { realStallTimerDeps, StallProbe } from './stall-probe.js';
|
|
4
5
|
import { workerChannel } from '../workers/worker-channels.js';
|
|
@@ -34,10 +35,18 @@ export function ownGroupSpawnOptions(platform) {
|
|
|
34
35
|
* backgrounded. Best-effort: a group already gone is not an error. Kept beside
|
|
35
36
|
* the spawn shape so a platform change edits one file.
|
|
36
37
|
*/
|
|
37
|
-
export function reapProcessGroup(pid, sig, platform = process.platform) {
|
|
38
|
+
export function reapProcessGroup(pid, sig, { platform = process.platform, leaderExited = false } = {}) {
|
|
39
|
+
// taskkill walks the tree down from a live leader. Once the leader has exited its
|
|
40
|
+
// pid may already be another process's, and /T /F would kill that one instead.
|
|
41
|
+
if (platform === 'win32' && leaderExited)
|
|
42
|
+
return;
|
|
38
43
|
try {
|
|
39
44
|
if (platform === 'win32') {
|
|
40
|
-
|
|
45
|
+
// System32's taskkill by absolute path, as pi's killProcessTree finds it, so
|
|
46
|
+
// neither PATH nor the working directory can supply another. Synchronous,
|
|
47
|
+
// unlike pi's: runChild kills the leader next, and the walk must end first.
|
|
48
|
+
const taskkill = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'taskkill.exe');
|
|
49
|
+
spawnSyncDefault(taskkill, ['/pid', String(pid), '/T', '/F']);
|
|
41
50
|
}
|
|
42
51
|
else {
|
|
43
52
|
process.kill(-pid, sig);
|
|
@@ -285,27 +294,29 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
285
294
|
proc.stdin?.end();
|
|
286
295
|
}
|
|
287
296
|
// Reap the child's whole process group — the child itself AND anything it
|
|
288
|
-
// backgrounded.
|
|
297
|
+
// backgrounded.
|
|
298
|
+
let leaderExited = false;
|
|
299
|
+
proc.once('exit', () => (leaderExited = true));
|
|
289
300
|
const reapGroup = (sig) => {
|
|
290
301
|
if (!ownGroup || !proc.pid)
|
|
291
302
|
return;
|
|
292
|
-
reapProcessGroup(proc.pid, sig, platform);
|
|
303
|
+
reapProcessGroup(proc.pid, sig, { platform, leaderExited });
|
|
293
304
|
};
|
|
294
305
|
// One kill path for every source: SIGTERM, then SIGKILL after a grace
|
|
295
306
|
// period if the child ignored the term. For a group-owning (model) child,
|
|
296
307
|
// ALSO sweep the group so anything it backgrounded dies with it —
|
|
297
|
-
// proc.kill hits only the leader, reapGroup the grandchildren. The
|
|
298
|
-
//
|
|
308
|
+
// proc.kill hits only the leader, reapGroup the grandchildren. The sweep
|
|
309
|
+
// goes first because win32's taskkill walks the tree down from a live
|
|
310
|
+
// leader. The FIRST cause wins: a stall kill's SIGTERM can trip the abort
|
|
311
|
+
// path behind it.
|
|
299
312
|
const killProc = (cause) => {
|
|
300
313
|
kill ??= cause;
|
|
314
|
+
reapGroup('SIGTERM');
|
|
301
315
|
proc.kill('SIGTERM');
|
|
302
|
-
if (ownGroup)
|
|
303
|
-
reapGroup('SIGTERM');
|
|
304
316
|
setTimeout(() => {
|
|
317
|
+
reapGroup('SIGKILL');
|
|
305
318
|
if (!proc.killed)
|
|
306
319
|
proc.kill('SIGKILL');
|
|
307
|
-
if (ownGroup)
|
|
308
|
-
reapGroup('SIGKILL');
|
|
309
320
|
}, KILL_GRACE_MS);
|
|
310
321
|
};
|
|
311
322
|
// Stream watchdog (json-events children only): a silent stream is killed
|
|
@@ -164,8 +164,8 @@ export interface BootDeps {
|
|
|
164
164
|
* fake pid would signal something else entirely.
|
|
165
165
|
*/
|
|
166
166
|
killGroup?: (pid: number, signal: NodeJS.Signals) => void;
|
|
167
|
-
/** Which platform's
|
|
168
|
-
* from a POSIX host; production leaves it to `process.platform`. */
|
|
167
|
+
/** Which platform's spawn options, served-app rule and reap to use. Tests drive
|
|
168
|
+
* the win32 arm from a POSIX host; production leaves it to `process.platform`. */
|
|
169
169
|
platform?: NodeJS.Platform;
|
|
170
170
|
}
|
|
171
171
|
/** What `runBootCheck` passes to its spawn. */
|
package/dist/task/boot-probe.js
CHANGED
|
@@ -505,10 +505,6 @@ function holderIsOurs(command, boot) {
|
|
|
505
505
|
function defaultSpawnBoot(bin, args, o) {
|
|
506
506
|
return spawn(bin, args, o);
|
|
507
507
|
}
|
|
508
|
-
/** The real group teardown; the spawn shape's twin lives beside it. */
|
|
509
|
-
function defaultKillGroup(pid, sig) {
|
|
510
|
-
reapProcessGroup(pid, sig);
|
|
511
|
-
}
|
|
512
508
|
/**
|
|
513
509
|
* Exercise the start command ONCE. All four outcomes below were run against real
|
|
514
510
|
* child processes in throwaway projects.
|
|
@@ -557,7 +553,8 @@ function defaultKillGroup(pid, sig) {
|
|
|
557
553
|
* reports it that way — see isCommandNotFound) → skip.
|
|
558
554
|
*/
|
|
559
555
|
export async function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}) {
|
|
560
|
-
const
|
|
556
|
+
const platform = opts.deps?.platform ?? process.platform;
|
|
557
|
+
const expectServer = (opts.expectServer ?? false) && platform !== 'win32';
|
|
561
558
|
const groupHasListener = opts.deps?.groupHasListener ?? defaultGroupHasListener;
|
|
562
559
|
const httpProbe = opts.deps?.httpProbe ?? defaultHttpProbe;
|
|
563
560
|
const canEnumerate = expectServer ? (opts.deps?.enumerationCapable ?? canEnumerateListeners)() : true;
|
|
@@ -579,7 +576,7 @@ export async function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}
|
|
|
579
576
|
return new Promise(resolve => {
|
|
580
577
|
const child = spawnBoot(runner.bin, args, {
|
|
581
578
|
cwd,
|
|
582
|
-
...ownGroupSpawnOptions(
|
|
579
|
+
...ownGroupSpawnOptions(platform),
|
|
583
580
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
584
581
|
env: {
|
|
585
582
|
...runnerEnv(runner),
|
|
@@ -612,7 +609,9 @@ export async function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}
|
|
|
612
609
|
clearInterval(poll);
|
|
613
610
|
resolve(r);
|
|
614
611
|
};
|
|
615
|
-
|
|
612
|
+
let leaderExited = false;
|
|
613
|
+
const reapGroup = opts.deps?.killGroup
|
|
614
|
+
?? ((pid, sig) => reapProcessGroup(pid, sig, { platform, leaderExited }));
|
|
616
615
|
const killGroup = (sig) => {
|
|
617
616
|
// Truthiness, deliberately: `process.kill(0, sig)` signals the
|
|
618
617
|
// CALLER's own process group, so a pid of 0 turns a best-effort
|
|
@@ -725,6 +724,7 @@ export async function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}
|
|
|
725
724
|
let timer = setTimeout(onGrace, graceMs);
|
|
726
725
|
child.on('error', () => settle({ outcome: 'skip', spawnFailed: true }));
|
|
727
726
|
child.on('exit', (status, signal) => {
|
|
727
|
+
leaderExited = true;
|
|
728
728
|
if (status === 0) {
|
|
729
729
|
if (expectServer && !listenerSeen) {
|
|
730
730
|
if (!canEnumerate) {
|
|
@@ -616,6 +616,7 @@ async function drive(url, bin, userDataDir, credentials, onFacts, quietMs, signa
|
|
|
616
616
|
export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
617
617
|
let child = null;
|
|
618
618
|
let socket = null;
|
|
619
|
+
let reaped = false;
|
|
619
620
|
const close = () => {
|
|
620
621
|
try {
|
|
621
622
|
socket?.close();
|
|
@@ -623,8 +624,13 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
623
624
|
catch {
|
|
624
625
|
// socket already gone
|
|
625
626
|
}
|
|
626
|
-
|
|
627
|
-
|
|
627
|
+
// Once: a later pass would signal a pid the browser may have given up.
|
|
628
|
+
if (child?.pid && !reaped) {
|
|
629
|
+
reaped = true;
|
|
630
|
+
reapProcessGroup(child.pid, 'SIGKILL', {
|
|
631
|
+
leaderExited: child.exitCode !== null || child.signalCode !== null
|
|
632
|
+
});
|
|
633
|
+
}
|
|
628
634
|
return Promise.resolve();
|
|
629
635
|
};
|
|
630
636
|
signal?.addEventListener('abort', () => void close(), { once: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.45",
|
|
4
4
|
"description": "Deterministic task planning and spec-orchestration for local models — crash-safe /task pipelines with verify/enforce gates, a real-time remote web view, and web/docs/fetch/worker subagent tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|