mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* Use tree-kill for daemon shutdown * fix: update lockfile signatures and Nix hash * Use tree-kill for daemon shutdown --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
116 lines
2.8 KiB
TypeScript
116 lines
2.8 KiB
TypeScript
import treeKill from "tree-kill";
|
|
|
|
export interface TreeKillTarget {
|
|
pid?: number;
|
|
exitCode?: number | null;
|
|
signalCode?: NodeJS.Signals | null;
|
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
once?(event: "exit", listener: () => void): unknown;
|
|
}
|
|
|
|
interface TerminateWithTreeKillOptions {
|
|
gracefulSignal?: NodeJS.Signals;
|
|
forceSignal?: NodeJS.Signals;
|
|
gracefulTimeoutMs: number;
|
|
forceTimeoutMs?: number;
|
|
onForceSignal?: () => void;
|
|
}
|
|
|
|
export type TerminateWithTreeKillResult =
|
|
| "already-exited"
|
|
| "terminated"
|
|
| "killed"
|
|
| "kill-timeout";
|
|
|
|
export async function terminateWithTreeKill(
|
|
child: TreeKillTarget,
|
|
options: TerminateWithTreeKillOptions,
|
|
): Promise<TerminateWithTreeKillResult> {
|
|
if (isProcessExited(child)) {
|
|
return "already-exited";
|
|
}
|
|
|
|
const exitPromise = waitForProcessExit(child);
|
|
await signalTreeOrChild(child, options.gracefulSignal ?? "SIGTERM");
|
|
if (await waitForExitOrTimeout(exitPromise, options.gracefulTimeoutMs)) {
|
|
return "terminated";
|
|
}
|
|
|
|
options.onForceSignal?.();
|
|
await signalTreeOrChild(child, options.forceSignal ?? "SIGKILL");
|
|
if (options.forceTimeoutMs === undefined) {
|
|
return "killed";
|
|
}
|
|
return (await waitForExitOrTimeout(exitPromise, options.forceTimeoutMs))
|
|
? "killed"
|
|
: "kill-timeout";
|
|
}
|
|
|
|
function signalTreeOrChild(child: TreeKillTarget, signal: NodeJS.Signals): Promise<void> {
|
|
if (isProcessExited(child)) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const pid = child.pid;
|
|
if (typeof pid !== "number" || pid <= 0) {
|
|
signalDirectChild(child, signal);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
treeKill(pid, signal, (error) => {
|
|
if (error) {
|
|
signalDirectChild(child, signal);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function signalDirectChild(child: TreeKillTarget, signal: NodeJS.Signals): void {
|
|
try {
|
|
child.kill(signal);
|
|
} catch {
|
|
// Ignore cleanup races.
|
|
}
|
|
}
|
|
|
|
function isProcessExited(child: TreeKillTarget): boolean {
|
|
return (
|
|
(child.exitCode !== null && child.exitCode !== undefined) ||
|
|
(child.signalCode !== null && child.signalCode !== undefined)
|
|
);
|
|
}
|
|
|
|
function waitForProcessExit(child: TreeKillTarget): Promise<void> {
|
|
if (isProcessExited(child)) {
|
|
return Promise.resolve();
|
|
}
|
|
if (!child.once) {
|
|
return new Promise(() => undefined);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
child.once?.("exit", resolve);
|
|
});
|
|
}
|
|
|
|
async function waitForExitOrTimeout(
|
|
exitPromise: Promise<void>,
|
|
timeoutMs: number,
|
|
): Promise<boolean> {
|
|
let timer: NodeJS.Timeout | null = null;
|
|
try {
|
|
return await Promise.race([
|
|
exitPromise.then(() => true),
|
|
new Promise<boolean>((resolve) => {
|
|
timer = setTimeout(() => resolve(false), timeoutMs);
|
|
}),
|
|
]);
|
|
} finally {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}
|