fix(opencode): bound shell output after exit

This commit is contained in:
Hona 2026-07-21 00:40:22 +00:00
commit cb1cef6747
4 changed files with 101 additions and 5 deletions

View file

@ -268,17 +268,16 @@ export const make = Effect.gen(function* () {
Effect.callback<readonly [NodeChildProcess.ChildProcess, ExitSignal], PlatformError.PlatformError>((resume) => {
const signal = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
const proc = launch(command.command, command.args, opts)
let end = false
let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined
proc.on("error", (err) => {
resume(Effect.fail(toPlatformError("spawn", err, command)))
})
proc.on("exit", (...args) => {
exit = args
Deferred.doneUnsafe(signal, Exit.succeed(args))
})
proc.on("close", (...args) => {
if (end) return
end = true
// cross-spawn can suppress `exit` for a Windows ENOENT and only emit `close`.
Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args))
})
proc.on("spawn", () => {

View file

@ -285,6 +285,45 @@ describe("cross-spawn spawner", () => {
expect(running).toBe(false)
}),
)
fx.effect(
"resolves exit before inherited output pipes close",
Effect.gen(function* () {
const tmp = yield* Effect.acquireRelease(
Effect.promise(() => tmpdir()),
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
)
const file = path.join(tmp.path, "child.pid")
const child = "setInterval(() => {}, 10_000)"
const handle = yield* ChildProcess.make(process.execPath, [
"-e",
[
'const { spawn } = require("node:child_process")',
'const { writeFileSync } = require("node:fs")',
`const child = spawn(process.execPath, ["-e", ${JSON.stringify(child)}], { detached: true, stdio: "inherit" })`,
"child.unref()",
`writeFileSync(${JSON.stringify(file)}, String(child.pid))`,
].join("\n"),
])
const code = yield* handle.exitCode.pipe(
Effect.timeoutOrElse({
duration: "2 seconds",
orElse: () => Effect.die("exitCode waited for inherited output pipes"),
}),
)
const pid = Number(yield* Effect.promise(() => fs.readFile(file, "utf8")))
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
try {
process.kill(pid, "SIGKILL")
} catch {}
}),
)
expect(code).toBe(ChildProcessSpawner.ExitCode(0))
expect(yield* handle.isRunning).toBe(false)
}),
)
})
describe("error handling", () => {