Skip to content

Commit 4c1e972

Browse files
fix: add stdio end event fallback, diagnostic logging, and tests
Complete the port of PR anomalyco#15757 with remaining pieces: - Add stdio end event redundancy as third fallback for exit detection (fires when pipe file descriptors close, independent of exit events) - Add diagnostic log.info calls at spawn, abort, timeout, and each exit detection path for debugging container issues - Add comprehensive tests: defensive patterns, polling watchdog isolation, Shell.killTree, server-level watchdog (stale/reap), stdio end events, and Process.spawn defensive patterns - Skip truncation tests on Windows (matching upstream) Co-Authored-By: Nacho F. Lizaur <NachoFLizaur@users.noreply.github.com>
1 parent bcf80c6 commit 4c1e972

File tree

3 files changed

+504
-5
lines changed

3 files changed

+504
-5
lines changed

packages/opencode/src/tool/bash.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ export const BashTool = Tool.define("bash", async () => {
218218
}
219219
}
220220

221+
log.info("spawned process", {
222+
pid: proc.pid,
223+
command: params.command.slice(0, 100),
224+
cwd,
225+
timeout,
226+
})
227+
221228
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024 // 10 MB cap
222229
const outputChunks: Buffer[] = []
223230
let outputLen = 0
@@ -263,23 +270,27 @@ export const BashTool = Tool.define("bash", async () => {
263270
}
264271

265272
const abortHandler = () => {
273+
log.info("process abort triggered", { pid: proc.pid })
266274
aborted = true
267275
void kill()
268276
}
269277

270278
ctx.abort.addEventListener("abort", abortHandler, { once: true })
271279

272280
const timeoutTimer = setTimeout(() => {
281+
log.info("process timeout triggered", { pid: proc.pid, timeout })
273282
timedOut = true
274283
void kill()
275284
}, timeout + 100)
276285

286+
const started = Date.now()
287+
277288
const callID = ctx.callID
278289
if (callID) {
279290
active.set(callID, {
280291
pid: proc.pid!,
281292
timeout,
282-
started: Date.now(),
293+
started,
283294
kill: () => Shell.killTree(proc, { exited: () => exited }),
284295
done: () => {},
285296
})
@@ -294,6 +305,8 @@ export const BashTool = Tool.define("bash", async () => {
294305
clearTimeout(timeoutTimer)
295306
clearInterval(poll)
296307
ctx.abort.removeEventListener("abort", abortHandler)
308+
proc.stdout?.removeListener("end", check)
309+
proc.stderr?.removeListener("end", check)
297310
}
298311

299312
const done = () => {
@@ -316,21 +329,62 @@ export const BashTool = Tool.define("bash", async () => {
316329
reject(error)
317330
}
318331

319-
proc.once("exit", done)
320-
proc.once("close", done)
332+
proc.once("exit", () => {
333+
log.info("process exit detected via 'exit' event", { pid: proc.pid, exitCode: proc.exitCode })
334+
done()
335+
})
336+
proc.once("close", () => {
337+
log.info("process exit detected via 'close' event", { pid: proc.pid, exitCode: proc.exitCode })
338+
done()
339+
})
321340
proc.once("error", fail)
322341

342+
// Redundancy: stdio end events fire when pipe file descriptors close
343+
// independent of process exit monitoring — catches missed exit events
344+
let streams = 0
345+
const total = (proc.stdout ? 1 : 0) + (proc.stderr ? 1 : 0)
346+
const check = () => {
347+
streams++
348+
if (streams < total) return
349+
if (proc.exitCode !== null || proc.signalCode !== null) {
350+
log.info("stdio end detected exit (exitCode already set)", {
351+
pid: proc.pid,
352+
exitCode: proc.exitCode,
353+
})
354+
done()
355+
return
356+
}
357+
setTimeout(() => {
358+
log.info("stdio end deferred check", {
359+
pid: proc.pid,
360+
exitCode: proc.exitCode,
361+
})
362+
done()
363+
}, 50)
364+
}
365+
proc.stdout?.once("end", check)
366+
proc.stderr?.once("end", check)
367+
323368
// Polling watchdog: detect process exit when Bun's event loop
324369
// fails to deliver the "exit" event (confirmed Bun bug in containers)
325370
const poll = setInterval(() => {
326371
if (proc.exitCode !== null || proc.signalCode !== null) {
372+
log.info("polling watchdog detected exit via exitCode/signalCode", {
373+
exitCode: proc.exitCode,
374+
signalCode: proc.signalCode,
375+
})
327376
done()
328377
return
329378
}
379+
380+
// Check 2: process.kill(pid, 0) throws ESRCH if process is dead
330381
if (proc.pid && process.platform !== "win32") {
331382
try {
332383
process.kill(proc.pid, 0)
333384
} catch {
385+
log.info("polling watchdog detected exit via kill(0) ESRCH", {
386+
pid: proc.pid,
387+
})
334388
done()
335389
return
336390
}
@@ -340,6 +394,14 @@ export const BashTool = Tool.define("bash", async () => {
340394

341395
if (callID) active.delete(callID)
342396

397+
log.info("process completed", {
398+
pid: proc.pid,
399+
exitCode: proc.exitCode,
400+
duration: Date.now() - started,
401+
timedOut,
402+
aborted,
403+
})
404+
343405
let output = Buffer.concat(outputChunks).toString()
344406
// Free the chunks array
345407
outputChunks.length = 0

0 commit comments

Comments
 (0)