Files
reverse-engineering-anything/tests/fixtures/directAnalysisInterrupt.mjs
T
Sravan AvvaruandN0zoM1z0 911152a04d fix(cli): complete cleanup after relayed interrupts (#1202)
* fix: Handle relayed SIGINT gracefully in withProcessCancellation

Changes `process.once` to `process.on` for SIGINT in `DirectAnalysis.ts`.
When REA is launched via npx, a single process-group SIGINT triggers two
SIGINT events (one direct, one relayed). Previously, the first event would
consume the handler and the second would abruptly terminate the Node process,
abandoning Ghidra resource cleanup.

This change ensures subsequent SIGINTs idempotently request cancellation
without crashing the process, matching the behaviour of `withCommandCancellation`.

* fix(cli): keep termination guards through provider cleanup

* test(cli): exercise repeated analysis signals on macOS

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 12:45:23 +08:00

82 lines
2.9 KiB
JavaScript

import { access, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import {
runDirectAnalysis,
runProviderAnalysis,
} from "../../dist/application/DirectAnalysis.js";
import { BinarySession } from "../../dist/application/binary/BinarySession.js";
import { SessionProviderRouter } from "../../dist/application/binary/SessionProviderRouter.js";
import { createAnalysisExecution } from "../../dist/application/AnalysisProvider.js";
import { AnalysisCancelledError } from "../../dist/domain/analysisErrorCore.js";
import { err, ok } from "../../dist/domain/result.js";
const [root, mode] = process.argv.slice(2);
if (!root || !["direct", "native", "managed"].includes(mode))
throw new Error(
"Expected a fixture root and a direct, native, or managed mode",
);
const target = join(root, "target.hop");
await writeFile(target, "fixture");
const client = {
async execute(operation, _arguments, options) {
if (operation === "health")
return ok(
createAnalysisExecution(null, {
id: "fixture",
name: "Fixture",
version: "1",
}),
);
await writeFile(join(root, "executing"), "ready");
await new Promise((resolve) => {
if (options.signal.aborted) resolve();
else options.signal.addEventListener("abort", resolve, { once: true });
});
return err(new AnalysisCancelledError(operation));
},
async close() {
await writeFile(join(root, "cleanup-started"), "ready");
while (true) {
try {
await access(join(root, "release-cleanup"));
break;
} catch (cause) {
if (cause.code !== "ENOENT") throw cause;
}
await delay(5);
}
await writeFile(join(root, "cleanup-complete"), "complete");
},
};
const createSession = () =>
new BinarySession(SessionProviderRouter.single(() => client));
const dependencies = {
createBinarySession: createSession,
createManagedBinarySession: createSession,
};
const interruptListeners = process.listenerCount("SIGINT");
const terminateListeners = process.listenerCount("SIGTERM");
// The production provider owns sockets/process handles; keep this fixture alive
// while its replacement waits only on a signal.
const keepAlive = setInterval(() => {}, 1000);
const result =
mode === "direct"
? await runDirectAnalysis(dependencies, target, "read_bytes", {
address: "0x0",
length: 1,
})
: await runProviderAnalysis(
dependencies,
target,
mode === "managed" ? "inspect_managed_artifact" : "procedure_address",
mode === "managed" ? {} : { procedure: "fixture" },
);
clearInterval(keepAlive);
if (
process.listenerCount("SIGINT") !== interruptListeners ||
process.listenerCount("SIGTERM") !== terminateListeners
)
throw new Error("Analysis leaked process cancellation listeners");
process.stdout.write(JSON.stringify(result));