@mjasnikovs/pi-task 0.40.42 → 0.40.43
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.
|
@@ -37,9 +37,37 @@ export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
|
|
|
37
37
|
* possible at all: with `detached`, a `kill(-pid)` issued after the child
|
|
38
38
|
* has exited still takes the backgrounded grandchild with it; without it
|
|
39
39
|
* the same call throws ESRCH and the grandchild survives. Set only for
|
|
40
|
-
* model children (json-events); plumbing stays in-group.
|
|
40
|
+
* model children (json-events); plumbing stays in-group. Never set on
|
|
41
|
+
* win32 — see ownGroupSpawnOptions. */
|
|
41
42
|
detached?: boolean;
|
|
43
|
+
/** win32 only: keep the child's console off-screen. */
|
|
44
|
+
windowsHide?: boolean;
|
|
42
45
|
}) => ProcLike;
|
|
46
|
+
/**
|
|
47
|
+
* Spawn options that make a model child reapable with everything it backgrounds.
|
|
48
|
+
*
|
|
49
|
+
* POSIX (linux, darwin): `detached` = own process group, so `kill(-pid)` sweeps
|
|
50
|
+
* the grandchildren. On win32 the same flag is a defect: libuv maps it to
|
|
51
|
+
* DETACHED_PROCESS, which gives the child NO console, so every console process
|
|
52
|
+
* the model then runs allocates a fresh visible one (issue #20). win32 gets
|
|
53
|
+
* `windowsHide` (CREATE_NO_WINDOW) instead: the child gets a windowless console
|
|
54
|
+
* that its descendants inherit. Either/or is load-bearing — Windows ignores
|
|
55
|
+
* CREATE_NO_WINDOW next to DETACHED_PROCESS. The win32 reap is `taskkill /T`,
|
|
56
|
+
* which walks the live tree and needs no flag; unlike a POSIX group kill it
|
|
57
|
+
* cannot catch what the child left behind after it exited.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ownGroupSpawnOptions(platform: NodeJS.Platform): OwnGroupSpawnOptions;
|
|
60
|
+
export type OwnGroupSpawnOptions = {
|
|
61
|
+
detached: true;
|
|
62
|
+
} | {
|
|
63
|
+
windowsHide: true;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Tear down a child spawned with `ownGroupSpawnOptions`, and whatever it
|
|
67
|
+
* backgrounded. Best-effort: a group already gone is not an error. Kept beside
|
|
68
|
+
* the spawn shape so a platform change edits one file.
|
|
69
|
+
*/
|
|
70
|
+
export declare function reapProcessGroup(pid: number, sig: NodeJS.Signals, platform?: NodeJS.Platform): void;
|
|
43
71
|
/**
|
|
44
72
|
* Why runChild killed the child. Five sources converge on one kill path, and
|
|
45
73
|
* each names itself here rather than in its own flag — so a consumer reads ONE
|
|
@@ -124,6 +152,9 @@ export interface RunChildTextOptions {
|
|
|
124
152
|
}
|
|
125
153
|
export interface RunChildJsonEventsOptions {
|
|
126
154
|
mode: 'json-events';
|
|
155
|
+
/** Which platform's group options and reap to use. Tests drive the win32 arm
|
|
156
|
+
* from a POSIX host with it; production leaves it to `process.platform`. */
|
|
157
|
+
platform?: NodeJS.Platform;
|
|
127
158
|
onLine?: (line: string) => void;
|
|
128
159
|
onContextUsage?: (snapshot: ContextSnapshot) => void;
|
|
129
160
|
/**
|
|
@@ -13,6 +13,40 @@ export const CHILD_BASE_ARGS = [
|
|
|
13
13
|
'--no-context-files',
|
|
14
14
|
'--no-session'
|
|
15
15
|
];
|
|
16
|
+
/**
|
|
17
|
+
* Spawn options that make a model child reapable with everything it backgrounds.
|
|
18
|
+
*
|
|
19
|
+
* POSIX (linux, darwin): `detached` = own process group, so `kill(-pid)` sweeps
|
|
20
|
+
* the grandchildren. On win32 the same flag is a defect: libuv maps it to
|
|
21
|
+
* DETACHED_PROCESS, which gives the child NO console, so every console process
|
|
22
|
+
* the model then runs allocates a fresh visible one (issue #20). win32 gets
|
|
23
|
+
* `windowsHide` (CREATE_NO_WINDOW) instead: the child gets a windowless console
|
|
24
|
+
* that its descendants inherit. Either/or is load-bearing — Windows ignores
|
|
25
|
+
* CREATE_NO_WINDOW next to DETACHED_PROCESS. The win32 reap is `taskkill /T`,
|
|
26
|
+
* which walks the live tree and needs no flag; unlike a POSIX group kill it
|
|
27
|
+
* cannot catch what the child left behind after it exited.
|
|
28
|
+
*/
|
|
29
|
+
export function ownGroupSpawnOptions(platform) {
|
|
30
|
+
return platform === 'win32' ? { windowsHide: true } : { detached: true };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Tear down a child spawned with `ownGroupSpawnOptions`, and whatever it
|
|
34
|
+
* backgrounded. Best-effort: a group already gone is not an error. Kept beside
|
|
35
|
+
* the spawn shape so a platform change edits one file.
|
|
36
|
+
*/
|
|
37
|
+
export function reapProcessGroup(pid, sig, platform = process.platform) {
|
|
38
|
+
try {
|
|
39
|
+
if (platform === 'win32') {
|
|
40
|
+
spawnSyncDefault('taskkill', ['/pid', String(pid), '/T', '/F']);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.kill(-pid, sig);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// group already gone
|
|
48
|
+
}
|
|
49
|
+
}
|
|
16
50
|
/** The cause a signal was aborted with, when its owner attached one. */
|
|
17
51
|
function abortCause(reason) {
|
|
18
52
|
const tagged = reason;
|
|
@@ -229,11 +263,12 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
229
263
|
// OWN process group so every such grandchild can be reaped as a unit on exit.
|
|
230
264
|
// Plumbing (git, mode:'text') never backgrounds anything and stays in-group.
|
|
231
265
|
const ownGroup = opts?.mode === 'json-events';
|
|
266
|
+
const platform = (ownGroup && opts.platform) || process.platform;
|
|
232
267
|
const proc = spawn(invocation.command, invocation.args, {
|
|
233
268
|
cwd,
|
|
234
269
|
shell: false,
|
|
235
270
|
stdio: [usesStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
236
|
-
...(ownGroup ?
|
|
271
|
+
...(ownGroup ? ownGroupSpawnOptions(platform) : {}),
|
|
237
272
|
...(invocation.env ? { env: invocation.env } : {})
|
|
238
273
|
});
|
|
239
274
|
if (usesStdin) {
|
|
@@ -250,23 +285,11 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
|
|
|
250
285
|
proc.stdin?.end();
|
|
251
286
|
}
|
|
252
287
|
// Reap the child's whole process group — the child itself AND anything it
|
|
253
|
-
// backgrounded. No-op unless the child owns a group (ownGroup) and we have a
|
|
254
|
-
// pid; ESRCH (group already gone) is swallowed. POSIX: negative-pid signals
|
|
255
|
-
// the group; Windows has no groups, so taskkill /T tears down the tree.
|
|
288
|
+
// backgrounded. No-op unless the child owns a group (ownGroup) and we have a pid.
|
|
256
289
|
const reapGroup = (sig) => {
|
|
257
290
|
if (!ownGroup || !proc.pid)
|
|
258
291
|
return;
|
|
259
|
-
|
|
260
|
-
if (process.platform === 'win32') {
|
|
261
|
-
spawnSyncDefault('taskkill', ['/pid', String(proc.pid), '/T', '/F']);
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
process.kill(-proc.pid, sig);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
catch {
|
|
268
|
-
// group already gone
|
|
269
|
-
}
|
|
292
|
+
reapProcessGroup(proc.pid, sig, platform);
|
|
270
293
|
};
|
|
271
294
|
// One kill path for every source: SIGTERM, then SIGKILL after a grace
|
|
272
295
|
// period if the child ignored the term. For a group-owning (model) child,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RenderOutcome } from './render-check.js';
|
|
2
|
+
import type { OwnGroupSpawnOptions } from '../shared/child-process.js';
|
|
2
3
|
import { type DeepRenderOutcome } from './deep-render-check.js';
|
|
3
4
|
import type { HealthCommand } from './repo-health-check.js';
|
|
4
5
|
/**
|
|
@@ -163,14 +164,16 @@ export interface BootDeps {
|
|
|
163
164
|
* fake pid would signal something else entirely.
|
|
164
165
|
*/
|
|
165
166
|
killGroup?: (pid: number, signal: NodeJS.Signals) => void;
|
|
167
|
+
/** Which platform's group options to spawn with. Tests drive the win32 arm
|
|
168
|
+
* from a POSIX host; production leaves it to `process.platform`. */
|
|
169
|
+
platform?: NodeJS.Platform;
|
|
166
170
|
}
|
|
167
171
|
/** What `runBootCheck` passes to its spawn. */
|
|
168
|
-
export
|
|
172
|
+
export type BootSpawnOptions = {
|
|
169
173
|
cwd: string;
|
|
170
|
-
detached: true;
|
|
171
174
|
stdio: ['ignore', 'pipe', 'pipe'];
|
|
172
175
|
env: Record<string, string | undefined>;
|
|
173
|
-
}
|
|
176
|
+
} & OwnGroupSpawnOptions;
|
|
174
177
|
/** A stream the boot check reads output from. */
|
|
175
178
|
export interface BootStream {
|
|
176
179
|
on: (event: 'data', cb: (chunk: Buffer | string) => void) => void;
|
|
@@ -255,8 +258,9 @@ export declare function defaultFindPortHolder(port: number): {
|
|
|
255
258
|
*
|
|
256
259
|
* - non-zero exit (or signal death) before the window closes → FAIL, output tail;
|
|
257
260
|
* - exit 0 before the window closes → PASS (a CLI-style "run" that finished);
|
|
258
|
-
* - still alive when the window closes → PASS, then the
|
|
259
|
-
* killed (
|
|
261
|
+
* - still alive when the window closes → PASS, then the child and everything it
|
|
262
|
+
* backgrounded are killed (ownGroupSpawnOptions + reapProcessGroup; SIGTERM,
|
|
263
|
+
* escalating to SIGKILL).
|
|
260
264
|
*
|
|
261
265
|
* For a SERVED app (`expectServer` true — the spec/plan promised an HTTP server) mere
|
|
262
266
|
* survival is not enough: a watcher (`dev` = tailwind/bundler --watch) stays alive
|
package/dist/task/boot-probe.js
CHANGED
|
@@ -25,6 +25,7 @@ import * as path from 'node:path';
|
|
|
25
25
|
import { runRenderCheck } from './render-check.js';
|
|
26
26
|
import { resolveRunner, runnerEnv, isCommandNotFound } from './runner-resolve.js';
|
|
27
27
|
import { outputTail } from './command-run.js';
|
|
28
|
+
import { ownGroupSpawnOptions, reapProcessGroup } from '../shared/child-process.js';
|
|
28
29
|
import { packageScripts, makeHasTarget } from './launch-manifest.js';
|
|
29
30
|
import { collectProjectEnv, pinnedLocalPort, runDeepRenderCheck } from './deep-render-check.js';
|
|
30
31
|
/** Leading `FOO=bar` env assignments and `sudo`/`exec` wrappers carry no verb. */
|
|
@@ -435,7 +436,8 @@ function pgidOf(pid) {
|
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
438
|
/** Default listener probe: any LISTENing socket owned by a pid in process group
|
|
438
|
-
* `pgid
|
|
439
|
+
* `pgid`. POSIX only (expectServer is fenced off win32): there the detached boot
|
|
440
|
+
* child leads its own group, so pgid === child.pid. */
|
|
439
441
|
function defaultGroupHasListener(pgid) {
|
|
440
442
|
for (const { pid } of listeningSockets()) {
|
|
441
443
|
if (pgidOf(pid) === pgid)
|
|
@@ -503,26 +505,9 @@ function holderIsOurs(command, boot) {
|
|
|
503
505
|
function defaultSpawnBoot(bin, args, o) {
|
|
504
506
|
return spawn(bin, args, o);
|
|
505
507
|
}
|
|
506
|
-
/**
|
|
507
|
-
* The real group teardown, best-effort. A group already gone is not an error.
|
|
508
|
-
*
|
|
509
|
-
* On POSIX the negative pid signals the whole group, which is what makes a
|
|
510
|
-
* `detached` spawn reapable together with anything it backgrounded. Windows has
|
|
511
|
-
* neither process groups nor a negative-pid kill, so that branch shells out to
|
|
512
|
-
* `taskkill /T /F` as a single forced tree teardown instead of escalating.
|
|
513
|
-
*/
|
|
508
|
+
/** The real group teardown; the spawn shape's twin lives beside it. */
|
|
514
509
|
function defaultKillGroup(pid, sig) {
|
|
515
|
-
|
|
516
|
-
if (process.platform === 'win32') {
|
|
517
|
-
spawnSync('taskkill', ['/pid', String(pid), '/T', '/F']);
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
process.kill(-pid, sig);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
// group already gone
|
|
525
|
-
}
|
|
510
|
+
reapProcessGroup(pid, sig);
|
|
526
511
|
}
|
|
527
512
|
/**
|
|
528
513
|
* Exercise the start command ONCE. All four outcomes below were run against real
|
|
@@ -533,8 +518,9 @@ function defaultKillGroup(pid, sig) {
|
|
|
533
518
|
*
|
|
534
519
|
* - non-zero exit (or signal death) before the window closes → FAIL, output tail;
|
|
535
520
|
* - exit 0 before the window closes → PASS (a CLI-style "run" that finished);
|
|
536
|
-
* - still alive when the window closes → PASS, then the
|
|
537
|
-
* killed (
|
|
521
|
+
* - still alive when the window closes → PASS, then the child and everything it
|
|
522
|
+
* backgrounded are killed (ownGroupSpawnOptions + reapProcessGroup; SIGTERM,
|
|
523
|
+
* escalating to SIGKILL).
|
|
538
524
|
*
|
|
539
525
|
* For a SERVED app (`expectServer` true — the spec/plan promised an HTTP server) mere
|
|
540
526
|
* survival is not enough: a watcher (`dev` = tailwind/bundler --watch) stays alive
|
|
@@ -593,7 +579,7 @@ export async function runBootCheck(cwd, [bin, args], graceMs = 10_000, opts = {}
|
|
|
593
579
|
return new Promise(resolve => {
|
|
594
580
|
const child = spawnBoot(runner.bin, args, {
|
|
595
581
|
cwd,
|
|
596
|
-
|
|
582
|
+
...ownGroupSpawnOptions(opts.deps?.platform ?? process.platform),
|
|
597
583
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
598
584
|
env: {
|
|
599
585
|
...runnerEnv(runner),
|
|
@@ -44,6 +44,7 @@ import * as os from 'node:os';
|
|
|
44
44
|
import * as path from 'node:path';
|
|
45
45
|
import WebSocket from 'ws';
|
|
46
46
|
import { findHeadlessBrowser, judgeRenderedDom } from './render-check.js';
|
|
47
|
+
import { ownGroupSpawnOptions, reapProcessGroup } from '../shared/child-process.js';
|
|
47
48
|
/** Identifier halves of a credential pair, in preference order. */
|
|
48
49
|
const IDENTIFIER_SUFFIXES = ['PHONE', 'EMAIL', 'USERNAME', 'USER', 'LOGIN', 'IDENTIFIER'];
|
|
49
50
|
const PASSWORD_SUFFIXES = ['PASSWORD', 'PASSWD', 'PASS'];
|
|
@@ -622,13 +623,8 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
622
623
|
catch {
|
|
623
624
|
// socket already gone
|
|
624
625
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
process.kill(-child.pid, 'SIGKILL');
|
|
628
|
-
}
|
|
629
|
-
catch {
|
|
630
|
-
// group already gone
|
|
631
|
-
}
|
|
626
|
+
if (child?.pid)
|
|
627
|
+
reapProcessGroup(child.pid, 'SIGKILL');
|
|
632
628
|
return Promise.resolve();
|
|
633
629
|
};
|
|
634
630
|
signal?.addEventListener('abort', () => void close(), { once: true });
|
|
@@ -647,7 +643,7 @@ export async function launchBrowser(bin, userDataDir, { signal } = {}) {
|
|
|
647
643
|
`--user-data-dir=${userDataDir}`,
|
|
648
644
|
'--remote-debugging-port=0',
|
|
649
645
|
'about:blank'
|
|
650
|
-
], {
|
|
646
|
+
], { ...ownGroupSpawnOptions(process.platform), stdio: ['ignore', 'pipe', 'pipe'] });
|
|
651
647
|
const proc = child;
|
|
652
648
|
proc.unref();
|
|
653
649
|
const wsUrl = await new Promise((resolve, reject) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mjasnikovs/pi-task",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.43",
|
|
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",
|