fix(bash): kept persistent shells after cancel

Stopped marking persistent bash sessions as permanently broken when the JavaScript abort or timeout race wins.

Stopped the Rust descendant kill-wave helper once no cancellation targets remain so later commands are not swept into old cancels.

Fixes #1347
This commit is contained in:
roboomp
2026-05-25 18:40:22 +00:00
parent de5b0a51e2
commit 8e5c7c9bf1
3 changed files with 24 additions and 12 deletions
+10 -9
View File
@@ -972,21 +972,22 @@ async fn read_output_bytes(
}
// Rescan-and-signal loop for cancellation. Each pass picks up descendants
// spawned during the previous wave's grace period; empty scans keep going so a
// cancellation that wins before `run_string` exposes its child still escalates.
// spawned during the previous wave's grace period, then exits as soon as no
// targets remain so unrelated later commands are not swept into old cancels.
async fn terminate_new_descendants<S: std::hash::BuildHasher>(baseline: &HashSet<i32, S>) {
const WAVES: u32 = 3;
for wave in 0..WAVES {
let mut targets = process::TerminationTargets::new();
process::add_new_descendants(&mut targets, baseline);
if !targets.is_empty() {
let signal = if wave == 0 {
process::TERM_SIGNAL
} else {
process::KILL_SIGNAL
};
targets.signal(signal);
if targets.is_empty() {
return;
}
let signal = if wave == 0 {
process::TERM_SIGNAL
} else {
process::KILL_SIGNAL
};
targets.signal(signal);
if wave + 1 < WAVES {
let pause = if wave == 0 {
Duration::from_millis(75)
@@ -207,9 +207,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
void runPromise.catch(() => undefined);
if (shellSession) {
resetSession = true;
// Fall back to one-shot execution for the rest of the process once
// a persistent session has stopped responding to cancellation.
brokenShellSessions.add(sessionKey);
}
return {
exitCode: undefined,
@@ -197,6 +197,20 @@ describe("executeBash", () => {
expect(raced.result.output).toContain("Command cancelled");
}
expect(abortSpy).toHaveBeenCalled();
vi.restoreAllMocks();
await executeBash("export PI_AFTER_ABORT=still_persistent", {
cwd: tempDir,
timeout: 5000,
sessionKey: "hung-native-abort",
});
const next = await executeBash("printf '%s\n' \"$PI_AFTER_ABORT\"", {
cwd: tempDir,
timeout: 5000,
sessionKey: "hung-native-abort",
});
expect(next.output.trim()).toBe("still_persistent");
});
it("returns at the JavaScript timeout when native timeout cleanup stalls", async () => {