Skip to content

Commit 77b7deb

Browse files
authored
fix(lsp): ensure child process terminates when parent exits (#137)
Address #41 Implemented a robust cleanup strategy for the spawned LSP process to prevent zombie processes: - Added `detached: false` to the spawn configuration - Implemented a `cleanup` function that attempts a graceful `SIGTERM`, followed by a forced `SIGKILL` after 1s if needed - Added a listener on `stdin` end for graceful shutdown - Added a watchdog interval to monitor the parent process (PPID); if the parent dies ungracefully, the LSP process will now self-terminate.
1 parent b58e22f commit 77b7deb

File tree

1 file changed

+53
-3
lines changed

1 file changed

+53
-3
lines changed

src/proxy.mjs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Buffer } from "node:buffer";
2-
import { spawn } from "node:child_process";
2+
import { spawn, exec } from "node:child_process";
33
import { EventEmitter } from "node:events";
44
import {
55
existsSync,
@@ -27,9 +27,59 @@ const args = process.argv.slice(3);
2727

2828
const PROXY_ID = Buffer.from(process.cwd().replace(/\/+$/, "")).toString("hex");
2929
const PROXY_HTTP_PORT_FILE = join(workdir, "proxy", PROXY_ID);
30-
const command = process.platform === "win32" ? `"${bin}"` : bin;
30+
const isWindows = process.platform === "win32";
31+
const command = isWindows ? `"${bin}"` : bin;
32+
33+
const lsp = spawn(command, args, {
34+
shell: isWindows,
35+
detached: false
36+
});
37+
38+
function cleanup() {
39+
if (!lsp || lsp.killed || lsp.exitCode !== null) {
40+
return;
41+
}
42+
43+
if (isWindows) {
44+
// Windows: Use taskkill to kill the process tree (cmd.exe + the child)
45+
// /T = Tree kill (child processes), /F = Force
46+
exec(`taskkill /pid ${lsp.pid} /T /F`);
47+
}
48+
else {
49+
lsp.kill('SIGTERM');
50+
setTimeout(() => {
51+
if (!lsp.killed && lsp.exitCode === null) {
52+
lsp.kill('SIGKILL');
53+
}
54+
}, 1000);
55+
}
56+
}
57+
58+
// Handle graceful IDE shutdown via stdin close
59+
process.stdin.on('end', () => {
60+
cleanup();
61+
process.exit(0);
62+
});
63+
// Ensure node is monitoring the pipe
64+
process.stdin.resume();
65+
66+
// Fallback: monitor parent process for ungraceful shutdown
67+
setInterval(() => {
68+
try {
69+
// Check if parent is still alive
70+
process.kill(process.ppid, 0);
71+
} catch (e) {
72+
// On Windows, checking a process you don't own might throw EPERM.
73+
// We only want to kill if the error is ESRCH (No Such Process).
74+
if (e.code === 'ESRCH') {
75+
cleanup();
76+
process.exit(0);
77+
}
78+
// If e.code is EPERM, the parent is alive but we don't have permission to signal it.
79+
// Do nothing.
80+
}
81+
}, 5000);
3182

32-
const lsp = spawn(command, args, { shell: process.platform === "win32" });
3383
const proxy = createLspProxy({ server: lsp, proxy: process });
3484

3585
proxy.on("client", (data, passthrough) => {

0 commit comments

Comments
 (0)