diff --git a/src/process/OwnedDebuggerSessionRegistry.ts b/src/process/OwnedDebuggerSessionRegistry.ts index 98e770a2e9..60e8aee102 100644 --- a/src/process/OwnedDebuggerSessionRegistry.ts +++ b/src/process/OwnedDebuggerSessionRegistry.ts @@ -139,6 +139,8 @@ export class OwnedDebuggerSessionRegistry< controller.abort(callerSignal?.reason); }; callerSignal?.addEventListener("abort", relayAbort, { once: true }); + // An abort dispatched while the listener was being added never reaches it. + if (callerSignal?.aborted) relayAbort(); const started = Promise.resolve() .then(() => run(controller.signal)) .finally(() => { diff --git a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts index 1027f20ffa..26cd7d41f3 100644 --- a/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts +++ b/tests/boundary/providers/RizinDebugSessionCoverage.part1.test.ts @@ -10,9 +10,9 @@ import type { SpawnedOwnedProviderProcess } from "../../../src/process/ProviderP import { createRizinCoverageHarness, createInjectedProcess, - abortOnCheck, incompleteStop, } from "./RizinDebugSessionCoverage.fixtures.js"; +import { OwnerCancellation } from "../../../src/domain/analysisErrorCore.js"; const { cleanup, @@ -135,34 +135,10 @@ it.skipIf(process.platform === "win32")( ); it.skipIf(process.platform === "win32")( - "does not spawn Rizin when cancellation arrives during TTY validation", - async () => { - let spawned = false; - const manager = createManager( - { REA_RIZIN_COMMAND: "injected-rizin" }, - { - spawnProcess: async () => { - spawned = true; - throw new Error("must not spawn after cancellation"); - }, - }, - ); - const result = await manager.start( - { - path: "/tmp/cancel-before-spawn", - inferior_tty: await rizinTestInferiorTty(), - }, - abortOnCheck(2), - ); - expect(result.ok).toBe(false); - expect(spawned).toBe(false); - }, -); - -it.skipIf(process.platform === "win32")( - "closeAll drains concurrent startups and blocks later admission", + "closeAll aborts concurrent startups as owner-closed, waits for them, and blocks later admission", async () => { let spawnCount = 0; + let stopCount = 0; let resolveSpawnCalls!: () => void; const bothSpawnsCalled = new Promise((resolve) => { resolveSpawnCalls = resolve; @@ -181,7 +157,10 @@ it.skipIf(process.platform === "win32")( await spawnGate; return injected.spawnProcess(options); }, - stopProcess: async () => ({ status: "stopped" }), + stopProcess: async () => { + stopCount += 1; + return { status: "stopped" }; + }, }, ); const tty = await rizinTestInferiorTty(); @@ -195,14 +174,21 @@ it.skipIf(process.platform === "win32")( closeAllResolved = true; }); await Promise.resolve(); + // The injected spawns ignore the abort, so closeAll waits for them. expect(closeAllResolved).toBe(false); releaseSpawns(); const started = await Promise.all(starts); - expect(started.every((result) => result.ok)).toBe(true); + for (const result of started) + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + cause: OwnerCancellation.ownerClosed, + }, + }); await closing; - expect( - manager.status(started[0]!.ok ? started[0]!.value.session_id : "missing"), - ).toBeUndefined(); + // Each process spawned before the abort was observed is stopped. + expect(stopCount).toBe(2); expect( (await manager.start({ path: "/tmp/after-close-all", inferior_tty: tty })) .ok, diff --git a/tests/boundary/providers/RizinTtyCleanup.test.ts b/tests/boundary/providers/RizinTtyCleanup.test.ts index 04abee7475..bd59fff2fc 100644 --- a/tests/boundary/providers/RizinTtyCleanup.test.ts +++ b/tests/boundary/providers/RizinTtyCleanup.test.ts @@ -81,6 +81,48 @@ it.skipIf(process.platform === "win32")( }, ); +it.skipIf(process.platform === "win32")( + "does not spawn Rizin when cancellation arrives during TTY validation", + async () => { + const actual = + await vi.importActual( + "node:fs/promises", + ); + vi.mocked(isatty).mockReturnValue(true); + const controller = new AbortController(); + const reason = { cancelled: "during TTY validation" }; + vi.mocked(open).mockImplementation(async (...args) => { + const handle = await actual.open(...args); + controller.abort(reason); + return handle; + }); + onTestFinished(() => { + vi.restoreAllMocks(); + }); + let spawned = false; + const manager = new RizinDebugSessionManager( + {}, + { + platform: "linux", + spawnProcess: () => { + spawned = true; + throw new Error("must not spawn after cancellation"); + }, + }, + ); + const result = await manager.start( + { path: "/fixture", inferior_tty: "/dev/null" }, + controller.signal, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError", cause: reason }, + }); + expect(spawned).toBe(false); + await manager.closeAll(); + }, +); + it.skipIf(process.platform === "win32")( "preserves cancellation during final TTY close with failed cleanup", async () => {