mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
fix(reverse): preserve owned command failure attribution (#9964)
fix(reverse): preserve owned command failure provenance during cancellation
This commit is contained in:
1 parent
7c0a658cb4
commit
fcf8b99a77
2 files changed
+106
-6
No files matched your search
@@ -65,6 +65,7 @@ import {
|
||||
} from "../../windows/WindowsOwnedProcess.js";
|
||||
import { absoluteCallerPath } from "../../filesystem/callerPath.js";
|
||||
import { parseAnalysisInput } from "../AnalysisInput.js";
|
||||
import { analysisErrorWithCancellation } from "../../domain/analysisErrorCancellation.js";
|
||||
|
||||
export type ReverseEngineeringOperation =
|
||||
| "inspect_with_objdump"
|
||||
@@ -441,11 +442,24 @@ export class ReverseEngineeringService {
|
||||
...request,
|
||||
signal: operationSignal,
|
||||
});
|
||||
const tracked = running.finally(() => {
|
||||
signal?.removeEventListener("abort", relayAbort);
|
||||
this.#activeOperations.delete(controller);
|
||||
this.#reservedOutputBytes -= outputBudgetBytes;
|
||||
});
|
||||
const tracked = running
|
||||
.then((result) =>
|
||||
!result.ok && operationSignal?.aborted
|
||||
? err(
|
||||
analysisErrorWithCancellation(
|
||||
result.error,
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: operationSignal.reason,
|
||||
}),
|
||||
),
|
||||
)
|
||||
: result,
|
||||
)
|
||||
.finally(() => {
|
||||
signal?.removeEventListener("abort", relayAbort);
|
||||
this.#activeOperations.delete(controller);
|
||||
this.#reservedOutputBytes -= outputBudgetBytes;
|
||||
});
|
||||
this.#activeOperations.set(controller, tracked);
|
||||
return tracked;
|
||||
}
|
||||
@@ -1499,7 +1513,14 @@ const commandCancelled = (
|
||||
signal: AbortSignal | undefined,
|
||||
cause: unknown,
|
||||
code: string | number | undefined,
|
||||
): boolean => code === "ABORT_ERR" || preflightCancelled(signal, cause, code);
|
||||
): boolean => {
|
||||
if (
|
||||
cause instanceof OwnedOutputFailure &&
|
||||
cause.cause instanceof OwnedCommandFailure
|
||||
)
|
||||
return cause.cause.reason === "cancelled";
|
||||
return code === "ABORT_ERR" || preflightCancelled(signal, cause, code);
|
||||
};
|
||||
|
||||
const callerCancellationCause = (
|
||||
signal: AbortSignal | undefined,
|
||||
|
||||
@@ -6,8 +6,87 @@ import { expect, it, vi } from "vitest";
|
||||
import { ReverseEngineeringService } from "../../src/application/reverse/ReverseEngineeringService.js";
|
||||
import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js";
|
||||
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
|
||||
import { OwnedCommandFailure } from "../../src/process/OwnedCommand.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps owned output collection failure primary after late cancellation=%s",
|
||||
async (cancel) => {
|
||||
const controller = new AbortController();
|
||||
const abortReason = { caller: "cancelled after collection failure" };
|
||||
const physical = Object.assign(new Error("output pipe read failed"), {
|
||||
code: "EIO",
|
||||
});
|
||||
const owned = new OwnedCommandFailure(
|
||||
"process",
|
||||
"owned output collection failed",
|
||||
{
|
||||
stdout: { text: "retained", bytes: 8, observedBytes: 8 },
|
||||
stderr: { text: "", bytes: 0, observedBytes: 0 },
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
},
|
||||
null,
|
||||
{ cause: physical, collectionFailure: true },
|
||||
);
|
||||
const profile = {
|
||||
executable: "unused",
|
||||
arguments: [],
|
||||
identity: { id: "fixture", name: "fixture", version: null },
|
||||
};
|
||||
const service = new ReverseEngineeringService({
|
||||
commands: { objdump: () => profile, rizin: () => profile },
|
||||
runOwned: async () => {
|
||||
if (cancel) controller.abort(abortReason);
|
||||
throw owned;
|
||||
},
|
||||
});
|
||||
const result = await service.inspectWithObjdump(
|
||||
{ operation: "architectures" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.ok) throw new Error("Expected output collection failure");
|
||||
expect(result.error._tag).toBe("AnalysisOutputError");
|
||||
expect(result.error.cause).toMatchObject({ cause: owned });
|
||||
expect(owned.cause).toBe(physical);
|
||||
expect(result.error.capturedOutput).toMatchObject({
|
||||
stdout: "retained",
|
||||
stdout_bytes: 8,
|
||||
exit_code: null,
|
||||
});
|
||||
expect(result.error.secondaryFailures).toHaveLength(cancel ? 1 : 0);
|
||||
if (cancel)
|
||||
expect(result.error.secondaryFailures[0]?.cause).toBe(abortReason);
|
||||
await service.close();
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the owned producer cancellation reason authoritative", async () => {
|
||||
const controller = new AbortController();
|
||||
const abortReason = { caller: "owned process cancelled" };
|
||||
const profile = {
|
||||
executable: "unused",
|
||||
arguments: [],
|
||||
identity: { id: "fixture", name: "fixture", version: null },
|
||||
};
|
||||
const service = new ReverseEngineeringService({
|
||||
commands: { objdump: () => profile, rizin: () => profile },
|
||||
runOwned: async () => {
|
||||
controller.abort(abortReason);
|
||||
throw new OwnedCommandFailure("cancelled", "owned process cancelled");
|
||||
},
|
||||
});
|
||||
const result = await service.inspectWithObjdump(
|
||||
{ operation: "architectures" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
if (result.ok) throw new Error("Expected producer cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(abortReason);
|
||||
expect(result.error.secondaryFailures).toEqual([]);
|
||||
await service.close();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ stage: "stat", cause: null },
|
||||
{ stage: "stat", cause: undefined },
|
||||
|
||||
Reference in new issue
Block a user