Files
reverse-engineering-anything/src/process/capture/ProcessScenarioRuntimeValidation.ts
T
morlutoandDeryFerd d6ca7b3f20 fix(process): gate launch and retain cancellation provenance
Check cancellation at the PTY execution boundary after executable sampling
and descriptor cleanup settle. Completed file observations cannot authorize
launch for a cancelled request. Preserve exact caller reasons in the shared
capture cancellation constructor and its admission, exit and finalization
consumers instead of reconstructing them downstream.

Exercise real executable reads and closes through the capture API with a
throwing PTY sentinel at five schedules. Retain a real completed Node capture
and its cleanup facts when the final progress callback cancels the request.
Strengthen existing metadata and baseline cancellation controls.

Adapted after review of #4836 at 014afa3892.
Raw snapshot/sampler descriptor cleanup ownership remains tracked in #5140;
this execution-admission change does not claim to solve failed close recovery.

Closes #4512
Closes #5142

Co-authored-by: DeryFerd <deryferdikao125@gmail.com>
2026-10-19 09:14:00 +08:00

124 lines
4.1 KiB
TypeScript

import { constants as fsConstants } from "node:fs";
import { access, realpath, stat } from "node:fs/promises";
import { delimiter, isAbsolute, join, resolve, sep } from "node:path";
import type { ProcessScenario } from "../../domain/process/processScenario.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { snapshotEnvironment } from "../snapshotEnvironment.js";
import { processCaptureCancelled } from "./ProcessCaptureError.js";
/** Resolve caller-selected executable and observation paths for this execution. */
export const resolveProcessScenarioRuntimePaths = async (
scenario: ProcessScenario,
environment: Readonly<Record<string, string | undefined>> = process.env,
platform: NodeJS.Platform = process.platform,
): Promise<ProcessScenario> => {
const selectedWorkingDirectory = resolve(scenario.working_directory);
let workingDirectory: string;
try {
workingDirectory = await realpath(selectedWorkingDirectory);
} catch (cause: unknown) {
throw invalidScenarioPath(
"working_directory",
`working directory could not be resolved: ${selectedWorkingDirectory}${osFailure(cause)}`,
cause,
);
}
let workingDirectoryStat;
try {
workingDirectoryStat = await stat(workingDirectory);
} catch (cause: unknown) {
throw invalidScenarioPath(
"working_directory",
`working directory could not be inspected: ${workingDirectory}${osFailure(cause)}`,
cause,
);
}
if (!workingDirectoryStat.isDirectory())
throw invalidScenarioPath(
"working_directory",
`working directory is not a directory: ${workingDirectory}`,
);
const executable = await resolveExecutable(
scenario.executable,
workingDirectory,
snapshotEnvironment(scenario.environment, platform).PATH ??
snapshotEnvironment(environment, platform).PATH ??
"",
);
const observationPaths = scenario.filesystem_observation_paths.map((path) =>
resolve(workingDirectory, path),
);
return {
...scenario,
executable,
working_directory: workingDirectory,
filesystem_observation_paths: observationPaths,
};
};
/** Resolve a selected executable through the caller's effective PATH. */
export const resolveExecutable = async (
command: string,
workingDirectory: string,
searchPath: string,
): Promise<string> => {
const directories = searchPath
.split(delimiter)
.map((directory) =>
directory === ""
? workingDirectory
: resolve(workingDirectory, directory),
);
const candidates =
isAbsolute(command) ||
command.includes("/") ||
(sep === "\\" && command.includes("\\"))
? [resolve(workingDirectory, command)]
: directories.map((directory) => join(directory, command));
const failures: string[] = [];
for (const candidate of candidates) {
try {
await access(candidate, fsConstants.X_OK);
if ((await stat(candidate)).isFile()) return candidate;
failures.push(`${candidate} (not a regular file)`);
} catch (cause: unknown) {
failures.push(`${candidate}${osFailure(cause)}`);
continue;
}
}
throw invalidScenarioPath(
"executable",
`executable could not be resolved: ${command}; checked ${failures.join("; ")}`,
);
};
const invalidScenarioPath = (
field: "executable" | "working_directory",
message: string,
cause?: unknown,
): AnalysisInputError =>
new AnalysisInputError(
"capture_process_scenario",
cause === undefined ? undefined : { cause },
[{ path: [field], reason: "invalid_value", message }],
);
const osErrorCode = (cause: unknown): string | undefined =>
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
typeof cause.code === "string"
? cause.code
: undefined;
const osFailure = (cause: unknown): string => {
const code = osErrorCode(cause);
return code === undefined ? "" : ` (${code})`;
};
/** Reject cancellation before a process capture operation begins. */
export const assertNotCancelled = (signal: AbortSignal | undefined): void => {
if (signal?.aborted === true) throw processCaptureCancelled(signal.reason);
};