import { randomUUID } from "node:crypto"; import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError, AnalysisProtocolError, AnalysisResourceConstraintError, AnalysisTimeoutError, AnalysisUnsupportedTargetError, } from "../domain/analysisErrorCore.js"; import { OwnedCommandFailure, ownedCommandResources, prepareOwnedCommand, runOwnedCommand, } from "../process/OwnedCommand.js"; import type { ProviderProcessSupervisor } from "../process/ProviderProcess.js"; import { isWindowsBatchFile, windowsBatchCommand, WindowsBatchCommandError, } from "../process/WindowsBatchCommand.js"; import { pcapToolUnavailable } from "./PcapConfiguration.js"; export type PcapCommandLauncher = NonNullable< Parameters[2] >["launcher"]; export interface PcapCommandCleanup { readonly reason: string; readonly resources: readonly string[]; readonly capturedOutput: { readonly stdout: string; readonly stderr: string; readonly truncated: boolean; }; } export interface PcapCommandOutput { readonly stdout: string; readonly stderr: string; readonly invalidUtf8Streams: readonly ("stdout" | "stderr")[]; readonly cleanup?: PcapCommandCleanup; } export interface PcapCommandLimits { readonly timeoutMs: number; readonly maxOutputBytes: number; } /** Owned process execution shared by version probes and capture reads. */ export const runPcapCommand = async (options: { readonly operation: string; readonly command: string; readonly arguments: readonly string[]; readonly limits: PcapCommandLimits; readonly environment: Readonly>; readonly signal: AbortSignal | undefined; readonly inputPath?: string; readonly nativeBound?: { readonly limiter?: { command: string; arguments: readonly string[] }; readonly memoryLimitBytes?: number; }; readonly launcher?: PcapCommandLauncher; readonly retain: ( owner: ProviderProcessSupervisor, resources: readonly string[], reason: string, capturedOutput: PcapCommandCleanup["capturedOutput"], ) => void; }): Promise => { const { operation, command, limits, signal } = options; if (signal?.aborted) throw new AnalysisCancelledError(operation, { cause: signal.reason }); const hostEnvironment = { ...process.env, ...options.environment, LC_ALL: "C", LANG: "C", }; let cleanup: PcapCommandCleanup | undefined; try { const batch = process.platform === "win32" && isWindowsBatchFile(command); const invocation = batch ? windowsBatchCommand({ environment: hostEnvironment, batchPath: command, arguments: options.arguments, }) : { command, arguments: options.arguments }; const boundedInvocation = options.nativeBound?.limiter === undefined ? invocation : { command: options.nativeBound.limiter.command, arguments: [ ...options.nativeBound.limiter.arguments, invocation.command, ...invocation.arguments, ], }; // Native ownership preparation has its own bounded compiler lifecycle; // do not spend the tool's execution deadline rebuilding a cold SDK cache. await prepareOwnedCommand(signal); const output = await runOwnedCommand( { ...boundedInvocation, runId: randomUUID(), hostEnvironment, windowsVerbatimArguments: batch, expectedCommand: null, ...(options.nativeBound?.memoryLimitBytes === undefined ? {} : { memoryLimitBytes: options.nativeBound.memoryLimitBytes }), }, { timeoutMs: limits.timeoutMs, diagnosticBytes: limits.maxOutputBytes }, { ...(signal === undefined ? {} : { signal }), ...(options.launcher === undefined ? {} : { launcher: options.launcher }), retainCleanup: (owner, stopped) => { const resources = owner.launch.ownership === undefined ? ["owned-process"] : ownedCommandResources(owner.launch.ownership); const snapshot = owner.snapshot(); const capturedOutput = { stdout: snapshot.stdout.text, stderr: snapshot.stderr.text, truncated: snapshot.diagnosticTruncated === true || snapshot.outputIncomplete === true, }; options.retain(owner, resources, stopped.reason, capturedOutput); cleanup = { reason: stopped.reason, resources, capturedOutput }; }, }, ); return { stdout: output.stdout.text, stderr: output.stderr.text, invalidUtf8Streams: [ ...(output.stdout.invalidUtf8 === true ? (["stdout"] as const) : []), ...(output.stderr.invalidUtf8 === true ? (["stderr"] as const) : []), ], ...(cleanup === undefined ? {} : { cleanup }), }; } catch (cause) { if (cause instanceof AnalysisError) throw cause; if ( cause instanceof OwnedCommandFailure && cause.cleanupOwner !== undefined ) options.retain( cause.cleanupOwner, cause.resources, cause.cleanupFailure ?? cause.message, cause.snapshot === null ? { stdout: "", stderr: "", truncated: true } : { stdout: cause.snapshot.stdout.text, stderr: cause.snapshot.stderr.text, truncated: cause.snapshot.diagnosticTruncated === true || cause.snapshot.outputIncomplete === true, }, ); const details = { cause, ...(cause instanceof OwnedCommandFailure && cause.snapshot !== null ? { capturedOutput: { stdout: cause.snapshot.stdout.text, stderr: cause.snapshot.stderr.text, truncated: cause.snapshot.diagnosticTruncated === true || cause.snapshot.outputIncomplete === true, exit_code: cause.snapshot.exitCode ?? null, signal: cause.snapshot.signal ?? null, }, } : {}), ...(cleanup !== undefined ? { cleanup } : cause instanceof OwnedCommandFailure && cause.cleanupFailure !== null ? { cleanup: { reason: cause.cleanupFailure, resources: cause.resources, }, } : {}), }; if (cause instanceof OwnedCommandFailure && cause.reason === "cancelled") throw new AnalysisCancelledError(operation, { ...details, cause: cause.cancellation?.reason, }); if ( cause instanceof OwnedCommandFailure && cause.snapshot?.memoryLimitReached === true ) throw new AnalysisResourceConstraintError( operation, "memory", "The owned Windows tool tree reached its enforced committed-memory limit.", { process_memory_limit_bytes: options.nativeBound?.memoryLimitBytes ?? null, }, details, ); if ( cause instanceof OwnedCommandFailure && (cause.snapshot?.signal === "SIGXCPU" || cause.snapshot?.signal === "SIGXFSZ") ) throw new AnalysisResourceConstraintError( operation, cause.snapshot.signal === "SIGXCPU" ? "cpu" : "file-size", `Producer terminated with ${cause.snapshot.signal}; its exact cause is not independently established.`, null, details, ); if (cause instanceof OwnedCommandFailure && cause.reason === "timeout") throw new AnalysisTimeoutError(operation, limits.timeoutMs, details); if (cause instanceof OwnedCommandFailure && cause.reason === "output-limit") throw new AnalysisResourceConstraintError( operation, "memory", "Tool output exceeded its complete-output budget", { max_output_bytes: limits.maxOutputBytes }, { ...details, ...fixedAllowanceOptions( "The packet-tool output capture allowance", "capture", ), }, ); if ( cause instanceof WindowsBatchCommandError || (cause instanceof Error && "code" in cause && (cause.code === "ENOENT" || cause.code === "EACCES")) ) throw pcapToolUnavailable( operation, `Cannot run selected tool ${command}: ${cause.message}`, details, ); const diagnostic = cause instanceof OwnedCommandFailure ? cause.snapshot?.stderr.text.trim() || cause.message : cause instanceof Error ? cause.message : String(cause); if (options.inputPath === undefined) throw pcapToolUnavailable( operation, `The selected tool ${command} cannot report its version: ${diagnostic}`, details, ); if ( options.inputPath !== undefined && /not a capture file|file.*isn't a capture file|file format.*not supported/iu.test( diagnostic, ) ) throw new AnalysisUnsupportedTargetError( operation, options.inputPath, diagnostic, details, ); throw new AnalysisProtocolError( `Packet capture tool ${command} failed: ${diagnostic}`, details, ); } };