fix(background-agent): run manager cleanup on uncaughtException and unhandledRejection
Signal handlers covered SIGINT/SIGTERM/SIGBREAK/beforeExit/exit, but a synchronous throw or a top-level rejected promise terminated the process without letting TmuxSessionManager (or any other registered manager) run its shutdown hook. That reliably left orphan tmux panes after an opencode crash. Added registration for uncaughtException and unhandledRejection that fan out through the existing cleanupAll() path, set process.exitCode = 1, and arm the same 6 second forced-exit guard we use for signals. Test helpers hold process-level spies so the new tests do not leak listeners between runs.
This commit is contained in:
@@ -1,33 +1,51 @@
|
||||
import { log } from "../../shared"
|
||||
|
||||
type ProcessCleanupEvent = NodeJS.Signals | "beforeExit" | "exit"
|
||||
type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit"
|
||||
type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection"
|
||||
|
||||
function scheduleForcedExit(cleanupResult: void | Promise<void>, exitCode: number): void {
|
||||
process.exitCode = exitCode
|
||||
const exitTimeout = setTimeout(() => process.exit(), 6000)
|
||||
void Promise.resolve(cleanupResult).finally(() => {
|
||||
clearTimeout(exitTimeout)
|
||||
})
|
||||
}
|
||||
|
||||
function registerProcessSignal(
|
||||
signal: ProcessCleanupEvent,
|
||||
signal: ProcessCleanupSignal,
|
||||
handler: () => void | Promise<void>,
|
||||
exitAfter: boolean
|
||||
): () => void {
|
||||
const listener = () => {
|
||||
const cleanupResult = handler()
|
||||
if (exitAfter) {
|
||||
process.exitCode = 0
|
||||
const exitTimeout = setTimeout(() => process.exit(), 6000)
|
||||
void Promise.resolve(cleanupResult).finally(() => {
|
||||
clearTimeout(exitTimeout)
|
||||
})
|
||||
scheduleForcedExit(cleanupResult, 0)
|
||||
}
|
||||
}
|
||||
process.on(signal, listener)
|
||||
return listener
|
||||
}
|
||||
|
||||
function registerErrorEvent(
|
||||
signal: ProcessCleanupErrorEvent,
|
||||
handler: (error: unknown) => void | Promise<void>
|
||||
): (error: unknown) => void {
|
||||
const listener = (error: unknown) => {
|
||||
log(`[background-agent] ${signal} received during shutdown cleanup:`, error)
|
||||
scheduleForcedExit(handler(error), 1)
|
||||
}
|
||||
process.on(signal, listener)
|
||||
return listener
|
||||
}
|
||||
|
||||
interface CleanupTarget {
|
||||
shutdown(): void | Promise<void>
|
||||
}
|
||||
|
||||
const cleanupManagers = new Set<CleanupTarget>()
|
||||
let cleanupRegistered = false
|
||||
const cleanupHandlers = new Map<ProcessCleanupEvent, () => void>()
|
||||
const cleanupSignalHandlers = new Map<ProcessCleanupSignal, () => void>()
|
||||
const cleanupErrorHandlers = new Map<ProcessCleanupErrorEvent, (error: unknown) => void>()
|
||||
|
||||
export function registerManagerForCleanup(manager: CleanupTarget): void {
|
||||
cleanupManagers.add(manager)
|
||||
@@ -59,9 +77,9 @@ export function registerManagerForCleanup(manager: CleanupTarget): void {
|
||||
return cleanupPromise
|
||||
}
|
||||
|
||||
const registerSignal = (signal: ProcessCleanupEvent, exitAfter: boolean): void => {
|
||||
const registerSignal = (signal: ProcessCleanupSignal, exitAfter: boolean): void => {
|
||||
const listener = registerProcessSignal(signal, cleanupAll, exitAfter)
|
||||
cleanupHandlers.set(signal, listener)
|
||||
cleanupSignalHandlers.set(signal, listener)
|
||||
}
|
||||
|
||||
registerSignal("SIGINT", true)
|
||||
@@ -71,6 +89,8 @@ export function registerManagerForCleanup(manager: CleanupTarget): void {
|
||||
}
|
||||
registerSignal("beforeExit", false)
|
||||
registerSignal("exit", false)
|
||||
cleanupErrorHandlers.set("uncaughtException", registerErrorEvent("uncaughtException", cleanupAll))
|
||||
cleanupErrorHandlers.set("unhandledRejection", registerErrorEvent("unhandledRejection", cleanupAll))
|
||||
}
|
||||
|
||||
export function unregisterManagerForCleanup(manager: CleanupTarget): void {
|
||||
@@ -78,10 +98,14 @@ export function unregisterManagerForCleanup(manager: CleanupTarget): void {
|
||||
|
||||
if (cleanupManagers.size > 0) return
|
||||
|
||||
for (const [signal, listener] of cleanupHandlers.entries()) {
|
||||
for (const [signal, listener] of cleanupSignalHandlers.entries()) {
|
||||
process.off(signal, listener)
|
||||
}
|
||||
cleanupHandlers.clear()
|
||||
for (const [signal, listener] of cleanupErrorHandlers.entries()) {
|
||||
process.off(signal, listener)
|
||||
}
|
||||
cleanupSignalHandlers.clear()
|
||||
cleanupErrorHandlers.clear()
|
||||
cleanupRegistered = false
|
||||
}
|
||||
|
||||
@@ -90,9 +114,13 @@ export function _resetForTesting(): void {
|
||||
for (const manager of [...cleanupManagers]) {
|
||||
cleanupManagers.delete(manager)
|
||||
}
|
||||
for (const [signal, listener] of cleanupHandlers.entries()) {
|
||||
for (const [signal, listener] of cleanupSignalHandlers.entries()) {
|
||||
process.off(signal, listener)
|
||||
}
|
||||
cleanupHandlers.clear()
|
||||
for (const [signal, listener] of cleanupErrorHandlers.entries()) {
|
||||
process.off(signal, listener)
|
||||
}
|
||||
cleanupSignalHandlers.clear()
|
||||
cleanupErrorHandlers.clear()
|
||||
cleanupRegistered = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user