fix(ida): share concurrent session cleanup (#950)

This commit is contained in:
morluto 2026-10-07 19:58:43 +08:00 committed by GitHub
parent 118d43e787
commit f14b6a8430
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 41 additions and 0 deletions

View File

@ -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<void>((resolve) => {
notify = resolve;
});
producer.beforeCall = async (name) => {
if (name !== "idb_close") return;
notify?.();
await new Promise<void>((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) => {

View File

@ -95,6 +95,7 @@ export class IdaSessionClient implements AnalysisClient {
#startup: Promise<void> | undefined;
#closed = false;
#closing = false;
#closePromise: Promise<Result<null, AnalysisError>> | undefined;
#workspace: IdaWorkspace | undefined;
#database: string | undefined;
#openAttempted = false;
@ -385,6 +386,16 @@ export class IdaSessionClient implements AnalysisClient {
}
async closeWithOutcome(): Promise<Result<null, AnalysisError>> {
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<Result<null, AnalysisError>> {
this.#closing = true;
await this.#tail;
if (this.#closed) return ok(null);