mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
fix(cli): require cancellation through command finalization
Adapt the forwarding proposal in #4652 with a required signal before optional resource diagnostics. Migrate all callers and cover actual command registration during final close. Refs #4939. Co-authored-by: Tapraj-Pathak <165466587+Tapraj-Pathak@users.noreply.github.com>
This commit is contained in:
1 parent
2ae4e1608f
commit
279c590ef6
30 files changed
+154
-20
No files matched your search
@@ -29,6 +29,7 @@ export const registerAdbCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"adb",
|
||||
operation,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ export const registerAndroidCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"jadx",
|
||||
operation,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ export const registerApktoolCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"apktool",
|
||||
operation,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
pe,
|
||||
"rea-pe-resources",
|
||||
"inspect_pe_resources",
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -88,7 +88,6 @@ const registerWebMcp = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"discover_webmcp_tools",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -200,7 +199,6 @@ const registerScreenshot = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"capture_web_screenshot",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -75,7 +75,6 @@ const registerTargetList = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"list_browser_targets",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -132,7 +131,6 @@ const registerPageInspection = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"inspect_web_page",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -191,7 +189,6 @@ const registerBundleAnalysis = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"analyze_web_bundle",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -243,7 +240,6 @@ const registerObservationSession = (
|
||||
},
|
||||
context.provider.identity().id,
|
||||
"observe_web_session",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -60,6 +60,7 @@ export const registerCaptureCommands = (
|
||||
service,
|
||||
"tshark-capture",
|
||||
"inspect_packet_fields",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -112,6 +113,7 @@ export const registerCaptureCommands = (
|
||||
service,
|
||||
"tshark-capture",
|
||||
"inspect_packet_capture",
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -123,7 +123,6 @@ export const registerCutterCommands = (
|
||||
cutter,
|
||||
"cutter",
|
||||
"cutter_command",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -64,7 +64,6 @@ const registerElectronFuseCommand = (
|
||||
service,
|
||||
service.provider.identity.id,
|
||||
"inspect_electron_fuses",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -134,7 +133,6 @@ const registerElectronActiveCommand = (
|
||||
},
|
||||
provider.identity().id,
|
||||
"capture_electron_scenario",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
return "code" in result
|
||||
@@ -217,7 +215,6 @@ const registerElectronTargetList = (
|
||||
},
|
||||
"rea-cdp-electron",
|
||||
"list_electron_targets",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -259,7 +256,6 @@ const registerElectronPageInspection = (
|
||||
},
|
||||
"rea-cdp-electron",
|
||||
"inspect_electron_page",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -37,6 +37,7 @@ export const registerEvmCommands = (
|
||||
service,
|
||||
"evmole-interface",
|
||||
"inspect_evm_interface",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -18,8 +18,8 @@ export const finishAnalysisCommand = async <
|
||||
service: { close(): Promise<void> },
|
||||
providerId: string,
|
||||
canonicalOperation: string,
|
||||
signal: AbortSignal,
|
||||
fallbackResource?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Value | ReturnType<typeof projectAnalysisError>> => {
|
||||
let result: Result<Value, AnalysisError>;
|
||||
try {
|
||||
@@ -69,7 +69,7 @@ export const finishAnalysisCommand = async <
|
||||
),
|
||||
);
|
||||
}
|
||||
if (result.ok && signal?.aborted === true)
|
||||
if (result.ok && signal.aborted)
|
||||
return projectAnalysisError(
|
||||
new AnalysisCancelledError(canonicalOperation, {
|
||||
cause: signal.reason,
|
||||
|
||||
@@ -35,7 +35,6 @@ export const registerFirmwareCommands = (
|
||||
service,
|
||||
operation === "inspect_firmware_regions" ? "binwalk" : "unblob",
|
||||
operation,
|
||||
undefined,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -84,7 +84,6 @@ export const registerFlutterCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"flutter",
|
||||
spec.operation,
|
||||
undefined,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -32,6 +32,7 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
service,
|
||||
"go-build-info",
|
||||
"inspect_go_binary",
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -70,6 +70,7 @@ export const registerJavaScriptRecoveryCommands = (
|
||||
service,
|
||||
"wakaru",
|
||||
"recover_javascript_sources",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -26,6 +26,7 @@ export const registerJebCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"jeb",
|
||||
operation,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ export const registerPcapCommands = (
|
||||
{ close: () => provider.close() },
|
||||
"pcap",
|
||||
operation,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -32,7 +32,6 @@ export const registerRustCommands = (
|
||||
service,
|
||||
"rust-auditable",
|
||||
"inspect_rust_binary",
|
||||
undefined,
|
||||
signal,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -62,6 +62,7 @@ export const registerSignalCommands = (
|
||||
service,
|
||||
"sigrok-recording",
|
||||
"inspect_signal_decoders",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -114,6 +115,7 @@ export const registerSignalCommands = (
|
||||
service,
|
||||
"sigrok-recording",
|
||||
"decode_signal_recording",
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -73,6 +73,7 @@ export const registerSqliteCommands = (
|
||||
service,
|
||||
"sqlite",
|
||||
"inspect_sqlite_database",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -76,6 +76,7 @@ export const registerWasmCommands = (
|
||||
service,
|
||||
"wabt-artifact",
|
||||
"inspect_wasm_artifact",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -66,6 +66,7 @@ export const registerWebModuleCommands = (
|
||||
service,
|
||||
"web-module-trace",
|
||||
"trace_web_module_imports",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -59,6 +59,7 @@ export const registerWebNetworkCaptureCommands = (
|
||||
service,
|
||||
"web-network-capture",
|
||||
"inspect_web_network_capture",
|
||||
signal,
|
||||
"Web network capture resources",
|
||||
);
|
||||
},
|
||||
|
||||
@@ -59,7 +59,6 @@ export const registerWebRuntimeCommands = (
|
||||
},
|
||||
service.provider.identity().id,
|
||||
"observe_web_execution",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
@@ -100,7 +99,6 @@ export const registerWebRuntimeCommands = (
|
||||
},
|
||||
service.provider.identity().id,
|
||||
"inspect_web_event_listeners",
|
||||
undefined,
|
||||
signal,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -54,6 +54,7 @@ export const registerWebSourceLocationCommands = (
|
||||
service,
|
||||
"web-source-location",
|
||||
"trace_web_source_location",
|
||||
signal,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Cli } from "incur";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
|
||||
import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js";
|
||||
import { registerWasmCommands } from "../../../src/cli/wasmCommands.js";
|
||||
import { wasmArtifactModule } from "../../../src/composition/wasm.js";
|
||||
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import {
|
||||
wasmArtifactExecution,
|
||||
wasmArtifactFixture,
|
||||
} from "../../fixtures/wasm/artifact.js";
|
||||
|
||||
const host = vi.hoisted(() => ({
|
||||
listeners: new Map<string, () => void>(),
|
||||
exitCode: undefined as number | undefined,
|
||||
on(signal: string, listener: () => void) {
|
||||
this.listeners.set(signal, listener);
|
||||
},
|
||||
off(signal: string, listener: () => void) {
|
||||
if (this.listeners.get(signal) === listener) this.listeners.delete(signal);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/cli/commandCancellation.js", async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<
|
||||
typeof import("../../../src/cli/commandCancellation.js")
|
||||
>();
|
||||
return {
|
||||
...original,
|
||||
withCommandCancellation: <Value>(
|
||||
operation: (signal: AbortSignal) => Promise<Value>,
|
||||
) => original.withCommandCancellation(operation, host),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
host.listeners.clear();
|
||||
host.exitCode = undefined;
|
||||
});
|
||||
|
||||
it.each([
|
||||
"control",
|
||||
"interrupt",
|
||||
"primary-failure",
|
||||
"cleanup-failure",
|
||||
"interrupt-cleanup-failure",
|
||||
] as const)(
|
||||
"finishes the registered WASM command through final close: %s",
|
||||
async (scenario) => {
|
||||
const value = wasmArtifactFixture();
|
||||
const primary = new AnalysisOutputError(
|
||||
"inspect_wasm_artifact",
|
||||
"primary reply failure",
|
||||
);
|
||||
const cleanup = new ProviderCleanupError(
|
||||
"wabt-artifact",
|
||||
["exact-owned-root"],
|
||||
{ reason: "close refused" },
|
||||
{ operation: "inspect_wasm_artifact" },
|
||||
);
|
||||
let closed = false;
|
||||
const service = new WasmArtifactService({
|
||||
inspect: async () =>
|
||||
scenario === "primary-failure"
|
||||
? err(primary)
|
||||
: ok(wasmArtifactExecution(value)),
|
||||
close: async () => {
|
||||
await Promise.resolve();
|
||||
if (scenario !== "control" && scenario !== "cleanup-failure")
|
||||
host.listeners.get("SIGINT")?.();
|
||||
closed = true;
|
||||
if (
|
||||
scenario === "cleanup-failure" ||
|
||||
scenario === "interrupt-cleanup-failure"
|
||||
)
|
||||
throw cleanup;
|
||||
},
|
||||
});
|
||||
vi.spyOn(wasmArtifactModule, "create").mockReturnValue(service);
|
||||
const cli = Cli.create("finalization-test", { sync: false });
|
||||
registerWasmCommands(cli, silentLogger, {});
|
||||
let output = "";
|
||||
const previousExitCode = process.exitCode;
|
||||
try {
|
||||
await cli.serve(
|
||||
["inspect-wasm-artifact", value.artifact.path, "--json"],
|
||||
{
|
||||
stdout: (part) => {
|
||||
output += part;
|
||||
},
|
||||
exit: () => {},
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
process.exitCode = previousExitCode;
|
||||
}
|
||||
const result = JSON.parse(output);
|
||||
expect(closed).toBe(true);
|
||||
expect(host.listeners.size).toBe(0);
|
||||
if (scenario === "control") {
|
||||
expect(result.normalized_result).toEqual(value);
|
||||
expect(host.exitCode).toBeUndefined();
|
||||
} else if (scenario === "primary-failure") {
|
||||
expect(result).toMatchObject({
|
||||
code: "unreadable_output",
|
||||
details: { reason: "primary reply failure" },
|
||||
});
|
||||
expect(host.exitCode).toBe(130);
|
||||
} else {
|
||||
expect(result).toMatchObject({
|
||||
code: scenario === "interrupt" ? "cancelled" : "cleanup_incomplete",
|
||||
details: {
|
||||
partial_observation: {
|
||||
operation: "inspect_wasm_artifact",
|
||||
normalized_result: value,
|
||||
},
|
||||
...(scenario === "interrupt"
|
||||
? {}
|
||||
: { resources: ["exact-owned-root"] }),
|
||||
},
|
||||
});
|
||||
if (scenario !== "cleanup-failure") expect(host.exitCode).toBe(130);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -21,6 +21,7 @@ it("preserves producer-owned cleanup failure when service close does not resolve
|
||||
{ close: async () => {} },
|
||||
"fixture-provider",
|
||||
"inspect_web_page",
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
@@ -39,7 +40,6 @@ it("returns completed partial Evidence when cancellation arrives during close",
|
||||
},
|
||||
"fixture-provider",
|
||||
"capture_browser_scenario",
|
||||
undefined,
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
@@ -61,7 +61,6 @@ it("preserves the primary operation error when cancellation arrives during close
|
||||
},
|
||||
"fixture-provider",
|
||||
"inspect_web_page",
|
||||
undefined,
|
||||
controller.signal,
|
||||
);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ it("delivers cancellation with completed WASM Evidence through the CLI boundary"
|
||||
service,
|
||||
"wabt-artifact",
|
||||
"inspect_wasm_artifact",
|
||||
signal,
|
||||
),
|
||||
{
|
||||
on: events.on.bind(events),
|
||||
|
||||
@@ -684,6 +684,7 @@ it("retains failed snapshot cleanup and retries it on close", async () => {
|
||||
{ close: () => provider.close() },
|
||||
"flutter",
|
||||
"identify_flutter_build",
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
|
||||
@@ -107,6 +107,7 @@ it("retains cancelled source read ownership until service close retries it", asy
|
||||
service,
|
||||
"web-module-artifacts",
|
||||
"trace_web_module_imports",
|
||||
new AbortController().signal,
|
||||
);
|
||||
expect(finalized).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
|
||||
Reference in new issue
Block a user