mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-22 01:07:18 +08:00
3be906fcf4
A full suite run on Windows left a `serve --mcp --path %TEMP%\cg-rebuild-…` process behind. Its parent was gone, and its one thread was suspended with 0 CPU used. It would never exit, and it held the checkout's dist. libuv creates every detached child suspended on Windows and resumes it only at the end of `uv_spawn`. A launcher killed inside that call never resumes its daemon. The daemon never runs, so it writes no daemon.pid, and nothing can find or stop it. The rebuild test's teardown killed its launcher at once. The test stops the daemon on purpose, so by then the launcher is serving calls in-process with a retry timer running. When the retry comes due during the last call, the launcher spawns its daemon as soon as that call answers, just as the teardown begins. Under load the two coincide. The teardown now: - closes each launcher's stdin and waits for it to exit. A graceful exit can't land inside the synchronous spawn. - records the daemons each launcher spawns (#1773's recorder) and stops them until all have exited. One spawned as its launcher exited may still be starting, and stopDaemonAt leaves a daemon running until it answers. The recorder's comment now says that a launcher killed mid-spawn records nothing. CODEGRAPH_DAEMON_RETRY_MS=2500 puts the retry inside the last call on a quiet box. Under it, main's test left a daemon in 6 of 12 runs: 2 suspended ones, plus 4 live ones whose fixture then failed to delete. This test left nothing in 12 runs. Of the mutants, killing launchers again, a single stopDaemonAt and no spawn recorder each leaked. The other daemon test files left nothing in 2 runs each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 lines
1.1 KiB
JavaScript
25 lines
1.1 KiB
JavaScript
/**
|
|
* Preloaded with `--require` into the `serve --mcp` launchers a test starts, so
|
|
* the test learns the pid of every detached daemon candidate they spawn
|
|
* (#1773). Racing launchers may each spawn one; the losers exit on their own
|
|
* once they see the winner's lock, but on a loaded machine one can still be
|
|
* starting when the test tears down. Written synchronously as the spawn
|
|
* returns, so the pid is on disk before the launcher can exit. A launcher
|
|
* killed inside the spawn records nothing, and on Windows leaves that candidate
|
|
* suspended for good. Candidates inherit the preload through
|
|
* `process.execArgv`; they spawn nothing detached.
|
|
*/
|
|
const childProcess = require('child_process');
|
|
const fs = require('fs');
|
|
|
|
const log = process.env.CG_TEST_SPAWN_LOG;
|
|
if (log) {
|
|
const spawn = childProcess.spawn;
|
|
childProcess.spawn = function recordDetached(...args) {
|
|
const child = spawn.apply(this, args);
|
|
const options = args.find((arg) => arg !== null && typeof arg === 'object' && !Array.isArray(arg));
|
|
if (options && options.detached && child.pid) fs.appendFileSync(log, `${child.pid}\n`);
|
|
return child;
|
|
};
|
|
}
|