Compare commits

...

3 Commits

Author SHA1 Message Date
d3c9178958 0.0.13 2026-03-12 16:06:42 -07:00
481807255a Isolate per-command output from background procs 2026-03-12 16:06:33 -07:00
38b02ea21c 0.0.12 2026-03-12 15:27:54 -07:00
3 changed files with 33 additions and 4 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@because/shout",
"version": "0.0.11",
"version": "0.0.13",
"description": "shell output tester",
"module": "src/index.ts",
"type": "module",

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"`,
)
}