mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-12 01:41:31 -10:00
On macOS (Node 24.12) child.kill() on a child whose spawn failed (no pid, the 'error' event not yet delivered) sends the signal to pid 0, which is the
caller's whole process group. folder-dialog.test.mjs's Linux case let the real portalFolder spawn a missing gdbus and end it, so on a Mac the SIGTERM
reached the test runner and every file after it was cancelled ('Promise resolution is still pending'). Linux and Windows do nothing for such a child.
- process-tree.endTree only calls child.kill() when the child has a pid; a new test runs a never-started child through endTree/endTreeThenKill in a
group of its own and checks the caller survives (fails on the Mac without the fix, passes with it).
- folder-dialog.test.mjs: the Linux cases pass portal: async () => null like external-env.test.mjs does, so no real gdbus is started.
- load-pictures, packaged-control and the native fixture build their temp folders under the resolved temp path (/private/var on a Mac), as chat-files
already does; the host reports resolved paths.
m1001 (M4 Mini, macOS 27.2, Node 24.12.0): npm test went from killed after 2 s (722 of 1632 run, 172 cancelled) to 1632 tests, 1522 pass, 0 fail, 110 skipped.
The time-skip tests (timeskip-real, time-skip, timeskip-fork, timeskip-compare-server, tune-and-timeskip-routes, process-tree) passed 20 of 20 runs
before the change and 20 of 20 after.
45 lines
2.3 KiB
JavaScript
45 lines
2.3 KiB
JavaScript
// End a program this host started together with everything that program started (#4529 #4570).
|
|
//
|
|
// Windows: `taskkill /T` walks the tree. Linux and macOS have no such call, and signalling the child alone leaves its own children running (they are
|
|
// re-parented to init). A child started with `detached: ownGroup()` leads a process group of its own, so a signal to the negative pid reaches the
|
|
// whole group and nothing else: the group is the ownership record, no other process (a same-named one included) is ever signalled by name.
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
/** The spawn option that makes a child lead its own process group on Linux and macOS (Windows needs none: taskkill /T). */
|
|
export const ownGroup = () => process.platform !== 'win32';
|
|
|
|
/** Whether the group the child leads still has a member. */
|
|
export function groupAlive(child) {
|
|
if (process.platform === 'win32' || !child?.pid) return false;
|
|
try { process.kill(-child.pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; }
|
|
}
|
|
|
|
/**
|
|
* Signal the child's whole tree once. A child that does not lead its own group (it was not started with ownGroup()) is signalled alone, which is what
|
|
* every caller did before.
|
|
*/
|
|
export function endTree(child, signal = 'SIGTERM') {
|
|
if (!child) return;
|
|
try {
|
|
if (process.platform === 'win32') {
|
|
if (child.pid) spawnSync('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 10000 });
|
|
return;
|
|
}
|
|
if (child.pid) {
|
|
try { process.kill(-child.pid, signal); return; } catch { /* no such group: it did not lead one, or it is gone */ }
|
|
}
|
|
// A child that never started has no pid. On macOS, kill() on it right after a failed spawn signals the caller's own process group (pid 0), so
|
|
// it is left alone: there is nothing to end.
|
|
if (child.pid) child.kill(signal);
|
|
} catch { /* already gone */ }
|
|
}
|
|
|
|
/** SIGTERM to the tree, then SIGKILL to whatever of its group is still there after `graceMs`. Returns the timer so a caller may clear it. */
|
|
export function endTreeThenKill(child, graceMs = 2000) {
|
|
endTree(child, 'SIGTERM');
|
|
if (process.platform === 'win32' || !child?.pid) return null;
|
|
const timer = setTimeout(() => { if (groupAlive(child)) endTree(child, 'SIGKILL'); }, graceMs);
|
|
timer.unref?.();
|
|
return timer;
|
|
}
|