mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
refactor(errors): require a reason for every analysis cancellation (#10566)
* refactor(errors): require a reason for every analysis cancellation Add cancelledBy, the one constructor for AnalysisCancelledError. Its reason argument is required: an aborted signal, an earlier cancellation, a reason carried from an inner failure, or an OwnerCancellation when REA closed the work's owner itself. Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and binary-session record owners now report OwnerCancellation.ownerClosed instead of an absent reason. Firmware's predecessor wait returns its cancellation as a Result, removing an unreachable reason-less fallback. An architecture guard rejects new direct constructions, with a counted per-file baseline that can only shrink. * refactor(errors): share one aborted-signal predicate with cancelledBy * refactor(process): return a departing caller's reason from a shared startup wait * refactor(providers): build decompiler-provider cancellations with a required reason (#10587) Migrate every AnalysisCancelledError construction under src/ghidra, src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the guard baseline from 671 to 572. Closed Kuna target commands report OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and JEB close abort their lifetimes with that typed reason. * fix(errors): read the reason of any signal-shaped cancellation source * test(ci): cover the counted-baseline comparison behind the architecture guard
This commit is contained in:
1 parent
2f4c588584
commit
85da0bd4ee
51 files changed
+900
-488
No files matched your search
@@ -64,6 +64,8 @@ import {
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
@@ -1273,7 +1275,7 @@ export class AdbProvider {
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
if (this.#closing)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
return err(cancelledBy(request.operation, OwnerCancellation.ownerClosed));
|
||||
const release = this.#beginOperation();
|
||||
try {
|
||||
try {
|
||||
|
||||
@@ -46,6 +46,8 @@ import {
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
@@ -300,7 +302,7 @@ export class ApktoolProvider {
|
||||
inspectAvailability(signal?: AbortSignal): Promise<ProviderAvailability> {
|
||||
if (this.#closing)
|
||||
return Promise.reject(
|
||||
new AnalysisCancelledError("inspect_apktool_client"),
|
||||
cancelledBy("inspect_apktool_client", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
const operationSignal =
|
||||
signal === undefined
|
||||
@@ -376,7 +378,7 @@ export class ApktoolProvider {
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
if (this.#closing)
|
||||
return Promise.resolve(
|
||||
err(new AnalysisCancelledError(request.operation)),
|
||||
err(cancelledBy(request.operation, OwnerCancellation.ownerClosed)),
|
||||
);
|
||||
const signal =
|
||||
options?.signal === undefined
|
||||
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { InvestigationStatistics } from "../../domain/investigationLifecycle.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { UnknownRegistryError } from "../../domain/unknownRegistryError.js";
|
||||
import type {
|
||||
@@ -120,7 +121,7 @@ export abstract class BinarySessionRecords {
|
||||
evidence: Evidence,
|
||||
): Result<"added" | "duplicate", AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("record_evidence"));
|
||||
return err(cancelledBy("record_evidence", OwnerCancellation.ownerClosed));
|
||||
const recorded = this.#records.recordEvidence(evidence);
|
||||
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
|
||||
return recorded;
|
||||
@@ -200,7 +201,9 @@ export abstract class BinarySessionRecords {
|
||||
bundle: unknown,
|
||||
): Result<EvidenceImportReceipt, AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("import_evidence_bundle"));
|
||||
return err(
|
||||
cancelledBy("import_evidence_bundle", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
const imported = this.#records.mergeEvidenceBundle(bundle);
|
||||
if (!imported.ok) return imported;
|
||||
this.#snapshot.refreshEvidenceMetadata(imported.value.replacements);
|
||||
@@ -266,7 +269,9 @@ export abstract class BinarySessionRecords {
|
||||
snapshot: AnalysisSnapshot,
|
||||
): Result<number, AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("import_analysis_snapshot"));
|
||||
return err(
|
||||
cancelledBy("import_analysis_snapshot", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
const active = this.activeAnalysisBinding();
|
||||
if (active !== undefined && this.#snapshotInvalidated)
|
||||
return err(
|
||||
@@ -465,7 +470,7 @@ export abstract class BinarySessionRecords {
|
||||
input: RecordUnknownInput,
|
||||
): Result<ResidualUnknown, AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("record_unknown"));
|
||||
return err(cancelledBy("record_unknown", OwnerCancellation.ownerClosed));
|
||||
const target = this.activeAnalysisBinding()?.target;
|
||||
const recorded = this.#records.recordUnknown(input, target);
|
||||
if (recorded.ok) this.#emitSnapshotChanged();
|
||||
@@ -477,7 +482,12 @@ export abstract class BinarySessionRecords {
|
||||
input: RecordUnknownInput,
|
||||
): Result<ResidualUnknown | null, AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("record_evidence_with_unknown"));
|
||||
return err(
|
||||
cancelledBy(
|
||||
"record_evidence_with_unknown",
|
||||
OwnerCancellation.ownerClosed,
|
||||
),
|
||||
);
|
||||
const recorded = this.#records.recordEvidenceWithUnknown(evidence, input);
|
||||
if (recorded.ok) this.#emitSnapshotChanged();
|
||||
return recorded;
|
||||
@@ -487,7 +497,7 @@ export abstract class BinarySessionRecords {
|
||||
input: UpdateUnknownInput,
|
||||
): Result<ResidualUnknown, AnalysisError> {
|
||||
if (this.#investigationDisposed)
|
||||
return err(new AnalysisCancelledError("update_unknown"));
|
||||
return err(cancelledBy("update_unknown", OwnerCancellation.ownerClosed));
|
||||
const target = this.activeAnalysisBinding()?.target;
|
||||
const updated = this.#records.updateUnknown(input, target);
|
||||
if (updated.ok) this.#emitSnapshotChanged();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type {
|
||||
AnalyzeWebBundleInput,
|
||||
@@ -267,6 +268,26 @@ it("carries the exact caller cancellation reason and completed prefix facts", as
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses work after close with REA's own cancellation reason, not an absent one", async () => {
|
||||
const analyzer = new IsolatedWebBundleAnalyzer(() => ({
|
||||
analyze: async (script) => ok(analyzeCapturedWebScript(script)),
|
||||
close: async () => {},
|
||||
}));
|
||||
await analyzer.close();
|
||||
const captured = inspection(["fetch('/one');"]);
|
||||
|
||||
const reserved = await analyzer.reserve();
|
||||
const analyzed = await analyzer.analyze(captured, noSourceMaps, input);
|
||||
|
||||
for (const result of [reserved, analyzed]) {
|
||||
if (result.ok) throw new Error("Closed analyzer accepted work");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(OwnerCancellation.ownerClosed);
|
||||
}
|
||||
if (analyzed.ok) throw new Error("Closed analyzer accepted work");
|
||||
expect(analyzed.error.partialObservation).toBe(captured);
|
||||
});
|
||||
|
||||
it("retains all completed facts when cancellation arrives during worker close", async () => {
|
||||
const captured = inspection(["fetch('/one');", "fetch('/two');"]);
|
||||
const closeStarted = deferred<void>();
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
AnalysisTimeoutError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisOutputError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { analysisErrorWithCancellation } from "../domain/analysisErrorCancellation.js";
|
||||
@@ -91,7 +93,9 @@ export class IsolatedWebBundleAnalyzer {
|
||||
/** Reserve capacity before capture, so concurrent captures cannot retain source inventories. */
|
||||
async reserve(): Promise<Result<void, AnalysisError>> {
|
||||
if (this.#closed)
|
||||
return err(new AnalysisCancelledError("analyze_web_bundle"));
|
||||
return err(
|
||||
cancelledBy("analyze_web_bundle", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
if (this.#admission.active() !== undefined)
|
||||
return err(
|
||||
new AnalysisResourceConstraintError(
|
||||
@@ -124,7 +128,9 @@ export class IsolatedWebBundleAnalyzer {
|
||||
}
|
||||
if (this.#closed) {
|
||||
this.#admission.release(this);
|
||||
return err(new AnalysisCancelledError("analyze_web_bundle"));
|
||||
return err(
|
||||
cancelledBy("analyze_web_bundle", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
@@ -142,7 +148,7 @@ export class IsolatedWebBundleAnalyzer {
|
||||
if (this.#closed)
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCancelledError("analyze_web_bundle", {
|
||||
cancelledBy("analyze_web_bundle", OwnerCancellation.ownerClosed, {
|
||||
partialObservation: inspection,
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -13,12 +13,13 @@ import {
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { isUtf8 } from "node:buffer";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
reattributedResourceConstraint,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
randomUUID,
|
||||
@@ -178,10 +179,7 @@ const assertBridgeActive = (
|
||||
signal: AbortSignal | undefined,
|
||||
operation: "cutter_command" | "list_cutter_sessions",
|
||||
): void => {
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
});
|
||||
if (signal?.aborted) throw cancelledBy(operation, signal);
|
||||
};
|
||||
|
||||
const mapDiscovery = async <Input, Output>(
|
||||
@@ -421,9 +419,10 @@ export class CutterBridgeClient {
|
||||
if (cause instanceof AnalysisResourceConstraintError) throw cause;
|
||||
if (cause instanceof CutterBridgeRequestFailure) {
|
||||
if (cause.cancellation !== undefined)
|
||||
throw new AnalysisCancelledError("list_cutter_sessions", {
|
||||
cause: cause.cancellation.reason,
|
||||
});
|
||||
throw cancelledBy(
|
||||
"list_cutter_sessions",
|
||||
carriedReason(cause.cancellation.reason),
|
||||
);
|
||||
if (cause.limit !== undefined)
|
||||
throw reattributedResourceConstraint(
|
||||
"list_cutter_sessions",
|
||||
@@ -604,9 +603,10 @@ export class CutterBridgeClient {
|
||||
// A transport failure escaping here precedes command admission.
|
||||
if (!(cause instanceof CutterBridgeRequestFailure)) throw cause;
|
||||
if (cause.cancellation !== undefined)
|
||||
throw new AnalysisCancelledError("cutter_command", {
|
||||
cause: cause.cancellation.reason,
|
||||
});
|
||||
throw cancelledBy(
|
||||
"cutter_command",
|
||||
carriedReason(cause.cancellation.reason),
|
||||
);
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"cutter",
|
||||
"cutter_command",
|
||||
@@ -1013,9 +1013,10 @@ export class CutterBridgeClient {
|
||||
const onAbort = (): void => {
|
||||
settle({
|
||||
ok: false,
|
||||
cause: new AnalysisCancelledError("list_cutter_sessions", {
|
||||
cause: signal?.reason,
|
||||
}),
|
||||
cause: cancelledBy(
|
||||
"list_cutter_sessions",
|
||||
carriedReason(signal?.reason),
|
||||
),
|
||||
});
|
||||
};
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
@@ -1241,9 +1242,7 @@ const discoveryFailure = (
|
||||
outcome?.kind === "failed"
|
||||
? discoveryFailure(outcome.cause, operation)
|
||||
: cancellation !== undefined
|
||||
? new AnalysisCancelledError(operation, {
|
||||
cause: cancellation.reason,
|
||||
})
|
||||
? cancelledBy(operation, carriedReason(cancellation.reason))
|
||||
: undefined;
|
||||
if (primary === undefined && outcome?.kind !== "failed") return cleanup;
|
||||
const original =
|
||||
@@ -1257,8 +1256,9 @@ const discoveryFailure = (
|
||||
).bindCleanupDependencies([cleanup]);
|
||||
}
|
||||
if (cause.reason === "cancelled")
|
||||
return new AnalysisCancelledError(operation, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
});
|
||||
return cancelledBy(
|
||||
operation,
|
||||
carriedReason(artifactFileCancellationCause(cause)),
|
||||
);
|
||||
return cause;
|
||||
};
|
||||
@@ -2,10 +2,10 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
@@ -94,19 +94,11 @@ export class CutterBridgeService {
|
||||
): Promise<Result<CutterBridgeDiscovery, AnalysisError>> {
|
||||
const { signal } = options;
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("list_cutter_sessions", {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("list_cutter_sessions", signal));
|
||||
try {
|
||||
const discovery = await this.client.listSessions(options);
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("list_cutter_sessions", {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("list_cutter_sessions", signal));
|
||||
return ok(discovery);
|
||||
} catch (cause) {
|
||||
if (cause instanceof AnalysisError) return err(cause);
|
||||
@@ -156,11 +148,7 @@ export class CutterBridgeService {
|
||||
const parsed = cutterCommandInputSchema.safeParse(rawInput);
|
||||
if (!parsed.success) return err(new AnalysisInputError("cutter_command"));
|
||||
if (options.signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("cutter_command", {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("cutter_command", options.signal));
|
||||
const input = parsed.data;
|
||||
const cleanup = await this.retryCleanup();
|
||||
if (!cleanup.ok) return cleanup;
|
||||
@@ -244,8 +232,7 @@ export class CutterBridgeService {
|
||||
}
|
||||
if (options.signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("cutter_command", {
|
||||
cause: options.signal.reason,
|
||||
cancelledBy("cutter_command", options.signal, {
|
||||
partialObservation: evidence,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
isAborted,
|
||||
OwnerCancellation,
|
||||
} from "./analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./analysisErrorProjection.js";
|
||||
|
||||
it("takes the cause from each kind of required cancellation reason", () => {
|
||||
const callerReason = { caller: "stop" };
|
||||
const controller = new AbortController();
|
||||
controller.abort(callerReason);
|
||||
|
||||
const fromSignal = cancelledBy("probe", controller.signal);
|
||||
const fromEarlier = cancelledBy("outer", fromSignal);
|
||||
const fromCarried = cancelledBy("carried", carriedReason(callerReason));
|
||||
const fromOwner = cancelledBy("closed", OwnerCancellation.ownerClosed);
|
||||
|
||||
expect(fromSignal).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(fromSignal.operation).toBe("probe");
|
||||
expect(fromSignal.cause).toBe(callerReason);
|
||||
expect(fromEarlier.cause).toBe(callerReason);
|
||||
expect(fromCarried.cause).toBe(callerReason);
|
||||
expect(fromOwner.cause).toBe(OwnerCancellation.ownerClosed);
|
||||
expect(OwnerCancellation.ownerClosed.kind).toBe("owner-closed");
|
||||
});
|
||||
|
||||
it("keeps the cancellation projection unchanged by the reason it carries", () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error("caller detail"));
|
||||
const cleanup = { reason: "retained", resources: ["/tmp/owned"] };
|
||||
|
||||
const fromSignal = cancelledBy("probe", controller.signal, {
|
||||
userMessage: "Stopped.",
|
||||
cleanup,
|
||||
});
|
||||
const fromOwner = cancelledBy("probe", OwnerCancellation.ownerClosed, {
|
||||
userMessage: "Stopped.",
|
||||
cleanup,
|
||||
});
|
||||
|
||||
expect(projectAnalysisError(fromOwner)).toEqual(
|
||||
projectAnalysisError(fromSignal),
|
||||
);
|
||||
expect(JSON.stringify(projectAnalysisError(fromOwner))).not.toContain(
|
||||
"OwnerCancellation",
|
||||
);
|
||||
});
|
||||
|
||||
it("narrows only an aborted signal for cancelledBy", () => {
|
||||
const controller = new AbortController();
|
||||
const optional: AbortSignal | undefined = controller.signal;
|
||||
expect(isAborted(undefined)).toBe(false);
|
||||
expect(isAborted(optional)).toBe(false);
|
||||
controller.abort("caller");
|
||||
if (!isAborted(optional)) throw new Error("Expected an aborted signal");
|
||||
expect(cancelledBy("probe", optional).cause).toBe("caller");
|
||||
});
|
||||
|
||||
it("reads the reason of a signal that is not this realm's AbortSignal", () => {
|
||||
const reason = { caller: "foreign" };
|
||||
const foreign = { aborted: true, reason } as unknown as AbortSignal;
|
||||
expect(cancelledBy("probe", foreign).cause).toBe(reason);
|
||||
});
|
||||
|
||||
it("does not accept a cause beside the required reason", () => {
|
||||
const controller = new AbortController();
|
||||
controller.abort("caller");
|
||||
// @ts-expect-error The reason argument owns the cause.
|
||||
const error = cancelledBy("probe", controller.signal, { cause: "other" });
|
||||
expect(error.cause).toBe("caller");
|
||||
});
|
||||
@@ -310,7 +310,11 @@ export class AnalysisCapabilityUnavailableError extends AnalysisError {
|
||||
}
|
||||
}
|
||||
|
||||
/** Caller cancellation won before provider-neutral work completed. */
|
||||
/**
|
||||
* Cancellation, by the caller or by REA closing the work's owner, won before
|
||||
* provider-neutral work completed. Build it with `cancelledBy`, which requires
|
||||
* the reason; `cause` holds that reason.
|
||||
*/
|
||||
export class AnalysisCancelledError extends AnalysisError {
|
||||
readonly _tag = "AnalysisCancelledError";
|
||||
override readonly userMessage: string | undefined;
|
||||
@@ -326,6 +330,85 @@ export class AnalysisCancelledError extends AnalysisError {
|
||||
}
|
||||
}
|
||||
|
||||
/** Why REA ended work on its own authority when no caller abort exists. */
|
||||
export type OwnerCancellationKind =
|
||||
/** The provider, session, or service owning the work was closed or disposed. */
|
||||
"owner-closed";
|
||||
|
||||
/**
|
||||
* A cancellation REA initiated itself, such as a provider closing. It stands
|
||||
* in for a caller abort reason, so it is never mistaken for one.
|
||||
*/
|
||||
export class OwnerCancellation extends Error {
|
||||
override readonly name = "OwnerCancellation";
|
||||
private constructor(readonly kind: OwnerCancellationKind) {
|
||||
super("REA closed the owner of this operation before it completed");
|
||||
}
|
||||
|
||||
/**
|
||||
* One shared reason per kind, so repeated owner cancellations compare equal
|
||||
* the way repeated observations of one caller abort do.
|
||||
*/
|
||||
static readonly ownerClosed = new OwnerCancellation("owner-closed");
|
||||
}
|
||||
|
||||
/** A caller abort reason recovered from an earlier observation of that abort. */
|
||||
export interface CarriedCancellationReason {
|
||||
readonly carried: unknown;
|
||||
}
|
||||
|
||||
/** Mark a reason already observed from the caller's abort, such as an inner failure's recorded reason. */
|
||||
export const carriedReason = (reason: unknown): CarriedCancellationReason => ({
|
||||
carried: reason,
|
||||
});
|
||||
|
||||
/**
|
||||
* Where a cancellation's reason comes from:
|
||||
* - an aborted signal, whose `reason` is the caller's (or parent operation's) reason;
|
||||
* - an earlier `AnalysisCancelledError`, whose reason is carried unchanged;
|
||||
* - a reason carried from another earlier observation;
|
||||
* - an owner cancellation that REA initiated itself.
|
||||
*/
|
||||
export type CancellationReason =
|
||||
| AbortSignal
|
||||
| AnalysisCancelledError
|
||||
| CarriedCancellationReason
|
||||
| OwnerCancellation;
|
||||
|
||||
/** A signal observed aborted, so its `reason` is the cancellation's reason. */
|
||||
export type AbortedSignal = AbortSignal & { readonly aborted: true };
|
||||
|
||||
/** Whether an optional signal has aborted; narrows it to pass to `cancelledBy`. */
|
||||
export const isAborted = (
|
||||
signal: AbortSignal | undefined,
|
||||
): signal is AbortedSignal => signal?.aborted === true;
|
||||
|
||||
/** Options for a cancellation; its cause is always the required reason. */
|
||||
export type CancelledOptions = Omit<AnalysisErrorOptions, "cause"> & {
|
||||
readonly cause?: never;
|
||||
readonly userMessage?: string;
|
||||
};
|
||||
|
||||
// Signals are matched structurally last: callers and tests may pass an
|
||||
// AbortSignal implementation from another realm or a signal-shaped fake.
|
||||
const cancellationCause = (reason: CancellationReason): unknown => {
|
||||
if (reason instanceof AnalysisCancelledError) return reason.cause;
|
||||
if (reason instanceof OwnerCancellation) return reason;
|
||||
if ("carried" in reason) return reason.carried;
|
||||
return reason.reason;
|
||||
};
|
||||
|
||||
/** The one constructor for a cancellation: its reason is required. */
|
||||
export const cancelledBy = (
|
||||
operation: string,
|
||||
reason: CancellationReason,
|
||||
options?: CancelledOptions,
|
||||
): AnalysisCancelledError =>
|
||||
new AnalysisCancelledError(operation, {
|
||||
...options,
|
||||
cause: cancellationCause(reason),
|
||||
});
|
||||
|
||||
/** Provider-neutral operation exceeded its declared execution deadline. */
|
||||
export class AnalysisTimeoutError extends AnalysisError {
|
||||
readonly _tag = "AnalysisTimeoutError";
|
||||
|
||||
@@ -19,11 +19,13 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
||||
import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js";
|
||||
import { SafeOutputTree } from "../artifacts/SafeOutputTree.js";
|
||||
@@ -116,17 +118,18 @@ const waitForPredecessor = (
|
||||
predecessor: Promise<void>,
|
||||
operation: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const abort = () =>
|
||||
reject(new AnalysisCancelledError(operation, { cause: signal?.reason }));
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
if (signal?.aborted === true) abort();
|
||||
): Promise<Result<void, AnalysisCancelledError>> => {
|
||||
if (signal === undefined) return predecessor.then(() => ok(undefined));
|
||||
return new Promise((resolve) => {
|
||||
const abort = () => resolve(err(cancelledBy(operation, signal)));
|
||||
signal.addEventListener("abort", abort, { once: true });
|
||||
if (signal.aborted) abort();
|
||||
void predecessor.then(() => {
|
||||
signal?.removeEventListener("abort", abort);
|
||||
resolve();
|
||||
signal.removeEventListener("abort", abort);
|
||||
resolve(ok(undefined));
|
||||
});
|
||||
});
|
||||
};
|
||||
const limitations = [
|
||||
"Signature matches and reported sizes do not establish partition validity, encryption or runtime mappings. Binwalk may infer unknown lengths before emitting its report.",
|
||||
"Only Linux is supported by this integration. One worker, inherited per-process address-space/file-size/CPU limits and a deadline constrain execution; staging byte and entry budgets are polled and may transiently overshoot. Limits are not aggregate process-memory or filesystem quotas.",
|
||||
@@ -166,26 +169,28 @@ export class FirmwareProvider implements FirmwareAnalysisPort {
|
||||
request: FirmwareRequest,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Outcome> {
|
||||
if (this.#closed) return err(new AnalysisCancelledError(request.operation));
|
||||
if (this.#closed)
|
||||
return err(cancelledBy(request.operation, OwnerCancellation.ownerClosed));
|
||||
const predecessor = this.#tail;
|
||||
let release = () => {};
|
||||
this.#tail = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
// Cancel the caller promptly without letting a later request bypass its predecessor.
|
||||
try {
|
||||
await waitForPredecessor(predecessor, request.operation, options?.signal);
|
||||
} catch (cause: unknown) {
|
||||
const waited = await waitForPredecessor(
|
||||
predecessor,
|
||||
request.operation,
|
||||
options?.signal,
|
||||
);
|
||||
if (!waited.ok) {
|
||||
void predecessor.then(release);
|
||||
return err(
|
||||
cause instanceof AnalysisError
|
||||
? cause
|
||||
: new AnalysisCancelledError(request.operation),
|
||||
);
|
||||
return err(waited.error);
|
||||
}
|
||||
try {
|
||||
if (this.#closed)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
return err(
|
||||
cancelledBy(request.operation, OwnerCancellation.ownerClosed),
|
||||
);
|
||||
const cleanup = await this.#retryCleanup();
|
||||
if (cleanup.executionBlocked && cleanup.failure !== undefined)
|
||||
return err(cleanup.failure);
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
GDB_SESSION_TOOLS,
|
||||
@@ -695,7 +697,7 @@ export class GdbSessionManager {
|
||||
return err(
|
||||
signal?.aborted
|
||||
? cancelledWithCallerReason("start_gdb_session", signal)
|
||||
: new AnalysisCancelledError("start_gdb_session"),
|
||||
: cancelledBy("start_gdb_session", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
if (!persistentDebuggerHostSupported(this.#platform))
|
||||
return err(
|
||||
@@ -1497,7 +1499,9 @@ export class GdbSessionManager {
|
||||
>
|
||||
> {
|
||||
if (this.#closing)
|
||||
return err(new AnalysisCancelledError("close_gdb_session"));
|
||||
return err(
|
||||
cancelledBy("close_gdb_session", OwnerCancellation.ownerClosed),
|
||||
);
|
||||
const session = this.#sessions.get(sessionId);
|
||||
if (session === undefined) return err(gdbCloseUnavailable());
|
||||
if (session.exited) return this.#finishSessionCleanup(session);
|
||||
|
||||
@@ -9,8 +9,8 @@ import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import type { GhidraLanguageOverride } from "../config/ghidraLanguageOverride.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -154,9 +154,7 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
disabledAnalyzers?: readonly string[],
|
||||
): Promise<Result<AnalysisProfileResolution, AnalysisError>> => {
|
||||
if (signal?.aborted === true)
|
||||
return Promise.resolve(
|
||||
err(new AnalysisCancelledError("open_binary", { cause: signal.reason })),
|
||||
);
|
||||
return Promise.resolve(err(cancelledBy("open_binary", signal)));
|
||||
if (target.kind !== "executable")
|
||||
return Promise.resolve(ok({ profile: null }));
|
||||
if (installation.status === "unavailable")
|
||||
|
||||
@@ -17,10 +17,11 @@ import {
|
||||
} from "../filesystem/readStableFileBytes.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -137,11 +138,11 @@ export const ghidraFileFailure = (
|
||||
}
|
||||
const error =
|
||||
cause instanceof ArtifactReaderFailure && cause.reason === "cancelled"
|
||||
? new AnalysisCancelledError(operation, {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
cleanup,
|
||||
diagnostics,
|
||||
})
|
||||
? cancelledBy(
|
||||
operation,
|
||||
carriedReason(artifactFileCancellationCause(cause)),
|
||||
{ cleanup, diagnostics },
|
||||
)
|
||||
: new ProviderAdapterError("ghidra", operation, {
|
||||
cause,
|
||||
cleanup,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import {
|
||||
probeGhidraJava,
|
||||
@@ -344,9 +347,7 @@ export const inspectGhidraInstallation = async (
|
||||
host: GhidraInstallationHost = systemGhidraInstallationHost(),
|
||||
): Promise<GhidraInstallationInspection> => {
|
||||
if (options.signal?.aborted)
|
||||
throw new AnalysisCancelledError("ghidra:installation", {
|
||||
cause: options.signal.reason,
|
||||
});
|
||||
throw cancelledBy("ghidra:installation", options.signal);
|
||||
const platform = options.platform ?? process.platform;
|
||||
const architecture = options.architecture ?? process.arch;
|
||||
const coordinates = await installationCoordinates(
|
||||
@@ -415,8 +416,7 @@ export const inspectGhidraInstallation = async (
|
||||
},
|
||||
};
|
||||
if (options.signal?.aborted)
|
||||
throw new AnalysisCancelledError("ghidra:installation", {
|
||||
cause: options.signal.reason,
|
||||
throw cancelledBy("ghidra:installation", options.signal, {
|
||||
diagnostics: ghidraInstallationDiagnostics(
|
||||
availableInstallation(coordinates, observation, options.javaHome),
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
@@ -235,17 +235,20 @@ const observeGhidraJava = async (
|
||||
) {
|
||||
primary =
|
||||
inspectionCancellation ??
|
||||
new AnalysisCancelledError("ghidra:java", {
|
||||
cause: commandFailure.cancellation.reason,
|
||||
...(output === undefined ? {} : { capturedOutput: output }),
|
||||
...(interpreted?.ok === true
|
||||
? {
|
||||
diagnostics: {
|
||||
java_observation: jsonValueSchema.parse(interpreted.value),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
cancelledBy(
|
||||
"ghidra:java",
|
||||
carriedReason(commandFailure.cancellation.reason),
|
||||
{
|
||||
...(output === undefined ? {} : { capturedOutput: output }),
|
||||
...(interpreted?.ok === true
|
||||
? {
|
||||
diagnostics: {
|
||||
java_observation: jsonValueSchema.parse(interpreted.value),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
} else if (inspectionCancellation !== undefined) {
|
||||
primary = inspectionCancellation;
|
||||
} else if (
|
||||
|
||||
@@ -33,7 +33,12 @@ import {
|
||||
primaryAnalysisError,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
isAborted,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
|
||||
@@ -283,11 +288,7 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
},
|
||||
): Promise<Result<GhidraLaunch, GhidraLaunchError | AnalysisCancelledError>> {
|
||||
if (isAborted(options.signal))
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", {
|
||||
cause: options.signal?.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("open_binary", options.signal));
|
||||
const paths = ghidraRuntimePaths(session.runtimeRoot);
|
||||
const platform = this.options.platform ?? process.platform;
|
||||
const existingProject = this.options.existingProject;
|
||||
@@ -422,11 +423,7 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
);
|
||||
}
|
||||
if (isAborted(options.signal))
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", {
|
||||
cause: options.signal?.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("open_binary", options.signal));
|
||||
const headlessArguments = ghidraHeadlessArguments({
|
||||
platform,
|
||||
...(psp === undefined ? {} : { psp: true as const }),
|
||||
@@ -593,9 +590,7 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
platform,
|
||||
);
|
||||
if (isAborted(options.signal)) {
|
||||
throw new AnalysisCancelledError("open_binary", {
|
||||
cause: options.signal?.reason,
|
||||
});
|
||||
throw cancelledBy("open_binary", options.signal);
|
||||
}
|
||||
return ok(partialLaunch);
|
||||
} catch (cause: unknown) {
|
||||
@@ -605,9 +600,10 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
|
||||
? cause
|
||||
: cause instanceof GhidraLaunchSupportError &&
|
||||
cause.cancellation !== undefined
|
||||
? new AnalysisCancelledError("open_binary", {
|
||||
cause: cause.cancellation.reason,
|
||||
})
|
||||
? cancelledBy(
|
||||
"open_binary",
|
||||
carriedReason(cause.cancellation.reason),
|
||||
)
|
||||
: undefined);
|
||||
const capturedOutput = ghidraLaunchCommandOutput(cause);
|
||||
const primary =
|
||||
@@ -1199,5 +1195,3 @@ export const ghidraHeadlessJavaOptions = (
|
||||
// argv items instead of the shell-split analyzeHeadless option list.
|
||||
return { JDK_JAVA_OPTIONS: "", GHIDRA_HEADLESS_JAVA_OPTIONS: "" };
|
||||
};
|
||||
|
||||
const isAborted = (signal?: AbortSignal): boolean => signal?.aborted === true;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { AnalysisOperation } from "../application/AnalysisProvider.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { cancelledBy } from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
@@ -52,8 +52,7 @@ export const attestGhidraNativeLoadImage = async (
|
||||
if (!result.ok) return result;
|
||||
return input.signal?.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(input.operation, {
|
||||
cause: input.signal.reason,
|
||||
cancelledBy(input.operation, input.signal, {
|
||||
partialObservation: {
|
||||
kind: "native-load-image",
|
||||
stage: "completed",
|
||||
@@ -134,8 +133,7 @@ const compareLoadImage = async (
|
||||
}
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
cancelledBy(operation, signal, {
|
||||
partialObservation: {
|
||||
kind: "native-load-image",
|
||||
stage: "measured",
|
||||
|
||||
@@ -6,7 +6,8 @@ import type {
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
@@ -139,9 +140,7 @@ export class GhidraNativeAotSnapshotOwner {
|
||||
),
|
||||
);
|
||||
if (waited.status === "cancelled")
|
||||
return err(
|
||||
this.#cancelled(operation, callerSignal?.reason, observations),
|
||||
);
|
||||
return err(this.#cancelled(operation, waited.reason, observations));
|
||||
if (callerSignal?.aborted)
|
||||
return err(
|
||||
waited.value.ok
|
||||
@@ -163,9 +162,7 @@ export class GhidraNativeAotSnapshotOwner {
|
||||
const attempt = this.#attempt ?? this.#start(normalized);
|
||||
const waited = await attempt.wait(callerSignal);
|
||||
if (waited.status === "cancelled")
|
||||
return err(
|
||||
this.#cancelled(operation, callerSignal?.reason, observations),
|
||||
);
|
||||
return err(this.#cancelled(operation, waited.reason, observations));
|
||||
const settled = waited.value;
|
||||
// A completed physical failure or retained cleanup outranks a later abort.
|
||||
if (!settled.ok) {
|
||||
@@ -362,8 +359,7 @@ export class GhidraNativeAotSnapshotOwner {
|
||||
observations: JsonValue,
|
||||
partialObservation?: AnalysisPartialObservation,
|
||||
): AnalysisError {
|
||||
return new AnalysisCancelledError(operation, {
|
||||
cause: reason,
|
||||
return cancelledBy(operation, carriedReason(reason), {
|
||||
diagnostics: { measured_observation: observations },
|
||||
partialObservation,
|
||||
});
|
||||
|
||||
@@ -68,7 +68,11 @@ import {
|
||||
type GhidraInstallationHost,
|
||||
type GhidraInstallationInspection,
|
||||
} from "./GhidraInstallation.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { resolveGhidraAnalysisSeeds } from "./GhidraAnalysisSeeds.js";
|
||||
import {
|
||||
admitGhidraLanguageOverride,
|
||||
@@ -200,8 +204,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
: undefined;
|
||||
const availability = this.#availability(installation, readiness);
|
||||
if (availability.status === "available" && signal.aborted)
|
||||
throw new AnalysisCancelledError("binary_session", {
|
||||
cause: signal.reason,
|
||||
throw cancelledBy("binary_session", signal, {
|
||||
diagnostics: availability.diagnostics,
|
||||
});
|
||||
return {
|
||||
@@ -391,8 +394,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
);
|
||||
if (result.ok && signal.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", {
|
||||
cause: signal.reason,
|
||||
cancelledBy("open_binary", signal, {
|
||||
diagnostics: {
|
||||
completed_profile_digest: result.value.profile?.digest ?? null,
|
||||
},
|
||||
@@ -496,11 +498,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
);
|
||||
if (!extensions.ok) return extensions;
|
||||
if (options?.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("open_binary", options.signal));
|
||||
const seeds = await resolveGhidraAnalysisSeeds(
|
||||
this.config.ghidraSeedFile,
|
||||
this.#inspectionResources,
|
||||
@@ -622,10 +620,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
callerSignal === undefined
|
||||
? this.#lifetime.signal
|
||||
: AbortSignal.any([callerSignal, this.#lifetime.signal]);
|
||||
if (signal.aborted)
|
||||
throw new AnalysisCancelledError("ghidra:inspection", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
if (signal.aborted) throw cancelledBy("ghidra:inspection", signal);
|
||||
const work = Promise.resolve().then(() => operation(signal));
|
||||
this.#inspectionWork.add(work);
|
||||
try {
|
||||
@@ -640,10 +635,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
operation: string,
|
||||
scope?: ProviderInspectionScope,
|
||||
): Promise<GhidraInstallationInspection> {
|
||||
if (callerSignal?.aborted)
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: callerSignal.reason,
|
||||
});
|
||||
if (callerSignal?.aborted) throw cancelledBy(operation, callerSignal);
|
||||
if (scope?.active) {
|
||||
const observed = this.#observations.get(scope);
|
||||
if (observed?.lifetime === this.#lifetime.signal)
|
||||
@@ -679,9 +671,7 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
}
|
||||
const waited = await attempt.wait(callerSignal);
|
||||
if (waited.status === "cancelled")
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: callerSignal?.reason,
|
||||
});
|
||||
throw cancelledBy(operation, carriedReason(waited.reason));
|
||||
if (
|
||||
draining &&
|
||||
!callerSignal?.aborted &&
|
||||
@@ -701,12 +691,13 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||
waited.value.value.status === "available" &&
|
||||
(callerSignal?.aborted || attempt.signal.aborted)
|
||||
)
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: callerSignal?.aborted
|
||||
? callerSignal.reason
|
||||
: attempt.signal.reason,
|
||||
diagnostics: ghidraInstallationDiagnostics(waited.value.value),
|
||||
});
|
||||
throw cancelledBy(
|
||||
operation,
|
||||
carriedReason(
|
||||
callerSignal?.aborted ? callerSignal.reason : attempt.signal.reason,
|
||||
),
|
||||
{ diagnostics: ghidraInstallationDiagnostics(waited.value.value) },
|
||||
);
|
||||
if (scope?.active && !callerSignal?.aborted && !attempt.signal.aborted)
|
||||
this.#observations.set(scope, {
|
||||
lifetime: this.#lifetime.signal,
|
||||
|
||||
@@ -75,6 +75,8 @@ import {
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
reattributedResourceConstraint,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
|
||||
@@ -613,8 +615,7 @@ export const createGhidraProviderClient = (input: {
|
||||
: [];
|
||||
if (input.signal.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("create_analysis_client", {
|
||||
cause: input.signal.reason,
|
||||
cancelledBy("create_analysis_client", input.signal, {
|
||||
diagnostics: ghidraInstallationDiagnostics(installation),
|
||||
}),
|
||||
);
|
||||
@@ -1525,14 +1526,18 @@ const projectPrimarySessionError = (
|
||||
if (
|
||||
failure.kind === "cancelled" ||
|
||||
(failure.kind === "remote" && failure.remoteCode === "decompile_cancelled")
|
||||
)
|
||||
return new AnalysisCancelledError(operation, {
|
||||
...context,
|
||||
cause:
|
||||
) {
|
||||
const { cause: _failure, ...cancellationContext } = context;
|
||||
return cancelledBy(
|
||||
operation,
|
||||
carriedReason(
|
||||
failure.cancellation === undefined
|
||||
? failure
|
||||
: failure.cancellation.reason,
|
||||
});
|
||||
),
|
||||
cancellationContext,
|
||||
);
|
||||
}
|
||||
if (failure.kind === "remote" && failure.remoteCode === "decompile_failed")
|
||||
return new ProviderAdapterError("ghidra", operation, {
|
||||
...context,
|
||||
|
||||
@@ -5,8 +5,9 @@ import { isAbsolute, join } from "node:path";
|
||||
import { z } from "zod";
|
||||
import { sameRegularFileState } from "../filesystem/RegularFile.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
@@ -248,10 +249,11 @@ const ghidraPspInspectionError = (cause: unknown): AnalysisError => {
|
||||
: { code: admission.code, ...admission.detail };
|
||||
const error =
|
||||
cause instanceof ArtifactReaderFailure && cause.reason === "cancelled"
|
||||
? new AnalysisCancelledError("resolve_analysis_profile", {
|
||||
cause: artifactFileCancellationCause(cause),
|
||||
cleanup,
|
||||
})
|
||||
? cancelledBy(
|
||||
"resolve_analysis_profile",
|
||||
carriedReason(artifactFileCancellationCause(cause)),
|
||||
{ cleanup },
|
||||
)
|
||||
: new AnalysisCapabilityUnavailableError(
|
||||
"ghidra",
|
||||
"resolve_analysis_profile",
|
||||
|
||||
@@ -6,9 +6,9 @@ import { z } from "zod";
|
||||
import type { AppConfig } from "../../config/types.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
cancelledBy,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { assertArtifactReadActive } from "../../artifacts/ArtifactFileOperation.js";
|
||||
@@ -104,10 +104,7 @@ export const resolveGhidraExtensions = async (
|
||||
.run(async () => {
|
||||
const extensions: GhidraExtension[] = [];
|
||||
for (const adapter of adapters) {
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", { cause: signal.reason }),
|
||||
);
|
||||
if (signal?.aborted) return err(cancelledBy("open_binary", signal));
|
||||
const configured = adapter.configuredPath(config);
|
||||
if (configured === undefined) continue;
|
||||
const refusal = adapter.unsupportedReason(target, platform);
|
||||
@@ -133,10 +130,7 @@ export const resolveGhidraExtensions = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("open_binary", { cause: signal.reason }),
|
||||
);
|
||||
if (signal?.aborted) return err(cancelledBy("open_binary", signal));
|
||||
return ok(extensions);
|
||||
})
|
||||
.catch((cause: unknown) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { idaRegistrationTimeoutGuidance } from "./IdaConfiguration.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -64,10 +64,7 @@ export const readSettledHealth = async (
|
||||
const remaining = (): number => timeoutMs - (clock.now() - started);
|
||||
let lastBusy: LastBusyHealth | undefined;
|
||||
for (;;) {
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError("server_health", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
if (signal?.aborted) throw cancelledBy("server_health", signal);
|
||||
const budgetMs = remaining();
|
||||
if (budgetMs <= 0)
|
||||
throw lastBusy === undefined
|
||||
@@ -115,15 +112,10 @@ export const readSettledHealth = async (
|
||||
cause instanceof Error &&
|
||||
cause.name === "AbortError"
|
||||
)
|
||||
throw new AnalysisCancelledError("server_health", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
throw cancelledBy("server_health", signal);
|
||||
throw cause;
|
||||
}
|
||||
if (signal?.aborted)
|
||||
throw new AnalysisCancelledError("server_health", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
if (signal?.aborted) throw cancelledBy("server_health", signal);
|
||||
if (remaining() <= 0) throw expired();
|
||||
}
|
||||
};
|
||||
|
||||
+21
-13
@@ -4,7 +4,11 @@ import { jsonRecordSchema } from "../domain/jsonRecord.js";
|
||||
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisInputIssue } from "../domain/analysisErrorCore.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
@@ -229,22 +233,26 @@ export const readIdaConfiguration = (
|
||||
const cancellation = regularFileCancellation(cause);
|
||||
if (cancellation !== undefined) {
|
||||
if (cause instanceof RegularFileCleanupFailure) {
|
||||
const error = new AnalysisCancelledError("ida:registration", {
|
||||
cause: cancellation.reason,
|
||||
cleanup: {
|
||||
reason:
|
||||
cause.cleanupCause instanceof Error
|
||||
? cause.cleanupCause.message
|
||||
: String(cause.cleanupCause),
|
||||
resources: [path],
|
||||
const error = cancelledBy(
|
||||
"ida:registration",
|
||||
carriedReason(cancellation.reason),
|
||||
{
|
||||
cleanup: {
|
||||
reason:
|
||||
cause.cleanupCause instanceof Error
|
||||
? cause.cleanupCause.message
|
||||
: String(cause.cleanupCause),
|
||||
resources: [path],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
closeLease?.bindCleanupError(error);
|
||||
throw error;
|
||||
}
|
||||
throw new AnalysisCancelledError("ida:registration", {
|
||||
cause: cancellation.reason,
|
||||
});
|
||||
throw cancelledBy(
|
||||
"ida:registration",
|
||||
carriedReason(cancellation.reason),
|
||||
);
|
||||
}
|
||||
if (cause instanceof RegularFileCleanupFailure) {
|
||||
const primary =
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisProtocolError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -465,8 +466,7 @@ export const createIdaMcpConnection = (
|
||||
return await request(controller.signal);
|
||||
} catch (cause: unknown) {
|
||||
if (cause === cancellation)
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
throw cancelledBy(operation, signal, {
|
||||
capturedOutput: capturedTransportOutput(),
|
||||
});
|
||||
throw cause;
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
} from "../process/RegexMatcher.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisProtocolError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
functionDossierSchema,
|
||||
@@ -131,9 +131,7 @@ export class IdaOperationRunner {
|
||||
args: Readonly<Record<string, JsonValue>>,
|
||||
): Promise<JsonValue> {
|
||||
if (this.options.signal?.aborted)
|
||||
throw new AnalysisCancelledError(tool, {
|
||||
cause: this.options.signal.reason,
|
||||
});
|
||||
throw cancelledBy(tool, this.options.signal);
|
||||
const scoped =
|
||||
this.database === undefined ? args : { ...args, database: this.database };
|
||||
const result = await this.connection.call(tool, scoped, {
|
||||
@@ -141,9 +139,7 @@ export class IdaOperationRunner {
|
||||
});
|
||||
this.raw.push({ tool, arguments: scoped, result });
|
||||
if (this.options.signal?.aborted)
|
||||
throw new AnalysisCancelledError(tool, {
|
||||
cause: this.options.signal.reason,
|
||||
});
|
||||
throw cancelledBy(tool, this.options.signal);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type BinaryTarget,
|
||||
} from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { cancelledBy } from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type {
|
||||
AnalysisClient,
|
||||
@@ -368,11 +368,7 @@ export class IdaProvider implements AnalysisProviderCandidate {
|
||||
options?: AnalysisProfileResolutionOptions,
|
||||
) {
|
||||
if (options?.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError("ida:profile", {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("ida:profile", options.signal));
|
||||
const config = await this.#configuration(options);
|
||||
if (!config.ok) return config;
|
||||
const profile = createAnalysisProfile(IDA_PROVIDER_IDENTITY, {
|
||||
|
||||
+33
-46
@@ -15,6 +15,9 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisTimeoutError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
isAborted,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -97,9 +100,6 @@ const operationSet: ReadonlySet<string> = new Set(IDA_OPERATIONS);
|
||||
|
||||
const isIdaOperation = (operation: string): operation is IdaOperation =>
|
||||
operationSet.has(operation);
|
||||
const wasCancelled = (options: ExecutionOptions | undefined): boolean =>
|
||||
options?.signal?.aborted === true;
|
||||
|
||||
interface IdaOperationFailureContext {
|
||||
runner: IdaOperationRunner | undefined;
|
||||
metadataBefore: JsonValue | undefined;
|
||||
@@ -261,12 +261,8 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
return Promise.resolve(undefined);
|
||||
const call = this.#tail.then(() => {
|
||||
if (!this.#ready || this.#closed || this.#closing) return undefined;
|
||||
if (wasCancelled(options))
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
}),
|
||||
);
|
||||
if (isAborted(options?.signal))
|
||||
return err(cancelledBy(operation, options?.signal));
|
||||
return this.#execute(operation, parameters, options, true);
|
||||
});
|
||||
this.#tail = call.then(
|
||||
@@ -275,11 +271,7 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
);
|
||||
const result = await waitForAbortable(call, options?.signal);
|
||||
return result === ABORTED
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
}),
|
||||
)
|
||||
? err(cancelledBy(operation, carriedReason(options?.signal?.reason)))
|
||||
: result;
|
||||
}
|
||||
|
||||
@@ -295,12 +287,8 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
"IDA session is closing or closed; reopen the target.",
|
||||
),
|
||||
);
|
||||
if (wasCancelled(options))
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
}),
|
||||
);
|
||||
if (isAborted(options?.signal))
|
||||
return err(cancelledBy(operation, options?.signal));
|
||||
if (operation !== "health" && !isIdaOperation(operation))
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
@@ -359,10 +347,8 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
if (!readyOnly) await this.#start(operation);
|
||||
const before = await this.#observe(options);
|
||||
failureContext.metadataBefore = before;
|
||||
if (wasCancelled(options))
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
});
|
||||
if (isAborted(options?.signal))
|
||||
throw cancelledBy(operation, options?.signal);
|
||||
if (operation === "health") return this.#health(before);
|
||||
if (!isIdaOperation(operation))
|
||||
throw new AnalysisProtocolError(
|
||||
@@ -371,20 +357,16 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
const runner = this.#runner(options);
|
||||
failureContext.runner = runner;
|
||||
const result = await runner.run(operation, parameters);
|
||||
if (wasCancelled(options))
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
});
|
||||
if (isAborted(options?.signal))
|
||||
throw cancelledBy(operation, options?.signal);
|
||||
failureContext.postOperationIdentityCheck = "started";
|
||||
const after = await this.#observe(
|
||||
options,
|
||||
(observed) => (failureContext.metadataAfter = observed),
|
||||
);
|
||||
failureContext.postOperationIdentityCheck = "verified";
|
||||
if (wasCancelled(options))
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: options?.signal?.reason,
|
||||
});
|
||||
if (isAborted(options?.signal))
|
||||
throw cancelledBy(operation, options?.signal);
|
||||
return ok(
|
||||
createAnalysisExecution(result, IDA_PROVIDER_IDENTITY, {
|
||||
rawResult: {
|
||||
@@ -483,22 +465,27 @@ export class IdaSessionClient implements AnalysisClient {
|
||||
diagnostics: cause.diagnostics,
|
||||
});
|
||||
} else if (artifactFailure?.reason === "cancelled") {
|
||||
failure = new AnalysisCancelledError(operation, {
|
||||
cause: artifactFileCancellationCause(artifactFailure),
|
||||
});
|
||||
failure = cancelledBy(
|
||||
operation,
|
||||
carriedReason(artifactFileCancellationCause(artifactFailure)),
|
||||
);
|
||||
} else if (cause instanceof AnalysisCancelledError) {
|
||||
failure = new AnalysisCancelledError(operation, {
|
||||
cause:
|
||||
failure = cancelledBy(
|
||||
operation,
|
||||
carriedReason(
|
||||
cause.cause === undefined ? options?.signal?.reason : cause.cause,
|
||||
capturedOutput: cause.capturedOutput,
|
||||
diagnostics: { ...cause.diagnostics, ...endpoint },
|
||||
partialObservation: cause.partialObservation,
|
||||
cleanup:
|
||||
cause.cleanup ??
|
||||
(cause.cleanupIncomplete
|
||||
? { reason: cause.message, resources: cause.cleanupResources }
|
||||
: undefined),
|
||||
})
|
||||
),
|
||||
{
|
||||
capturedOutput: cause.capturedOutput,
|
||||
diagnostics: { ...cause.diagnostics, ...endpoint },
|
||||
partialObservation: cause.partialObservation,
|
||||
cleanup:
|
||||
cause.cleanup ??
|
||||
(cause.cleanupIncomplete
|
||||
? { reason: cause.message, resources: cause.cleanupResources }
|
||||
: undefined),
|
||||
},
|
||||
)
|
||||
.bindCleanupDependencies([cause])
|
||||
.bindCleanupResourceOwner(cause);
|
||||
} else if (
|
||||
|
||||
@@ -8,6 +8,9 @@ import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisProtocolError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { JebOperation } from "../domain/jeb/jebAnalysis.js";
|
||||
import {
|
||||
@@ -121,9 +124,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
: waited.value;
|
||||
if (outcome !== undefined && !outcome.ok) throw outcome.error;
|
||||
if (outcome === undefined || signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("jeb_mcp_catalog", {
|
||||
cause: signal?.reason,
|
||||
});
|
||||
throw cancelledBy("jeb_mcp_catalog", carriedReason(signal?.reason));
|
||||
return outcome.value;
|
||||
};
|
||||
|
||||
@@ -184,10 +185,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
};
|
||||
|
||||
const connect = async (signal?: AbortSignal): Promise<readonly string[]> => {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("jeb_mcp_catalog", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
if (signal?.aborted === true) throw cancelledBy("jeb_mcp_catalog", signal);
|
||||
if (closing !== undefined) await closing;
|
||||
if (closed) {
|
||||
if (state !== undefined) await dispose(state);
|
||||
@@ -338,7 +336,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
|
||||
const owner = state;
|
||||
if (owner === undefined) return;
|
||||
owner.tools = undefined;
|
||||
owner.signal.abort();
|
||||
owner.signal.abort(OwnerCancellation.ownerClosed);
|
||||
if (owner.acquisition !== undefined)
|
||||
await owner.acquisition.promise.catch(() => undefined);
|
||||
if (state === owner) await dispose(owner);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
|
||||
@@ -82,9 +82,7 @@ export class JebMcpDeadline {
|
||||
cause: this.#deadline.signal.reason,
|
||||
...jebMcpTimeoutGuidance(this.phase, this.timeoutMs, this.source),
|
||||
})
|
||||
: new AnalysisCancelledError(operation, {
|
||||
cause: this.#deadline.signal.reason,
|
||||
});
|
||||
: cancelledBy(operation, this.#deadline.signal);
|
||||
}
|
||||
if (cause instanceof SdkError && cause.code === SdkErrorCode.RequestTimeout)
|
||||
return new AnalysisTimeoutError(operation, this.timeoutMs, {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisProtocolError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -251,8 +252,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
if (options?.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError(request.operation, {
|
||||
cause: options.signal.reason,
|
||||
cancelledBy(request.operation, options.signal, {
|
||||
diagnostics: { endpoint: this.#selectedEndpoint },
|
||||
}),
|
||||
);
|
||||
@@ -269,8 +269,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<Result<AnalysisExecution, AnalysisError>> {
|
||||
const cancelled = (operation: JebOperation) => {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError(operation, { cause: signal.reason });
|
||||
if (signal?.aborted === true) throw cancelledBy(operation, signal);
|
||||
};
|
||||
switch (request.operation) {
|
||||
case "inspect_jeb_client": {
|
||||
@@ -354,8 +353,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
operation: JebOperation,
|
||||
signal: AbortSignal | undefined,
|
||||
) {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError(operation, { cause: signal.reason });
|
||||
if (signal?.aborted === true) throw cancelledBy(operation, signal);
|
||||
const payload = await this.#connection.call(
|
||||
"get_client_information",
|
||||
{},
|
||||
@@ -555,7 +553,6 @@ export class JebProvider implements JebAnalysisPort {
|
||||
cause instanceof AnalysisTimeoutError
|
||||
) {
|
||||
const options = {
|
||||
cause: cause.cause,
|
||||
diagnostics: {
|
||||
...cause.diagnostics,
|
||||
endpoint: this.#selectedEndpoint,
|
||||
@@ -568,6 +565,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
cause instanceof AnalysisTimeoutError
|
||||
? new AnalysisTimeoutError(operation, cause.timeoutMs, {
|
||||
...options,
|
||||
cause: cause.cause,
|
||||
...(cause.userMessage === undefined
|
||||
? {}
|
||||
: { userMessage: cause.userMessage }),
|
||||
@@ -576,7 +574,7 @@ export class JebProvider implements JebAnalysisPort {
|
||||
: { remediationAction: cause.remediationAction }),
|
||||
...(cause.limit === undefined ? {} : { limit: cause.limit }),
|
||||
})
|
||||
: new AnalysisCancelledError(operation, options);
|
||||
: cancelledBy(operation, cause, options);
|
||||
return failure
|
||||
.bindCleanupDependencies([cause])
|
||||
.bindCleanupResourceOwner(cause);
|
||||
|
||||
+9
-10
@@ -9,10 +9,11 @@ import {
|
||||
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
@@ -116,8 +117,7 @@ export const kunaFileCleanupError = (
|
||||
const diagnostics = kunaInstallationFailureDiagnostics(failure);
|
||||
const cancellation = regularFileCancellation(failure);
|
||||
if (cancellation !== undefined)
|
||||
return new AnalysisCancelledError(operation, {
|
||||
cause: cancellation.reason,
|
||||
return cancelledBy(operation, carriedReason(cancellation.reason), {
|
||||
diagnostics,
|
||||
cleanup: {
|
||||
reason: message(failure.cleanupCause),
|
||||
@@ -246,9 +246,7 @@ export const runKunaCommand = async (
|
||||
cause instanceof AnalysisError
|
||||
? cause
|
||||
: cancellation !== undefined
|
||||
? new AnalysisCancelledError(request.operation, {
|
||||
cause: cancellation.reason,
|
||||
})
|
||||
? cancelledBy(request.operation, carriedReason(cancellation.reason))
|
||||
: new ProviderAdapterError(KUNA_PROVIDER_ID, request.operation, {
|
||||
cause,
|
||||
userMessage: `Cannot verify ${installation.executable}: ${message(cause)}`,
|
||||
@@ -488,10 +486,11 @@ const commandFailure = (
|
||||
};
|
||||
let primary: AnalysisError;
|
||||
if (cause.reason === "cancelled")
|
||||
primary = new AnalysisCancelledError(operation, {
|
||||
...details,
|
||||
cause: cause.cancellation?.reason,
|
||||
});
|
||||
primary = cancelledBy(
|
||||
operation,
|
||||
carriedReason(cause.cancellation?.reason),
|
||||
capturedOutput === undefined ? undefined : { capturedOutput },
|
||||
);
|
||||
else if (cause.reason === "timeout") {
|
||||
const { setting, ms } = request.deadline;
|
||||
const analysisSetting =
|
||||
|
||||
@@ -9,7 +9,11 @@ import { appendCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import {
|
||||
@@ -216,8 +220,7 @@ export const createKunaResourceScope = (
|
||||
const cancellation = kunaInstallationCancellation(cause);
|
||||
if (cancellation === undefined) return undefined;
|
||||
const pending = pendingLeases();
|
||||
const error = new AnalysisCancelledError(operation, {
|
||||
cause: cancellation.reason,
|
||||
const error = cancelledBy(operation, carriedReason(cancellation.reason), {
|
||||
diagnostics: cancellation.diagnostics,
|
||||
...(pending.length === 0
|
||||
? {}
|
||||
|
||||
@@ -33,8 +33,9 @@ import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -574,11 +575,7 @@ export class KunaProjectCache {
|
||||
request: KunaProjectRequest,
|
||||
): Promise<Result<KunaProject, AnalysisError>> {
|
||||
if (request.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError(KUNA_EXPORT_OPERATION, {
|
||||
cause: request.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy(KUNA_EXPORT_OPERATION, request.signal));
|
||||
const key = kunaProjectKey({
|
||||
targetSha256: request.targetSha256,
|
||||
targetBasename: basename(request.targetPath),
|
||||
@@ -954,9 +951,10 @@ export class KunaProjectCache {
|
||||
if (entry.waiters === 0) entry.controller.abort();
|
||||
resolve(
|
||||
err(
|
||||
new AnalysisCancelledError(KUNA_EXPORT_OPERATION, {
|
||||
cause: request.signal?.reason,
|
||||
}),
|
||||
cancelledBy(
|
||||
KUNA_EXPORT_OPERATION,
|
||||
carriedReason(request.signal?.reason),
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ import { basename, join } from "node:path";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
@@ -177,12 +178,14 @@ export const kunaExportFailure = (
|
||||
const { completion } = kunaCopyFailureFacts(cause);
|
||||
const primary =
|
||||
cause.reason === "cancelled" || completion?.cancellation !== undefined
|
||||
? new AnalysisCancelledError(KUNA_EXPORT_OPERATION, {
|
||||
cause:
|
||||
? cancelledBy(
|
||||
KUNA_EXPORT_OPERATION,
|
||||
carriedReason(
|
||||
completion?.cancellation === undefined
|
||||
? artifactFileCancellationCause(cause)
|
||||
: completion.cancellation.reason,
|
||||
})
|
||||
),
|
||||
)
|
||||
: new ProviderAdapterError(KUNA_PROVIDER_ID, KUNA_EXPORT_OPERATION, {
|
||||
cause,
|
||||
userMessage: `Kuna project cache failed: ${message(cause)}`,
|
||||
@@ -207,12 +210,13 @@ export const kunaExportFailure = (
|
||||
return kunaFileCleanupError(cause, KUNA_EXPORT_OPERATION);
|
||||
const cancellation = regularFileCancellation(cause);
|
||||
if (cancellation !== undefined)
|
||||
return new AnalysisCancelledError(KUNA_EXPORT_OPERATION, {
|
||||
cause: cancellation.reason,
|
||||
});
|
||||
return cancelledBy(
|
||||
KUNA_EXPORT_OPERATION,
|
||||
carriedReason(cancellation.reason),
|
||||
);
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
if (!(cause instanceof StableFileHashFailure) && signal?.aborted === true)
|
||||
return new AnalysisCancelledError(KUNA_EXPORT_OPERATION, { cause });
|
||||
return cancelledBy(KUNA_EXPORT_OPERATION, carriedReason(cause));
|
||||
return new ProviderAdapterError(KUNA_PROVIDER_ID, KUNA_EXPORT_OPERATION, {
|
||||
cause,
|
||||
userMessage: `Kuna project cache failed: ${message(cause)}`,
|
||||
|
||||
@@ -15,6 +15,8 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -112,10 +114,7 @@ const readSpan = async (
|
||||
const callerAborted = (): AbortSignal | undefined =>
|
||||
signal?.aborted === true ? signal : undefined;
|
||||
const cancelled = callerAborted();
|
||||
if (cancelled !== undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: cancelled.reason }),
|
||||
);
|
||||
if (cancelled !== undefined) return err(cancelledBy(operation, cancelled));
|
||||
const opened = await ownership.admitSpan(operation, signal, () =>
|
||||
open(path, "r"),
|
||||
);
|
||||
@@ -134,9 +133,7 @@ const readSpan = async (
|
||||
// Preserve a fully acquired span for owned finalization, which records
|
||||
// cancellation alongside its text if descriptor cleanup fails.
|
||||
if (bytesRead < length && signal?.aborted)
|
||||
throw (spanCancellation = new AnalysisCancelledError(operation, {
|
||||
cause: signal.reason,
|
||||
}));
|
||||
throw (spanCancellation = cancelledBy(operation, signal));
|
||||
},
|
||||
);
|
||||
if (buffer.length !== length)
|
||||
@@ -204,9 +201,7 @@ const readSpan = async (
|
||||
(cause instanceof RegularFileOperationCancelled ||
|
||||
cause instanceof RegularFileCancelled)
|
||||
)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: aborted.reason }),
|
||||
);
|
||||
return err(cancelledBy(operation, aborted));
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
@@ -267,9 +262,10 @@ const spanCloseFailure = (
|
||||
ownership.bindSpanCleanup(failure.owner, marker);
|
||||
if (failure.completionCancellation !== undefined)
|
||||
return analysisErrorWithCleanupFailure(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: failure.completionCancellation.reason,
|
||||
}),
|
||||
cancelledBy(
|
||||
operation,
|
||||
carriedReason(failure.completionCancellation.reason),
|
||||
),
|
||||
marker,
|
||||
operation,
|
||||
).bindCleanupDependencies([marker]);
|
||||
@@ -453,9 +449,7 @@ export class KunaProjectReader {
|
||||
);
|
||||
const index = await waitForAbortable(this.#assembly, signal);
|
||||
if (index === ABORTED)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
);
|
||||
return err(cancelledBy(operation, carriedReason(signal?.reason)));
|
||||
return index.ok
|
||||
? index
|
||||
: err(
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
ProviderAvailability,
|
||||
ProviderTargetSupport,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { cancelledBy } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
createAnalysisProfile,
|
||||
type AnalysisProfileCommitment,
|
||||
@@ -197,11 +197,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
|
||||
options?: AnalysisProfileResolutionOptions,
|
||||
) {
|
||||
if (options?.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError("kuna:profile", {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("kuna:profile", options.signal));
|
||||
let installation: Awaited<ReturnType<typeof inspectKunaInstallation>>;
|
||||
const resourceScope = options?.resourceScope;
|
||||
if (resourceScope === undefined)
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { SharedStartupAttempt } from "../process/SharedStartupAttempt.js";
|
||||
@@ -54,21 +56,13 @@ export class KunaQueryWork {
|
||||
): Promise<Result<KunaTargetCommandSnapshot, AnalysisError>> {
|
||||
const local = await this.#nativeSlots.acquire(request.signal);
|
||||
if (local === undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(request.operation, {
|
||||
cause: request.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy(request.operation, request.signal));
|
||||
let release: (() => void) | undefined;
|
||||
try {
|
||||
const foreground = await this.lifetime.runSlots.acquire(request.signal);
|
||||
if (foreground === undefined) {
|
||||
local();
|
||||
return err(
|
||||
new AnalysisCancelledError(request.operation, {
|
||||
cause: request.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy(request.operation, request.signal));
|
||||
}
|
||||
release = () => {
|
||||
foreground();
|
||||
@@ -133,10 +127,7 @@ export class KunaQueryWork {
|
||||
signal: AbortSignal,
|
||||
): ReturnType<typeof matchRegexValues> {
|
||||
const release = await this.#regexSlots.acquire(signal);
|
||||
if (release === undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(input.operation, { cause: signal.reason }),
|
||||
);
|
||||
if (release === undefined) return err(cancelledBy(input.operation, signal));
|
||||
try {
|
||||
return await matchRegexValues({
|
||||
...input,
|
||||
@@ -177,9 +168,7 @@ export class KunaQueryWork {
|
||||
// Final departure retains all producer failures, including a typed
|
||||
// cancellation with captured facts or incomplete cleanup.
|
||||
if (waited.status === "stopped" && !waited.value.ok) return waited.value;
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
);
|
||||
return err(cancelledBy(operation, carriedReason(signal?.reason)));
|
||||
}
|
||||
|
||||
/** Retire abandoned work before retrying, retaining its physical failures. */
|
||||
@@ -195,8 +184,6 @@ export class KunaQueryWork {
|
||||
result.error.cleanupIncomplete)
|
||||
)
|
||||
return result;
|
||||
return signal?.aborted
|
||||
? err(new AnalysisCancelledError(operation, { cause: signal.reason }))
|
||||
: ok(null);
|
||||
return signal?.aborted ? err(cancelledBy(operation, signal)) : ok(null);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,10 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
isAborted,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import {
|
||||
@@ -217,7 +221,7 @@ const waitFor = async <T>(
|
||||
): Promise<Result<T, AnalysisError>> => {
|
||||
const result = await waitForAbortable(work, signal);
|
||||
return result === ABORTED
|
||||
? err(new AnalysisCancelledError(operation, { cause: signal?.reason }))
|
||||
? err(cancelledBy(operation, carriedReason(signal?.reason)))
|
||||
: result;
|
||||
};
|
||||
|
||||
@@ -230,9 +234,6 @@ const isUnavailableProject = (cause: unknown): boolean =>
|
||||
cause.code === "ESTALE");
|
||||
|
||||
/** Read through a call so a check before an await does not narrow later ones. */
|
||||
const cancelled = (signal: AbortSignal | undefined): boolean =>
|
||||
signal?.aborted === true;
|
||||
|
||||
const message = (cause: unknown): string =>
|
||||
cause instanceof Error ? cause.message : String(cause);
|
||||
|
||||
@@ -507,12 +508,8 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
// under #admitSpan, which shares its queue with that descriptor's close.
|
||||
const pendingDescriptors = await this.#pendingDescriptorCleanup(operation);
|
||||
if (pendingDescriptors !== undefined) return err(pendingDescriptors);
|
||||
if (cancelled(options.signal))
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options.signal?.reason,
|
||||
}),
|
||||
);
|
||||
if (isAborted(options.signal))
|
||||
return err(cancelledBy(operation, options.signal));
|
||||
if (operation !== "health" && !isKunaOperation(operation))
|
||||
return err(
|
||||
kunaUnavailable(
|
||||
@@ -599,12 +596,8 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
);
|
||||
if (!answer.ok)
|
||||
return err(kunaQueryFailureForCaller(answer.error, operation));
|
||||
if (cancelled(options.signal))
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options.signal?.reason,
|
||||
}),
|
||||
);
|
||||
if (isAborted(options.signal))
|
||||
return err(cancelledBy(operation, options.signal));
|
||||
return ok(
|
||||
this.#execution(
|
||||
kunaLimitations(operation, this.#deadline, this.#heapRequest),
|
||||
@@ -613,12 +606,15 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cancelled(options.signal)) {
|
||||
if (isAborted(options.signal)) {
|
||||
const cancellation = regularFileCancellation(cause);
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: cancellation === undefined ? cause : cancellation.reason,
|
||||
}),
|
||||
cancelledBy(
|
||||
operation,
|
||||
carriedReason(
|
||||
cancellation === undefined ? cause : cancellation.reason,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return err(failure(operation, cause));
|
||||
@@ -633,7 +629,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
if (this.#closed) return ok(null);
|
||||
if (!this.#closing) {
|
||||
this.#closing = true;
|
||||
this.#session.abort();
|
||||
this.#session.abort(OwnerCancellation.ownerClosed);
|
||||
}
|
||||
// Cancellation starts process shutdown; its completion proves whether it
|
||||
// is safe to remove private copies and exposes any retained cleanup owner.
|
||||
@@ -1551,10 +1547,8 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
installation: KunaInstallation,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<SingleFunctionOutcome> {
|
||||
if (cancelled(signal))
|
||||
return Promise.resolve(
|
||||
err(new AnalysisCancelledError(operation, { cause: signal?.reason })),
|
||||
);
|
||||
if (isAborted(signal))
|
||||
return Promise.resolve(err(cancelledBy(operation, signal)));
|
||||
const kept = this.#singleFunctionTexts.get(address);
|
||||
if (kept !== undefined) {
|
||||
this.#singleFunctionTexts.delete(address);
|
||||
@@ -1598,11 +1592,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
if (!waiting) return;
|
||||
leave();
|
||||
if (shared.waiters > 0) {
|
||||
resolve(
|
||||
err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
),
|
||||
);
|
||||
resolve(err(cancelledBy(operation, carriedReason(signal?.reason))));
|
||||
return;
|
||||
}
|
||||
// A run whose waiters all left winds down; a new request starts afresh.
|
||||
@@ -1654,15 +1644,10 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
? await this.#runSlots.acquire(signal)
|
||||
: await this.#admitSingleFunction(signal);
|
||||
if (admitted === EXPORT_READY) return admitted;
|
||||
if (admitted === undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal.reason }),
|
||||
);
|
||||
if (admitted === undefined) return err(cancelledBy(operation, signal));
|
||||
if (signal.aborted) {
|
||||
admitted();
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal.reason }),
|
||||
);
|
||||
return err(cancelledBy(operation, signal));
|
||||
}
|
||||
let deadline: KunaDeadline;
|
||||
try {
|
||||
@@ -1984,9 +1969,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
}
|
||||
const waited = await attempt.wait(signal);
|
||||
if (waited.status === "cancelled")
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
);
|
||||
return err(cancelledBy(operation, carriedReason(waited.reason)));
|
||||
if (!waited.value.ok) {
|
||||
// A fresh caller can arrive while the previous final waiter joins stop.
|
||||
// Retry only proven cancellation, never an authored physical failure.
|
||||
@@ -1999,10 +1982,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return this.#ensureInstallation(operation, signal);
|
||||
return err(kunaQueryFailureForCaller(waited.value.error, operation));
|
||||
}
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal.reason }),
|
||||
);
|
||||
if (signal?.aborted) return err(cancelledBy(operation, signal));
|
||||
return waited.value;
|
||||
}
|
||||
|
||||
@@ -2255,20 +2235,12 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
return this.#enqueueSpan(async () => {
|
||||
const cancelledBefore = callerAborted();
|
||||
if (cancelledBefore !== undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: cancelledBefore.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy(operation, cancelledBefore));
|
||||
const pending = await this.#pendingDescriptorCleanup(operation);
|
||||
if (pending !== undefined) return err(pending);
|
||||
const cancelledAfter = callerAborted();
|
||||
if (cancelledAfter !== undefined)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: cancelledAfter.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy(operation, cancelledAfter));
|
||||
return ok(await open());
|
||||
});
|
||||
}
|
||||
@@ -2431,11 +2403,7 @@ export class KunaSessionClient implements AnalysisClient {
|
||||
const exported = await this.#exportInventory;
|
||||
if (exported !== undefined) return ok(exported);
|
||||
if (this.#session.signal.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("list_procedures", {
|
||||
cause: this.#session.signal.reason,
|
||||
}),
|
||||
);
|
||||
return err(cancelledBy("list_procedures", this.#session.signal));
|
||||
let deadline: KunaDeadline;
|
||||
try {
|
||||
deadline = kunaFunctionDeadline(installation.configuration);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { RegexConfigurationSnapshot } from "../process/RegexMatcher.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
cancelledBy,
|
||||
isAborted,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
analysisSearchPredicate,
|
||||
@@ -44,10 +45,7 @@ export class KunaStringQueries {
|
||||
AnalysisError
|
||||
>
|
||||
> {
|
||||
if (signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
);
|
||||
if (signal?.aborted === true) return err(cancelledBy(operation, signal));
|
||||
const address =
|
||||
operation === "list_strings" && parameters.address !== undefined
|
||||
? normalizeKunaAddress(parameters.address)
|
||||
@@ -67,10 +65,7 @@ export class KunaStringQueries {
|
||||
const inventory = await this.#load(installation, operation, signal);
|
||||
if (!inventory.ok)
|
||||
return err(kunaQueryFailureForCaller(inventory.error, operation));
|
||||
if (this.#cancelled(signal))
|
||||
return err(
|
||||
new AnalysisCancelledError(operation, { cause: signal?.reason }),
|
||||
);
|
||||
if (isAborted(signal)) return err(cancelledBy(operation, signal));
|
||||
const available = inventory.value.items;
|
||||
let items: KunaStringItem[] | undefined;
|
||||
if (address !== undefined)
|
||||
@@ -162,10 +157,6 @@ export class KunaStringQueries {
|
||||
return this.work.wait(this.#pending, signal, operation);
|
||||
}
|
||||
|
||||
#cancelled(signal: AbortSignal | undefined): boolean {
|
||||
return signal?.aborted === true;
|
||||
}
|
||||
|
||||
/** Called only after session-owned work has drained. */
|
||||
clear(): void {
|
||||
this.#inventory = undefined;
|
||||
|
||||
@@ -14,7 +14,9 @@ import type { Writable } from "node:stream";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
cancelledBy,
|
||||
carriedReason,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { KunaDocumentError } from "./KunaDocumentError.js";
|
||||
@@ -108,11 +110,11 @@ export class KunaTargetCommands {
|
||||
request.releaseRun?.();
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCancelledError(
|
||||
cancelledBy(
|
||||
request.operation,
|
||||
request.signal.aborted
|
||||
? { cause: request.signal.reason }
|
||||
: undefined,
|
||||
? request.signal
|
||||
: OwnerCancellation.ownerClosed,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -125,11 +127,11 @@ export class KunaTargetCommands {
|
||||
if (this.#closing || this.lifetime.signal.aborted)
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCancelledError(
|
||||
cancelledBy(
|
||||
"kuna:workspace",
|
||||
this.lifetime.signal.aborted
|
||||
? { cause: this.lifetime.signal.reason }
|
||||
: undefined,
|
||||
? this.lifetime.signal
|
||||
: OwnerCancellation.ownerClosed,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -149,11 +151,11 @@ export class KunaTargetCommands {
|
||||
if (this.#closing || this.lifetime.signal.aborted)
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCancelledError(
|
||||
cancelledBy(
|
||||
operation,
|
||||
this.lifetime.signal.aborted
|
||||
? { cause: this.lifetime.signal.reason }
|
||||
: undefined,
|
||||
? this.lifetime.signal
|
||||
: OwnerCancellation.ownerClosed,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -327,7 +329,7 @@ export class KunaTargetCommands {
|
||||
return err(failure);
|
||||
}
|
||||
if (this.#closing && cause instanceof ArtifactReaderFailure)
|
||||
return err(new AnalysisCancelledError(operation, { cause }));
|
||||
return err(cancelledBy(operation, carriedReason(cause)));
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
@@ -425,9 +427,7 @@ export class KunaTargetCommands {
|
||||
: undefined);
|
||||
const primary =
|
||||
cancellation !== undefined
|
||||
? new AnalysisCancelledError("kuna:workspace", {
|
||||
cause: cancellation.reason,
|
||||
})
|
||||
? cancelledBy("kuna:workspace", carriedReason(cancellation.reason))
|
||||
: completion === undefined
|
||||
? new ProviderAdapterError(KUNA_PROVIDER_ID, "kuna:workspace", {
|
||||
cause: primaryCause,
|
||||
@@ -482,9 +482,7 @@ export class KunaTargetCommands {
|
||||
this.lifetime.signal.aborted
|
||||
? analysisErrorWithCancellation(
|
||||
failure,
|
||||
new AnalysisCancelledError("kuna:workspace", {
|
||||
cause: this.lifetime.signal.reason,
|
||||
}),
|
||||
cancelledBy("kuna:workspace", this.lifetime.signal),
|
||||
)
|
||||
: failure,
|
||||
);
|
||||
|
||||
@@ -6,14 +6,19 @@ import {
|
||||
STARTUP_OWNER_CLOSED,
|
||||
} from "./SharedStartupAttempt.js";
|
||||
|
||||
it("keeps shared startup running for an interested peer", async () => {
|
||||
it("keeps shared startup running for an interested peer and returns the departing caller's reason", async () => {
|
||||
const completed = createDeferred<string>();
|
||||
const attempt = new SharedStartupAttempt(() => completed.promise);
|
||||
const controller = new AbortController();
|
||||
const departing = attempt.wait(controller.signal);
|
||||
const staying = attempt.wait();
|
||||
controller.abort();
|
||||
await expect(departing).resolves.toEqual({ status: "cancelled" });
|
||||
const reason = { caller: "departing" };
|
||||
controller.abort(reason);
|
||||
const departed = await departing;
|
||||
expect(departed).toEqual({ status: "cancelled", reason });
|
||||
expect(departed.status === "cancelled" ? departed.reason : undefined).toBe(
|
||||
reason,
|
||||
);
|
||||
expect(attempt.signal.aborted).toBe(false);
|
||||
completed.resolve("ready");
|
||||
await expect(staying).resolves.toEqual({
|
||||
@@ -28,9 +33,13 @@ it("retains the final cleanup result when all callers cancel together", async ()
|
||||
const first = new AbortController();
|
||||
const last = new AbortController();
|
||||
const waits = [attempt.wait(first.signal), attempt.wait(last.signal)];
|
||||
first.abort();
|
||||
const firstReason = new Error("first caller left");
|
||||
first.abort(firstReason);
|
||||
last.abort();
|
||||
await expect(waits[0]).resolves.toEqual({ status: "cancelled" });
|
||||
await expect(waits[0]).resolves.toEqual({
|
||||
status: "cancelled",
|
||||
reason: firstReason,
|
||||
});
|
||||
expect(attempt.signal.aborted).toBe(true);
|
||||
expect(attempt.settled).toBe(false);
|
||||
const failure = { cleanupIncomplete: true };
|
||||
|
||||
@@ -6,7 +6,7 @@ export const STARTUP_OWNER_CLOSED = Symbol("provider startup owner closed");
|
||||
/** The final departing caller retains the producer's complete cleanup result. */
|
||||
export type SharedStartupWait<Value> =
|
||||
| { readonly status: "completed"; readonly value: Value }
|
||||
| { readonly status: "cancelled" }
|
||||
| { readonly status: "cancelled"; readonly reason: unknown }
|
||||
| { readonly status: "stopped"; readonly value: Value };
|
||||
|
||||
type StartupSettlement<Value> =
|
||||
@@ -68,7 +68,8 @@ export class SharedStartupAttempt<Value> {
|
||||
try {
|
||||
const waited = await waitForAbortable(this.promise, signal);
|
||||
if (waited !== ABORTED) return { status: "completed", value: waited };
|
||||
if (this.#waiters > 1) return { status: "cancelled" };
|
||||
if (this.#waiters > 1)
|
||||
return { status: "cancelled", reason: signal?.reason };
|
||||
if (!this.settled) this.#controller.abort(signal?.reason);
|
||||
return { status: "stopped", value: await this.promise };
|
||||
} finally {
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
cancelledBy,
|
||||
OwnerCancellation,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -93,7 +95,8 @@ export class SqliteDatabaseProvider implements SqliteDatabasePort {
|
||||
cause: options.signal.reason,
|
||||
}),
|
||||
);
|
||||
if (this.#closed) return err(new AnalysisCancelledError(operation));
|
||||
if (this.#closed)
|
||||
return err(cancelledBy(operation, OwnerCancellation.ownerClosed));
|
||||
if (this.#active.size >= SQLITE_DATABASE_LIMITS.concurrentInspections)
|
||||
return err(
|
||||
new AnalysisResourceConstraintError(
|
||||
@@ -278,7 +281,8 @@ export class SqliteDatabaseProvider implements SqliteDatabasePort {
|
||||
throw new AnalysisCancelledError(operation, {
|
||||
cause: options.signal.reason,
|
||||
});
|
||||
if (this.#closed) throw new AnalysisCancelledError(operation);
|
||||
if (this.#closed)
|
||||
throw cancelledBy(operation, OwnerCancellation.ownerClosed);
|
||||
root = await this.createRuntime();
|
||||
phase = "artifact-read";
|
||||
const snapshot = await captureSqliteDatabaseSnapshot(
|
||||
|
||||
Reference in new issue
Block a user