mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
01788bac86
* fix(android): name the JADX deadline and recover apktool decode JADX operation timeouts now name REA_JADX_OPERATION_TIMEOUT_MS, the effective milliseconds, the 1..2147483647 range, and that a change applies on the next process start. Apktool decode accepts decode_timeout_ms, omitted means 600000, and a timeout names that key. ADB deadlines and apktool and Java version probes stay fixed and say there is no runtime override. Refs #5910 Co-authored-by: morluto <morluto@users.noreply.github.com> * fix(android): derive capture recovery from the fixed allowance Main now stores a fixed allowance instead of a prebuilt remediation string. Apktool and adb capture refusals keep the no-runtime-override sentence in that allowance name, including that decode_timeout_ms does not raise the apktool output cap. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(apktool): observe a real decode deadline and process cleanup A hanging interpreter launcher and its pipe-holding child stay alive until decode_timeout_ms elapses. The provider then reports provider_timeout with completed cleanup, and both processes are gone. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
385 lines
14 KiB
TypeScript
385 lines
14 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
|
import {
|
|
AnalysisError,
|
|
type AnalysisCapturedOutput,
|
|
} from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisCancelledError,
|
|
AnalysisCapabilityUnavailableError,
|
|
AnalysisTimeoutError,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
|
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
|
|
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
import {
|
|
prepareOwnedCommand,
|
|
runOwnedCommand,
|
|
OwnedCommandFailure,
|
|
} from "../process/OwnedCommand.js";
|
|
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
|
|
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
|
import { retainOwnedCommandCleanup } from "../providers/OwnedCommandCleanup.js";
|
|
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
|
|
import { jadxOperationTimeoutGuidance } from "./JadxRelease.js";
|
|
|
|
const JAVA_MODULE_LISTING_TIMEOUT_MS = 10_000;
|
|
const JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
|
|
export interface JadxJavaProbeOptions {
|
|
readonly resourceOwner: RetainedResourceScope;
|
|
readonly signal?: AbortSignal | undefined;
|
|
readonly operation?: string;
|
|
/** The Android operation owns this deadline; the probe also has its own bound. */
|
|
readonly operationDeadline?: {
|
|
readonly signal: AbortSignal;
|
|
readonly timeoutMs: number;
|
|
};
|
|
readonly launcher?: NonNullable<
|
|
Parameters<typeof runOwnedCommand>[2]
|
|
>["launcher"];
|
|
readonly timeoutMs?: number;
|
|
}
|
|
|
|
export interface JadxJavaProbeRefusal {
|
|
readonly code:
|
|
| "runtime_missing"
|
|
| "runtime_unavailable"
|
|
| "version_unresolved"
|
|
| "unsupported_version";
|
|
readonly message: string;
|
|
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
|
readonly kind: "java_probe" | "java_runtime";
|
|
}
|
|
interface JavaModules {
|
|
readonly compiler: true;
|
|
readonly major: number;
|
|
}
|
|
const refusal = (
|
|
code: JadxJavaProbeRefusal["code"],
|
|
message: string,
|
|
diagnostics: JadxJavaProbeRefusal["diagnostics"],
|
|
kind: JadxJavaProbeRefusal["kind"],
|
|
): Result<never, JadxJavaProbeRefusal> =>
|
|
err({ code, message, diagnostics, kind });
|
|
|
|
const capturedOutput = (
|
|
snapshot: ProviderProcessSnapshot,
|
|
): AnalysisCapturedOutput => ({
|
|
stdout: snapshot.stdout.text,
|
|
stderr: snapshot.stderr.text,
|
|
stdout_bytes: snapshot.stdout.observedBytes,
|
|
stderr_bytes: snapshot.stderr.observedBytes,
|
|
truncated:
|
|
snapshot.diagnosticTruncated === true || snapshot.outputIncomplete === true,
|
|
exit_code: snapshot.exitCode ?? null,
|
|
signal: snapshot.signal ?? null,
|
|
});
|
|
const projectRefusal = (
|
|
failure: JadxJavaProbeRefusal,
|
|
operation: string,
|
|
cause: unknown,
|
|
output?: AnalysisCapturedOutput,
|
|
) => {
|
|
const { stdout: _stdout, stderr: _stderr, ...facts } = failure.diagnostics;
|
|
return new AnalysisCapabilityUnavailableError(
|
|
"jadx",
|
|
operation,
|
|
failure.message,
|
|
{
|
|
cause,
|
|
userMessage: failure.message,
|
|
diagnostics: {
|
|
...facts,
|
|
failure_kind: failure.kind,
|
|
failure_code: failure.code,
|
|
},
|
|
...(output === undefined ? {} : { capturedOutput: output }),
|
|
},
|
|
);
|
|
};
|
|
/** Interpret one complete module listing before joining final cleanup. */
|
|
export const inspectJadxJava = async (
|
|
java: string,
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
options: JadxJavaProbeOptions,
|
|
): Promise<Result<JavaModules, JadxJavaProbeRefusal>> => {
|
|
const operation = options.operation ?? "inspect_jadx_configuration";
|
|
try {
|
|
return await options.resourceOwner.run(async () => {
|
|
let observed: Result<JavaModules, JadxJavaProbeRefusal> | undefined;
|
|
try {
|
|
await prepareOwnedCommand(options.signal);
|
|
await runOwnedCommand(
|
|
{
|
|
command: java,
|
|
arguments: ["--list-modules"],
|
|
runId: randomUUID(),
|
|
expectedCommand: null,
|
|
hostEnvironment: environment,
|
|
sweepTokenOwnedProcesses: true,
|
|
},
|
|
{
|
|
timeoutMs: options.timeoutMs ?? JAVA_MODULE_LISTING_TIMEOUT_MS,
|
|
diagnosticBytes: JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES,
|
|
acceptNonZeroExit: true,
|
|
observeCompleted: (snapshot) => {
|
|
observed = parseJavaModules(java, snapshot);
|
|
},
|
|
},
|
|
{
|
|
signal: options.signal,
|
|
...(options.launcher === undefined
|
|
? {}
|
|
: { launcher: options.launcher }),
|
|
},
|
|
);
|
|
if (observed === undefined)
|
|
throw new Error("Java probe completed without an observation");
|
|
return observed;
|
|
} catch (cause: unknown) {
|
|
const command =
|
|
cause instanceof OwnedCommandFailure ? cause : undefined;
|
|
const preparation =
|
|
command?.preparationFailure ??
|
|
(cause instanceof DarwinProcessOwnershipInspectionError
|
|
? cause
|
|
: undefined);
|
|
const output =
|
|
command?.snapshot == null
|
|
? preparation?.capturedOutput
|
|
: capturedOutput(command.snapshot);
|
|
const cleanups = retainOwnedCommandCleanup(
|
|
cause,
|
|
options.resourceOwner,
|
|
"jadx",
|
|
operation,
|
|
);
|
|
let primary: AnalysisError;
|
|
if (observed !== undefined && !observed.ok) {
|
|
if (cleanups.length === 0) return observed;
|
|
primary = projectRefusal(observed.error, operation, cause, output);
|
|
} else if (
|
|
command?.reason === "cancelled" &&
|
|
command.cancellation !== undefined
|
|
) {
|
|
const reason = command.cancellation.reason;
|
|
const errorOptions = {
|
|
cause: reason,
|
|
...(output === undefined ? {} : { capturedOutput: output }),
|
|
...(observed?.ok === true
|
|
? {
|
|
diagnostics: {
|
|
java_module_observation: {
|
|
compiler: observed.value.compiler,
|
|
major: observed.value.major,
|
|
},
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
primary =
|
|
options.operationDeadline?.signal.aborted === true &&
|
|
reason === options.operationDeadline.signal.reason
|
|
? new AnalysisTimeoutError(
|
|
operation,
|
|
options.operationDeadline.timeoutMs,
|
|
{
|
|
...errorOptions,
|
|
...jadxOperationTimeoutGuidance(
|
|
options.operationDeadline.timeoutMs,
|
|
),
|
|
},
|
|
)
|
|
: new AnalysisCancelledError(operation, errorOptions);
|
|
} else if (
|
|
observed?.ok === true &&
|
|
command?.cleanupOwner !== undefined
|
|
) {
|
|
primary = new ProviderAdapterError("jadx", operation, {
|
|
cause,
|
|
...(output === undefined ? {} : { capturedOutput: output }),
|
|
diagnostics: {
|
|
stage: "cleanup",
|
|
java_module_observation: {
|
|
compiler: observed.value.compiler,
|
|
major: observed.value.major,
|
|
},
|
|
},
|
|
});
|
|
} else if (preparation !== undefined) {
|
|
primary = new ProviderAdapterError("jadx", operation, {
|
|
cause,
|
|
...(output === undefined ? {} : { capturedOutput: output }),
|
|
diagnostics: {
|
|
stage: "ownership_preparation",
|
|
...(preparation.compilerOutcome === undefined
|
|
? {}
|
|
: {
|
|
compiler_outcome: jsonValueSchema.parse(
|
|
preparation.compilerOutcome,
|
|
),
|
|
}),
|
|
},
|
|
});
|
|
} else if (cause instanceof AnalysisError) {
|
|
primary = cause;
|
|
} else {
|
|
const failure = commandProbeRefusal(
|
|
java,
|
|
command,
|
|
cause,
|
|
options.timeoutMs ?? JAVA_MODULE_LISTING_TIMEOUT_MS,
|
|
);
|
|
if (cleanups.length === 0) return err(failure);
|
|
primary = projectRefusal(failure, operation, cause, output);
|
|
}
|
|
for (const cleanup of cleanups)
|
|
primary = analysisErrorWithCleanupFailure(
|
|
primary,
|
|
cleanup,
|
|
operation,
|
|
).bindCleanupDependencies([primary, cleanup]);
|
|
throw primary;
|
|
}
|
|
});
|
|
} catch (cause: unknown) {
|
|
if (cause instanceof AnalysisError) throw cause;
|
|
throw analysisClientFailureForOperation(cause, "jadx", operation);
|
|
}
|
|
};
|
|
|
|
const commandProbeRefusal = (
|
|
java: string,
|
|
command:
|
|
| Pick<OwnedCommandFailure, "snapshot" | "reason" | "processError">
|
|
| undefined,
|
|
cause: unknown,
|
|
timeoutMs: number,
|
|
): JadxJavaProbeRefusal => {
|
|
const output =
|
|
command?.snapshot == null ? undefined : capturedOutput(command.snapshot);
|
|
const processError =
|
|
command?.processError ?? (cause instanceof Error ? cause : undefined);
|
|
const systemCode =
|
|
processError === undefined ? undefined : Reflect.get(processError, "code");
|
|
const limited = command?.reason === "output-limit";
|
|
const timedOut = command?.reason === "timeout";
|
|
return {
|
|
code:
|
|
systemCode === "ENOENT"
|
|
? "runtime_missing"
|
|
: limited || timedOut
|
|
? "version_unresolved"
|
|
: "runtime_unavailable",
|
|
kind: systemCode === "ENOENT" ? "java_runtime" : "java_probe",
|
|
message: limited
|
|
? `The selected Java launcher ${java} exceeded the ${String(JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES)}-byte module-listing output budget. This release has no runtime override. REA retained partial output but could not establish compiler or version availability. Inspect the selected launcher for verbose or unbounded output; java --list-modules should return a concise module listing. REA_JADX_OPERATION_TIMEOUT_MS does not raise this probe.`
|
|
: timedOut
|
|
? `The selected Java launcher ${java} did not finish the module-listing probe within ${String(timeoutMs)} ms. This release has no runtime override. Inspect its startup responsiveness and verify it with java --list-modules. REA_JADX_OPERATION_TIMEOUT_MS does not raise this probe.`
|
|
: `Cannot inspect selected Java runtime ${java}: ${systemCode === "ENOENT" ? "spawn java ENOENT" : cause instanceof Error ? cause.message : cause === undefined ? `exit ${String(output?.exit_code ?? "unknown")}${output?.signal == null ? "" : ` (${output.signal})`}` : String(cause)}. Review the selected launcher and retained diagnostics.`,
|
|
diagnostics: {
|
|
java_executable: java,
|
|
phase: "module-inspection",
|
|
exit_code: output?.exit_code ?? null,
|
|
process_error_code: typeof systemCode === "string" ? systemCode : null,
|
|
signal: output?.signal ?? null,
|
|
killed: null,
|
|
stdout: output?.stdout ?? "",
|
|
stderr: output?.stderr ?? "",
|
|
output_truncated: output?.truncated ?? true,
|
|
...(limited
|
|
? {
|
|
failure: "output_limit",
|
|
output_limit_bytes: JAVA_MODULE_LISTING_MAX_OUTPUT_BYTES,
|
|
}
|
|
: {}),
|
|
...(timedOut
|
|
? { failure: "probe_terminated", deadline_ms: timeoutMs }
|
|
: {}),
|
|
},
|
|
};
|
|
};
|
|
|
|
const parseJavaModules = (
|
|
java: string,
|
|
snapshot: ProviderProcessSnapshot,
|
|
): Result<JavaModules, JadxJavaProbeRefusal> => {
|
|
if (snapshot.exitCode !== 0 || snapshot.signal !== null)
|
|
return err(
|
|
commandProbeRefusal(
|
|
java,
|
|
{ reason: "process", snapshot, processError: undefined },
|
|
undefined,
|
|
JAVA_MODULE_LISTING_TIMEOUT_MS,
|
|
),
|
|
);
|
|
const stdout = snapshot.stdout.text;
|
|
const stderr = snapshot.stderr.text;
|
|
const compiler = /^jdk\.compiler(?:@|$)/mu.test(stdout);
|
|
if (!compiler)
|
|
return refusal(
|
|
"runtime_missing",
|
|
`Selected Java runtime ${java} does not provide jdk.compiler. Select a full JDK including jdk.compiler via JAVA_HOME or PATH; verify it with java --list-modules. A JRE cannot compile REA's source bridge.`,
|
|
{
|
|
java_executable: java,
|
|
phase: "module-inspection",
|
|
jdk_compiler: false,
|
|
stdout,
|
|
stderr,
|
|
output_truncated: false,
|
|
},
|
|
"java_runtime",
|
|
);
|
|
const major = /^java\.base@(\d+)(?:[.+_-]|$)/mu.exec(stdout)?.[1];
|
|
if (major === undefined)
|
|
return refusal(
|
|
"version_unresolved",
|
|
`The selected Java launcher ${java} did not report a parseable java.base version in its module listing. Inspect the launcher output and verify it with java --list-modules.`,
|
|
{
|
|
java_executable: java,
|
|
phase: "module-inspection",
|
|
jdk_compiler: true,
|
|
java_major: null,
|
|
stdout,
|
|
stderr,
|
|
output_truncated: false,
|
|
},
|
|
"java_probe",
|
|
);
|
|
const majorNumber = Number(major);
|
|
if (!Number.isSafeInteger(majorNumber) || majorNumber <= 0)
|
|
return refusal(
|
|
"version_unresolved",
|
|
`The selected Java launcher ${java} reported an invalid java.base major version. Review the retained module listing.`,
|
|
{
|
|
java_executable: java,
|
|
phase: "module-inspection",
|
|
jdk_compiler: true,
|
|
java_major: null,
|
|
unparsed_java_major: major,
|
|
stdout,
|
|
stderr,
|
|
output_truncated: false,
|
|
},
|
|
"java_probe",
|
|
);
|
|
if (majorNumber < 17)
|
|
return refusal(
|
|
"unsupported_version",
|
|
`Selected Java runtime ${java} must be JDK 17 or newer and include jdk.compiler; verify it with java --list-modules.`,
|
|
{
|
|
java_executable: java,
|
|
phase: "module-inspection",
|
|
jdk_compiler: true,
|
|
java_major: major ?? null,
|
|
stdout,
|
|
stderr,
|
|
output_truncated: false,
|
|
},
|
|
"java_runtime",
|
|
);
|
|
return ok({ compiler: true, major: majorNumber });
|
|
};
|