fix(process): relay a start abort missed during listener registration and expect owner-closed Rizin starts (#11200)

* test(rizin): expect closeAll to abort in-flight starts and cancel TTY validation with a real signal

* fix(process): relay a caller abort dispatched while the start registered its listener
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 10:12:33 -05:00
1 parent 456ca8e973
commit 4c5f5a3338
3 files changed
+62 -32

No files matched your search

@@ -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(() => {
@@ -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<void>((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,
@@ -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<typeof import("node:fs/promises")>(
"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 () => {