Files
rea/src/cli/finishAnalysisCommand.ts
T
morluto 48f700ee35 fix!: retain Frida observations through shared finalization
Reuse the shared one-shot finalizer for discovery and instrumentation, preserve finalization cancellation with completed observations, and bind Frida cleanup errors to exact owners so confirmed retry recovers the primary error.

BREAKING CHANGE: instrument-with-frida JSON now returns the canonical Evidence record directly. Read normalized_result.cleanup_error instead of the earlier top-level cleanup_error wrapper field.
2026-10-24 01:47:31 +08:00

87 lines
2.9 KiB
TypeScript

import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import {
AnalysisError,
type AnalysisPartialObservation,
} from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { Result } from "../domain/result.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
/** Finish a one-shot CLI operation while retaining primary results through close. */
export const finishAnalysisCommand = async <
Value extends AnalysisPartialObservation,
>(
run: () => Promise<Result<Value, AnalysisError>>,
service: { close(): Promise<void> },
providerId: string,
canonicalOperation: string,
signal: AbortSignal,
fallbackResource?: string,
): Promise<Value | ReturnType<typeof projectAnalysisError>> => {
let result: Result<Value, AnalysisError>;
let cleanup: AnalysisError | undefined;
try {
result = await run();
} catch (cause) {
result = {
ok: false,
error:
cause instanceof AnalysisError
? cause
: new ProviderAdapterError(providerId, canonicalOperation, { cause }),
};
}
try {
// Resource owners may resolve their retained errors during close. This
// adapter cannot infer resolution of a producer's earlier cleanup failure.
await service.close();
} catch (cause) {
if (cause instanceof AnalysisError) cleanup = cause;
else {
const observation =
cause instanceof ArtifactReaderFailure
? ArtifactReaderFailure.cleanupObservation(
cause,
fallbackResource ?? `${canonicalOperation} resources`,
)
: {
reason: cause instanceof Error ? cause.message : String(cause),
resources: [],
};
cleanup = new ProviderCleanupError(
providerId,
observation.resources,
{ reason: observation.reason },
{ cause, operation: canonicalOperation },
);
}
}
await dispatchQueuedInterrupts();
if (result.ok && signal.aborted)
result = {
ok: false,
error: new AnalysisCancelledError(canonicalOperation, {
cause: signal.reason,
partialObservation: result.value,
}),
};
if (cleanup !== undefined) {
if (result.ok) cleanup.retainPartialObservation(result.value);
result = {
ok: false,
error: result.ok
? cleanup
: analysisErrorWithCleanupFailure(
result.error,
cleanup,
canonicalOperation,
).bindCleanupDependencies([result.error, cleanup]),
};
}
return result.ok ? result.value : projectAnalysisError(result.error);
};