diff --git a/scripts/lib/ghidra-installation-inspection.d.mts b/scripts/lib/ghidra-installation-inspection.d.mts new file mode 100644 index 000000000..18e2c1bac --- /dev/null +++ b/scripts/lib/ghidra-installation-inspection.d.mts @@ -0,0 +1,11 @@ +import type { + GhidraInstallationHost, + GhidraInstallationInspection, + GhidraInstallationOptions, +} from "../../src/ghidra/GhidraInstallation.js"; + +/** Inspect prerequisites with a real owner, including cleanup before completion. */ +export function inspectVerifierGhidraInstallation( + options: Omit, + host?: GhidraInstallationHost, +): Promise; diff --git a/scripts/lib/ghidra-installation-inspection.mjs b/scripts/lib/ghidra-installation-inspection.mjs new file mode 100644 index 000000000..ad0b0bf62 --- /dev/null +++ b/scripts/lib/ghidra-installation-inspection.mjs @@ -0,0 +1,28 @@ +import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js"; +import { AnalysisCleanupLease } from "../../dist/artifacts/AnalysisCleanupLease.js"; +import { inspectGhidraInstallation } from "../../dist/ghidra/GhidraInstallation.js"; +import { closeVerifierResources } from "./verifier-cleanup.mjs"; + +/** Complete one prerequisite inspection and release its exact retained owners. */ +export async function inspectVerifierGhidraInstallation(options, host) { + const resources = new ArtifactResourceScope(); + const resourceOwner = { + retain(owner, resource) { + resources.retain({ + kind: "lease", + resource, + lease: AnalysisCleanupLease.forCloseOwner(owner, resource), + }); + }, + run: (operation) => resources.run(operation), + }; + let primaryError; + try { + return await inspectGhidraInstallation({ ...options, resourceOwner }, host); + } catch (cause) { + primaryError = cause; + throw cause; + } finally { + await closeVerifierResources([() => resources.close()], primaryError); + } +} diff --git a/scripts/lib/real-ghidra-namespace-annotations.mjs b/scripts/lib/real-ghidra-namespace-annotations.mjs index c65c02e87..6966e77ad 100644 --- a/scripts/lib/real-ghidra-namespace-annotations.mjs +++ b/scripts/lib/real-ghidra-namespace-annotations.mjs @@ -7,7 +7,7 @@ import { tmpdir } from "node:os"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; -import { inspectGhidraInstallation } from "../../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./ghidra-installation-inspection.mjs"; /** Exercise qualified annotation names against real demangled C++ ABI symbols. */ export async function verifyGhidraNamespaceAnnotations({ @@ -353,8 +353,8 @@ async function verifyAddressNames(call, symbols, kind) { } async function requireNamespaceDemangler(env) { - const inspection = inspectGhidraInstallation({ - environment: process.env, + const inspection = await inspectVerifierGhidraInstallation({ + environment: env, installDir: env.GHIDRA_INSTALL_DIR, ...(env.JAVA_HOME === undefined ? {} : { javaHome: env.JAVA_HOME }), }); diff --git a/scripts/verify-real-ghidra-com.mjs b/scripts/verify-real-ghidra-com.mjs index 5fabc1b8b..8a82331d8 100644 --- a/scripts/verify-real-ghidra-com.mjs +++ b/scripts/verify-real-ghidra-com.mjs @@ -12,7 +12,7 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseEvidence } from "../dist/domain/evidence.js"; import { functionDossierSchema } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildDosComFixture } from "../tests/conformance/ghidra/dos-com-fixture.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; import { verifyGhidraAddressBoundaries } from "./lib/real-ghidra-address-boundaries.mjs"; @@ -28,7 +28,7 @@ if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error( "verify:ghidra:com prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).", ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-dos.mjs b/scripts/verify-real-ghidra-dos.mjs index b212a018d..6743e518d 100644 --- a/scripts/verify-real-ghidra-dos.mjs +++ b/scripts/verify-real-ghidra-dos.mjs @@ -22,7 +22,7 @@ import { functionBodySchema, functionDossierSchema, } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildDosMzFixture } from "../tests/conformance/ghidra/dos-mz-fixture.mjs"; import { assertVerifierCleanupComplete, @@ -42,7 +42,7 @@ if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error( "verify:ghidra:dos prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).", ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-mips-relocatable.mjs b/scripts/verify-real-ghidra-mips-relocatable.mjs index 700b93b8d..5ce1704e2 100755 --- a/scripts/verify-real-ghidra-mips-relocatable.mjs +++ b/scripts/verify-real-ghidra-mips-relocatable.mjs @@ -13,7 +13,7 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { parseEvidence } from "../dist/domain/evidence.js"; import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js"; import { functionDossierSchema } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildMipsRelocatableFixture } from "../tests/conformance/ghidra/mips-fixture.mjs"; import { assertMipsRelocations, @@ -34,7 +34,7 @@ if (!["linux", "darwin"].includes(process.platform)) ); if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR"); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-mips.mjs b/scripts/verify-real-ghidra-mips.mjs index 350ee3b90..2edecf4f5 100644 --- a/scripts/verify-real-ghidra-mips.mjs +++ b/scripts/verify-real-ghidra-mips.mjs @@ -22,7 +22,7 @@ import { import { parseEvidence } from "../dist/domain/evidence.js"; import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js"; import { functionDossierSchema } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildMipsFixture } from "../tests/conformance/ghidra/mips-fixture.mjs"; import { buildPspFixture, @@ -50,7 +50,7 @@ if (!["linux", "darwin"].includes(process.platform)) ); if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR"); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-noreturn.mjs b/scripts/verify-real-ghidra-noreturn.mjs index 884bcadd9..95c354a98 100644 --- a/scripts/verify-real-ghidra-noreturn.mjs +++ b/scripts/verify-real-ghidra-noreturn.mjs @@ -10,7 +10,7 @@ import { promisify } from "node:util"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { parseConfig } from "../dist/config/parseConfig.js"; import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; import { GhidraProvider } from "../dist/ghidra/GhidraProvider.js"; import { silentLogger } from "../dist/logger.js"; @@ -35,7 +35,7 @@ assert.ok( ); const config = parseConfig(process.env); if (!config.ok) throw config.error; -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: config.value.ghidraInstallDir, javaHome: config.value.ghidraJavaHome, diff --git a/scripts/verify-real-ghidra-ppc.mjs b/scripts/verify-real-ghidra-ppc.mjs index 591d32b2b..84313625d 100644 --- a/scripts/verify-real-ghidra-ppc.mjs +++ b/scripts/verify-real-ghidra-ppc.mjs @@ -19,7 +19,7 @@ import { import { parseEvidence } from "../dist/domain/evidence.js"; import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js"; import { functionDossierSchema } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildPowerPcFixture } from "../tests/conformance/ghidra/ppc-fixture.mjs"; import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; @@ -33,7 +33,7 @@ if (!["linux", "darwin"].includes(process.platform)) ); if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error("Missing prerequisite: caller-supplied GHIDRA_INSTALL_DIR"); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-raw.mjs b/scripts/verify-real-ghidra-raw.mjs index 4341b5cfd..1db0f8cf9 100644 --- a/scripts/verify-real-ghidra-raw.mjs +++ b/scripts/verify-real-ghidra-raw.mjs @@ -11,7 +11,7 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseEvidence } from "../dist/domain/evidence.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; @@ -25,7 +25,7 @@ if (process.env.GHIDRA_INSTALL_DIR === undefined) throw new Error( "Set GHIDRA_INSTALL_DIR to a bring-your-own Ghidra installation.", ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-riscv.mjs b/scripts/verify-real-ghidra-riscv.mjs index a87adeeb5..b0c036b98 100644 --- a/scripts/verify-real-ghidra-riscv.mjs +++ b/scripts/verify-real-ghidra-riscv.mjs @@ -22,7 +22,7 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { parseAnalysisSnapshot } from "../dist/domain/analysisSnapshot.js"; import { parseEvidence } from "../dist/domain/evidence.js"; import { functionDossierSchema } from "../dist/domain/hopperValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { buildRiscvFixture } from "../tests/conformance/ghidra/riscv-fixture.mjs"; import { mcpTextValue, @@ -42,7 +42,7 @@ for (const key of ["REA_GHIDRA_LANGUAGE_ID", "REA_GHIDRA_COMPILER_SPEC_ID"]) throw new Error( `Unset ${key}: this lane verifies provider-pinned selection.`, ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-swift.mjs b/scripts/verify-real-ghidra-swift.mjs index 5cdf04427..ecb823950 100644 --- a/scripts/verify-real-ghidra-swift.mjs +++ b/scripts/verify-real-ghidra-swift.mjs @@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { cleanupOwnedProcessGroup } from "../dist/process/ProcessOwnership.js"; import { ProviderProcessSupervisor, @@ -27,7 +27,7 @@ import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; const execute = promisify(execFile); const run = createVerifierRun(); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, javaHome: process.env.JAVA_HOME, diff --git a/scripts/verify-real-ghidra-switch.mjs b/scripts/verify-real-ghidra-switch.mjs index 624c14a42..5d43c2bc4 100644 --- a/scripts/verify-real-ghidra-switch.mjs +++ b/scripts/verify-real-ghidra-switch.mjs @@ -68,13 +68,13 @@ for (const [role, command] of Object.entries(commands)) { } let installation; if (!compilerOnly) { - const { inspectGhidraInstallation } = - await import("../dist/ghidra/GhidraInstallation.js"); + const { inspectVerifierGhidraInstallation } = + await import("./lib/ghidra-installation-inspection.mjs"); assert.ok( process.env.GHIDRA_INSTALL_DIR, "Ghidra switch lane prerequisite missing: GHIDRA_INSTALL_DIR (bring your own)", ); - installation = inspectGhidraInstallation({ + installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir: process.env.GHIDRA_INSTALL_DIR, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs index f34f32bfa..fa297fd71 100644 --- a/scripts/verify-real-ghidra-windows.mjs +++ b/scripts/verify-real-ghidra-windows.mjs @@ -18,7 +18,7 @@ import { parseGhidraInventoryInput, parseGhidraInventoryResult, } from "../dist/ghidra/GhidraInventoryValues.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js"; import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js"; @@ -59,7 +59,7 @@ if (installDir === undefined || !isAbsolute(installDir)) throw new Error( "Set GHIDRA_INSTALL_DIR to the absolute root of Ghidra 12.1.4.", ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir, ...(process.env.JAVA_HOME === undefined diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs index 74c0197b2..612e4f9f1 100644 --- a/scripts/verify-real-ghidra.mjs +++ b/scripts/verify-real-ghidra.mjs @@ -10,7 +10,7 @@ import { promisify } from "node:util"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js"; import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; -import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js"; +import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js"; import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js"; @@ -49,7 +49,7 @@ if (installDir === undefined || !isAbsolute(installDir)) throw new Error( "Set GHIDRA_INSTALL_DIR to the absolute root of an extracted Ghidra 12.1.4 release.", ); -const installation = inspectGhidraInstallation({ +const installation = await inspectVerifierGhidraInstallation({ environment: process.env, installDir, ...(process.env.JAVA_HOME === undefined diff --git a/tests/boundary/process/verifierFileResources.test.ts b/tests/boundary/process/verifierFileResources.test.ts index 366774bfb..a87824e27 100644 --- a/tests/boundary/process/verifierFileResources.test.ts +++ b/tests/boundary/process/verifierFileResources.test.ts @@ -1,9 +1,11 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; -import { readFile, writeFile } from "node:fs/promises"; +import { readFile, readdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; +import ts from "typescript6"; +import type { inspectVerifierGhidraInstallation } from "../../../scripts/lib/ghidra-installation-inspection.mjs"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; @@ -84,3 +86,178 @@ it("hashes and closes a clean verifier input", async () => { .digest("hex"), }); }); + +it("Ghidra verifier probes await the shared owned inspection contract", async () => { + const names = (await readdir("scripts")).filter( + (name) => name.startsWith("verify-real-ghidra") && name.endsWith(".mjs"), + ); + const paths = [ + ...names.map((name) => `scripts/${name}`), + "scripts/lib/real-ghidra-namespace-annotations.mjs", + ]; + const probes: string[] = []; + const violations: { path: string; reason: string }[] = []; + for (const path of paths) { + const source = ts.createSourceFile( + path, + await readFile(path, "utf8"), + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.JS, + ); + const visit = (node: ts.Node): void => { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + [ + "inspectGhidraInstallation", + "inspectVerifierGhidraInstallation", + ].includes(node.expression.text) + ) { + probes.push(path); + if (!ts.isAwaitExpression(node.parent)) + violations.push({ + path, + reason: "installation result was not awaited", + }); + if (node.expression.text !== "inspectVerifierGhidraInstallation") + violations.push({ + path, + reason: "inspection has no shared cleanup owner", + }); + } + ts.forEachChild(node, visit); + }; + visit(source); + } + expect(probes.length).toBeGreaterThan(0); + expect(violations).toEqual([]); +}); + +it.each(["late-close", "retry-close", "retained-close", "opaque-cancellation"])( + "owns Ghidra verifier prerequisite finalization: %s", + async (mode) => { + const root = await createTestTempDirectory("rea-verifier-ghidra-owner-"); + const options = { + environment: {}, + installDir: root, + platform: process.platform, + architecture: "x64", + } satisfies Parameters[0]; + const script = String.raw` + import assert from 'node:assert/strict'; + import { mkdir, open, writeFile } from 'node:fs/promises'; + import { join } from 'node:path'; + import { inspectVerifierGhidraInstallation } from ${JSON.stringify(new URL("../../../scripts/lib/ghidra-installation-inspection.mjs", import.meta.url).href)}; + import { systemGhidraInstallationHost } from ${JSON.stringify(new URL("../../../dist/ghidra/GhidraInstallation.js", import.meta.url).href)}; + import { SUPPORTED_GHIDRA_VERSION, SUPPORTED_GHIDRA_JAVA_MAJOR } from ${JSON.stringify(new URL("../../../dist/ghidra/GhidraReleasePolicy.js", import.meta.url).href)}; + import { OwnedFileHandle } from ${JSON.stringify(new URL("../../../dist/filesystem/OwnedFileHandle.js", import.meta.url).href)}; + import { RegularFileCleanupFailure } from ${JSON.stringify(new URL("../../../dist/filesystem/OwnedRegularFile.js", import.meta.url).href)}; + import { ArtifactResourceRetryFailure } from ${JSON.stringify(new URL("../../../dist/artifacts/ArtifactResourceScope.js", import.meta.url).href)}; + const root = process.argv[1], mode = process.argv[2], options = JSON.parse(process.argv[3]); + await mkdir(join(root, 'Ghidra')); + await writeFile(join(root, 'Ghidra', 'application.properties'), 'application.version=' + SUPPORTED_GHIDRA_VERSION + '\napplication.java.min=' + SUPPORTED_GHIDRA_JAVA_MAJOR + '\n'); + const controller = new AbortController(); + let foreign; + if (mode === 'opaque-cancellation') { + const path = join(root, 'foreign'); await writeFile(path, 'foreign'); + foreign = new OwnedFileHandle(await open(path, 'r')); + controller.abort(new RegularFileCleanupFailure(path, foreign, { kind: 'completed', value: 'opaque' }, new Error('not this inspection owner'))); + } + let entered, release; + const entering = new Promise(resolve => { entered = resolve; }); + const finishing = new Promise(resolve => { release = resolve; }); + const handles = new Set(); + let attempts = 0, denied = true; + const originalClose = OwnedFileHandle.prototype.close; + OwnedFileHandle.prototype.close = async function () { + handles.add(this.handle); attempts++; + if (mode === 'late-close' && attempts === 1) { entered(); await finishing; } + if ((mode === 'retry-close' && attempts === 1) || (mode === 'retained-close' && denied)) throw new Error('controlled inspection close refusal'); + return originalClose.call(this); + }; + const host = { + ...systemGhidraInstallationHost(), + executable: () => true, + executableHeader: () => ({ ok: true, value: undefined }), + probeJava: async () => ({ ok: true, value: { major: SUPPORTED_GHIDRA_JAVA_MAJOR, version: '21.0.11', home: '/fixture/jdk', bits: 64, runtime: 'jdk', osArch: 'amd64', osName: options.platform === 'win32' ? 'Windows' : options.platform === 'darwin' ? 'Mac OS X' : 'Linux' } }), + }; + let settled = false; + const pending = inspectVerifierGhidraInstallation({ ...options, signal: controller.signal }, host).then( + value => { settled = true; return { value }; }, + error => { settled = true; return { error }; }, + ); + try { + if (mode === 'late-close') { await entering; assert.equal(settled, false); release(); } + const outcome = await pending; + if (mode === 'late-close') { assert.equal(outcome.value?.status, 'available'); assert.equal(handles.size, 1); } + else if (mode === 'retry-close') { + assert.ok(outcome.error); assert.equal(outcome.error.cleanupIncomplete, false); + assert.ok(attempts >= 2); assert.equal(handles.size, 1); + } else if (mode === 'retained-close') { + assert.ok(outcome.error instanceof AggregateError); + const primary = outcome.error.errors.find(error => error.cleanupIncomplete === true); + const cleanup = outcome.error.errors.find(error => error instanceof ArtifactResourceRetryFailure); + assert.ok(primary); assert.ok(cleanup); assert.ok(cleanup.cleanupLeases.length > 0); + denied = false; + for (const lease of cleanup.cleanupLeases) await lease.release(); + assert.equal(primary.cleanupIncomplete, false); + assert.equal(handles.size, 1); + } else { + assert.equal(outcome.error?._tag, 'AnalysisCancelledError'); + assert.equal(outcome.error.cause, controller.signal.reason); + assert.equal(handles.size, 0); assert.ok(foreign.handle.fd >= 0); + } + for (const handle of handles) assert.equal(handle.fd, -1); + console.log(JSON.stringify({ mode, verified: true })); + } finally { + denied = false; release(); + await pending; + OwnedFileHandle.prototype.close = originalClose; + for (const handle of handles) await handle.close(); + await foreign?.close(); + } + `; + const result = spawnSync( + process.execPath, + [ + "--input-type=module", + "-e", + script, + root, + mode, + JSON.stringify(options), + ], + { encoding: "utf8", timeout: 10_000 }, + ); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual({ mode, verified: true }); + }, +); + +it("Ghidra verifier awaits native prerequisite rejection before creating target fixtures", async () => { + const root = await createTestTempDirectory( + "rea-verifier-ghidra-prerequisite-", + ); + const result = spawnSync( + process.execPath, + ["scripts/verify-real-ghidra.mjs"], + { + encoding: "utf8", + timeout: 10_000, + env: { + PATH: root, + HOME: root, + USERPROFILE: root, + GHIDRA_INSTALL_DIR: join(root, "absent-installation"), + JAVA_HOME: join(root, "absent-jdk"), + }, + }, + ); + expect(result.status, result.stderr).toBe(1); + expect(result.stderr).toContain( + 'Ghidra installation is unavailable: {"status":"unavailable"', + ); + expect(result.stderr).not.toContain("TypeError"); + expect(result.stderr).not.toContain("Ghidra installation is unavailable: {}"); +});