Ensure child processes are stopped
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user