fix: clean up dev services on SIGHUP (#16239)

This commit is contained in:
george larson
2026-08-03 13:41:58 +02:00
committed by GitHub
parent e0b115757b
commit 54d9f30cd3
2 changed files with 100 additions and 1 deletions
+99 -1
View File
@@ -12,7 +12,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { describe, expect, it, afterEach } from "vitest";
import {
buildAgentServerAutomationEnv,
@@ -631,6 +631,104 @@ describe("setServiceLogListener", () => {
});
describe("dev-with-automation CLI", () => {
it.skipIf(process.platform === "win32")(
"cleans up detached services when the launcher receives SIGHUP",
async () => {
const moduleUrl = pathToFileURL(
path.join(repoRoot, "scripts", "dev-with-automation.mjs"),
).href;
const fixtureSource = [
'import net from "node:net";',
"const server = net.createServer(() => {});",
'server.listen(0, "127.0.0.1", () => console.log("READY", process.pid, server.address().port));',
].join("\n");
const supervisorSource = [
`import { spawnService } from ${JSON.stringify(moduleUrl)};`,
`const fixture = spawnService("fixture", process.execPath, ["--input-type=module", "--eval", ${JSON.stringify(fixtureSource)}]);`,
'console.log("SPAWNED", fixture.pid);',
"setInterval(() => {}, 1_000);",
].join("\n");
const supervisor = spawn(
process.execPath,
["--input-type=module", "--eval", supervisorSource],
{
cwd: repoRoot,
stdio: ["ignore", "pipe", "pipe"],
},
);
let output = "";
let fixturePid: number | undefined;
let fixturePort: number | undefined;
const capture = (chunk: Buffer) => {
output += chunk.toString();
const spawnedMatch = output.match(/SPAWNED (\d+)/);
const readyMatch = output.match(/READY \d+ (\d+)/);
if (spawnedMatch) fixturePid = Number(spawnedMatch[1]);
if (readyMatch) fixturePort = Number(readyMatch[1]);
};
supervisor.stdout.on("data", capture);
supervisor.stderr.on("data", capture);
try {
const readyDeadline = Date.now() + 5_000;
while (!fixturePort && Date.now() < readyDeadline) {
if (supervisor.exitCode !== null) break;
await delay(25);
}
expect(fixturePid, output).toBeDefined();
expect(fixturePort, output).toBeDefined();
supervisor.kill("SIGHUP");
const exitResult = await Promise.race([
once(supervisor, "exit").then(([code, signal]) => ({
code,
signal,
timedOut: false,
})),
delay(6_000).then(() => ({
code: null,
signal: null,
timedOut: true,
})),
]);
expect(exitResult.timedOut).toBe(false);
expect(exitResult).toMatchObject({ code: 0, signal: null });
const releaseDeadline = Date.now() + 2_000;
let portIsOpen = true;
while (portIsOpen && Date.now() < releaseDeadline) {
portIsOpen = await new Promise<boolean>((resolve) => {
const socket = net.connect(fixturePort!, "127.0.0.1");
socket.setTimeout(250);
socket.once("connect", () => {
socket.destroy();
resolve(true);
});
socket.once("error", () => resolve(false));
socket.once("timeout", () => {
socket.destroy();
resolve(false);
});
});
if (portIsOpen) await delay(50);
}
expect(portIsOpen).toBe(false);
} finally {
if (supervisor.exitCode === null) supervisor.kill("SIGKILL");
if (fixturePid) {
try {
process.kill(-fixturePid, "SIGKILL");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
}
}
}
},
15_000,
);
it("shows help with --help flag", async () => {
const child = spawn(
process.execPath,
+1
View File
@@ -958,6 +958,7 @@ function shutdown() {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
process.on("SIGHUP", shutdown);
function startIngress(config) {
logService("ingress", `Starting on port ${config.ingressPort}...`, c.yellow);