diff --git a/scripts/verify-real-ghidra-checkpoints.mjs b/scripts/verify-real-ghidra-checkpoints.mjs index 540c870d22..322d08125e 100644 --- a/scripts/verify-real-ghidra-checkpoints.mjs +++ b/scripts/verify-real-ghidra-checkpoints.mjs @@ -7,14 +7,11 @@ import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; import { copyFile, - mkdtemp, mkdir, readFile, readdir, - rm, writeFile, } from "node:fs/promises"; -import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -33,6 +30,7 @@ import { completeVerifierRun, assertVerifierCleanupComplete, } from "./lib/verifier-run.mjs"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; if (process.platform === "win32") throw new Error( @@ -40,7 +38,12 @@ if (process.platform === "win32") ); const run = createVerifierRun(), exec = promisify(execFile); -const root = await mkdtemp(join(tmpdir(), "rea-ghidra-checkpoints-")); +// Ghidra children use the root as HOME and TMPDIR; a plain root admits them on every host. +const workspace = await VerifierFixture.open({ + prefix: "rea-ghidra-checkpoints-", + private: false, +}); +const root = workspace.path; const runtime = join(root, "runtime"), checkpointDirectory = join(root, "checkpoints"); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); @@ -474,7 +477,7 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf } if (cleanupVerified) { try { - await rm(root, { recursive: true, force: true }); + await workspace.release(); } catch (cause) { report.cleanup_error = String(cause); report.retained_workspace = root; diff --git a/scripts/verify-real-ghidra-function-bundle.mjs b/scripts/verify-real-ghidra-function-bundle.mjs index ebc3d7bb48..a1ce060f3a 100644 --- a/scripts/verify-real-ghidra-function-bundle.mjs +++ b/scripts/verify-real-ghidra-function-bundle.mjs @@ -4,28 +4,27 @@ // is intentionally synthetic; it is not a claim about Ghidra's analysis history. import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { - mkdtemp, - mkdir, - readFile, - readdir, - rm, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; -import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; const installation = process.env.GHIDRA_INSTALL_DIR; if (!installation || !process.env.JAVA_HOME) throw new Error( "Set GHIDRA_INSTALL_DIR and JAVA_HOME to caller-installed Ghidra and JDK 21.", ); const commands = await createVerifierCommands(); -let root; -let commandsClosed = false; -const targetHash = await withVerifierCleanup(async () => { - root = await mkdtemp(join(tmpdir(), "rea-function-bundle-")); +const workspace = await VerifierFixture.open({ + prefix: "rea-function-bundle-", + private: false, +}); +// Ghidra and the CLI run as owned commands that may use the root until they close. +workspace.releaseAfter(() => commands.close()); +const targetHash = await withVerifierFixture(workspace, async () => { + const root = workspace.path; const target = join(root, "fixture"); const projectDir = join(root, "project"); const scriptDir = join(root, "scripts"); @@ -172,16 +171,7 @@ public class SetHistory extends GhidraScript { assert.equal(hash(await readFile(target)), targetHash); assert.deepEqual(await tree(projectDir), before); return targetHash; -}, [ - async () => { - await commands.close(); - commandsClosed = true; - }, - async () => { - if (commandsClosed && root !== undefined) - await rm(root, { recursive: true, force: true }); - }, -]); +}); process.stdout.write( `PASS Ghidra function bundles: complete, summary, no-overwrite; target and project unchanged (${targetHash})\n`, ); diff --git a/scripts/verify-real-ghidra-project-coverage.mjs b/scripts/verify-real-ghidra-project-coverage.mjs index 3b7093d15b..461b562c83 100644 --- a/scripts/verify-real-ghidra-project-coverage.mjs +++ b/scripts/verify-real-ghidra-project-coverage.mjs @@ -2,16 +2,8 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { - mkdir, - mkdtemp, - readFile, - readdir, - rm, - writeFile, -} from "node:fs/promises"; +import { mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import { realpathSync } from "node:fs"; -import { tmpdir } from "node:os"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -24,6 +16,10 @@ import { createAnalysisProfile } from "../dist/domain/analysisProfile.js"; import { createEvidenceBundle } from "../dist/domain/evidenceBundle.js"; import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; +import { + VerifierFixture, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; import { createVerifierMcpOwner, VerifierStdioTransport, @@ -56,10 +52,14 @@ export async function verifyProjectCoverage(run) { new URL("../tests/conformance/ghidra/", import.meta.url), ); const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url)); - const workspace = await mkdtemp( - join(tmpdir(), "rea-ghidra-project-coverage-"), - ); + const fixture = await VerifierFixture.open({ + prefix: "rea-ghidra-project-coverage-", + private: false, + }); + const workspace = fixture.path; const connections = createVerifierMcpOwner(); + // The workspace is removed only after every MCP connection confirms. + fixture.releaseAfter(() => connections.close()); const target = join(workspace, "fixture"); const projects = join(workspace, "projects"); const env = { @@ -105,7 +105,6 @@ export async function verifyProjectCoverage(run) { ); return requireMcpOutput(reply, name); } - let commandCleanupIncomplete = false; const command = async (executable, args, timeoutMs) => { try { return await runOwnedCommand( @@ -119,13 +118,19 @@ export async function verifyProjectCoverage(run) { { timeoutMs, diagnosticBytes: 16 * 1024 * 1024 }, ); } catch (cause) { - commandCleanupIncomplete = + // A command whose process group was not confirmed stopped may still + // use the workspace, so the fixture keeps it. + if ( cause instanceof OwnedCommandFailure && - cause.cleanupOwner !== undefined; + cause.cleanupOwner !== undefined + ) + fixture.retain( + `${executable} cleanup is incomplete; it may still use ${workspace}`, + ); throw cause; } }; - const result = await withVerifierCleanup(async () => { + return await withVerifierFixture(fixture, async () => { await mkdir(projects); // REA_CC is the invoking developer's explicitly selected compiler, not target input. await command( @@ -400,14 +405,7 @@ export async function verifyProjectCoverage(run) { provider: installation.providerVersion, results: report, }; - }, [ - async () => { - await connections.close(); - if (!commandCleanupIncomplete) - await rm(workspace, { recursive: true, force: true }); - }, - ]); - return result; + }); } if ( diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs index 15323f9a1c..e7de4dd322 100644 --- a/scripts/verify-real-ghidra.mjs +++ b/scripts/verify-real-ghidra.mjs @@ -3,8 +3,7 @@ import { verifyProjectCoverage } from "./verify-real-ghidra-project-coverage.mjs"; import { execFile } from "node:child_process"; -import { mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { writeFile } from "node:fs/promises"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -33,6 +32,11 @@ import { verifyInventoryOperations, } from "./verify-real-ghidra-inventory.mjs"; import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs"; +import { + VerifierFixture, + onceConfirmed, + withVerifierFixture, +} from "./lib/verifier-fixture.mjs"; import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs"; import { verifyGhidraRelocationSymbols } from "./lib/real-ghidra-relocation-symbols.mjs"; @@ -75,7 +79,12 @@ const expectedNativeTarget = nativeFixtureTarget( process.platform, process.arch, ); -const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-ghidra-fixtures-")); +// Ghidra children read and write under the root; a plain root admits them on every host. +const fixtures = await VerifierFixture.open({ + prefix: "rea-ghidra-fixtures-", + private: false, +}); +const fixtureRoot = fixtures.path; const sourcePath = fileURLToPath( new URL("../tests/conformance/ghidra/inventory.c", import.meta.url), ); @@ -123,7 +132,7 @@ for (const [command, role] of toolchain) { ); } } -try { +await withVerifierFixture(fixtures, async () => { const common = ["-O0", "-g", "-fno-inline"]; if (expectedNativeTarget.format === "elf") common.push("-fno-pie", "-no-pie"); await exec(compiler, [...common, sourcePath, "-o", debugPath]); @@ -351,9 +360,7 @@ try { custom_target: custom === null ? null : summary(custom), cleanup: "complete", }; -} finally { - await rm(fixtureRoot, { recursive: true, force: true }); -} +}); process.stdout.write( `${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`, ); @@ -521,6 +528,14 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) { profileDigest: profile.value.profile.digest, }); + // The fixture root is removed only after this client confirms it closed; + // a failed close is retried by the fixture's release. + const closeClient = fixtures.releaseAfter( + onceConfirmed(async () => { + const closed = await client.close(); + if (!closed.ok) throw closed.error; + }), + ); let runtimeCoordinates; let primaryFailure; try { @@ -615,14 +630,16 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) { primaryFailure = cause; throw cause; } finally { - const closed = await client.close(); - if (!closed.ok) + try { + await closeClient(); + } catch (closeFailure) { throw primaryFailure === undefined - ? closed.error + ? closeFailure : new AggregateError( - [primaryFailure, closed.error], + [primaryFailure, closeFailure], "Ghidra verification and cleanup failed", ); + } if (runtimeCoordinates !== undefined) await assertCleanup(runtimeCoordinates); } diff --git a/scripts/verify-real-kuna.mjs b/scripts/verify-real-kuna.mjs index 77da367af7..9e9e283b95 100644 --- a/scripts/verify-real-kuna.mjs +++ b/scripts/verify-real-kuna.mjs @@ -10,16 +10,7 @@ import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; -import { - mkdir, - mkdtemp, - readFile, - readdir, - rm, - stat, - writeFile, -} from "node:fs/promises"; -import { tmpdir } from "node:os"; +import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { parseArgs, promisify } from "node:util"; @@ -52,6 +43,7 @@ import { completeVerifierRun, createVerifierRun, } from "./lib/verifier-run.mjs"; +import { VerifierFixture } from "./lib/verifier-fixture.mjs"; const exec = promisify(execFile); const { values } = parseArgs({ @@ -94,7 +86,12 @@ const resourceScope = { }), run: (operation) => inspectionResources.run(operation), }; -const workspace = await mkdtemp(join(tmpdir(), "rea-kuna-lane-")); +// Kuna children cache under the root; a plain root admits them on every host. +const workspaceFixture = await VerifierFixture.open({ + prefix: "rea-kuna-lane-", + private: false, +}); +const workspace = workspaceFixture.path; const cacheRoot = join(workspace, "cache"); // A server retains its workspace until both provider and transport cleanup // have positively completed, even if later lineage observation is empty. @@ -844,7 +841,7 @@ try { } if (cleanupComplete) try { - await rm(workspace, { recursive: true, force: true }); + await workspaceFixture.release(); } catch (cause) { cleanupComplete = false; failures.push(cause); diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index b88b0607f8..bfff3749d4 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -407,11 +407,6 @@ "scripts/verify-pe-load-config.mjs": 1, "scripts/verify-published-package.mjs": 1, "scripts/verify-real-cutter.mjs": 1, - "scripts/verify-real-ghidra-checkpoints.mjs": 1, - "scripts/verify-real-ghidra-function-bundle.mjs": 1, - "scripts/verify-real-ghidra-project-coverage.mjs": 1, - "scripts/verify-real-ghidra.mjs": 1, - "scripts/verify-real-kuna.mjs": 1, "scripts/verify-release-artifact.mjs": 1, "scripts/verify-release-exposure.mjs": 1, "scripts/verify-test-discovery.mjs": 1,