Files
rea/src/process/ArtifactAnalysisResources.ts
T
Ysopwnable 91b8f587cd feat(signals): decode recordings with bounded sigrok output (#2339)
Decode caller-selected protocols from existing recordings through shared
CLI/MCP Evidence. Page literal output lines after complete decoding;
preserve explicit full output, source identity and failed cleanup ownership.

Verified real sigrok-cli 0.7.2 workflows, published UART/SPI/I2C recordings,
independent UART values, selector limits, SDK schemas and cleanup regressions.

Co-authored-by: Olorinm <59321404+Olorinm@users.noreply.github.com>
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-12 20:59:02 +00:00

273 lines
8.2 KiB
TypeScript

import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
type AnalysisPartialObservation,
} from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisResourceConstraintError,
AnalysisOutputError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, type Result } from "../domain/result.js";
import {
readLinuxResourceLimits,
type LinuxResourceLimits,
} from "./LinuxResourceLimits.js";
import type { PrivateRuntimeRoot } from "./PrivateRuntimeRoot.js";
import type { ProviderProcessSupervisor } from "./ProviderProcess.js";
import { OwnedCommandFailure } from "./OwnedCommand.js";
export type ArtifactAnalysisRuntime = Pick<
PrivateRuntimeRoot,
"path" | "close"
>;
/** Own admission, artifact descriptors, producer cleanup and private workspaces together. */
export class ArtifactAnalysisResources {
readonly #artifacts = new ArtifactResourceScope();
readonly #pending = new Map<
ArtifactAnalysisRuntime,
ProviderProcessSupervisor | undefined
>();
#tail: Promise<void> = Promise.resolve();
#busy = false;
#closed = false;
constructor(
readonly providerId: string,
readonly operation: string,
readonly readArtifact: typeof readStableArtifact,
readonly readKernelLimits: typeof readLinuxResourceLimits,
readonly capacity: {
readonly message: string;
readonly details: Readonly<Record<string, JsonValue>>;
},
) {}
run<T>(
work: () => Promise<Result<T, AnalysisError>>,
signal?: AbortSignal,
): Promise<Result<T, AnalysisError>> {
if (this.#closed || signal?.aborted)
return Promise.resolve(err(new AnalysisCancelledError(this.operation)));
if (this.#busy)
return Promise.resolve(
err(
new AnalysisResourceConstraintError(
this.operation,
"capacity",
this.capacity.message,
this.capacity.details,
),
),
);
this.#busy = true;
const running = this.#tail
.then(() => this.#artifacts.run(work))
.catch((cause: unknown) => err(this.#artifactFailure(cause)))
.finally(() => {
this.#busy = false;
});
this.#tail = running.then(
() => undefined,
() => undefined,
);
return running;
}
read(path: string, maximumBytes: number, signal?: AbortSignal) {
return this.readArtifact(
path,
maximumBytes,
signal,
undefined,
this.#artifacts,
);
}
readLimits(limits: LinuxResourceLimits): Promise<LinuxResourceLimits> {
return this.readKernelLimits(limits, this.#artifacts);
}
retain(
root: ArtifactAnalysisRuntime,
owner?: ProviderProcessSupervisor,
): void {
this.#pending.set(root, owner);
}
get pendingPaths(): readonly string[] {
return [...this.#pending.keys()].map((root) => root.path);
}
async release(): Promise<void> {
for (const [root, owner] of this.#pending) {
try {
if (owner !== undefined) {
const stopped = await owner.stop();
if (stopped.status === "incomplete") throw new Error(stopped.reason);
this.#pending.set(root, undefined);
}
await root.close();
this.#pending.delete(root);
} catch (cause: unknown) {
throw new ProviderCleanupError(
this.providerId,
[root.path],
{ reason: String(cause) },
{ operation: this.operation, cause },
);
}
}
}
cleanupFailure(
observation: NonNullable<ArtifactReaderFailure["cleanup"]>,
cause: unknown,
capturedOutput?: AnalysisCapturedOutput,
): ProviderCleanupError {
return new ProviderCleanupError(
this.providerId,
observation.resources,
{
reason: observation.reason,
...(capturedOutput === undefined
? {}
: { captured_output: { ...capturedOutput } }),
},
{ operation: this.operation, cause },
);
}
commandFailure(
cause: OwnedCommandFailure,
limits: {
readonly command_timeout_ms: number;
readonly output_bytes: number;
},
effectiveLimits: LinuxResourceLimits | undefined,
): AnalysisError {
const capturedOutput =
cause.snapshot === null
? undefined
: {
stdout: cause.snapshot.stdout.text,
stderr: cause.snapshot.stderr.text,
truncated: cause.snapshot.diagnosticTruncated ?? false,
exit_code: cause.snapshot.exitCode ?? null,
signal: cause.snapshot.signal ?? null,
};
const detail = {
cause,
...(capturedOutput === undefined ? {} : { capturedOutput }),
};
if (cause.reason === "cancelled")
return new AnalysisCancelledError(this.operation, detail);
if (cause.reason === "timeout")
return new AnalysisTimeoutError(
this.operation,
limits.command_timeout_ms,
detail,
);
if (cause.reason === "output-limit")
return new AnalysisResourceConstraintError(
this.operation,
"memory",
cause.message,
{ output_bytes: limits.output_bytes },
detail,
);
if (
cause.reason === "process" &&
(cause.snapshot?.signal === "SIGXFSZ" ||
cause.snapshot?.signal === "SIGXCPU")
)
return new AnalysisResourceConstraintError(
this.operation,
cause.snapshot.signal === "SIGXFSZ" ? "file-size" : "cpu",
`Producer terminated with ${cause.snapshot.signal}; the signal's exact cause is not independently established.`,
effectiveLimits === undefined ? null : { ...effectiveLimits },
detail,
);
return new AnalysisOutputError(this.operation, cause.message, detail);
}
async finish<T>(
result: Result<T, AnalysisError>,
observation: (value: T) => AnalysisPartialObservation,
): Promise<Result<T, AnalysisError>> {
try {
await this.release();
return result;
} catch (cause: unknown) {
const partialObservation = result.ok
? observation(result.value)
: result.error.partialObservation;
return err(
new ProviderCleanupError(
this.providerId,
this.pendingPaths,
{
reason: String(cause),
previous_error: result.ok
? null
: projectAnalysisError(result.error),
},
{
operation: this.operation,
cause,
...(partialObservation === undefined ? {} : { partialObservation }),
},
),
);
}
}
close(): Promise<void> {
this.#closed = true;
const work = this.#tail.then(() => this.#closeResources());
this.#tail = work.catch(() => undefined);
return work;
}
async #closeResources(): Promise<void> {
let descriptorFailure: AnalysisError | undefined;
try {
await this.#artifacts.close();
} catch (cause: unknown) {
descriptorFailure = this.#artifactFailure(cause);
}
try {
await this.release();
} catch (cause: unknown) {
if (descriptorFailure !== undefined && cause instanceof AnalysisError)
throw analysisErrorWithCleanupFailure(
descriptorFailure,
cause,
this.operation,
);
throw cause;
}
if (descriptorFailure !== undefined) throw descriptorFailure;
}
#artifactFailure(cause: unknown): AnalysisError {
if (cause instanceof AnalysisError) return cause;
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
return this.cleanupFailure(cause.cleanup, cause);
return new ProviderAdapterError(this.providerId, this.operation, {
cause,
diagnostics: { stage: "artifact-resources", reason: String(cause) },
});
}
}