import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { promisify } from "node:util"; import { pathToFileURL } from "node:url"; import { access, readFile, rename, writeFile, type FileHandle, } from "node:fs/promises"; import { basename, join } from "node:path"; import { afterEach, beforeEach, expect, it, vi } from "vitest"; const fault = vi.hoisted(() => ({ path: "", matchEndpoints: false, reads: 0, handles: [] as FileHandle[], blocked: false, abortClose: undefined as AbortController | undefined, readFailure: undefined as Error | undefined, abortRead: undefined as AbortController | undefined, readGate: undefined as (() => Promise) | undefined, })); vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, open: async (...args: Parameters) => { const handle = await actual.open(...args); if ( args[0] !== fault.path && !( fault.matchEndpoints && typeof args[0] === "string" && basename(args[0]) === "bridge-endpoint.json" ) ) return handle; fault.handles.push(handle); const close = handle.close.bind(handle); handle.close = async () => { fault.abortClose?.abort(null); if (fault.blocked) throw new Error("controlled endpoint close failure"); await close(); }; const read = handle.read.bind(handle); handle.read = (async (...values: unknown[]) => { fault.reads += 1; await fault.readGate?.(); if (fault.readFailure !== undefined) { fault.abortRead?.abort(null); throw fault.readFailure; } return await Reflect.apply(read, handle, values); }) as FileHandle["read"]; const readFile = handle.readFile.bind(handle); handle.readFile = (async (...values: unknown[]) => { fault.reads += 1; await fault.readGate?.(); if (fault.readFailure !== undefined) { fault.abortRead?.abort(null); throw fault.readFailure; } return await Reflect.apply(readFile, handle, values); }) as FileHandle["readFile"]; return handle; }, }; }); import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; import { observeGhidraEndpoint } from "../../src/ghidra/GhidraTransport.js"; import { GhidraClient } from "../../src/ghidra/GhidraClient.js"; import { GhidraFixtureLauncher, ghidraFixturePath, } from "../boundary/providers/ghidra/ghidraClient.fixture.js"; import { waitForExit } from "../support/process/processFixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; const clients: GhidraClient[] = []; const operations: Promise[] = []; let releaseRead: (() => void) | undefined; beforeEach(() => { fault.path = ""; fault.matchEndpoints = false; fault.reads = 0; fault.handles = []; fault.blocked = false; fault.abortClose = undefined; fault.readFailure = undefined; fault.abortRead = undefined; fault.readGate = undefined; }); afterEach(async () => { fault.blocked = false; fault.abortClose = undefined; fault.readFailure = undefined; fault.abortRead = undefined; releaseRead?.(); releaseRead = undefined; await Promise.allSettled(operations.splice(0)); for (const client of clients.splice(0)) await client.close(); for (const handle of fault.handles) if (handle.fd !== -1) await handle.close(); }); const fixture = async (text = '{"host":"127.0.0.1","port":49152}\n') => { const root = await createTestTempDirectory("rea-endpoint-lifetime-"); fault.path = join(root, "bridge-endpoint.json"); await writeFile(fault.path, text); return { endpoint: { transport: "authenticated-loopback-tcp" as const, path: fault.path, }, resources: new ArtifactResourceScope(), }; }; it.skipIf(process.platform === "win32").each(["valid", "invalid"] as const)( "retains the exact descriptor and %s endpoint outcome after close failure", async (kind) => { const { endpoint, resources } = await fixture( kind === "valid" ? undefined : "{", ); fault.blocked = true; const result = await observeGhidraEndpoint(endpoint, { resourceScope: resources, }); expect(result).toMatchObject({ ok: false, error: { kind: "start", cleanupFailure: { cleanupIncomplete: true } }, }); if (result.ok) throw new Error("Expected typed endpoint failure"); const failure = result.error; if (kind === "invalid") expect(failure.message).toBe("Ghidra TCP endpoint is invalid"); expect(fault.handles[0]?.fd).not.toBe(-1); expect(resources.hasPending).toBe(true); fault.blocked = false; await resources.retryCleanup(); expect(fault.handles[0]?.fd).toBe(-1); expect(failure.cleanupFailure?.cleanupIncomplete).toBe(false); }, ); it.skipIf(process.platform === "win32")( "observes exact null cancellation during the endpoint's final close", async () => { const { endpoint, resources } = await fixture(); const controller = new AbortController(); fault.abortClose = controller; const result = await observeGhidraEndpoint(endpoint, { resourceScope: resources, signal: controller.signal, }); expect(result).toMatchObject({ ok: false, error: { kind: "cancelled", cancellation: { reason: null } }, }); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }, ); it.skipIf(process.platform === "win32").each(["released", "failed"] as const)( "preserves endpoint IO failure during cancellation and %s close", async (closing) => { const { endpoint, resources } = await fixture(); const controller = new AbortController(); const cause = Object.assign(new Error("controlled endpoint EIO"), { code: "EIO", }); fault.readFailure = cause; fault.abortRead = controller; fault.blocked = closing === "failed"; const result = await observeGhidraEndpoint(endpoint, { resourceScope: resources, signal: controller.signal, }); if (result.ok) throw new Error("Expected IO failure"); expect(result.error.kind).toBe("start"); expect(result.error.cause).toBe(cause); if (closing === "failed") expect(result.error.cleanupFailure?.cleanupIncomplete).toBe(true); fault.blocked = false; await resources.retryCleanup(); expect(result.error.cleanupFailure?.cleanupIncomplete ?? false).toBe(false); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }, ); it.skipIf(process.platform === "win32")( "keeps endpoint observation admitted until its read and close finish", async () => { const { endpoint, resources } = await fixture(); let entered: (() => void) | undefined; const observed = new Promise((resolve) => { entered = resolve; }); const gate = new Promise((resolve) => { releaseRead = resolve; }); fault.readGate = async () => { entered?.(); await gate; }; const pending = observeGhidraEndpoint(endpoint, { resourceScope: resources, }); operations.push(pending); await observed; let closed = false; const closing = resources.close().then(() => { closed = true; }); operations.push(closing); await new Promise((resolve) => setImmediate(resolve)); expect(closed).toBe(false); releaseRead?.(); await expect(pending).resolves.toMatchObject({ ok: true }); await closing; expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }, ); it .skipIf(process.platform === "win32") .each(["completed", "cancelled"] as const)( "preserves endpoint and provider cleanup owners through %s startup and retry", async (kind) => { const launcher = new GhidraFixtureLauncher(); const controller = new AbortController(); fault.matchEndpoints = true; fault.blocked = true; if (kind === "cancelled") fault.abortClose = controller; const client = new GhidraClient({ launcher, targetPath: ghidraFixturePath, targetSha256: createHash("sha256") .update(await readFile(ghidraFixturePath)) .digest("hex"), transport: "authenticated-loopback-tcp", providerVersion: "12.1.4", profileDigest: "a".repeat(64), startupTimeoutMs: 5000, }); clients.push(client); const result = await client.start(controller.signal); if (result.ok) throw new Error("Expected endpoint finalization failure"); expect(result.error.kind).toBe( kind === "cancelled" ? "cancelled" : "start", ); if (kind === "cancelled") expect(result.error.cancellation?.reason).toBe(null); expect(result.error.cleanupFailure?.cleanupIncomplete).toBe(true); expect(result.error.cleanupFailure?.cleanupResources).toContain( launcher.endpointPaths[0], ); expect(fault.handles.some((handle) => handle.fd !== -1)).toBe(true); fault.blocked = false; fault.abortClose = undefined; await expect(client.close()).resolves.toEqual({ ok: true, value: null }); expect(result.error.cleanupFailure?.cleanupIncomplete).toBe(false); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); const child = launcher.processes[0]; if (child === undefined) throw new Error("Expected admitted fixture process"); expect(await waitForExit(child, 5000)).toBe(true); await expect(access(launcher.runtimeRoots[0] ?? "")).rejects.toMatchObject({ code: "ENOENT", }); }, ); it.skipIf(process.platform === "win32")( "rejects endpoint pathname replacement instead of reporting readiness", async () => { const { endpoint, resources } = await fixture(); fault.readGate = async () => { fault.readGate = undefined; await rename(endpoint.path, endpoint.path + ".old"); await writeFile(endpoint.path, '{"host":"127.0.0.1","port":49153}\n'); }; await expect( observeGhidraEndpoint(endpoint, { resourceScope: resources }), ).resolves.toMatchObject({ ok: false, error: { kind: "start", message: "Ghidra TCP endpoint changed during observation", }, }); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }, ); it.skipIf(process.platform === "win32")( "rejects an oversized endpoint before reading its contents", async () => { const { endpoint, resources } = await fixture(" ".repeat(65537)); await expect( observeGhidraEndpoint(endpoint, { resourceScope: resources }), ).resolves.toMatchObject({ ok: false, error: { kind: "start", cause: { reason: "limit" } }, }); expect(fault.reads).toBe(0); expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true); }, ); it.skipIf(process.platform === "win32")( "rejects a FIFO endpoint through the compiled observer without a writer", async () => { const { endpoint } = await fixture(); await rename(endpoint.path, endpoint.path + ".old"); await promisify(execFile)("mkfifo", [endpoint.path]); const module = pathToFileURL( join(process.cwd(), "dist/ghidra/GhidraTransport.js"), ).href; const script = ` const { observeGhidraEndpoint } = await import(${JSON.stringify(module)}); const result = await observeGhidraEndpoint({ transport: "authenticated-loopback-tcp", path: process.argv[1] }); if (result.ok) throw new Error("Unexpected FIFO readiness"); process.stdout.write(JSON.stringify({ kind: result.error.kind, code: result.error.cause.code })); `; const result = await promisify(execFile)( process.execPath, ["--input-type=module", "--eval", script, endpoint.path], { timeout: 5000 }, ); expect(JSON.parse(result.stdout)).toEqual({ kind: "start", code: "ENOTFILE", }); }, ); it .skipIf(process.platform === "win32") .each([null, "caller stopped", new Error("caller stopped")])( "rejects pre-aborted endpoint admission with exact reason %j", async (reason) => { const { endpoint, resources } = await fixture(); const controller = new AbortController(); controller.abort(reason); const result = await observeGhidraEndpoint(endpoint, { resourceScope: resources, signal: controller.signal, }); if (result.ok) throw new Error("Expected pre-admission cancellation"); expect(result.error.kind).toBe("cancelled"); expect(result.error.cancellation?.reason).toBe(reason); expect(fault.handles).toEqual([]); }, );