From f14b6a84306116716e78c5ffc84353ac07939593 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 19:58:43 +0800 Subject: [PATCH] fix(ida): share concurrent session cleanup (#950) --- src/ida/IdaSessionClient.test.ts | 30 ++++++++++++++++++++++++++++++ src/ida/IdaSessionClient.ts | 11 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/ida/IdaSessionClient.test.ts b/src/ida/IdaSessionClient.test.ts index 3be1b4b37..30ef7e07e 100644 --- a/src/ida/IdaSessionClient.test.ts +++ b/src/ida/IdaSessionClient.test.ts @@ -312,6 +312,36 @@ describe("headless IDA lifecycle boundaries", () => { }); describe("headless IDA session ownership and cleanup", () => { + it("shares an in-flight close across concurrent cleanup callers", async () => { + const { producer, client } = await fixture("headless"); + expect((await client.execute("health", {})).ok).toBe(true); + + let release: (() => void) | undefined; + let notify: (() => void) | undefined; + const closeStarted = new Promise((resolve) => { + notify = resolve; + }); + producer.beforeCall = async (name) => { + if (name !== "idb_close") return; + notify?.(); + await new Promise((resolve) => { + release = resolve; + }); + }; + + const first = client.closeWithOutcome(); + await closeStarted; + const second = client.closeWithOutcome(); + release?.(); + const results = await Promise.all([first, second]); + + expect(results.every((result) => result.ok)).toBe(true); + expect( + producer.calls.filter(({ name }) => name === "idb_close"), + ).toHaveLength(1); + expect(producer.closes).toBe(1); + }); + it("retains the workspace after an unconfirmed open, even when inventory is empty, and never retries startup implicitly", async () => { const { producer, client, root } = await fixture("headless"); producer.beforeCall = async (name) => { diff --git a/src/ida/IdaSessionClient.ts b/src/ida/IdaSessionClient.ts index d8504b174..a086ddcd5 100644 --- a/src/ida/IdaSessionClient.ts +++ b/src/ida/IdaSessionClient.ts @@ -95,6 +95,7 @@ export class IdaSessionClient implements AnalysisClient { #startup: Promise | undefined; #closed = false; #closing = false; + #closePromise: Promise> | undefined; #workspace: IdaWorkspace | undefined; #database: string | undefined; #openAttempted = false; @@ -385,6 +386,16 @@ export class IdaSessionClient implements AnalysisClient { } async closeWithOutcome(): Promise> { + if (this.#closePromise !== undefined) return this.#closePromise; + const closing = this.#closeOnce(); + const shared = closing.finally(() => { + if (this.#closePromise === shared) this.#closePromise = undefined; + }); + this.#closePromise = shared; + return shared; + } + + async #closeOnce(): Promise> { this.#closing = true; await this.#tail; if (this.#closed) return ok(null);