import { spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process"; import { SHUTDOWN_KILL_TIMEOUT_MS, SHUTDOWN_TERM_TIMEOUT_MS, SHUTDOWN_WRITE_TIMEOUT_MS } from "./constants"; export function isChildRunning(child: ChildProcessWithoutNullStreams): boolean { return child.exitCode === null && child.signalCode === null; } function canWriteToChild(child: ChildProcessWithoutNullStreams): boolean { return isChildRunning(child) && child.stdin.writable && !child.stdin.destroyed && !child.stdin.writableEnded; } export function killChild(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): boolean { let signaled = false; try { if (process.platform === "win32" && child.pid) { const result = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore", timeout: 1_000, windowsHide: true, }); if (!result.error && result.status === 0) return true; } if (child.pid) { try { // LSP clients are spawned in their own process group on POSIX. Kill the // whole group so wrapper scripts also lose children such as Godot and nc. process.kill(-child.pid, signal); signaled = true; } catch { // Fall back to the direct child below. The process may not be detached // or may have already exited. } } return child.kill(signal) || signaled; } catch { return signaled; } } function waitForChildExit(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise { if (!isChildRunning(child)) return Promise.resolve(true); return new Promise((resolve) => { let done = false; const finish = (exited: boolean) => { if (done) return; done = true; clearTimeout(timeout); child.off("exit", onExit); resolve(exited); }; const onExit = () => finish(true); const timeout = setTimeout(() => finish(false), timeoutMs); timeout.unref(); child.once("exit", onExit); if (!isChildRunning(child)) finish(true); }); } export async function terminateChild(child: ChildProcessWithoutNullStreams): Promise { if (!isChildRunning(child)) return; if (!killChild(child, "SIGTERM")) return; const exited = await waitForChildExit(child, SHUTDOWN_TERM_TIMEOUT_MS); if (exited || !isChildRunning(child)) return; killChild(child, "SIGKILL"); await waitForChildExit(child, SHUTDOWN_KILL_TIMEOUT_MS); } export async function bestEffortWriteJsonRpc(child: ChildProcessWithoutNullStreams, message: Record): Promise { if (!canWriteToChild(child)) return; const json = JSON.stringify(message); const payload = `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`; await new Promise((resolve) => { let done = false; const finish = () => { if (done) return; done = true; clearTimeout(timeout); resolve(); }; const timeout = setTimeout(finish, SHUTDOWN_WRITE_TIMEOUT_MS); timeout.unref(); try { child.stdin.write(payload, "utf8", finish); } catch { finish(); } }); }