diff --git a/src/config/runtimeAnalysisSettings.ts b/src/config/runtimeAnalysisSettings.ts index 9b6a22d8eb..87d6782b15 100644 --- a/src/config/runtimeAnalysisSettings.ts +++ b/src/config/runtimeAnalysisSettings.ts @@ -3,7 +3,7 @@ import { currentAnalysisOperationSnapshot, currentAnalysisSettingOverride, } from "../domain/analysisOperationSnapshot.js"; -import { ConfigurationError } from "../domain/configurationErrors.js"; +import type { ConfigurationError } from "../domain/configurationErrors.js"; import { ok, type Result } from "../domain/result.js"; import type { AppConfig } from "./types.js"; import { @@ -165,18 +165,6 @@ export const runtimeAnalysisConfig = ( }; }; -/** Capability text may describe the built-in default. The operation still admits the seed. */ -export const capabilityLimit = ( - read: () => Value, - fallback: Value, -): Value => { - try { - return read(); - } catch (cause) { - if (cause instanceof ConfigurationError) return fallback; - throw cause; - } -}; export const runtimeAsarHeaderAdmission = ( environment: Readonly>, read?: AnalysisSettingsReader, diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index 7eb41e3853..c6a894d599 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -1,3 +1,5 @@ +import { AnalysisSettings } from "../application/AnalysisSettings.js"; +import { runtimeAnalysisConfig } from "../config/runtimeAnalysisSettings.js"; import { describe, expect, it, vi } from "vitest"; import { parseConfig } from "../config/parseConfig.js"; @@ -409,3 +411,38 @@ describe("Hopper caller reconciliation failures", () => { } }); }); + +it("reports invalid regex configuration honestly without blocking capability discovery", () => { + const environment = { REA_REGEX_MATCHING_DEADLINE_MS: "invalid" }; + const parsed = parseConfig(environment); + if (!parsed.ok) throw parsed.error; + const settings = new AnalysisSettings(environment); + const provider = new HopperProvider( + runtimeAnalysisConfig(parsed.value, settings.read), + silentLogger, + environment, + ); + const limitation = () => + provider + .capabilities() + .find(({ operation }) => operation === "search_strings") + ?.limitations.join(" "); + expect(limitation()).toContain("invalid launch configuration"); + expect(limitation()).not.toContain("5000 ms matching deadline"); + expect( + settings.update({ + changes: [ + { setting: "regex.matching_deadline_ms", action: "set", value: 3000 }, + ], + }).ok, + ).toBe(true); + expect(settings.run(limitation)).toContain( + "3000 ms matching deadline (source runtime)", + ); + expect( + settings.update({ + changes: [{ setting: "regex.matching_deadline_ms", action: "reset" }], + }).ok, + ).toBe(true); + expect(limitation()).toContain("invalid launch configuration"); +}); diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index 410ba7d076..8e0589aa3f 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -49,9 +49,6 @@ import { reconciliationChildOptions, } from "./HopperCallerReconciliation.js"; import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js"; -import { capabilityLimit } from "../config/runtimeAnalysisSettings.js"; -import { DEFAULT_REGEX_MATCHING_DEADLINE } from "../config/regexMatchingDeadline.js"; -import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "../config/regexWorkerHeapRequest.js"; import { hopperCapabilities } from "./HopperProviderCapabilities.js"; import { HOPPER_PROVIDER_IDENTITY } from "./HopperProviderMetadata.js"; @@ -94,16 +91,8 @@ export class HopperProvider implements AnalysisProviderCandidate { capabilities(): readonly CapabilityDescriptor[] { return hopperCapabilities( - capabilityLimit(() => { - const deadline = this.config.regexMatchingDeadline; - void deadline.deadlineMs; - return deadline; - }, DEFAULT_REGEX_MATCHING_DEADLINE), - capabilityLimit(() => { - const request = this.config.regexWorkerHeapRequest; - void request.heapMiB; - return request; - }, DEFAULT_REGEX_WORKER_HEAP_REQUEST), + this.config.regexMatchingDeadline, + this.config.regexWorkerHeapRequest, ); } diff --git a/src/ida/IdaProvider.ts b/src/ida/IdaProvider.ts index 8d872a3724..e12688bf13 100644 --- a/src/ida/IdaProvider.ts +++ b/src/ida/IdaProvider.ts @@ -27,9 +27,6 @@ import type { RetainedResourceScope, } from "../application/AnalysisProvider.js"; import type { AppConfig } from "../config/types.js"; -import { DEFAULT_REGEX_MATCHING_DEADLINE } from "../config/regexMatchingDeadline.js"; -import { DEFAULT_REGEX_WORKER_HEAP_REQUEST } from "../config/regexWorkerHeapRequest.js"; -import { capabilityLimit } from "../config/runtimeAnalysisSettings.js"; import type { ConfigurationError } from "../domain/configurationErrors.js"; import { idaRegistrationDigest } from "./IdaRegistrationIdentity.js"; import { isDeepStrictEqual } from "node:util"; @@ -434,16 +431,8 @@ export class IdaProvider implements AnalysisProviderCandidate { } #regexLimitation(): string { return launchRegexMatchingLimitation( - capabilityLimit(() => { - const deadline = this.config.regexMatchingDeadline; - void deadline.deadlineMs; - return deadline; - }, DEFAULT_REGEX_MATCHING_DEADLINE), - capabilityLimit(() => { - const request = this.config.regexWorkerHeapRequest; - void request.heapMiB; - return request; - }, DEFAULT_REGEX_WORKER_HEAP_REQUEST), + this.config.regexMatchingDeadline, + this.config.regexWorkerHeapRequest, ); } #createBoundClient( diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index 2a12ffb142..5978870dec 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -18,16 +18,13 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import { err, ok } from "../domain/result.js"; import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js"; import { - DEFAULT_REGEX_MATCHING_DEADLINE, resolveRegexMatchingDeadline, type RegexMatchingDeadline, } from "../config/regexMatchingDeadline.js"; import { - DEFAULT_REGEX_WORKER_HEAP_REQUEST, resolveRegexWorkerHeapRequest, type RegexWorkerHeapRequest, } from "../config/regexWorkerHeapRequest.js"; -import { capabilityLimit } from "../config/runtimeAnalysisSettings.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { kunaFileCleanupError, @@ -119,14 +116,8 @@ export class KunaProvider implements AnalysisProviderCandidate { capabilities() { return kunaCapabilities( this.inspectAvailability(), - capabilityLimit(() => { - void this.#deadline.deadlineMs; - return this.#deadline; - }, DEFAULT_REGEX_MATCHING_DEADLINE), - capabilityLimit(() => { - void this.#heapRequest.heapMiB; - return this.#heapRequest; - }, DEFAULT_REGEX_WORKER_HEAP_REQUEST), + this.#deadline, + this.#heapRequest, ); } diff --git a/src/process/RegexMatcher.test.ts b/src/process/RegexMatcher.test.ts index 305e3565da..b2f25b2dbb 100644 --- a/src/process/RegexMatcher.test.ts +++ b/src/process/RegexMatcher.test.ts @@ -1,3 +1,7 @@ +import { AnalysisSettings } from "../application/AnalysisSettings.js"; +import { parseConfig } from "../config/parseConfig.js"; +import { runtimeAnalysisConfig } from "../config/runtimeAnalysisSettings.js"; +import { matchRegexValues } from "./RegexMatcher.js"; import { expect, it } from "vitest"; import { @@ -481,3 +485,58 @@ it("keeps a selected heap request unless a positive Node flag overrides it", () ); expect(projected.details?.limit).toBeUndefined(); }); + +it.each([ + ["regex.matching_deadline_ms", "REA_REGEX_MATCHING_DEADLINE_MS", 3000], + ["regex.worker_heap_request_mib", "REA_REGEX_WORKER_HEAP_REQUEST_MIB", 128], +] as const)( + "preserves pre-aborted cancellation before invalid %s and rejects the invalid seed until overridden", + async (setting, environmentName, value) => { + const environment = { [environmentName]: "invalid" }; + const parsed = parseConfig(environment); + if (!parsed.ok) throw parsed.error; + const settings = new AnalysisSettings(environment); + const config = runtimeAnalysisConfig(parsed.value, settings.read); + const input = { + operation: "search_strings", + pattern: "x", + caseSensitive: true, + values: ["x"], + deadline: config.regexMatchingDeadline, + heapRequest: config.regexWorkerHeapRequest, + }; + const controller = new AbortController(); + const reason = { stopped: true }; + controller.abort(reason); + const cancelled = await settings.run(() => + matchRegexValues({ ...input, signal: controller.signal }), + ); + expect(cancelled).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + if (!cancelled.ok) expect(cancelled.error.cause).toBe(reason); + const rejected = await settings.run(() => matchRegexValues(input)); + expect(rejected).toMatchObject({ + ok: false, + error: { + _tag: "ConfigurationError", + settings: [expect.objectContaining({ setting: environmentName })], + }, + }); + expect( + settings.update({ changes: [{ setting, action: "set", value }] }).ok, + ).toBe(true); + expect(await settings.run(() => matchRegexValues(input))).toEqual({ + ok: true, + value: [0], + }); + expect( + settings.update({ changes: [{ setting, action: "reset" }] }).ok, + ).toBe(true); + expect(await settings.run(() => matchRegexValues(input))).toMatchObject({ + ok: false, + error: { _tag: "ConfigurationError" }, + }); + }, +); diff --git a/src/process/RegexMatcher.ts b/src/process/RegexMatcher.ts index 337e036d08..e0eb224536 100644 --- a/src/process/RegexMatcher.ts +++ b/src/process/RegexMatcher.ts @@ -1,3 +1,4 @@ +import { ConfigurationError } from "../domain/configurationErrors.js"; import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js"; import { readFileSync } from "node:fs"; import { Worker } from "node:worker_threads"; @@ -441,12 +442,18 @@ export const regexMatchingLimitation = ( export const launchRegexMatchingLimitation = ( deadline: RegexMatchingDeadline = DEFAULT_REGEX_MATCHING_DEADLINE, request: RegexWorkerHeapRequest = DEFAULT_REGEX_WORKER_HEAP_REQUEST, -): string => - regexMatchingLimitation( - resolveLaunchRegexWorkerHeap(launchExecArgv, launchNodeOptions, request), - deadline, - request, - ); +): string => { + try { + return regexMatchingLimitation( + resolveLaunchRegexWorkerHeap(launchExecArgv, launchNodeOptions, request), + deadline, + request, + ); + } catch (cause: unknown) { + if (!(cause instanceof ConfigurationError)) throw cause; + return "Regex mode is unavailable until its invalid launch configuration is corrected with update_analysis_settings or by restarting REA. Inspect analysis settings for the affected value."; + } +}; /** * Capability text for the default deadline on this process's launch heap. @@ -583,24 +590,30 @@ export const matchRegexValues = async (input: { /** Resolved heap request. Omitted means the 128 MiB default, not ambient `process.env`. */ readonly heapRequest?: RegexWorkerHeapRequest | undefined; }): Promise> => { - const selectedDeadline = input.deadline ?? DEFAULT_REGEX_MATCHING_DEADLINE; - const selectedHeap = input.heapRequest ?? DEFAULT_REGEX_WORKER_HEAP_REQUEST; - // Copy the values this match accepted. A later override reset must not - // rewrite the deadline or heap reported if the match fails. - const deadline: RegexMatchingDeadline = { - deadlineMs: selectedDeadline.deadlineMs, - source: selectedDeadline.source, - }; - const heapRequest: RegexWorkerHeapRequest = { - heapMiB: selectedHeap.heapMiB, - source: selectedHeap.source, - }; if (input.signal?.aborted) return err( new AnalysisCancelledError(input.operation, { cause: input.signal.reason, }), ); + const selectedDeadline = input.deadline ?? DEFAULT_REGEX_MATCHING_DEADLINE; + const selectedHeap = input.heapRequest ?? DEFAULT_REGEX_WORKER_HEAP_REQUEST; + let deadline: RegexMatchingDeadline; + let heapRequest: RegexWorkerHeapRequest; + try { + // Capture the accepted values before work begins; subsequent updates do not alter this match. + deadline = { + deadlineMs: selectedDeadline.deadlineMs, + source: selectedDeadline.source, + }; + heapRequest = { + heapMiB: selectedHeap.heapMiB, + source: selectedHeap.source, + }; + } catch (cause: unknown) { + if (cause instanceof ConfigurationError) return err(cause); + throw cause; + } let worker: Worker; try { worker = new Worker(WORKER_SOURCE, {