From 6069a030c4ed50b4fb809c1fbe56d0e78f72cf58 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 20 Jun 2026 11:08:26 +0200 Subject: [PATCH] fix(scripts): keep closed runtime command groups tracked --- .../runtime-smoke.mjs | 4 +- ...led-plugin-install-uninstall-probe.test.ts | 66 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs index e8a2d91b72ee..329c8bfc56ae 100644 --- a/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs +++ b/scripts/e2e/lib/bundled-plugin-install-uninstall/runtime-smoke.mjs @@ -552,7 +552,9 @@ function trackGatewayChild(child) { function trackCommandChild(child) { activeCommandChildren.add(child); const untrack = () => { - activeCommandChildren.delete(child); + if (!processTreeIsAlive(child)) { + activeCommandChildren.delete(child); + } }; child.once("error", untrack); child.once("close", untrack); diff --git a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts index 09e9a3029605..3dcee832f085 100644 --- a/test/scripts/bundled-plugin-install-uninstall-probe.test.ts +++ b/test/scripts/bundled-plugin-install-uninstall-probe.test.ts @@ -767,6 +767,72 @@ describe("bundled plugin install/uninstall probe", () => { }, ); + it.runIf(process.platform !== "win32")( + "keeps closed runtime command groups tracked for parent cleanup", + async () => { + const root = makePackageRoot(); + const commandPath = path.join(root, "closed-command.mjs"); + const runnerPath = path.join(root, "run-closed-runtime-command.mjs"); + const commandSettledPath = path.join(root, "command-settled"); + const descendantPidPath = path.join(root, "closed-command-descendant.pid"); + const descendantScript = [ + "import fs from 'node:fs';", + `fs.writeFileSync(${JSON.stringify(descendantPidPath)}, String(process.pid));`, + "process.on('SIGTERM', () => {});", + "setInterval(() => {}, 1000);", + ].join("\n"); + fs.writeFileSync( + commandPath, + [ + "import childProcess from 'node:child_process';", + `const child = childProcess.spawn(process.execPath, ["--input-type=module", "--eval", ${JSON.stringify( + descendantScript, + )}], { stdio: "ignore" });`, + "child.unref();", + "", + ].join("\n"), + "utf8", + ); + fs.writeFileSync( + runnerPath, + [ + "import fs from 'node:fs';", + `const runtimeSmoke = await import(${JSON.stringify(pathToFileURL(runtimeSmokePath).href)});`, + `runtimeSmoke.runCommand(process.execPath, [${JSON.stringify(commandPath)}], {`, + " timeoutMs: 60_000,", + "}).finally(() => {", + ` fs.writeFileSync(${JSON.stringify(commandSettledPath)}, "1");`, + "});", + "setInterval(() => {}, 1000);", + "", + ].join("\n"), + "utf8", + ); + + const runner = spawn(process.execPath, [runnerPath], { + stdio: "ignore", + }); + let descendantPid: number | undefined; + try { + await waitForFile(descendantPidPath, 1000); + descendantPid = Number(fs.readFileSync(descendantPidPath, "utf8")); + expect(pidIsAlive(descendantPid)).toBe(true); + await waitForFile(commandSettledPath, 1000); + + runner.kill("SIGTERM"); + + await waitForDead(descendantPid, 2000); + } finally { + if (runner.pid && pidIsAlive(runner.pid)) { + runner.kill("SIGKILL"); + } + if (descendantPid !== undefined && pidIsAlive(descendantPid)) { + process.kill(descendantPid, "SIGKILL"); + } + } + }, + ); + it.runIf(process.platform !== "win32")( "cleans detached runtime gateway groups when the parent is signaled", async () => {