Use the shared command supervisor for installation probes and retain exact process cleanup owners through doctor, profile resolution, and session health. Preserve exit status, bounded captured output, cancellation reasons, and live cleanup facts. Block subsequent inspection until retained cleanup is retried. Generalize the existing inspection resource scope and migrate its consumers, including verifier scripts; remove the verifier's duplicate raw version probe. Validation: 116 distinct focused tests passed, including built CLI doctor with inherited output, cancellation/finalization, cleanup retry, and fake-provider verifier workflows. Build, check:fast, and test discovery passed. No real Kuna distribution or analysis target was executed. Synthetic Darwin inspection timing: first-call median 707ms versus 5.79ms; same-process repeat 37.9ms versus 3.08ms. First use includes ownership-helper preparation. Family CPU and actual peak family memory remain unmeasured. Fixes #5557
92 lines
2.4 KiB
TypeScript
92 lines
2.4 KiB
TypeScript
import { spawn, type ChildProcess } from "node:child_process";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
|
|
import type { ProcessCleanupResult } from "../../../../src/process/ProcessOwnership.js";
|
|
import type {
|
|
OwnedProviderProcessSpawnOptions,
|
|
SpawnedOwnedProviderProcess,
|
|
} from "../../../../src/process/ProviderProcess.js";
|
|
|
|
/** Cleanup authority for the exact detached group created by this test launcher. */
|
|
export const ownedGroupCleanup = async (
|
|
pid: number,
|
|
): Promise<ProcessCleanupResult> => {
|
|
let signaled = false;
|
|
try {
|
|
process.kill(-pid, "SIGTERM");
|
|
signaled = true;
|
|
} catch {
|
|
// The test-owned process group may have exited before cleanup began.
|
|
}
|
|
await delay(50);
|
|
try {
|
|
process.kill(-pid, "SIGKILL");
|
|
signaled = true;
|
|
} catch {
|
|
// An empty process group confirms cleanup.
|
|
}
|
|
const deadline = Date.now() + 1_000;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
process.kill(-pid, 0);
|
|
} catch {
|
|
return { cleaned: true, signaled };
|
|
}
|
|
await delay(10);
|
|
}
|
|
return {
|
|
cleaned: false,
|
|
reason: `test-owned process group ${pid} remains live`,
|
|
};
|
|
};
|
|
|
|
export const makeLauncher = (
|
|
cleanup: (
|
|
child: ChildProcess,
|
|
pid: number,
|
|
) => Promise<ProcessCleanupResult> = (_child, pid) => ownedGroupCleanup(pid),
|
|
) => {
|
|
let launches = 0;
|
|
const groups: number[] = [];
|
|
const launcher = async (
|
|
options: OwnedProviderProcessSpawnOptions,
|
|
): Promise<SpawnedOwnedProviderProcess> => {
|
|
options.signal?.throwIfAborted();
|
|
const child = spawn(options.command, [...options.arguments], {
|
|
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
env: {
|
|
...options.hostEnvironment,
|
|
...options.env,
|
|
REA_PROCESS_RUN_ID: options.runId,
|
|
},
|
|
detached: true,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
await new Promise<void>((resolve, reject) => {
|
|
child.once("spawn", resolve);
|
|
child.once("error", reject);
|
|
});
|
|
const pid = child.pid;
|
|
if (pid === undefined)
|
|
throw new Error("test launcher did not receive a PID");
|
|
launches += 1;
|
|
groups.push(pid);
|
|
return {
|
|
process: child,
|
|
ownership: {
|
|
runId: options.runId,
|
|
leaderPid: pid,
|
|
processGroupId: pid,
|
|
},
|
|
cleanup: () => cleanup(child, pid),
|
|
};
|
|
};
|
|
return {
|
|
launcher,
|
|
groups,
|
|
get launches() {
|
|
return launches;
|
|
},
|
|
};
|
|
};
|