mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 08:17:02 +00:00
feat(android): make the JADX operation deadline configurable (#4466)
* feat(android): make the JADX operation deadline configurable JADX operations used a fixed 120 s provider deadline and a separate fixed 120 s per-call bound in the session, so an APK whose cold load exceeds 120 s could not complete, and raising only the MCP client timeout could not extend either. Add REA_JADX_OPERATION_TIMEOUT_MS, parsed once by the JADX configuration owner (decimal milliseconds from 1 to 2147483647, default 120000). The provider resolves it before acquiring anything and uses it for the operation deadline and its timeout errors, and the session uses it for every upstream call. Doctor reports the effective value, and setup forwards an explicit value like the other JADX settings. Fixes #3918 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(android): isolate JADX fixtures from inherited shell settings The shared JADX protocol fixture spread the developer's process.env, so an exported REA_JADX_OPERATION_TIMEOUT_MS (or REA_JADX_HEAP_MIB) changed every provider test that never selected one. Keep inheriting the host environment for process launch, but take REA_JADX_* settings only from the fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29b8769d2c
commit
f433786463
12 files changed
+356
-14
No files matched your search
@@ -3,9 +3,11 @@ import { access, realpath, stat } from "node:fs/promises";
|
||||
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
||||
import { inspectWindowsNativeReadiness } from "../process/WindowsAuthority.js";
|
||||
import { systemWindowsNativeAuthority } from "../windows/WindowsNativeLoader.js";
|
||||
import { providerStartupTimeoutSchema } from "../config/providerStartupTimeout.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||
import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js";
|
||||
import {
|
||||
@@ -14,10 +16,12 @@ import {
|
||||
} from "../process/ExecFileOutput.js";
|
||||
import type { DoctorProviderCheck } from "../application/Doctor.js";
|
||||
import {
|
||||
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
|
||||
JADX_JAR_CONFIGURATION_REMEDIATION,
|
||||
JADX_JAVA_PROBE_REMEDIATION,
|
||||
JADX_JAVA_RUNTIME_REMEDIATION,
|
||||
JADX_JVM_OPTIONS_REMEDIATION,
|
||||
JADX_OPERATION_TIMEOUT_REMEDIATION,
|
||||
JADX_UNSUPPORTED_HOST_REMEDIATION,
|
||||
JADX_WINDOWS_NATIVE_REMEDIATION,
|
||||
} from "./JadxRelease.js";
|
||||
@@ -57,6 +61,7 @@ export type JadxFailureKind =
|
||||
| "java_runtime"
|
||||
| "java_home"
|
||||
| "jvm_options"
|
||||
| "operation_timeout"
|
||||
| "jar"
|
||||
| "java_probe"
|
||||
| "unsupported_host";
|
||||
@@ -68,6 +73,7 @@ export const JADX_FAILURE_KINDS: readonly JadxFailureKind[] = [
|
||||
"java_runtime",
|
||||
"java_home",
|
||||
"jvm_options",
|
||||
"operation_timeout",
|
||||
"jar",
|
||||
"java_probe",
|
||||
"unsupported_host",
|
||||
@@ -114,6 +120,11 @@ export const projectJadxFailureOutcome = (
|
||||
remediation: JADX_JVM_OPTIONS_REMEDIATION,
|
||||
classification: drift,
|
||||
};
|
||||
case "operation_timeout":
|
||||
return {
|
||||
remediation: JADX_OPERATION_TIMEOUT_REMEDIATION,
|
||||
classification: drift,
|
||||
};
|
||||
case "jar":
|
||||
return {
|
||||
remediation: JADX_JAR_CONFIGURATION_REMEDIATION,
|
||||
@@ -155,6 +166,7 @@ export const inspectJadxAvailability = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProviderAvailability> => {
|
||||
try {
|
||||
const operationTimeoutMs = readOperationTimeout(environment);
|
||||
const configuration = await readJadxConfigurationInputs(
|
||||
environment,
|
||||
signal,
|
||||
@@ -169,6 +181,7 @@ export const inspectJadxAvailability = async (
|
||||
java_executable: configuration.java,
|
||||
jdk_compiler: true,
|
||||
jar_path: configuration.jar,
|
||||
operation_timeout_ms: operationTimeoutMs,
|
||||
},
|
||||
};
|
||||
} catch (cause) {
|
||||
@@ -184,6 +197,22 @@ export const inspectJadxAvailability = async (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the whole-operation deadline before any admission work starts, so
|
||||
* one value bounds the provider operation and each upstream call.
|
||||
*/
|
||||
export const resolveJadxOperationTimeout = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
operation: AndroidOperation,
|
||||
): Result<number, AnalysisCapabilityUnavailableError> => {
|
||||
try {
|
||||
return ok(readOperationTimeout(environment));
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
|
||||
return err(projectConfigurationFailure(operation, cause));
|
||||
}
|
||||
};
|
||||
|
||||
/** Resolve caller-selected paths and JVM options without probing their contents. */
|
||||
export const resolveJadxConfigurationInputs = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
@@ -508,6 +537,21 @@ const resolveJvmArguments = (
|
||||
return result;
|
||||
};
|
||||
|
||||
const readOperationTimeout = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): number => {
|
||||
const value = environment.REA_JADX_OPERATION_TIMEOUT_MS;
|
||||
if (value === undefined) return DEFAULT_JADX_OPERATION_TIMEOUT_MS;
|
||||
const parsed = providerStartupTimeoutSchema.safeParse(value);
|
||||
if (parsed.success) return parsed.data;
|
||||
throw configurationFailure(
|
||||
"open_options_invalid",
|
||||
`REA_JADX_OPERATION_TIMEOUT_MS must be a decimal millisecond count from 1 to 2147483647; received ${value}.`,
|
||||
{ setting: "REA_JADX_OPERATION_TIMEOUT_MS", value },
|
||||
"operation_timeout",
|
||||
);
|
||||
};
|
||||
|
||||
const inspectJavaRuntime = async (
|
||||
java: string,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
|
||||
@@ -49,6 +49,10 @@ export const inspectJadxProvider = async (
|
||||
"REA_JADX_ACTIVE_PROCESSOR_COUNT",
|
||||
environment.REA_JADX_ACTIVE_PROCESSOR_COUNT,
|
||||
),
|
||||
...optionalEntry(
|
||||
"REA_JADX_OPERATION_TIMEOUT_MS",
|
||||
environment.REA_JADX_OPERATION_TIMEOUT_MS,
|
||||
),
|
||||
},
|
||||
checks: [configurationCheck(configured, availability, jarPath)],
|
||||
};
|
||||
@@ -70,6 +74,7 @@ const configurationCheck = (
|
||||
): DoctorProviderCheck => {
|
||||
const code = availability.status === "unavailable" ? availability.code : null;
|
||||
if (availability.status === "available" && jarPath !== undefined) {
|
||||
const operationTimeout = availability.diagnostics["operation_timeout_ms"];
|
||||
return {
|
||||
name: "configuration",
|
||||
ok: true,
|
||||
@@ -77,6 +82,9 @@ const configurationCheck = (
|
||||
detail: `JADX MCP bridge JAR validated at ${jarPath}; its path is recorded for agent registrations.`,
|
||||
remediation: null,
|
||||
classification: "config_drift",
|
||||
...(typeof operationTimeout === "number"
|
||||
? { details: { operation_timeout_ms: operationTimeout } }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
// The failure kind is tagged at the configuration boundary; project its
|
||||
|
||||
@@ -34,13 +34,13 @@ import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
|
||||
import {
|
||||
inspectJadxAvailability,
|
||||
resolveJadxConfigurationInputs,
|
||||
resolveJadxOperationTimeout,
|
||||
validateJadxConfiguration,
|
||||
} from "./JadxConfiguration.js";
|
||||
import type { JadxLauncher } from "./JadxMcpTransport.js";
|
||||
|
||||
import { JadxSession } from "./JadxSession.js";
|
||||
|
||||
const OPERATION_TIMEOUT_MS = 120_000;
|
||||
const BRIDGE_SOURCE = fileURLToPath(
|
||||
new URL("../../bridge/android/ReaJadxBridge.java", import.meta.url),
|
||||
);
|
||||
@@ -65,10 +65,11 @@ const executionError = (context: {
|
||||
cause: unknown;
|
||||
request: AndroidRequest;
|
||||
timeout: AbortSignal;
|
||||
timeoutMs: number;
|
||||
signal: AbortSignal | undefined;
|
||||
session: JadxSession | undefined;
|
||||
}): AnalysisError => {
|
||||
const { cause, request, timeout, signal, session } = context;
|
||||
const { cause, request, timeout, timeoutMs, signal, session } = context;
|
||||
if (cause instanceof RegularFileCleanupFailure) {
|
||||
const primaryCause =
|
||||
cause.outcome.kind === "failed"
|
||||
@@ -86,7 +87,7 @@ const executionError = (context: {
|
||||
: timeout.aborted
|
||||
? new AnalysisTimeoutError(
|
||||
request.operation,
|
||||
OPERATION_TIMEOUT_MS,
|
||||
timeoutMs,
|
||||
primaryOptions,
|
||||
)
|
||||
: primaryCause instanceof AnalysisError
|
||||
@@ -114,7 +115,7 @@ const executionError = (context: {
|
||||
if (signal?.aborted === true)
|
||||
return new AnalysisCancelledError(request.operation, { cause });
|
||||
if (timeout.aborted)
|
||||
return new AnalysisTimeoutError(request.operation, OPERATION_TIMEOUT_MS);
|
||||
return new AnalysisTimeoutError(request.operation, timeoutMs);
|
||||
if (cause instanceof AnalysisError) return cause;
|
||||
const protocolFailure = session?.transport.failureReason();
|
||||
if (protocolFailure !== undefined && protocolFailure !== null)
|
||||
@@ -440,7 +441,14 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
request: AndroidRequest,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Outcome> {
|
||||
const timeout = AbortSignal.timeout(OPERATION_TIMEOUT_MS);
|
||||
// The deadline is fixed per provider environment; an unusable value is
|
||||
// refused before any file handle, workspace, or engine is acquired.
|
||||
const timeoutMs = resolveJadxOperationTimeout(
|
||||
this.environment,
|
||||
request.operation,
|
||||
);
|
||||
if (!timeoutMs.ok) return timeoutMs;
|
||||
const timeout = AbortSignal.timeout(timeoutMs.value);
|
||||
const signal =
|
||||
options?.signal === undefined
|
||||
? timeout
|
||||
@@ -596,6 +604,7 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
jarHash: retained.jarHash,
|
||||
bridgeHash: retained.bridgeHash,
|
||||
signal: activeSignal,
|
||||
timeoutMs: timeoutMs.value,
|
||||
}),
|
||||
);
|
||||
activeSignal.throwIfAborted();
|
||||
@@ -606,6 +615,7 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
cause,
|
||||
request,
|
||||
timeout,
|
||||
timeoutMs: timeoutMs.value,
|
||||
signal: options?.signal,
|
||||
session,
|
||||
});
|
||||
|
||||
@@ -23,6 +23,12 @@ export const JADX_JAVA_RUNTIME_REMEDIATION =
|
||||
export const JADX_JVM_OPTIONS_REMEDIATION =
|
||||
"Set REA_JADX_HEAP_MIB to a positive integer (heap size in MiB) and REA_JADX_ACTIVE_PROCESSOR_COUNT to a positive integer within the JVM's signed 32-bit range.";
|
||||
|
||||
/** Whole-operation deadline used when REA_JADX_OPERATION_TIMEOUT_MS is unset. */
|
||||
export const DEFAULT_JADX_OPERATION_TIMEOUT_MS = 120_000;
|
||||
|
||||
/** Remediation when the JADX operation deadline override is malformed. */
|
||||
export const JADX_OPERATION_TIMEOUT_REMEDIATION = `Set REA_JADX_OPERATION_TIMEOUT_MS to a decimal millisecond count from 1 to 2147483647, or unset it to keep the ${String(DEFAULT_JADX_OPERATION_TIMEOUT_MS)} ms default.`;
|
||||
|
||||
/** Remediation when the host cannot run the Android JADX subprocess. */
|
||||
export const JADX_UNSUPPORTED_HOST_REMEDIATION =
|
||||
"Use Linux, macOS, or Windows x64 with its bundled native controls for Android JADX analysis.";
|
||||
|
||||
@@ -69,8 +69,10 @@ export class JadxSession {
|
||||
readonly jarHash: string;
|
||||
readonly bridgeHash: string;
|
||||
readonly signal: AbortSignal;
|
||||
/** Selected whole-operation deadline; also bounds each upstream call. */
|
||||
readonly timeoutMs: number;
|
||||
}): Promise<AnalysisExecution> {
|
||||
const { request, target, snapshot, jarHash, signal } = context;
|
||||
const { request, target, snapshot, jarHash, signal, timeoutMs } = context;
|
||||
this.#partialContext = {
|
||||
operation: request.operation,
|
||||
selectedPath: request.input.path,
|
||||
@@ -105,7 +107,7 @@ export class JadxSession {
|
||||
request.operation,
|
||||
`Expected ${JADX_BRIDGE_IDENTITY.name} ${JADX_BRIDGE_IDENTITY.version}; server reported ${server?.name ?? "unknown"} ${server?.version ?? "unknown"}.`,
|
||||
);
|
||||
const tools = this.#tools(request, target, signal);
|
||||
const tools = this.#tools(request, target, signal, timeoutMs);
|
||||
const runtime = jadxRuntimeSchema.parse(
|
||||
await tools.json("rea_jvm_status", {}),
|
||||
);
|
||||
@@ -206,15 +208,17 @@ export class JadxSession {
|
||||
request: AndroidRequest,
|
||||
target: BinaryTarget,
|
||||
signal: AbortSignal,
|
||||
timeoutMs: number,
|
||||
): JadxToolPort {
|
||||
const call = async (
|
||||
name: string,
|
||||
input: Readonly<Record<string, JsonValue>>,
|
||||
): Promise<string> => {
|
||||
signal.throwIfAborted();
|
||||
// Keep an explicit bound: omitting it falls back to the SDK's 60-second default.
|
||||
const response = await this.#client.callTool(
|
||||
{ name, arguments: input },
|
||||
{ timeout: 120_000, signal },
|
||||
{ timeout: timeoutMs, signal },
|
||||
);
|
||||
const observedResponse = jsonValueSchema.parse(response);
|
||||
const call = { operation: name, input, response: observedResponse };
|
||||
|
||||
Reference in new issue
Block a user