mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 07:46:28 +00:00
fix(session): own target admission descriptors through finalization
This commit is contained in:
1 parent
a8276d1289
commit
2732d538e8
7 files changed
+695
-213
No files matched your search
@@ -1,4 +1,19 @@
|
||||
import { regularFileCancellation } from "../filesystem/OwnedRegularFile.js";
|
||||
import {
|
||||
regularFileCancellation,
|
||||
RegularFileCleanupFailure,
|
||||
RegularFileCancelled,
|
||||
withOwnedRegularFile,
|
||||
OwnedRegularFileOperationFailure,
|
||||
} from "../filesystem/OwnedRegularFile.js";
|
||||
import {
|
||||
bindRegularFileCleanup,
|
||||
RetainedRegularFileCleanupFailure,
|
||||
retryRegularFileCleanup,
|
||||
} from "./RegularFileRead.js";
|
||||
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
@@ -11,10 +26,7 @@ import {
|
||||
import { extname } from "node:path";
|
||||
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import {
|
||||
openRegularFile,
|
||||
sameRegularFileState,
|
||||
} from "../filesystem/RegularFile.js";
|
||||
import { sameRegularFileState } from "../filesystem/RegularFile.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
@@ -81,6 +93,7 @@ export interface BinaryTargetResolverOptions {
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly entryPoint?: string;
|
||||
readonly signal?: AbortSignal;
|
||||
readonly resources?: ArtifactResourceScope;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,13 +109,12 @@ export const parseBinaryTarget = async (
|
||||
Result<BinaryTarget, BinaryTargetError | AnalysisCancelledError>
|
||||
> => {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const hostArchitecture = options.hostArchitecture ?? process.arch;
|
||||
const { targetKind, formatHint, signal, architecture, entryPoint } = options;
|
||||
const { signal } = options;
|
||||
const candidate = absoluteCallerPath(input, { cwd });
|
||||
// Failures before canonicalization concern the selected path itself; later
|
||||
// failures concern the canonical path opened with symbolic links rejected.
|
||||
let stage: TargetFilesystemStage = "resolve";
|
||||
try {
|
||||
const resolve = async (): Promise<TargetResolution> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
await access(candidate, constants.R_OK);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
@@ -117,165 +129,344 @@ export const parseBinaryTarget = async (
|
||||
const { executable: path, infoPlist } = resolved.value;
|
||||
const bundle =
|
||||
infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist };
|
||||
const handle = await openRegularFile(path, {
|
||||
symlinks: "reject",
|
||||
signal,
|
||||
});
|
||||
try {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const metadata = await handle.stat();
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (formatHint === "raw-binary") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"raw-binary format requires an executable target kind",
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
return withOwnedRegularFile<TargetResolution>(
|
||||
path,
|
||||
{
|
||||
symlinks: "reject",
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
},
|
||||
async (handle) => {
|
||||
try {
|
||||
return await readOpenedBinaryTarget(
|
||||
handle,
|
||||
{ path, sourcePath, ...bundle },
|
||||
options,
|
||||
);
|
||||
const admitted = admitRawBinary({
|
||||
architecture,
|
||||
entryPoint,
|
||||
fileBytes: metadata.size,
|
||||
});
|
||||
if (!admitted.ok)
|
||||
return err(
|
||||
new BinaryTargetError(path, admitted.error.reason, {
|
||||
constraint: admitted.error.constraint,
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
format: "raw-binary",
|
||||
architecture: admitted.value.architecture,
|
||||
availableArchitectures: [admitted.value.architecture],
|
||||
entryPoint: admitted.value.entryPoint,
|
||||
entryPointSource: admitted.value.entryPointSource,
|
||||
});
|
||||
}
|
||||
const pairing = rawBinarySelectionError({
|
||||
...(formatHint === undefined ? {} : { format: formatHint }),
|
||||
architecture,
|
||||
entryPoint,
|
||||
});
|
||||
if (pairing !== undefined)
|
||||
} catch (cause: unknown) {
|
||||
throw new OwnedRegularFileOperationFailure(cause);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
try {
|
||||
const resolved = await (options.resources === undefined
|
||||
? resolve()
|
||||
: options.resources.run(resolve));
|
||||
return finalizedTargetResolution(resolved, signal);
|
||||
} catch (cause: unknown) {
|
||||
// A caller-selected abort value never transfers file ownership.
|
||||
if (signal?.aborted === true && cause === signal.reason)
|
||||
return err(
|
||||
new AnalysisCancelledError("resolve_binary_target", { cause }),
|
||||
);
|
||||
if (cause instanceof RegularFileCleanupFailure) {
|
||||
const fileFailure = cause as RegularFileCleanupFailure<TargetResolution>;
|
||||
if (options.resources === undefined)
|
||||
return err(
|
||||
new BinaryTargetError(path, pairing, {
|
||||
constraint: "load_selection_rejected",
|
||||
}),
|
||||
targetFileCleanupError(
|
||||
new RetainedRegularFileCleanupFailure(fileFailure),
|
||||
candidate,
|
||||
stage,
|
||||
),
|
||||
);
|
||||
if (formatHint === "dos-com") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"DOS COM format requires an executable target kind",
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
);
|
||||
const length = validateDosComLength(metadata.size);
|
||||
if (!length.ok)
|
||||
return err(
|
||||
new BinaryTargetError(path, length.error, {
|
||||
constraint: "file_format_rejected",
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
format: "dos-com",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
});
|
||||
}
|
||||
// Probe WASM content before suffix-only database routing; an explicit
|
||||
// database selection keeps the caller's chosen interpretation.
|
||||
const artifactFormat =
|
||||
targetKind === "database"
|
||||
? undefined
|
||||
: await detectArtifactFormat(path, handle, signal);
|
||||
if (
|
||||
targetKind === "database" ||
|
||||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))
|
||||
)
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "database",
|
||||
format: "analysis-database",
|
||||
});
|
||||
if (artifactFormat !== undefined) {
|
||||
const identity = {
|
||||
path,
|
||||
sourcePath,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
};
|
||||
return isArchiveFormat(artifactFormat)
|
||||
? ok({ ...identity, kind: "archive", format: artifactFormat })
|
||||
: ok({ ...identity, kind: "artifact", format: artifactFormat });
|
||||
}
|
||||
const detected = await readExecutableMetadata(
|
||||
handle,
|
||||
hostArchitecture,
|
||||
const retried = await retryRegularFileCleanup(
|
||||
fileFailure,
|
||||
options.resources,
|
||||
signal,
|
||||
);
|
||||
if (!detected.ok) {
|
||||
// Only bytes no other format claims may lead a located ZIP, so an
|
||||
// executable with an appended archive keeps its executable route.
|
||||
if (
|
||||
!(await claimsLeadingExecutableHeader(
|
||||
handle,
|
||||
metadata.size,
|
||||
signal,
|
||||
)) &&
|
||||
(await locateTrailingZipArchive(metadata.size, (position, length) =>
|
||||
readHandleRange(handle, position, length, signal),
|
||||
)) !== undefined
|
||||
)
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "archive",
|
||||
format: zipRootPackageFormat(path),
|
||||
});
|
||||
const reason =
|
||||
typeof detected.error === "string"
|
||||
? detected.error
|
||||
: detected.error.reason;
|
||||
const diagnostics =
|
||||
typeof detected.error === "string"
|
||||
? undefined
|
||||
: detected.error.diagnostics;
|
||||
return err(
|
||||
new BinaryTargetError(path, reason, {
|
||||
constraint: "file_format_rejected",
|
||||
...(diagnostics === undefined ? {} : { diagnostics }),
|
||||
}),
|
||||
if (retried.failure !== undefined)
|
||||
return err(targetFileCleanupError(retried.failure, candidate, stage));
|
||||
if (retried.outcome.kind === "failed")
|
||||
return targetResolutionFailure(
|
||||
fileFailure.operationFailure ?? retried.outcome.cause,
|
||||
candidate,
|
||||
stage,
|
||||
signal,
|
||||
);
|
||||
}
|
||||
return finalizedTargetResolution(retried.outcome.value, signal);
|
||||
}
|
||||
if (cause instanceof RegularFileCancelled)
|
||||
return finalizedTargetResolution(
|
||||
cause.outcome.value as TargetResolution,
|
||||
signal,
|
||||
);
|
||||
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined) {
|
||||
const error = new BinaryTargetError(
|
||||
candidate,
|
||||
"A previous target file could not be closed",
|
||||
{
|
||||
cause,
|
||||
cleanup: cause.cleanup,
|
||||
partialObservation: cause.partialObservation,
|
||||
},
|
||||
);
|
||||
bindRegularFileCleanup(cause, error);
|
||||
return err(error);
|
||||
}
|
||||
return targetResolutionFailure(cause, candidate, stage, signal);
|
||||
}
|
||||
};
|
||||
|
||||
type TargetResolution = Result<
|
||||
BinaryTarget,
|
||||
BinaryTargetError | AnalysisCancelledError
|
||||
>;
|
||||
|
||||
/** Classify and hash the one descriptor admitted by the lifetime owner. */
|
||||
const readOpenedBinaryTarget = async (
|
||||
handle: FileHandle,
|
||||
selected: {
|
||||
readonly path: string;
|
||||
readonly sourcePath: string;
|
||||
readonly bundleInfoPlist?: string;
|
||||
},
|
||||
options: BinaryTargetResolverOptions,
|
||||
): Promise<TargetResolution> => {
|
||||
const { path, sourcePath, bundleInfoPlist } = selected;
|
||||
const bundle = bundleInfoPlist === undefined ? {} : { bundleInfoPlist };
|
||||
const hostArchitecture = options.hostArchitecture ?? process.arch;
|
||||
const { targetKind, formatHint, signal, architecture, entryPoint } = options;
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const metadata = await handle.stat();
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (formatHint === "raw-binary") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"raw-binary format requires an executable target kind",
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
);
|
||||
const admitted = admitRawBinary({
|
||||
architecture,
|
||||
entryPoint,
|
||||
fileBytes: metadata.size,
|
||||
});
|
||||
if (!admitted.ok)
|
||||
return err(
|
||||
new BinaryTargetError(path, admitted.error.reason, {
|
||||
constraint: admitted.error.constraint,
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
format: "raw-binary",
|
||||
architecture: admitted.value.architecture,
|
||||
availableArchitectures: [admitted.value.architecture],
|
||||
entryPoint: admitted.value.entryPoint,
|
||||
entryPointSource: admitted.value.entryPointSource,
|
||||
});
|
||||
}
|
||||
const pairing = rawBinarySelectionError({
|
||||
...(formatHint === undefined ? {} : { format: formatHint }),
|
||||
architecture,
|
||||
entryPoint,
|
||||
});
|
||||
if (pairing !== undefined)
|
||||
return err(
|
||||
new BinaryTargetError(path, pairing, {
|
||||
constraint: "load_selection_rejected",
|
||||
}),
|
||||
);
|
||||
if (formatHint === "dos-com") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"DOS COM format requires an executable target kind",
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
);
|
||||
const length = validateDosComLength(metadata.size);
|
||||
if (!length.ok)
|
||||
return err(
|
||||
new BinaryTargetError(path, length.error, {
|
||||
constraint: "file_format_rejected",
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
format: "dos-com",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
});
|
||||
}
|
||||
// Probe WASM content before suffix-only database routing; an explicit
|
||||
// database selection keeps the caller's chosen interpretation.
|
||||
const artifactFormat =
|
||||
targetKind === "database"
|
||||
? undefined
|
||||
: await detectArtifactFormat(path, handle, signal);
|
||||
if (
|
||||
targetKind === "database" ||
|
||||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))
|
||||
)
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "database",
|
||||
format: "analysis-database",
|
||||
});
|
||||
if (artifactFormat !== undefined) {
|
||||
const identity = {
|
||||
path,
|
||||
sourcePath,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
};
|
||||
return isArchiveFormat(artifactFormat)
|
||||
? ok({ ...identity, kind: "archive", format: artifactFormat })
|
||||
: ok({ ...identity, kind: "artifact", format: artifactFormat });
|
||||
}
|
||||
const detected = await readExecutableMetadata(
|
||||
handle,
|
||||
hostArchitecture,
|
||||
signal,
|
||||
);
|
||||
if (!detected.ok) {
|
||||
// Only bytes no other format claims may lead a located ZIP, so an
|
||||
// executable with an appended archive keeps its executable route.
|
||||
if (
|
||||
!(await claimsLeadingExecutableHeader(handle, metadata.size, signal)) &&
|
||||
(await locateTrailingZipArchive(metadata.size, (position, length) =>
|
||||
readHandleRange(handle, position, length, signal),
|
||||
)) !== undefined
|
||||
)
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
...detected.value,
|
||||
kind: "archive",
|
||||
format: zipRootPackageFormat(path),
|
||||
});
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof AnalysisCancelledError) return err(cause);
|
||||
const reason =
|
||||
typeof detected.error === "string"
|
||||
? detected.error
|
||||
: detected.error.reason;
|
||||
const diagnostics =
|
||||
typeof detected.error === "string"
|
||||
? undefined
|
||||
: detected.error.diagnostics;
|
||||
return err(
|
||||
new BinaryTargetError(path, reason, {
|
||||
constraint: "file_format_rejected",
|
||||
...(diagnostics === undefined ? {} : { diagnostics }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return ok({
|
||||
path,
|
||||
sourcePath,
|
||||
...bundle,
|
||||
sha256: await sha256Handle(handle, path, metadata, signal),
|
||||
kind: "executable",
|
||||
...detected.value,
|
||||
});
|
||||
};
|
||||
|
||||
/** Preserve an earlier admission refusal over an interruption during final close. */
|
||||
const finalizedTargetResolution = (
|
||||
result: TargetResolution,
|
||||
signal?: AbortSignal,
|
||||
): TargetResolution => {
|
||||
if (!result.ok || signal?.aborted !== true) return result;
|
||||
return err(
|
||||
new AnalysisCancelledError("resolve_binary_target", {
|
||||
cause: signal.reason,
|
||||
diagnostics: { completed_target: jsonValueSchema.parse(result.value) },
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
/** Descriptor cleanup retains the physical result and binds its exact release lease. */
|
||||
const targetFileCleanupError = (
|
||||
failure: RetainedRegularFileCleanupFailure<TargetResolution>,
|
||||
candidate: string,
|
||||
stage: TargetFilesystemStage,
|
||||
): BinaryTargetError => {
|
||||
const cancellation = regularFileCancellation(failure);
|
||||
const completed =
|
||||
failure.outcome.kind === "completed" ? failure.outcome.value : undefined;
|
||||
const completedTarget =
|
||||
completed?.ok === true ? jsonValueSchema.parse(completed.value) : undefined;
|
||||
let primary: BinaryTargetError | AnalysisCancelledError | undefined;
|
||||
if (failure.outcome.kind === "failed") {
|
||||
const physical = failure.outcome.cause;
|
||||
primary =
|
||||
physical instanceof BinaryTargetError ||
|
||||
physical instanceof AnalysisCancelledError
|
||||
? physical
|
||||
: cancellation !== undefined
|
||||
? new AnalysisCancelledError("resolve_binary_target", {
|
||||
cause: cancellation.reason,
|
||||
})
|
||||
: (classifyTargetFilesystemFailure(candidate, physical, stage) ??
|
||||
new BinaryTargetError(
|
||||
failure.path,
|
||||
"Target read failed before file cleanup",
|
||||
{ cause: physical },
|
||||
));
|
||||
} else if (completed?.ok === false) primary = completed.error;
|
||||
else if (cancellation !== undefined)
|
||||
primary = new AnalysisCancelledError("resolve_binary_target", {
|
||||
cause: cancellation.reason,
|
||||
...(completedTarget === undefined
|
||||
? {}
|
||||
: { diagnostics: { completed_target: completedTarget } }),
|
||||
});
|
||||
const error = new BinaryTargetError(
|
||||
failure.path,
|
||||
"The target file handle could not be closed",
|
||||
{
|
||||
cause: failure,
|
||||
cleanupPrimary: primary,
|
||||
cleanup: {
|
||||
reason:
|
||||
failure.cleanupCause instanceof Error
|
||||
? failure.cleanupCause.message
|
||||
: String(failure.cleanupCause),
|
||||
resources: [failure.path],
|
||||
},
|
||||
diagnostics: {
|
||||
...(primary === undefined
|
||||
? {}
|
||||
: { primary_error: projectAnalysisError(primary) }),
|
||||
...(completedTarget === undefined
|
||||
? {}
|
||||
: { completed_target: completedTarget }),
|
||||
},
|
||||
},
|
||||
);
|
||||
bindRegularFileCleanup(failure, error);
|
||||
return error;
|
||||
};
|
||||
|
||||
const targetResolutionFailure = (
|
||||
cause: unknown,
|
||||
candidate: string,
|
||||
stage: TargetFilesystemStage,
|
||||
signal?: AbortSignal,
|
||||
): TargetResolution => {
|
||||
// Physical rejection values do not grant ownership of foreign descriptors.
|
||||
const physical =
|
||||
cause instanceof OwnedRegularFileOperationFailure
|
||||
? cause.operationCause
|
||||
: cause;
|
||||
if (
|
||||
physical instanceof AnalysisCancelledError ||
|
||||
physical instanceof BinaryTargetError
|
||||
)
|
||||
return err(physical);
|
||||
if (!(cause instanceof OwnedRegularFileOperationFailure)) {
|
||||
const cancellation = regularFileCancellation(cause);
|
||||
if (cancellation !== undefined)
|
||||
return err(
|
||||
@@ -283,16 +474,14 @@ export const parseBinaryTarget = async (
|
||||
cause: cancellation.reason,
|
||||
}),
|
||||
);
|
||||
if (signal?.aborted === true && cause === signal.reason)
|
||||
return err(
|
||||
new AnalysisCancelledError("resolve_binary_target", { cause }),
|
||||
);
|
||||
if (cause instanceof BinaryTargetError) return err(cause);
|
||||
if (!(cause instanceof Error)) throw cause;
|
||||
const failure = classifyTargetFilesystemFailure(candidate, cause, stage);
|
||||
if (failure === undefined) throw cause;
|
||||
return err(failure);
|
||||
}
|
||||
if (signal?.aborted === true && physical === signal.reason)
|
||||
return err(
|
||||
new AnalysisCancelledError("resolve_binary_target", { cause: physical }),
|
||||
);
|
||||
const failure = classifyTargetFilesystemFailure(candidate, physical, stage);
|
||||
if (failure === undefined) throw physical;
|
||||
return err(failure);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -359,7 +548,9 @@ const targetFailureReason = (
|
||||
|
||||
const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => {
|
||||
if (signal?.aborted === true)
|
||||
throw new AnalysisCancelledError("resolve_binary_target");
|
||||
throw new AnalysisCancelledError("resolve_binary_target", {
|
||||
cause: signal.reason,
|
||||
});
|
||||
};
|
||||
|
||||
const detectArtifactFormat = async (
|
||||
|
||||
@@ -28,12 +28,8 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { access, stat } from "node:fs/promises";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { readAnalysisSnapshot } from "./binary/AnalysisSnapshotFiles.js";
|
||||
import { requiresNativeSourceAdmissionBeforeReplay } from "./binary/AnalysisSnapshotCache.js";
|
||||
import {
|
||||
classifyBinaryTargetFilesystemFailure,
|
||||
parseBinaryTarget,
|
||||
} from "./BinaryTargetResolver.js";
|
||||
import { classifyBinaryTargetFilesystemFailure } from "./BinaryTargetResolver.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
snapshotEvidenceForQuery,
|
||||
@@ -341,6 +337,7 @@ const runAnalysis = async (
|
||||
);
|
||||
}
|
||||
const prepared = await prepareSnapshot({
|
||||
session,
|
||||
path,
|
||||
snapshotPath,
|
||||
signal,
|
||||
@@ -482,7 +479,7 @@ const runAnalysis = async (
|
||||
const recovered =
|
||||
written.error instanceof EvidenceFileError &&
|
||||
written.error.published !== undefined &&
|
||||
(await session.closeWriteResources()).ok;
|
||||
(await session.closeFileResources()).ok;
|
||||
if (!recovered)
|
||||
return err(written.error.retainPartialObservation(evidence));
|
||||
}
|
||||
@@ -653,6 +650,10 @@ const withProcessCancellation = async <Value>(
|
||||
};
|
||||
|
||||
const prepareSnapshot = async (options: {
|
||||
readonly session: Pick<
|
||||
BinarySession,
|
||||
"readAnalysisSnapshot" | "resolveTarget"
|
||||
>;
|
||||
readonly path: string;
|
||||
readonly formatHint?: ExecutableFormatHint;
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
@@ -665,12 +666,15 @@ const prepareSnapshot = async (options: {
|
||||
AnalysisError
|
||||
>
|
||||
> => {
|
||||
const { path, snapshotPath } = options;
|
||||
const { path, snapshotPath, session } = options;
|
||||
if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
|
||||
return ok({});
|
||||
const loaded = await readAnalysisSnapshot(snapshotPath, options.signal);
|
||||
const loaded = await session.readAnalysisSnapshot(
|
||||
snapshotPath,
|
||||
options.signal,
|
||||
);
|
||||
if (!loaded.ok) return loaded;
|
||||
const target = await parseBinaryTarget(path, {
|
||||
const target = await session.resolveTarget(path, {
|
||||
signal: options.signal,
|
||||
...(options.formatHint === undefined
|
||||
? {}
|
||||
|
||||
@@ -53,6 +53,10 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { profilelessNativeDispatchProfile } from "./ProfilelessNativeWorkflowSnapshot.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import { AnalysisSnapshotMismatchError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
parseBinaryTarget,
|
||||
type BinaryTargetResolverOptions,
|
||||
} from "../BinaryTargetResolver.js";
|
||||
import {
|
||||
resolveAppBundleResourceRoot,
|
||||
type ResolvedAppBundleResourceRoot,
|
||||
@@ -422,6 +426,17 @@ export class BinarySession
|
||||
return () => this.#availabilityListeners.delete(listener);
|
||||
}
|
||||
|
||||
/** Resolve target identity under this session's file cleanup owner. */
|
||||
resolveTarget(
|
||||
path: string,
|
||||
options: Omit<BinaryTargetResolverOptions, "resources"> = {},
|
||||
) {
|
||||
return parseBinaryTarget(path, {
|
||||
...options,
|
||||
resources: this.fileResources,
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve a target and provider profile without creating a provider client. */
|
||||
previewTarget(
|
||||
target: BinaryTarget,
|
||||
@@ -464,6 +479,7 @@ export class BinarySession
|
||||
return this.#open(
|
||||
(admitted) =>
|
||||
resolveSessionOpen({
|
||||
resources: this.fileResources,
|
||||
router: this.#providerRouter,
|
||||
current: this.#active,
|
||||
path,
|
||||
@@ -850,7 +866,7 @@ export class BinarySession
|
||||
});
|
||||
if (clearRecords) this.clearSessionRecords();
|
||||
this.#clearRuntimeAvailability();
|
||||
const fileResources = await this.closeWriteResources();
|
||||
const fileResources = await this.closeFileResources();
|
||||
if (!fileResources.ok) onCleanupFailure?.(fileResources.error);
|
||||
const candidateResources = await this.#providerRouter.closeOwnedResources();
|
||||
if (!candidateResources.ok) onCleanupFailure?.(candidateResources.error);
|
||||
@@ -865,20 +881,24 @@ export class BinarySession
|
||||
];
|
||||
let resourceFailure = resourceFailures[0];
|
||||
for (const next of resourceFailures.slice(1))
|
||||
if (resourceFailure !== undefined)
|
||||
if (resourceFailure !== undefined) {
|
||||
const previous = resourceFailure;
|
||||
resourceFailure = analysisErrorWithCleanupFailure(
|
||||
resourceFailure,
|
||||
previous,
|
||||
next,
|
||||
"close_binary",
|
||||
);
|
||||
if (!closed.ok && resourceFailure !== undefined)
|
||||
return err(
|
||||
analysisErrorWithCleanupFailure(
|
||||
closed.error,
|
||||
resourceFailure,
|
||||
"close_binary",
|
||||
),
|
||||
resourceFailure.bindCleanupDependencies([previous, next]);
|
||||
}
|
||||
if (!closed.ok && resourceFailure !== undefined) {
|
||||
const combined = analysisErrorWithCleanupFailure(
|
||||
closed.error,
|
||||
resourceFailure,
|
||||
"close_binary",
|
||||
);
|
||||
combined.bindCleanupDependencies([closed.error, resourceFailure]);
|
||||
return err(combined);
|
||||
}
|
||||
if (!closed.ok) return closed;
|
||||
return resourceFailure === undefined ? ok(null) : err(resourceFailure);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../domain/analysisSnapshot.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
EvidenceIntegrityError,
|
||||
@@ -56,6 +57,7 @@ export interface ResolvedSessionOpen {
|
||||
}
|
||||
|
||||
interface ResolveSessionOpenInput {
|
||||
readonly resources: ArtifactResourceScope;
|
||||
readonly router: SessionProviderRouter;
|
||||
readonly current: CurrentOpenBinding | undefined;
|
||||
readonly path: string;
|
||||
@@ -83,6 +85,7 @@ export const resolveSessionOpen = async (
|
||||
): Promise<Result<ResolvedSessionOpen, AnalysisError>> => {
|
||||
const { path, options } = input;
|
||||
const parsed = await parseBinaryTarget(path, {
|
||||
resources: input.resources,
|
||||
...(options.targetKind === undefined
|
||||
? {}
|
||||
: { targetKind: options.targetKind }),
|
||||
|
||||
@@ -33,7 +33,12 @@ import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
|
||||
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import { writeAnalysisSnapshot as writeSnapshotFile } from "./AnalysisSnapshotFiles.js";
|
||||
import { bindRegularFileCleanup } from "../RegularFileRead.js";
|
||||
import {
|
||||
writeAnalysisSnapshot as writeSnapshotFile,
|
||||
readAnalysisSnapshot as readSnapshotFile,
|
||||
} from "./AnalysisSnapshotFiles.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { workflowAnalysisProfile } from "../WorkflowAnalysisProfile.js";
|
||||
import {
|
||||
hasOnlyProfilelessNativeDispatchEntries,
|
||||
@@ -63,10 +68,10 @@ export interface WorkflowSnapshotRecordInput {
|
||||
/** Own binary snapshots and coordinate the composed investigation records. */
|
||||
export abstract class BinarySessionRecords {
|
||||
readonly #records: InvestigationRecords;
|
||||
readonly #writeResources = new ArtifactResourceScope();
|
||||
protected readonly fileResources = new ArtifactResourceScope();
|
||||
|
||||
protected get hasPendingFileResources(): boolean {
|
||||
return this.#writeResources.hasPending;
|
||||
return this.fileResources.hasPending;
|
||||
}
|
||||
readonly #snapshot = new AnalysisSnapshotCache();
|
||||
#snapshotInvalidated = false;
|
||||
@@ -112,6 +117,11 @@ export abstract class BinarySessionRecords {
|
||||
return this.#records.immutableEvidenceBundle();
|
||||
}
|
||||
|
||||
/** Read one snapshot while retaining failed descriptors under this session. */
|
||||
readAnalysisSnapshot(path: string, signal?: AbortSignal) {
|
||||
return readSnapshotFile(path, signal, this.fileResources);
|
||||
}
|
||||
|
||||
/** Persist one provider-neutral snapshot under this session's resource owner. */
|
||||
writeAnalysisSnapshot(
|
||||
snapshot: AnalysisSnapshot,
|
||||
@@ -120,28 +130,27 @@ export abstract class BinarySessionRecords {
|
||||
): Promise<
|
||||
Result<{ readonly path: string; readonly bytes: number }, AnalysisError>
|
||||
> {
|
||||
return writeSnapshotFile(snapshot, path, overwrite, this.#writeResources);
|
||||
return writeSnapshotFile(snapshot, path, overwrite, this.fileResources);
|
||||
}
|
||||
|
||||
/** Retry and release staged snapshot files at the owning session shutdown. */
|
||||
async closeWriteResources(): Promise<Result<null, AnalysisError>> {
|
||||
/** Retry retained target reads and snapshot files at the owning session shutdown. */
|
||||
async closeFileResources(): Promise<Result<null, AnalysisError>> {
|
||||
try {
|
||||
await this.#writeResources.retryCleanup();
|
||||
await this.fileResources.retryCleanup();
|
||||
return ok(null);
|
||||
} catch (cause: unknown) {
|
||||
const observation = ArtifactReaderFailure.cleanupObservation(
|
||||
cause,
|
||||
"analysis snapshot staging",
|
||||
"analysis session files",
|
||||
);
|
||||
return err(
|
||||
new EvidenceFileError("write", "io", {
|
||||
cause,
|
||||
cleanup: {
|
||||
reason: observation.reason,
|
||||
resources: observation.resources,
|
||||
},
|
||||
}),
|
||||
const error = new ProviderCleanupError(
|
||||
"rea",
|
||||
observation.resources,
|
||||
{ reason: observation.reason },
|
||||
{ cause, operation: "close_binary" },
|
||||
);
|
||||
bindRegularFileCleanup(cause, error);
|
||||
return err(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user