diff --git a/src/signals/SigrokRecordingProvider.ts b/src/signals/SigrokRecordingProvider.ts index aac0f0237c..d115e91e94 100644 --- a/src/signals/SigrokRecordingProvider.ts +++ b/src/signals/SigrokRecordingProvider.ts @@ -76,6 +76,21 @@ type Launcher = NonNullable[2]>["launcher"]; export const sigrokHostSupported = (platform: NodeJS.Platform): boolean => platform === "linux"; +/** + * A nonzero version probe ran under the resource limiter, so the status is not + * attributed to sigrok-cli alone. + */ +class ResourceLimitedVersionProbeFailure extends Error { + readonly statusOwner = "limiter-or-tool" as const; + + constructor() { + super( + "The resource-limited sigrok-cli --version probe failed; the status may come from the limiter or from sigrok-cli, so inspect both selected installations and the captured output.", + ); + this.name = "ResourceLimitedVersionProbeFailure"; + } +} + /** Decode selected recordings only, retaining cleanup ownership across failures. */ export class SigrokRecordingProvider implements SignalRecordingPort { readonly #resources: ArtifactAnalysisResources; @@ -270,10 +285,9 @@ export class SigrokRecordingProvider implements SignalRecordingPort { { capturedOutput, userMessage: rejection.message }, [rejection], ); + // The probe runs under the limiter, so either program may own the status. if (output.exitCode !== 0 && phase === "configuration") - throw new Error( - "The selected sigrok-cli installation failed its --version probe; inspect its captured output.", - ); + throw new ResourceLimitedVersionProbeFailure(); if (output.exitCode !== 0) throw new ProviderAdapterError( SIGROK_PROVIDER_IDENTITY.id, @@ -528,6 +542,13 @@ export class SigrokRecordingProvider implements SignalRecordingPort { ...(observedOutput === undefined ? {} : { capturedOutput: observedOutput }), + ...(cause instanceof ResourceLimitedVersionProbeFailure + ? { + diagnostics: { + version_probe_status_owner: cause.statusOwner, + }, + } + : {}), userMessage: `${reason} On Linux, configure sigrok-cli 0.7.2 through REA_SIGROK_COMMAND and util-linux prlimit through REA_SIGROK_PRLIMIT_COMMAND (default /usr/bin/prlimit).`, }, ); diff --git a/tests/boundary/signals/recordingProvider.test.ts b/tests/boundary/signals/recordingProvider.test.ts index 8757aefb8b..a6376729ad 100644 --- a/tests/boundary/signals/recordingProvider.test.ts +++ b/tests/boundary/signals/recordingProvider.test.ts @@ -95,6 +95,15 @@ const fixture = async ( const configuredCommand = mode === "symlink" ? join(directory, "configured-tool") : script; if (mode === "symlink") await symlink(script, configuredCommand); + // A separate limiter that refuses to start the wrapped tool. + const limiter = join(directory, "limiter.mjs"); + if (mode === "limiter-refused") { + await writeFile( + limiter, + "console.error('prlimit: failed to execute sigrok-cli'); process.exitCode = 1;\n", + ); + await chmod(limiter, 0o700); + } const controller = new AbortController(); let calls = 0; let mayClean = !failCleanup; @@ -104,7 +113,8 @@ const fixture = async ( { ...process.env, REA_SIGROK_COMMAND: configuredCommand, - REA_SIGROK_PRLIMIT_COMMAND: configuredCommand, + REA_SIGROK_PRLIMIT_COMMAND: + mode === "limiter-refused" ? limiter : configuredCommand, }, async (input) => { calls++; @@ -127,7 +137,10 @@ const fixture = async ( const spawned = await spawnOwnedProviderProcess({ ...input, command: process.execPath, - arguments: [script, ...input.arguments], + arguments: [ + mode === "limiter-refused" ? limiter : script, + ...input.arguments, + ], expectedCommand: process.execPath, }); if (mode === "wait" && !input.arguments.includes("--version")) @@ -1415,3 +1428,37 @@ it("classifies completed nonzero version probes as configuration failures for bo } expect(f.calls()).toBe(2); }); + +it("does not blame sigrok-cli alone when the limiter fails the version probe", async () => { + const f = await fixture("limiter-refused"); + for (const [operation, task] of [ + [ + "decode_signal_recording", + () => f.service.decode({ path: f.path, decoders: "uart" }), + ], + ["inspect_signal_decoders", () => f.service.inspectDecoders({})], + ] as const) { + const result = await task(); + if (result.ok) throw new Error("Expected version admission refusal"); + expect(result.error).toMatchObject({ + _tag: "AnalysisCapabilityUnavailableError", + providerId: "sigrok-recording", + operation, + }); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "capability_unavailable", + details: { + provider_id: "sigrok-recording", + operation, + // Absent when the probe is reported as a sigrok-cli failure alone. + diagnostics: { version_probe_status_owner: "limiter-or-tool" }, + captured_output: { + exit_code: 1, + stdout: "", + stderr: "prlimit: failed to execute sigrok-cli\n", + }, + }, + }); + } + expect(f.calls()).toBe(2); +});