diff --git a/aesel/package.json b/aesel/package.json index 177d94b7a8..261adfc699 100644 --- a/aesel/package.json +++ b/aesel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.8.4", + "version": "0.8.5", "private": true, "type": "module", "scripts": { diff --git a/aesel/src/workspace-tools.mjs b/aesel/src/workspace-tools.mjs index a1bfb2ebfe..2a21b6a47e 100644 --- a/aesel/src/workspace-tools.mjs +++ b/aesel/src/workspace-tools.mjs @@ -132,11 +132,22 @@ function run(command, args, { cwd, signal, timeout = COMMAND_TIMEOUT }) { clearTimeout(timer); done({ code: -1, output: output + error.message }); }); - child.on("close", (code) => { + // Done when the shell exits, not when every holder of its output lets go: + // a command that starts a server in the background (`… &`) leaves a child + // holding the pipe, and waiting for "close" waited forever. The output + // gets a moment to drain; the background process keeps running. + let finished = false; + const finish = (code) => { + if (finished) return; + finished = true; clearTimeout(timer); signal?.removeEventListener("abort", killGroup); + child.stdout.destroy(); + child.stderr.destroy(); done({ code: timedOut ? "timeout" : code, output }); - }); + }; + child.on("exit", (code) => setTimeout(() => finish(code), 150)); + child.on("close", (code) => finish(code)); }); } diff --git a/aesel/test/workspace-bash.test.mjs b/aesel/test/workspace-bash.test.mjs new file mode 100644 index 0000000000..f31d346d89 --- /dev/null +++ b/aesel/test/workspace-bash.test.mjs @@ -0,0 +1,18 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { runWorkspaceTool } from "../src/workspace-tools.mjs"; + +const approve = async () => true; + +test("a command that leaves a server running in the background still returns", async () => { + const started = Date.now(); + const out = await runWorkspaceTool("bash", { command: "echo up && (sleep 5 &) ; sleep 6 & echo done" }, { cwd: "/tmp", approve }); + assert.match(out, /up\ndone\n\[exit 0\]/); + assert.ok(Date.now() - started < 3000, "returned without waiting for the background sleeps"); +}); + +test("an interrupt kills the whole pipeline, not just the shell", async () => { + const started = Date.now(); + await runWorkspaceTool("bash", { command: "sleep 30 | cat" }, { cwd: "/tmp", approve, signal: AbortSignal.timeout(500) }); + assert.ok(Date.now() - started < 3000, "returned once interrupted"); +});