Isolate per-command output from background procs

This commit is contained in:
Chris Wanstrath 2026-03-12 16:06:33 -07:00
parent 38b02ea21c
commit 481807255a
2 changed files with 32 additions and 3 deletions

View File

@ -62,6 +62,24 @@ describe("runFile", () => {
}
})
test("background process output does not leak into subsequent commands", async () => {
const file = makeFile([
// Start a background process that writes to stdout after a delay
{ command: "{ sleep 0.1; echo LEAKED; } &" },
// Wait long enough for the background output to appear
{ command: "sleep 0.3; echo clean" },
])
const result = await runFile(file, defaultOpts)
try {
// The background command itself should have no output
expect(result.results[0]?.actual).toEqual([])
// The sleep command should only see its own output, not the background "LEAKED"
expect(result.results[1]?.actual).toEqual(["clean"])
} finally {
await cleanupTmpDir(result.tmpDir)
}
})
test("cleans up multiple backgrounded processes", async () => {
const file = makeFile([
{ command: "sleep 300 >/dev/null 2>&1 & P1=$!; sleep 300 >/dev/null 2>&1 & P2=$!; echo $P1 $P2" },

View File

@ -58,9 +58,10 @@ function buildScript(commands: Command[], sentinel: string, verbose: boolean): s
if (verbose) {
// Save original stderr to fd 3 before merging stderr into stdout
lines.push("exec 3>&2 2>&1")
// Save the pipe on fd 9 so we can restore after each command
lines.push("exec 3>&2 2>&1 9>&1")
} else {
lines.push("exec 2>&1")
lines.push("exec 2>&1 9>&1")
}
for (let i = 0; i < commands.length; i++) {
@ -68,11 +69,21 @@ function buildScript(commands: Command[], sentinel: string, verbose: boolean): s
if (verbose) {
lines.push(`printf '${VERBOSE_MARKER}${i}\\n' >&3`)
}
// Redirect stdout+stderr to a temp file so background processes
// from previous commands can't pollute this command's output.
// Background processes keep their fd pointing at the old temp file,
// which becomes orphaned after rm — their output goes nowhere.
lines.push(`__shout_out=$(mktemp)`)
lines.push(`exec 1>"$__shout_out" 2>&1`)
lines.push(cmd.command)
lines.push(`__shout_ec=$?`)
lines.push(`exec 1>&9 2>&1`)
lines.push(`cat "$__shout_out"`)
lines.push(`rm -f "$__shout_out"`)
// Sentinel: printf to avoid echo interpretation issues
// Format: __SHOUT_SENTINEL_<exitcode>_<index>__
lines.push(
`printf '\\n${sentinel}%s_${i}__\\n' "$?"`,
`printf '\\n${sentinel}%s_${i}__\\n' "$__shout_ec"`,
)
}