243 lines
8.1 KiB
TypeScript
243 lines
8.1 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
|
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisOutputError,
|
|
AnalysisTimeoutError,
|
|
} from "../../domain/analysisErrorCore.js";
|
|
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
|
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
|
import { err, ok, type Result } from "../../domain/result.js";
|
|
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
|
import {
|
|
PrivateRuntimeRoot,
|
|
PrivateRuntimeRootUnavailableError,
|
|
} from "../../process/PrivateRuntimeRoot.js";
|
|
import { ProviderStartupDeadline } from "../../process/ProviderDeadline.js";
|
|
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
|
|
import {
|
|
ProviderProcessSupervisor,
|
|
spawnOwnedProviderProcess,
|
|
type OwnedProviderProcessSpawnOptions,
|
|
type SpawnedOwnedProviderProcess,
|
|
} from "../../process/ProviderProcess.js";
|
|
|
|
const OPERATION = "trace_web_source_location";
|
|
const PROVIDER = "source-map-decoder";
|
|
/** Launcher seam retains the actual codec protocol and ownership supervisor. */
|
|
export type SourceMapCodecLauncher = (
|
|
options: OwnedProviderProcessSpawnOptions,
|
|
) => Promise<SpawnedOwnedProviderProcess>;
|
|
/** Runtime implementations establish privacy before returning an owned directory. */
|
|
export interface SourceMapCodecRuntime {
|
|
readonly path: string;
|
|
close(): Promise<void>;
|
|
}
|
|
/** Replaceable acquisition boundaries for an owned codec operation. */
|
|
export interface SourceMapDecoderDependencies {
|
|
readonly launcher?: SourceMapCodecLauncher;
|
|
readonly createRuntime?: () => Promise<SourceMapCodecRuntime>;
|
|
readonly environment?: Readonly<NodeJS.ProcessEnv>;
|
|
}
|
|
|
|
/** Run a trusted codec command and release its process/root before returning output. */
|
|
export const runSourceMapCommand = async (
|
|
request: string,
|
|
path: string,
|
|
options: ExecutionOptions | undefined,
|
|
dependencies: SourceMapDecoderDependencies,
|
|
): Promise<Result<string, AnalysisError>> => {
|
|
const deadline = new ProviderStartupDeadline(
|
|
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
|
|
options?.signal,
|
|
);
|
|
const command = new OwnedCodec(path, dependencies, deadline);
|
|
let result: Result<string, AnalysisError>;
|
|
try {
|
|
await command.start(request);
|
|
result = ok(await command.collect());
|
|
} catch (cause: unknown) {
|
|
result = err(command.failure(cause));
|
|
} finally {
|
|
deadline.dispose();
|
|
}
|
|
const cleanup = await command.close(result.ok ? null : result.error.message);
|
|
if (cleanup !== undefined) return err(cleanup);
|
|
return result.ok && options?.signal?.aborted
|
|
? err(new AnalysisCancelledError(OPERATION))
|
|
: result;
|
|
};
|
|
|
|
class OwnedCodec {
|
|
readonly #runId = `rea-source-map-${randomUUID()}`;
|
|
#runtime: SourceMapCodecRuntime | undefined;
|
|
#supervisor: ProviderProcessSupervisor | undefined;
|
|
#oversized = false;
|
|
#processError: string | undefined;
|
|
constructor(
|
|
readonly path: string,
|
|
readonly dependencies: SourceMapDecoderDependencies,
|
|
readonly deadline: ProviderStartupDeadline,
|
|
) {}
|
|
|
|
async start(request: string): Promise<void> {
|
|
this.check();
|
|
this.#runtime = await (
|
|
this.dependencies.createRuntime ??
|
|
(() => PrivateRuntimeRoot.create({ prefix: "rea-source-map-" }))
|
|
)();
|
|
this.check();
|
|
const requestPath = join(this.#runtime.path, "request.json");
|
|
await writeFile(requestPath, request, {
|
|
flag: "wx",
|
|
mode: 0o600,
|
|
signal: this.deadline.signal,
|
|
});
|
|
this.check();
|
|
const environment: NodeJS.ProcessEnv = {
|
|
...(this.dependencies.environment ?? process.env),
|
|
UV_THREADPOOL_SIZE: "1",
|
|
};
|
|
delete environment.NODE_OPTIONS;
|
|
const spawned = await (
|
|
this.dependencies.launcher ?? spawnOwnedProviderProcess
|
|
)({
|
|
command: process.execPath,
|
|
arguments: [
|
|
"--max-old-space-size=192",
|
|
"--max-semi-space-size=8",
|
|
"--v8-pool-size=1",
|
|
"--single-threaded",
|
|
"--stack_size=2048",
|
|
fileURLToPath(new URL("./SourceMapCodecProcess.js", import.meta.url)),
|
|
requestPath,
|
|
],
|
|
runId: this.#runId,
|
|
cwd: this.#runtime.path,
|
|
hostEnvironment: environment,
|
|
});
|
|
this.#supervisor = new ProviderProcessSupervisor(
|
|
{
|
|
...spawned,
|
|
ownsProcessLifetime: true,
|
|
cleanup:
|
|
spawned.cleanup ??
|
|
(() => cleanupOwnedProcessGroup(spawned.ownership)),
|
|
},
|
|
{
|
|
onDiagnostic: (event) => {
|
|
if (event.type === "error") this.#processError = event.message;
|
|
if (
|
|
event.type === "output" &&
|
|
event.totalBytes >
|
|
(event.stream === "stdout"
|
|
? WEB_SOURCE_MAP_LIMITS.outputBytes
|
|
: 1024 * 1024)
|
|
)
|
|
this.#oversized = true;
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async collect(): Promise<string> {
|
|
const supervisor = this.#supervisor;
|
|
if (supervisor === undefined)
|
|
throw new Error("Codec process was not acquired");
|
|
while (!(await supervisor.waitForOutputClose(50))) this.check();
|
|
this.check();
|
|
const snapshot = supervisor.snapshot();
|
|
if (snapshot.exitCode !== 0 || snapshot.signal !== null)
|
|
throw new ProviderAdapterError(PROVIDER, OPERATION, {
|
|
diagnostics: {
|
|
map_path: this.path,
|
|
exit_code: snapshot.exitCode ?? null,
|
|
signal: snapshot.signal ?? null,
|
|
stderr: snapshot.stderr.text,
|
|
},
|
|
});
|
|
return snapshot.stdout.text;
|
|
}
|
|
|
|
check(): void {
|
|
if (this.deadline.interruption === "cancelled")
|
|
throw new AnalysisCancelledError(OPERATION);
|
|
if (this.deadline.interruption === "timeout")
|
|
throw new AnalysisTimeoutError(
|
|
OPERATION,
|
|
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
|
|
);
|
|
if (this.#oversized)
|
|
throw new AnalysisOutputError(
|
|
OPERATION,
|
|
"Codec output exceeds its complete-evidence or diagnostic budget.",
|
|
);
|
|
if (this.#processError !== undefined)
|
|
throw new ProviderAdapterError(PROVIDER, OPERATION, {
|
|
diagnostics: { map_path: this.path, error_message: this.#processError },
|
|
});
|
|
}
|
|
|
|
failure(cause: unknown): AnalysisError {
|
|
if (this.deadline.interruption === "cancelled")
|
|
return new AnalysisCancelledError(OPERATION);
|
|
if (this.deadline.interruption === "timeout")
|
|
return new AnalysisTimeoutError(
|
|
OPERATION,
|
|
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
|
|
);
|
|
if (cause instanceof AnalysisError) return cause;
|
|
return cause instanceof PrivateRuntimeRootUnavailableError
|
|
? new AnalysisCapabilityUnavailableError(
|
|
PROVIDER,
|
|
OPERATION,
|
|
cause.message,
|
|
{ cause },
|
|
)
|
|
: new ProviderAdapterError(PROVIDER, OPERATION, {
|
|
cause,
|
|
diagnostics: { map_path: this.path, error_message: message(cause) },
|
|
});
|
|
}
|
|
|
|
async close(
|
|
previousError: string | null,
|
|
): Promise<ProviderCleanupError | undefined> {
|
|
const failures: string[] = [];
|
|
if (this.#supervisor !== undefined) {
|
|
const stopped = await this.#supervisor.stop();
|
|
if (stopped.status === "incomplete") failures.push(stopped.reason);
|
|
}
|
|
if (this.#runtime !== undefined) {
|
|
try {
|
|
await this.#runtime.close();
|
|
} catch (cause: unknown) {
|
|
failures.push(message(cause));
|
|
}
|
|
}
|
|
return failures.length === 0
|
|
? undefined
|
|
: new ProviderCleanupError(
|
|
PROVIDER,
|
|
[
|
|
this.#runId,
|
|
...(this.#runtime === undefined ? [] : [this.#runtime.path]),
|
|
],
|
|
{
|
|
map_path: this.path,
|
|
cleanup_failures: failures,
|
|
previous_error: previousError,
|
|
codec_pid: this.#supervisor?.launch.process.pid ?? null,
|
|
},
|
|
{ operation: OPERATION },
|
|
);
|
|
}
|
|
}
|
|
const message = (cause: unknown): string =>
|
|
cause instanceof Error ? cause.message : String(cause);
|