fix: plug resource leaks and add hook command timeout

- LSP signal handlers: store refs, return unregister handle, call in stopAll()
- session-tools-store: add per-session deleteSessionTools(), wire into session.deleted
- executeHookCommand: add 30s timeout with SIGTERM→SIGKILL escalation
This commit is contained in:
Cole Leavitt
2026-02-21 15:42:47 -07:00
parent c1ee4c8650
commit 6aa1e96f9e
5 changed files with 418 additions and 352 deletions
@@ -8,9 +8,14 @@ export interface CommandResult {
stderr?: string
}
const DEFAULT_HOOK_TIMEOUT_MS = 30_000
const SIGKILL_GRACE_MS = 5_000
export interface ExecuteHookOptions {
forceZsh?: boolean
zshPath?: string
/** Timeout in milliseconds. Process is killed after this. Default: 30000 */
timeoutMs?: number
}
export async function executeHookCommand(
@@ -20,6 +25,7 @@ export async function executeHookCommand(
options?: ExecuteHookOptions,
): Promise<CommandResult> {
const home = getHomeDirectory()
const timeoutMs = options?.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS
const expandedCommand = command
.replace(/^~(?=\/|$)/g, home)
@@ -43,6 +49,9 @@ export async function executeHookCommand(
}
return new Promise((resolve) => {
let settled = false
let killTimer: ReturnType<typeof setTimeout> | null = null
const proc = spawn(finalCommand, {
cwd,
shell: true,
@@ -52,19 +61,27 @@ export async function executeHookCommand(
let stdout = ""
let stderr = ""
proc.stdout?.on("data", (data) => {
proc.stdout?.on("data", (data: Buffer) => {
stdout += data.toString()
})
proc.stderr?.on("data", (data) => {
proc.stderr?.on("data", (data: Buffer) => {
stderr += data.toString()
})
proc.stdin?.write(stdin)
proc.stdin?.end()
const settle = (result: CommandResult) => {
if (settled) return
settled = true
if (killTimer) clearTimeout(killTimer)
if (timeoutTimer) clearTimeout(timeoutTimer)
resolve(result)
}
proc.on("close", (code) => {
resolve({
settle({
exitCode: code ?? 0,
stdout: stdout.trim(),
stderr: stderr.trim(),
@@ -72,7 +89,26 @@ export async function executeHookCommand(
})
proc.on("error", (err) => {
resolve({ exitCode: 1, stderr: err.message })
settle({ exitCode: 1, stderr: err.message })
})
const timeoutTimer = setTimeout(() => {
if (settled) return
// Try graceful shutdown first
proc.kill("SIGTERM")
killTimer = setTimeout(() => {
if (settled) return
try {
proc.kill("SIGKILL")
} catch {}
}, SIGKILL_GRACE_MS)
// Append timeout notice to stderr
stderr += `\nHook command timed out after ${timeoutMs}ms`
}, timeoutMs)
// Don't let the timeout timer keep the process alive
if (timeoutTimer && typeof timeoutTimer === "object" && "unref" in timeoutTimer) {
timeoutTimer.unref()
}
})
}