fix(pcap): preserve executable read failures

This commit is contained in:
morluto
2026-10-22 06:09:50 +08:00
parent 5e046bb374
commit f2cffbdfdc
2 changed files with 75 additions and 19 deletions
+46 -19
View File
@@ -50,6 +50,8 @@ import { createPcapEvidence } from "../domain/pcap/pcapEvidence.js";
import { preparePcapNativeBound } from "./PcapNativeBound.js";
import { PcapSnapshot } from "./PcapSnapshot.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js";
import { realpath } from "node:fs/promises";
@@ -133,6 +135,7 @@ const pcapCommandCleanupError = (
export interface PcapProviderOptions {
readonly environment?: Readonly<Record<string, string | undefined>>;
readonly launcher?: PcapCommandLauncher;
readonly artifactReader?: typeof readStableArtifact;
}
interface RetainedPcapCommand {
@@ -153,16 +156,7 @@ interface RetainedSnapshotFailure {
/** Inspect saved captures through bounded, owned tool processes and one stable input copy. */
export class PcapProvider {
readonly #resources = new ArtifactAnalysisResources(
"pcap",
"inspect_pcap_capture",
readStableArtifact,
readLinuxResourceLimits,
{
message: "A packet summary is already active; retry after it completes.",
details: { max_concurrent_inspections: 1 },
},
);
readonly #resources: ArtifactAnalysisResources;
private readonly environment: Readonly<Record<string, string | undefined>>;
private readonly launcher: PcapCommandLauncher | undefined;
private readonly wiresharkIdentities = new Map<
@@ -182,6 +176,17 @@ export class PcapProvider {
constructor(options: PcapProviderOptions = {}) {
this.environment = options.environment ?? process.env;
this.launcher = options.launcher;
this.#resources = new ArtifactAnalysisResources(
"pcap",
"inspect_pcap_capture",
options.artifactReader ?? readStableArtifact,
readLinuxResourceLimits,
{
message:
"A packet summary is already active; retry after it completes.",
details: { max_concurrent_inspections: 1 },
},
);
}
private async run(
@@ -664,17 +669,39 @@ export class PcapProvider {
);
}
} catch (cause) {
result = err(
cause instanceof AnalysisError
? cause
: cause instanceof PcapConfigurationFailure
? pcapToolUnavailable(request.operation, cause.message, {
cause,
const classifyArtifactFailure = (failure: unknown): AnalysisError =>
failure instanceof AnalysisError
? failure
: failure instanceof ArtifactReaderFailure &&
failure.reason === "cancelled"
? new AnalysisCancelledError(request.operation, {
cause: artifactFileCancellationCause(failure),
...(failure.partialObservation === undefined
? {}
: { partialObservation: failure.partialObservation }),
})
: new AnalysisProtocolError(
`Packet-capture analysis failed: ${cause instanceof Error ? cause.message : String(cause)}`,
{ cause },
),
`Packet-capture analysis failed: ${failure instanceof Error ? failure.message : String(failure)}`,
{ cause: failure },
);
result = err(
cause instanceof ArtifactReaderFailure
? this.#resources.cleanupFailure(
cause,
classifyArtifactFailure,
undefined,
request.operation,
)
: cause instanceof AnalysisError
? cause
: cause instanceof PcapConfigurationFailure
? pcapToolUnavailable(request.operation, cause.message, {
cause,
})
: new AnalysisProtocolError(
`Packet-capture analysis failed: ${cause instanceof Error ? cause.message : String(cause)}`,
{ cause },
),
);
}
if (signal?.aborted && result.ok)
@@ -3,6 +3,8 @@ import { dirname, join } from "node:path";
import { expect, onTestFinished, test, vi } from "vitest";
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { resolvedAnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
@@ -62,6 +64,7 @@ const fixture = async (
reason: unknown,
failedCleanupAttempts: number,
activeVersion = false,
artifactReader: typeof readStableArtifact = readStableArtifact,
) => {
const root = await createTestTempDirectory("rea-pcap-provider-cancel-");
const tools = await writeTools(root, mode, activeVersion);
@@ -100,6 +103,7 @@ const fixture = async (
REA_CAPINFOS_COMMAND: tools.capinfos,
},
launcher,
artifactReader,
});
onTestFinished(async () => provider.close());
return { provider, root, capture, cleanupAttempts: () => cleanupAttempts };
@@ -123,6 +127,31 @@ it("availability preserves the producer's exact cancellation reason", async () =
expect(failure).toMatchObject({ cause: reason });
});
it("executable identity hashing preserves artifact-read cancellation", async () => {
const controller = new AbortController();
const reason = { caller: "cancel executable identity read" };
const { provider } = await fixture(
"active",
controller,
reason,
0,
false,
async () => {
throw new ArtifactReaderFailure("cancelled", "identity read cancelled", {
cancellation: { reason },
});
},
);
const result = await provider.execute(
{ operation: "inspect_pcap_client", input: {} },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
});
it("active cancellation stays primary through retained command and snapshot cleanup retry", async () => {
const controller = new AbortController();
const reason = "caller cancellation token";