* test(#4601): pin the run-with-timeout tree reap on win32 * docs(#4601): add changeset for the win32 tree reap * fix(#4601): reap the wrapped command's whole tree on Windows timeouts * docs(#4601): backfill changeset PR number * fix(#4601): keep the mediated shim alive without depending on stdin --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -4543,7 +4543,7 @@ async function dispatchHostCommand({ command, args, cwd, raw, error, defaultValu
|
||||
// keep working. No shell is spawned (argv array) — no injection surface beyond
|
||||
// the old `timeout … bash -c "$CMD"`.
|
||||
function runWithTimeout(argv) {
|
||||
const { spawn } = require('node:child_process');
|
||||
const { spawn, spawnSync } = require('node:child_process');
|
||||
const os = require('node:os');
|
||||
|
||||
const USAGE = 'Usage: gsd_run run-with-timeout <seconds> [--] <command> [args...]';
|
||||
@@ -4568,9 +4568,11 @@ function runWithTimeout(argv) {
|
||||
|
||||
const isWin = process.platform === 'win32';
|
||||
// Detached (own process group) on POSIX so a timeout can reap the WHOLE tree —
|
||||
// a bare child.kill() misses grandchildren (e.g. a test runner's workers) and
|
||||
// would not actually bound the wall clock. Windows has no POSIX process
|
||||
// groups; a direct kill is the best portable option there.
|
||||
// a bare child.kill() misses grandchildren (e.g. a test runner's workers).
|
||||
// Windows has no POSIX process groups and process.kill(-pid) is unsupported
|
||||
// there, so EVERY killTree attempt on Windows tree-kills via
|
||||
// `taskkill /PID <pid> /T /F` while the root is alive (see killTree) — by the
|
||||
// time the direct child exits, its descendants are already orphaned.
|
||||
const detached = !isWin && secs > 0;
|
||||
const spawnFailureCode = (err) =>
|
||||
(err && err.code === 'ENOENT' ? 127 : err && err.code === 'EACCES' ? 126 : 125);
|
||||
@@ -4623,6 +4625,27 @@ function runWithTimeout(argv) {
|
||||
if (detached && child.pid) {
|
||||
try { process.kill(-child.pid, signal); return; } catch { /* group already gone */ }
|
||||
}
|
||||
if (isWin && child.pid) {
|
||||
// #4601: Windows has no POSIX process groups, so the tree kill rides
|
||||
// on `taskkill /T`, which walks the child's descendants the way
|
||||
// `process.kill(-pid)` reaches a POSIX group — this is what bounds
|
||||
// the wall clock when the direct child mediates (cmd.exe /c shim) or
|
||||
// spawns its own children. Deliberately NOT gated on the SIGKILL
|
||||
// stage: child.kill on Windows is TerminateProcess regardless of
|
||||
// signal, so by the time the direct child exits its descendants are
|
||||
// orphaned and no taskkill can reach them — the tree kill must ride
|
||||
// the FIRST attempt, while the root is still alive. /F is required:
|
||||
// without it taskkill posts WM_CLOSE, which a headless CLI never
|
||||
// pumps. Spawned as an argv array per the no-shell-for-argv-array
|
||||
// contract, and bounded — a non-zero/absent status means taskkill
|
||||
// lost a race with an exiting process, and we fall through to the
|
||||
// direct kill so the attempt is never weaker than before.
|
||||
const reap = spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 15000, // taskkill /T is sub-second in practice; bounded so a wedged taskkill can't hang the gate
|
||||
});
|
||||
if (reap.status === 0) return;
|
||||
}
|
||||
child.kill(signal);
|
||||
} catch { /* already exited */ }
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user