Ensure child processes are stopped

This commit is contained in:
Shantur Rathore
2026-01-07 19:35:33 +00:00
parent c825ff066e
commit e9241a1b93

View File

@@ -11,6 +11,7 @@ const ROOT_DIR = ".codenomad/background_processes"
const INDEX_FILE = "index.json" const INDEX_FILE = "index.json"
const OUTPUT_FILE = "output.txt" const OUTPUT_FILE = "output.txt"
const STOP_TIMEOUT_MS = 2000 const STOP_TIMEOUT_MS = 2000
const EXIT_WAIT_TIMEOUT_MS = 5000
const MAX_OUTPUT_BYTES = 20 * 1024 const MAX_OUTPUT_BYTES = 20 * 1024
const OUTPUT_PUBLISH_INTERVAL_MS = 1000 const OUTPUT_PUBLISH_INTERVAL_MS = 1000
@@ -21,6 +22,7 @@ interface ManagerDeps {
} }
interface RunningProcess { interface RunningProcess {
id: string
child: ChildProcess child: ChildProcess
outputPath: string outputPath: string
exitPromise: Promise<void> exitPromise: Promise<void>
@@ -61,9 +63,15 @@ export class BackgroundProcessManager {
const child = spawn("bash", ["-c", command], { const child = spawn("bash", ["-c", command], {
cwd: workspace.path, cwd: workspace.path,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
})
child.on("exit", () => {
this.killProcessTree(child, "SIGTERM")
}) })
const record: BackgroundProcess = { const record: BackgroundProcess = {
id, id,
workspaceId, workspaceId,
title, title,
@@ -91,7 +99,7 @@ export class BackgroundProcessManager {
}) })
}) })
this.running.set(id, { child, outputPath, exitPromise, workspaceId }) this.running.set(id, { id, child, outputPath, exitPromise, workspaceId })
let lastPublishAt = 0 let lastPublishAt = 0
const maybePublishSize = () => { const maybePublishSize = () => {
@@ -128,7 +136,7 @@ export class BackgroundProcessManager {
const running = this.running.get(processId) const running = this.running.get(processId)
if (running?.child && !running.child.killed) { if (running?.child && !running.child.killed) {
running.child.kill("SIGTERM") this.killProcessTree(running.child, "SIGTERM")
await this.waitForExit(running) await this.waitForExit(running)
} }
@@ -149,7 +157,7 @@ export class BackgroundProcessManager {
const running = this.running.get(processId) const running = this.running.get(processId)
if (running?.child && !running.child.killed) { if (running?.child && !running.child.killed) {
running.child.kill("SIGTERM") this.killProcessTree(running.child, "SIGTERM")
await this.waitForExit(running) await this.waitForExit(running)
} }
@@ -255,26 +263,64 @@ export class BackgroundProcessManager {
private async cleanupWorkspace(workspaceId: string) { private async cleanupWorkspace(workspaceId: string) {
for (const [, running] of this.running.entries()) { for (const [, running] of this.running.entries()) {
if (running.workspaceId !== workspaceId) continue if (running.workspaceId !== workspaceId) continue
running.child.kill("SIGTERM") this.killProcessTree(running.child, "SIGTERM")
await this.waitForExit(running) await this.waitForExit(running)
} }
await this.removeWorkspaceDir(workspaceId) await this.removeWorkspaceDir(workspaceId)
} }
private killProcessTree(child: ChildProcess, signal: NodeJS.Signals) {
const pid = child.pid
if (!pid) return
if (process.platform !== "win32") {
try {
process.kill(-pid, signal)
return
} catch {
// Fall back to killing the direct child.
}
}
try {
child.kill(signal)
} catch {
// ignore
}
}
private async waitForExit(running: RunningProcess) { private async waitForExit(running: RunningProcess) {
let resolved = false let exited = false
const timeout = setTimeout(() => { const exitPromise = running.exitPromise.finally(() => {
if (!resolved) { exited = true
running.child.kill("SIGKILL") })
const killTimeout = setTimeout(() => {
if (!exited) {
this.killProcessTree(running.child, "SIGKILL")
} }
}, STOP_TIMEOUT_MS) }, STOP_TIMEOUT_MS)
await running.exitPromise.finally(() => { try {
resolved = true await Promise.race([
clearTimeout(timeout) exitPromise,
}) new Promise<void>((resolve) => {
setTimeout(resolve, EXIT_WAIT_TIMEOUT_MS)
}),
])
if (!exited) {
this.killProcessTree(running.child, "SIGKILL")
this.running.delete(running.id)
this.deps.logger.warn({ pid: running.child.pid }, "Timed out waiting for background process to exit")
}
} finally {
clearTimeout(killTimeout)
}
} }
private statusFromExit(code: number | null): BackgroundProcessStatus { private statusFromExit(code: number | null): BackgroundProcessStatus {
if (code === null) return "stopped" if (code === null) return "stopped"
if (code === 0) return "stopped" if (code === 0) return "stopped"