refactor(native): build debugger and native cancellations with a required reason (#10599)

* 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

* refactor(native): build debugger and native cancellations with a required reason

Migrate every AnalysisCancelledError construction under src/native,
src/gdb and src/rizin to cancelledBy and delete Rizin's local
rizinCancellation constructor; its reason choice becomes a
CancellationReason. GDB close aborts active startups with
OwnerCancellation.ownerClosed.

* test(ci): cover the counted-baseline comparison behind the architecture guard

* refactor(native): drop an unused cancellation import from native UI observation
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 18:07:27 +08:00
1 parent f6752bbe2c
commit 57f75bb723
16 files changed
+229 -252

No files matched your search

@@ -79,22 +79,8 @@
"src/application/releaseExposure/ReleaseExposureService.ts": 3,
"src/application/reverse/ReverseEngineeringService.ts": 12,
"src/application/wasm/WasmInterfaceComparisonService.ts": 2,
"src/gdb/GdbSessionManager.ts": 13,
"src/native/AppleDispatchMetadata.ts": 3,
"src/native/LldbCallTracer.ts": 2,
"src/native/NativeCallObservation.ts": 2,
"src/native/NativeCallTarget.ts": 2,
"src/native/NativeCommandAnalysisError.ts": 1,
"src/native/NativeMacOSProvider.ts": 8,
"src/native/NativePlistSource.ts": 2,
"src/native/NativeUiInterruptionControl.ts": 1,
"src/native/NativeUiObservation.ts": 1,
"src/native/pe/PeBinaryFailure.ts": 1,
"src/native/pwndbg/PwndbgCoreContext.ts": 3,
"src/native/pwntools/PwntoolsDecoder.ts": 8,
"src/native/pwntools/PwntoolsRecordedCrashProvider.ts": 1,
"src/process/ownedDebuggerSession.ts": 1,
"src/rizin/RizinDebugSessionManager.ts": 1
"src/gdb/GdbSessionManager.ts": 1,
"src/process/ownedDebuggerSession.ts": 1
},
"parser-format-failure-class": {
"src/browser/assets/WebArtifactReads.ts": 1,
+27 -37
View File
@@ -27,6 +27,7 @@ import {
AnalysisTimeoutError,
cancelledBy,
OwnerCancellation,
carriedReason,
} from "../domain/analysisErrorCore.js";
import {
GDB_SESSION_TOOLS,
@@ -137,8 +138,7 @@ const cancelledWithCallerReason = (
signal: AbortSignal,
previous?: AnalysisCancelledError,
): AnalysisCancelledError => {
const cancellation = new AnalysisCancelledError(operation, {
cause: signal.reason,
const cancellation = cancelledBy(operation, signal, {
cleanupPrimary: previous,
...(previous?.diagnostics === undefined
? {}
@@ -550,12 +550,7 @@ export class GdbSessionManager {
AnalysisError
>
> {
if (signal?.aborted)
return err(
new AnalysisCancelledError("start_gdb_session", {
cause: signal.reason,
}),
);
if (signal?.aborted) return err(cancelledBy("start_gdb_session", signal));
const occupied = this.#sessions.occupied();
if (occupied >= this.#maxSessions.value)
return err(
@@ -639,7 +634,7 @@ export class GdbSessionManager {
(signal?.aborted && cause === signal.reason) ||
cause instanceof AnalysisCancelledError
)
return err(new AnalysisCancelledError("start_gdb_session", { cause }));
return err(cancelledBy("start_gdb_session", carriedReason(cause)));
const refusal = classifyLaunchFailure(cause);
if (refusal !== undefined && cause instanceof Error)
return err(
@@ -743,11 +738,7 @@ export class GdbSessionManager {
session.startupResolve = resolve;
session.startupReject = reject;
startupAbortListener = () =>
reject(
new AnalysisCancelledError("start_gdb_session", {
cause: signal?.reason,
}),
);
reject(cancelledBy("start_gdb_session", signal));
signal?.addEventListener("abort", startupAbortListener, {
once: true,
});
@@ -773,10 +764,7 @@ export class GdbSessionManager {
);
startupTimer.unref();
});
if (signal?.aborted)
throw new AnalysisCancelledError("start_gdb_session", {
cause: signal.reason,
});
if (signal?.aborted) throw cancelledBy("start_gdb_session", signal);
// -gdb-version loads nothing, so it may precede the auto-load setting.
const reported = await this.#request(
session,
@@ -847,16 +835,10 @@ export class GdbSessionManager {
);
session.inferiorTtyVerified = true;
}
if (signal?.aborted)
throw new AnalysisCancelledError("start_gdb_session", {
cause: signal.reason,
});
if (signal?.aborted) throw cancelledBy("start_gdb_session", signal);
// Give queued exit notifications a turn before publishing startup.
await new Promise<void>((resolve) => setImmediate(resolve));
if (signal?.aborted)
throw new AnalysisCancelledError("start_gdb_session", {
cause: signal.reason,
});
if (signal?.aborted) throw cancelledBy("start_gdb_session", signal);
if (session.exited || session.protocolError !== undefined)
throw new Error(
session.protocolError ?? "GDB exited during startup readiness.",
@@ -965,14 +947,14 @@ export class GdbSessionManager {
cleanupResources: [session.id],
});
const cleanup = { reason: stopped.reason, resources: [session.id] };
const details = {
cause,
const detailsWithoutCause = {
capturedOutput,
cleanup,
...(startupEvidence === undefined
? {}
: { partialObservation: startupEvidence }),
};
const details = { cause, ...detailsWithoutCause };
if (cause instanceof AnalysisTimeoutError)
return err(
retainCleanup(
@@ -984,7 +966,13 @@ export class GdbSessionManager {
);
if (cause instanceof AnalysisCancelledError)
return err(
retainCleanup(new AnalysisCancelledError(cause.operation, details)),
retainCleanup(
cancelledBy(
cause.operation,
carriedReason(cause),
detailsWithoutCause,
),
),
);
const startupFailureMessage = gdbErrorMessage(cause);
return err(
@@ -998,16 +986,20 @@ export class GdbSessionManager {
),
);
}
const errorDetails = {
cause,
const errorDetailsWithoutCause = {
capturedOutput,
...(startupEvidence === undefined
? {}
: { partialObservation: startupEvidence }),
};
const errorDetails = { cause, ...errorDetailsWithoutCause };
const error =
cause instanceof AnalysisCancelledError
? new AnalysisCancelledError("start_gdb_session", errorDetails)
? cancelledBy(
"start_gdb_session",
carriedReason(cause),
errorDetailsWithoutCause,
)
: cause instanceof AnalysisTimeoutError
? new AnalysisTimeoutError(
cause instanceof GdbCommandTimeoutError
@@ -1224,7 +1216,7 @@ export class GdbSessionManager {
const error =
cause instanceof AnalysisError
? cause
: new AnalysisCancelledError("gdb_console", { cause });
: cancelledBy("gdb_console", carriedReason(cause));
if (error.cleanupIncomplete) this.#sessions.retain(session, error);
return err(error);
}
@@ -2060,8 +2052,7 @@ export class GdbSessionManager {
void this.#stopDebuggerOnly(session).then((stopped) => {
if (stopped.status !== "incomplete") {
pending.reject(
new AnalysisCancelledError("gdb_console", {
cause: signal?.reason,
cancelledBy("gdb_console", carriedReason(signal?.reason), {
partialObservation: evidence,
}),
);
@@ -2078,8 +2069,7 @@ export class GdbSessionManager {
{ cause: cancellation },
);
pending.reject(
new AnalysisCancelledError("gdb_console", {
cause: signal?.reason,
cancelledBy("gdb_console", carriedReason(signal?.reason), {
cleanup: {
reason: stopped.reason,
resources: [session.id],
+12 -11
View File
@@ -18,12 +18,13 @@ import {
type BinaryTarget,
} from "../domain/binaryTargetTypes.js";
import {
AnalysisCancelledError,
AnalysisArtifactChangedError,
AnalysisAccessDeniedError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import {
ArtifactFileCleanupFailure,
@@ -369,10 +370,8 @@ export const inspectAppleDispatchMetadata = async (
}
: { maximum_bytes: options.maxInputBytes, source: "caller" as const };
const maximumBytes = admission.maximum_bytes;
const cancelled = () =>
new AnalysisCancelledError(OPERATION, {
cause: signal?.reason,
});
// Called only after the caller's signal aborted.
const cancelled = () => cancelledBy(OPERATION, carriedReason(signal?.reason));
const { bytes, digest } = await withArtifactRegularFile(
target.path,
resources,
@@ -438,9 +437,10 @@ export const appleDispatchMetadataFailure = (
)
: physical?.completionCancellation === undefined
? undefined
: new AnalysisCancelledError(OPERATION, {
cause: physical.completionCancellation.reason,
});
: cancelledBy(
OPERATION,
carriedReason(physical.completionCancellation.reason),
);
const failure = new ProviderCleanupError(
IDENTITY.id,
cause.cleanup.resources,
@@ -487,9 +487,10 @@ const appleDispatchMetadataPrimaryFailure = (
{ cause },
);
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
);
if (
cause instanceof ArtifactReaderFailure &&
cause.reason === "unavailable"
+16 -16
View File
@@ -15,10 +15,11 @@ import {
} from "../domain/native/nativeCallCaptureLimits.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import type {
@@ -788,15 +789,13 @@ export class LldbCallTracer implements NativeCallTracer {
);
};
if (exited.kind === "cancelled")
return failureWithPartial(
(partialObservation) =>
new AnalysisCancelledError(OPERATION, {
cause: signal?.reason,
...(lifecycleCleanup === undefined
? {}
: { cleanup: lifecycleCleanup }),
partialObservation,
}),
return failureWithPartial((partialObservation) =>
cancelledBy(OPERATION, carriedReason(signal?.reason), {
...(lifecycleCleanup === undefined
? {}
: { cleanup: lifecycleCleanup }),
partialObservation,
}),
);
if (exited.kind === "timeout")
return failureWithPartial(
@@ -1287,8 +1286,7 @@ const lldbPreparationFailure = (
): AnalysisError => {
const marker = cause.cleanupFailure;
const compiler = cause.compilerOutcome;
const options = {
cause,
const optionsWithoutCause = {
...(cause.capturedOutput === undefined
? {}
: { capturedOutput: cause.capturedOutput }),
@@ -1318,13 +1316,15 @@ const lldbPreparationFailure = (
}),
},
};
const options = { cause, ...optionsWithoutCause };
const error =
cause.cancellation === undefined
? new ProviderAdapterError(PROVIDER, OPERATION, options)
: new AnalysisCancelledError(OPERATION, {
...options,
cause: cause.cancellation.reason,
});
: cancelledBy(
OPERATION,
carriedReason(cause.cancellation.reason),
optionsWithoutCause,
);
return marker === undefined ? error : error.bindCleanupDependencies([marker]);
};
+3 -7
View File
@@ -1,8 +1,8 @@
import { nativeCallCaptureLimits } from "../domain/native/nativeCallCaptureLimits.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
cancelledBy,
} from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
@@ -70,10 +70,7 @@ export const observeNativeCalls = async (
return await resources.run(async () => {
const selected = await verifyNativeCallTarget(target, resources, signal);
if (!selected.ok) return selected;
if (signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: signal.reason }),
);
if (signal?.aborted) return err(cancelledBy(OPERATION, signal));
try {
const traced = await tracer.trace(
{
@@ -87,8 +84,7 @@ export const observeNativeCalls = async (
if (!traced.ok) return traced;
return signal?.aborted
? err(
new AnalysisCancelledError(OPERATION, {
cause: signal.reason,
cancelledBy(OPERATION, signal, {
partialObservation: projectPartialNativeCalls(
target,
parsed.data,
+8 -8
View File
@@ -1,7 +1,7 @@
import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
@@ -54,16 +54,17 @@ export const nativeCallTargetFailure = (
cause instanceof RegularFileOperationCancelled ||
cause instanceof RegularFileCancelled
)
return new AnalysisCancelledError(OPERATION, {
cause: cause.reason,
...(cause instanceof RegularFileCancelled
return cancelledBy(
OPERATION,
carriedReason(cause.reason),
cause instanceof RegularFileCancelled
? {
diagnostics: {
completed_hash: jsonValueSchema.parse(cause.outcome.value),
},
}
: {}),
});
: {},
);
const physical = stableFileHashCause(cause);
if (physical instanceof RegularFileChangedError)
return new EvidenceIntegrityError(
@@ -125,8 +126,7 @@ export const verifyNativeCallTarget = async (
? nativeCallTargetFailure(cause.outcome.cause, target.path)
: cancellation === undefined
? undefined
: new AnalysisCancelledError(OPERATION, {
cause: cancellation.reason,
: cancelledBy(OPERATION, carriedReason(cancellation.reason), {
diagnostics: completed,
});
const framed = new ArtifactFileCleanupFailure(cause, cleanupLease);
+15 -9
View File
@@ -10,10 +10,11 @@ import {
} from "../providers/OwnedCommandCleanup.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js";
@@ -163,11 +164,11 @@ export const translateNativeCommandFailure = (
reason: failure.cleanupFailure,
resources: [...failure.cleanupResources],
};
const errorOptions = {
cause: failure,
const errorOptionsWithoutCause = {
...(capturedOutput === undefined ? {} : { capturedOutput }),
...(cleanup === undefined ? {} : { cleanup }),
};
const errorOptions = { cause: failure, ...errorOptionsWithoutCause };
if (failure.reason === "unavailable") {
const reason =
presentation.unavailableMessage ??
@@ -181,12 +182,17 @@ export const translateNativeCommandFailure = (
}
if (failure.reason === "cancelled")
return bindCleanup(
new AnalysisCancelledError(operation, {
...errorOptions,
...(failure.cancellation === undefined
? {}
: { cause: failure.cancellation.reason }),
}),
cancelledBy(
operation,
carriedReason(
failure.cancellation === undefined
? failure
: failure.cancellation.reason,
),
{
...errorOptionsWithoutCause,
},
),
);
if (failure.reason === "timeout")
return bindCleanup(
+29 -38
View File
@@ -76,6 +76,8 @@ import {
AnalysisOutputError,
AnalysisUnsupportedTargetError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import {
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
@@ -287,9 +289,7 @@ class NativeMacOSClient implements AnalysisClient {
),
);
if (options?.signal?.aborted === true)
return err(
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
);
return err(cancelledBy(operation, options.signal));
if (operation === "health")
return ok(createAnalysisExecution(null, IDENTITY));
if (this.activeTarget === undefined && operation !== "inspect_plist")
@@ -1487,12 +1487,7 @@ class NativeMacOSClient implements AnalysisClient {
): Promise<Result<NativeObservation, AnalysisError>> {
try {
return await this.#machoOperandResources.run(async () => {
if (signal?.aborted)
return err(
new AnalysisCancelledError("inspect_macho", {
cause: signal.reason,
}),
);
if (signal?.aborted) return err(cancelledBy("inspect_macho", signal));
let root: Pick<PrivateRuntimeRoot, "path" | "close">;
try {
root = await (this.privateRuntimeRootFactory?.() ??
@@ -1524,11 +1519,7 @@ class NativeMacOSClient implements AnalysisClient {
const operand = join(root.path, "artifact.macho");
await symlink(this.target.path, operand);
result = signal?.aborted
? err(
new AnalysisCancelledError("inspect_macho", {
cause: signal.reason,
}),
)
? err(cancelledBy("inspect_macho", signal))
: await inspect(operand);
} catch (cause: unknown) {
result = err(
@@ -1558,8 +1549,7 @@ class NativeMacOSClient implements AnalysisClient {
new AnalysisCleanupLeaseGroup(cleanup, [released.lease]);
const primary = result.ok
? signal?.aborted
? new AnalysisCancelledError("inspect_macho", {
cause: signal.reason,
? cancelledBy("inspect_macho", signal, {
partialObservation: inspectMachoSchema.parse(
result.value.result,
),
@@ -1578,8 +1568,7 @@ class NativeMacOSClient implements AnalysisClient {
}
if (signal?.aborted && result.ok)
return err(
new AnalysisCancelledError("inspect_macho", {
cause: signal.reason,
cancelledBy("inspect_macho", signal, {
partialObservation: inspectMachoSchema.parse(result.value.result),
}),
);
@@ -1765,9 +1754,8 @@ const nativeMachoSourcePrimary = (
: undefined;
const completedFailure = nativeMachoCompletedFailure(completed);
const previous = failed ?? completedFailure;
const options = {
cause,
...(previous === undefined
const optionsWithoutCause =
previous === undefined
? {}
: {
diagnostics: {
@@ -1775,22 +1763,24 @@ const nativeMachoSourcePrimary = (
projectAnalysisError(previous),
),
},
}),
};
};
const options = { cause, ...optionsWithoutCause };
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled") {
// A returned failure is already the producer's physical outcome. A later
// cancelled identity check must not replace its output or cleanup owner.
if (completedFailure !== undefined)
return analysisErrorWithCancellation(
completedFailure,
new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
}),
cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
),
);
return new AnalysisCancelledError(operation, {
...options,
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
optionsWithoutCause,
);
}
if (cause instanceof ArtifactReaderFailure && cause.reason === "format")
return new AnalysisOutputError(operation, cause.message, options);
@@ -1841,12 +1831,13 @@ const nativeMachoCompletion = (
const completionCancellation =
completion?.cancellation === undefined
? undefined
: new AnalysisCancelledError(operation, {
cause: completion.cancellation.reason,
...(completedFailure === undefined ||
completedFailure.cleanupIncomplete ||
(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined)
: cancelledBy(
operation,
carriedReason(completion.cancellation.reason),
completedFailure === undefined ||
completedFailure.cleanupIncomplete ||
(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined)
? {}
: {
diagnostics: {
@@ -1854,8 +1845,8 @@ const nativeMachoCompletion = (
projectAnalysisError(completedFailure),
),
},
}),
});
},
);
return { observation, completedFailure, completionCancellation };
};
+10 -7
View File
@@ -13,10 +13,11 @@ import type {
} from "../domain/analysisErrorBase.js";
import {
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
@@ -155,9 +156,10 @@ export const nativePlistFailure = (
? nativePlistPrimaryFailure(artifactFilePrimaryFacts(cause), selection)
: physical?.completionCancellation === undefined
? undefined
: new AnalysisCancelledError("inspect_plist", {
cause: physical.completionCancellation.reason,
});
: cancelledBy(
"inspect_plist",
carriedReason(physical.completionCancellation.reason),
);
const previousProjection =
previous === undefined ? null : projectAnalysisError(previous);
const failure = new ProviderCleanupError(
@@ -185,9 +187,10 @@ const nativePlistPrimaryFailure = (
): AnalysisError => {
if (origin === "owned-or-classified") {
if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled")
return new AnalysisCancelledError("inspect_plist", {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
"inspect_plist",
carriedReason(artifactFileCancellationCause(cause)),
);
if (
cause instanceof ArtifactReaderFailure &&
cause.reason === "unavailable"
+3 -2
View File
@@ -2,6 +2,8 @@ import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js"
import {
AnalysisCancelledError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import {
DEFAULT_NATIVE_UI_DEADLINE_MS,
@@ -90,8 +92,7 @@ export class NativeUiInterruptionControl {
"The 67108864-byte scenario output allowance and the 180000 ms cap on one wait step have no runtime override.",
}),
})
: new AnalysisCancelledError(operation, {
cause: interruption.reason,
: cancelledBy(operation, carriedReason(interruption.reason), {
partialObservation,
});
}
+3 -2
View File
@@ -16,9 +16,10 @@ import {
nativeUiResultSchema,
} from "../domain/native/nativeUiObservation.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
cancelledBy,
carriedReason,
} from "../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -278,7 +279,7 @@ const observeWithHelper = async (
? ((signal.aborted && cause.cause === signal.reason
? interruption.error(operation)
: undefined) ??
new AnalysisCancelledError(operation, { cause: cause.cause }))
cancelledBy(operation, carriedReason(cause.cause)))
: new ProviderAdapterError("native-macos", operation, {
cause,
diagnostics: {
+6 -4
View File
@@ -14,9 +14,10 @@ import {
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisResourceConstraintError,
type ResourceConstraintRecovery,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { BinaryResourceFailure } from "../metadata/BinaryContainer.js";
@@ -58,9 +59,10 @@ const peBinaryFailurePrimary = <Limit extends string>(
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(operation, {
cause: artifactFileCancellationCause(cause),
});
return cancelledBy(
operation,
carriedReason(artifactFileCancellationCause(cause)),
);
if (cause instanceof BinaryResourceFailure) {
const limit = options.limit(cause);
return new AnalysisResourceConstraintError(
+19 -14
View File
@@ -16,6 +16,8 @@ import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import {
@@ -99,8 +101,7 @@ export const inspectPwndbgCore = async ({
});
const checkCancelled = () => {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
throw cancelledBy(OPERATION, options.signal, {
capturedOutput: diagnostics,
});
};
@@ -232,14 +233,17 @@ export const inspectPwndbgCore = async ({
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
) {
const error = new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
capturedOutput: recordedCrashStageOutput(diagnostics, output),
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
});
const error = cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
{
capturedOutput: recordedCrashStageOutput(diagnostics, output),
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
},
);
bindArtifactFileCleanup(cause, error);
throw pwndbgContextFinalizationFailure(error, finalization);
}
@@ -334,10 +338,11 @@ const completedReplyFailure = (
): AnalysisError => {
const cancellation =
cause.reason === "cancelled"
? new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
capturedOutput,
})
? cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
{ capturedOutput },
)
: undefined;
if (cause.cleanup === undefined) {
if (cancellation !== undefined) return cancellation;
+27 -34
View File
@@ -37,12 +37,13 @@ import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
cancelledBy,
carriedReason,
} from "../../domain/analysisErrorCore.js";
import {
projectAnalysisError,
@@ -214,11 +215,7 @@ export class PwntoolsDecoder<
): Promise<Result<Report, AnalysisError>> {
if (options?.signal?.aborted)
return Promise.resolve(
err(
new AnalysisCancelledError(this.profile.operation, {
cause: options?.signal?.reason,
}),
),
err(cancelledBy(this.profile.operation, options?.signal)),
);
if (this.#closing) return Promise.resolve(err(this.#closedError()));
const key = Symbol();
@@ -229,11 +226,7 @@ export class PwntoolsDecoder<
const operation = this.#tail.then(() => {
this.#admissions.settle(key, "admitted");
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(this.profile.operation, {
cause: options?.signal?.reason,
}),
);
return err(cancelledBy(this.profile.operation, options?.signal));
return this.#closing ? err(this.#closedError()) : inspect();
});
this.#tail = operation.then(
@@ -243,9 +236,10 @@ export class PwntoolsDecoder<
return admission.then((state) =>
state === "cancelled"
? err(
new AnalysisCancelledError(this.profile.operation, {
cause: options?.signal?.reason,
}),
cancelledBy(
this.profile.operation,
carriedReason(options?.signal?.reason),
),
)
: operation,
);
@@ -337,9 +331,7 @@ export class PwntoolsDecoder<
);
try {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
throw cancelledBy(OPERATION, options.signal);
if (!pwntoolsHostSupported(process.platform, process.arch)) {
const requirement = pwntoolsHostRequirement(
process.platform,
@@ -430,9 +422,7 @@ export class PwntoolsDecoder<
cause.code === "ABORT_ERR" &&
cause.cause === options.signal.reason
)
throw new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
});
throw cancelledBy(OPERATION, options.signal);
throw cause;
}
await writeFile(
@@ -501,10 +491,11 @@ export class PwntoolsDecoder<
if (cleanup !== undefined) bindArtifactFileCleanup(cause, cleanup);
const primary =
cause.reason === "cancelled"
? new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
capturedOutput,
})
? cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
{ capturedOutput },
)
: (cleanup ??
new AnalysisOutputError(OPERATION, cause.message, {
cause,
@@ -527,14 +518,17 @@ export class PwntoolsDecoder<
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
) {
const error = new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
capturedOutput,
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
});
const error = cancelledBy(
OPERATION,
carriedReason(artifactFileCancellationCause(cause)),
{
capturedOutput,
...(cause instanceof ArtifactReaderFailure &&
cause.cleanup !== undefined
? { cleanup: cause.cleanup }
: {}),
},
);
bindArtifactFileCleanup(cause, error);
throw error;
}
@@ -706,8 +700,7 @@ export class PwntoolsDecoder<
const cleanup = await this.#completeCleanup(() => this.#retire());
if (result.ok && options?.signal?.aborted)
result = err(
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
cancelledBy(OPERATION, options.signal, {
partialObservation: result.value,
capturedOutput: result.value.diagnostics,
}),
@@ -1,6 +1,6 @@
import {
AnalysisCancelledError,
AnalysisOutputError,
cancelledBy,
} from "../../domain/analysisErrorCore.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
@@ -145,8 +145,7 @@ export class PwntoolsRecordedCrashProvider
});
try {
if (options?.signal?.aborted)
throw new AnalysisCancelledError("inspect_recorded_crash", {
cause: options.signal.reason,
throw cancelledBy("inspect_recorded_crash", options.signal, {
capturedOutput: diagnostics,
});
const authority = validateCoreFileMappingAuthority(
+47 -44
View File
@@ -22,6 +22,10 @@ import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisTimeoutError,
cancelledBy,
carriedReason,
isAborted,
type CancellationReason,
} from "../domain/analysisErrorCore.js";
import {
RIZIN_DEBUG_SESSION_TOOLS,
@@ -422,7 +426,7 @@ export class RizinDebugSessionManager {
>
> {
if (signal?.aborted)
return err(rizinCancellation("start_rizin_debug_session", signal));
return err(cancelledBy("start_rizin_debug_session", signal));
if (!persistentDebuggerHostSupported(this.#platform))
return err(
new AnalysisCapabilityUnavailableError(
@@ -506,10 +510,9 @@ export class RizinDebugSessionManager {
},
],
)
: rizinCancellation(
: cancelledBy(
"start_rizin_debug_session",
undefined,
cancellation.reason,
carriedReason(cancellation.reason),
);
if (!(cause instanceof RegularFileCleanupFailure)) return err(primary);
const cleanup = new ProviderCleanupError(
@@ -533,7 +536,7 @@ export class RizinDebugSessionManager {
// TTY identity validation is asynchronous. Cancellation may arrive while
// realpath/open/stat are pending, and must prevent the debugger spawn.
if (signal?.aborted)
return err(rizinCancellation("start_rizin_debug_session", signal));
return err(cancelledBy("start_rizin_debug_session", signal));
const id = randomUUID();
const resolvedPath = absoluteCallerPath(input.path);
const launchProvenance: RizinLaunchProvenance = {
@@ -570,7 +573,10 @@ export class RizinDebugSessionManager {
} catch (cause) {
if (signal?.aborted)
return err(
rizinCancellation("start_rizin_debug_session", signal, cause),
cancelledBy(
"start_rizin_debug_session",
rizinCancellationReason(signal, cause),
),
);
return err(
new AnalysisCapabilityUnavailableError(
@@ -663,7 +669,7 @@ export class RizinDebugSessionManager {
// initial frame run before publishing the session as ready.
await new Promise<void>((resolve) => setImmediate(resolve));
if (signal?.aborted)
throw rizinCancellation("start_rizin_debug_session", signal);
throw cancelledBy("start_rizin_debug_session", signal);
if (session.exited)
throw new RizinProcessExitError("Rizin exited during startup.");
if (session.protocolError !== undefined)
@@ -675,19 +681,19 @@ export class RizinDebugSessionManager {
const protocolFailure = session.protocolError;
const cancellation =
signal?.aborted || caught instanceof AnalysisCancelledError
? rizinCancellation("start_rizin_debug_session", signal, caught)
? cancelledBy(
"start_rizin_debug_session",
rizinCancellationReason(signal, caught),
)
: undefined;
const stopped = await this.#stopDebuggerOnly(session);
const capturedOutput = this.#startupCapturedOutput(session);
if (stopped.status !== "incomplete") this.#sessions.remove(session);
let primary: AnalysisError;
if (cancellation !== undefined)
primary = rizinCancellation(
"start_rizin_debug_session",
undefined,
cancellation,
{ capturedOutput },
);
primary = cancelledBy("start_rizin_debug_session", cancellation, {
capturedOutput,
});
else if (protocolFailure !== undefined)
primary = new AnalysisCapabilityUnavailableError(
"rizin",
@@ -754,8 +760,7 @@ export class RizinDebugSessionManager {
stopped,
),
);
if (signal?.aborted)
return err(rizinCancellation("rizin_debug_command", signal));
if (signal?.aborted) return err(cancelledBy("rizin_debug_command", signal));
if (session.pendingCommandCount >= this.#maxPendingCommands.value)
return err(
debuggerResourceConstraintError(
@@ -807,7 +812,7 @@ export class RizinDebugSessionManager {
await waitForTurn(previous, signal);
acquired = true;
if (signal?.aborted)
return err(rizinCancellation("rizin_debug_command", signal));
return err(cancelledBy("rizin_debug_command", signal));
const stoppedWhileQueued = this.#stopReason(session);
if (stoppedWhileQueued !== undefined)
return err(
@@ -877,8 +882,7 @@ export class RizinDebugSessionManager {
throw interruption.cause;
}
const captured = received.value;
if (signal?.aborted)
throw rizinCancellation("rizin_debug_command", signal);
if (signal?.aborted) throw cancelledBy("rizin_debug_command", signal);
let stopped: Awaited<ReturnType<typeof stopOwnedDebuggerProcess>> = {
status: "stopped",
};
@@ -948,7 +952,12 @@ export class RizinDebugSessionManager {
});
if (signal?.aborted || cause instanceof AnalysisCancelledError) {
if (!commandSent)
return err(rizinCancellation("rizin_debug_command", signal, cause));
return err(
cancelledBy(
"rizin_debug_command",
rizinCancellationReason(signal, cause),
),
);
const evidence = partialEvidence([
"The Rizin command was cancelled; collected frame output is partial and command completion is unknown.",
"The owned debugger session was stopped because command output can no longer be correlated safely.",
@@ -956,10 +965,9 @@ export class RizinDebugSessionManager {
]);
session.protocolError =
"A Rizin command was cancelled; the owned session was stopped because command output can no longer be correlated safely.";
const primary = rizinCancellation(
const primary = cancelledBy(
"rizin_debug_command",
signal,
cause,
rizinCancellationReason(signal, cause),
{
...(capturedOutput === undefined ? {} : { capturedOutput }),
...(evidence === undefined ? {} : { partialObservation: evidence }),
@@ -1600,10 +1608,9 @@ export class RizinDebugSessionManager {
signal?: AbortSignal,
): Result<typeof observation, AnalysisError> {
if (signal?.aborted) {
const primary = rizinCancellation(
const primary = cancelledBy(
"rizin_debug_command",
signal,
undefined,
rizinCancellationReason(signal, undefined),
{
capturedOutput: {
stdout: observation.result.output,
@@ -1643,7 +1650,7 @@ export class RizinDebugSessionManager {
signal?: AbortSignal,
): AnalysisError {
if (signal?.aborted && !(primary instanceof AnalysisCancelledError))
primary.retainSecondaryFailure(rizinCancellation(operation, signal), {
primary.retainSecondaryFailure(cancelledBy(operation, signal), {
finalization_cancelled: true,
});
if (stopped.status !== "incomplete") return primary;
@@ -1956,7 +1963,7 @@ const waitForFrame = (
signal?: AbortSignal,
): Promise<RizinFrame> => {
if (signal?.aborted)
return Promise.reject(rizinCancellation("rizin_debug_command", signal));
return Promise.reject(cancelledBy("rizin_debug_command", signal));
const queued = session.frames.shift();
if (queued !== undefined) {
session.queuedFrameBytes -= queued.queueChargeBytes;
@@ -1973,7 +1980,7 @@ const waitForFrame = (
};
const onAbort = (): void => {
cleanup();
reject(rizinCancellation("rizin_debug_command", signal));
reject(cancelledBy("rizin_debug_command", carriedReason(signal?.reason)));
};
const timer = setTimeout(() => {
cleanup();
@@ -2002,21 +2009,17 @@ const waitForTurn = (
if (signal === undefined) return previous;
return waitForAbortable(previous, signal).then((outcome) => {
if (outcome instanceof AbortedWait)
throw rizinCancellation("rizin_debug_command", signal);
throw cancelledBy("rizin_debug_command", outcome);
});
};
/** Translate only this transport's typed cancellation, preserving opaque/null caller reasons. */
const rizinCancellation = (
operation: string,
signal?: AbortSignal,
cause?: unknown,
options: AnalysisErrorOptions = {},
): AnalysisCancelledError =>
new AnalysisCancelledError(operation, {
...options,
cause: signal?.aborted
? signal.reason
: cause instanceof AnalysisCancelledError
? cause.cause
: cause,
});
/** The caller's reason when it aborted, else the reason an earlier failure carried. */
const rizinCancellationReason = (
signal: AbortSignal | undefined,
cause: unknown,
): CancellationReason =>
isAborted(signal)
? signal
: cause instanceof AnalysisCancelledError
? cause
: carriedReason(cause);