Fix dev shutdown to stop orphaned services
This commit is contained in:
parent
4a22fcc635
commit
c8c8094594
2 changed files with 115 additions and 14 deletions
|
|
@ -12,12 +12,61 @@ type Child = {
|
||||||
const children: Child[] = [];
|
const children: Child[] = [];
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
|
|
||||||
|
const sleep = (delayMs: number): Promise<void> => {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForExit = async (proc: Bun.Subprocess, timeoutMs: number): Promise<boolean> => {
|
||||||
|
const result = await Promise.race([
|
||||||
|
proc.exited.then(() => true),
|
||||||
|
sleep(timeoutMs).then(() => false)
|
||||||
|
]);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const signalProcess = (pid: number, signal: NodeJS.Signals): boolean => {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
process.kill(pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopChild = async (child: Child, timeoutMs = 5000): Promise<void> => {
|
||||||
|
const pid = child.process.pid;
|
||||||
|
if (!pid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signalProcess(pid, "SIGINT")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exited = await waitForExit(child.process, timeoutMs);
|
||||||
|
if (exited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signalProcess(pid, "SIGKILL")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForExit(child.process, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
const spawnChild = ({ name, cmd, cwd }: ChildSpec): void => {
|
const spawnChild = ({ name, cmd, cwd }: ChildSpec): void => {
|
||||||
const proc = Bun.spawn(cmd, {
|
const proc = Bun.spawn(cmd, {
|
||||||
cwd,
|
cwd,
|
||||||
stdin: "inherit",
|
stdin: "inherit",
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit"
|
stderr: "inherit",
|
||||||
|
detached: true
|
||||||
});
|
});
|
||||||
|
|
||||||
children.push({ name, process: proc });
|
children.push({ name, process: proc });
|
||||||
|
|
@ -30,26 +79,26 @@ const spawnChild = ({ name, cmd, cwd }: ChildSpec): void => {
|
||||||
const exitCode = code ?? 0;
|
const exitCode = code ?? 0;
|
||||||
const statusLabel = exitCode === 0 ? "exited" : "failed";
|
const statusLabel = exitCode === 0 ? "exited" : "failed";
|
||||||
console.error(`[dev-services] ${name} ${statusLabel} (${exitCode})`);
|
console.error(`[dev-services] ${name} ${statusLabel} (${exitCode})`);
|
||||||
shutdown(exitCode);
|
void shutdown(exitCode);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shutdown = (code: number): void => {
|
const shutdown = async (code: number): Promise<void> => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
|
|
||||||
for (const child of children) {
|
if (children.length > 0) {
|
||||||
child.process.kill();
|
await Promise.all(children.map((child) => stopChild(child)));
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", () => shutdown(0));
|
process.on("SIGINT", () => void shutdown(0));
|
||||||
process.on("SIGTERM", () => shutdown(0));
|
process.on("SIGTERM", () => void shutdown(0));
|
||||||
|
|
||||||
const tasks: ChildSpec[] = [
|
const tasks: ChildSpec[] = [
|
||||||
{ name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" },
|
{ name: "ingest-options", cmd: ["bun", "run", "dev"], cwd: "services/ingest-options" },
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,50 @@ const sleep = (delayMs: number): Promise<void> => {
|
||||||
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const waitForExit = async (proc: Bun.Subprocess, timeoutMs: number): Promise<boolean> => {
|
||||||
|
const result = await Promise.race([
|
||||||
|
proc.exited.then(() => true),
|
||||||
|
sleep(timeoutMs).then(() => false)
|
||||||
|
]);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const signalProcess = (pid: number, signal: NodeJS.Signals): boolean => {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
process.kill(pid, signal);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopChild = async (child: Child, timeoutMs = 5000): Promise<void> => {
|
||||||
|
const pid = child.process.pid;
|
||||||
|
if (!pid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signalProcess(pid, "SIGINT")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exited = await waitForExit(child.process, timeoutMs);
|
||||||
|
if (exited) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signalProcess(pid, "SIGKILL")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForExit(child.process, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
const parseBool = (value: string | undefined): boolean => {
|
const parseBool = (value: string | undefined): boolean => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -75,7 +119,8 @@ const spawnChild = ({ name, cmd, cwd }: ChildSpec): void => {
|
||||||
cwd,
|
cwd,
|
||||||
stdin: "inherit",
|
stdin: "inherit",
|
||||||
stdout: "inherit",
|
stdout: "inherit",
|
||||||
stderr: "inherit"
|
stderr: "inherit",
|
||||||
|
detached: true
|
||||||
});
|
});
|
||||||
|
|
||||||
children.push({ name, process: proc });
|
children.push({ name, process: proc });
|
||||||
|
|
@ -93,26 +138,33 @@ const spawnChild = ({ name, cmd, cwd }: ChildSpec): void => {
|
||||||
"[dev] Infra failed. Ensure Docker is installed and the daemon is running (OrbStack or Docker Desktop), then retry."
|
"[dev] Infra failed. Ensure Docker is installed and the daemon is running (OrbStack or Docker Desktop), then retry."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
shutdown(exitCode);
|
void shutdown(exitCode);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const shutdown = (code: number): void => {
|
const shutdown = async (code: number): Promise<void> => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
|
|
||||||
for (const child of children) {
|
const infra = children.find((child) => child.name === "infra") ?? null;
|
||||||
child.process.kill();
|
const services = children.filter((child) => child.name !== "infra");
|
||||||
|
|
||||||
|
if (services.length > 0) {
|
||||||
|
await Promise.all(services.map((child) => stopChild(child)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infra) {
|
||||||
|
await stopChild(infra, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(code);
|
process.exit(code);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGINT", () => shutdown(0));
|
process.on("SIGINT", () => void shutdown(0));
|
||||||
process.on("SIGTERM", () => shutdown(0));
|
process.on("SIGTERM", () => void shutdown(0));
|
||||||
|
|
||||||
const waitForInfra = async (): Promise<void> => {
|
const waitForInfra = async (): Promise<void> => {
|
||||||
const natsTarget = parseUrlHostPort(process.env.NATS_URL ?? "", "127.0.0.1", 4222);
|
const natsTarget = parseUrlHostPort(process.env.NATS_URL ?? "", "127.0.0.1", 4222);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue