Files
reverse-engineering-anything/src/android/JadxJavaProbe.ts
T
morluto ab84ba6839 fix(process)!: retain inspection cancellation and cleanup ownership (#10113)
* fix(process): preserve producer-owned inspection cancellation

Prototype the explicit inspection origin and retained file/batch owner protocol. Downstream adapter migration and lease binding remain required before integration.

* fix(process): type inspection ownership handoffs precisely

* fix(process): retain plain inspection preparation cancellation

* fix(process): retain explicit inspection cleanup and cancellation facts

* fix(process): require explicit inspection facts and cleanup scope

* refactor(process): remove redundant cleanup forwarding wrapper

* fix(firmware): retain inspection cancellation and cleanup ownership

* fix(wakaru): retain typed inspection outcomes and caller scope

* fix(ghidra): preserve inspection facts and launch cleanup scope

* fix(providers): preserve typed process inspection outcomes

* refactor(process): remove obsolete inspection cancellation re-export

* fix(native): preserve inspection preparation cleanup and cancellation

* fix(registration): retain typed inspection cleanup during Git discovery

* fix(process): preserve inspection deadline origin in adapter handoff

* fix(process): consume the canonical inspection selector in binding

* fix(native): preserve inspection deadlines and cleanup retry completion

* test(process): narrow the retained Darwin preparation owner

* fix(providers): preserve preparation ownership and timeout policy

Narrow preparation facts through their canonical owner, preserve actual cleanup capabilities and recorded cancellation, and keep enclosing command deadlines authoritative. Retain raw inspection records through Java refusal and configuration projections without changing availability fallback policy.

* refactor(process): use one inspection failure type owner

* fix(providers): retain recorded secondary failures when rebinding

* fix(ghidra): preserve normalized inspection facts through public projection

* fix(process): normalize native process listing cancellation

* docs(development): clarify failure provenance and cleanup verification

* fix(process): preserve direct inspection cancellation facts

* fix(process): retain inspection owners through termination

* fix(process): preserve command inspection cleanup provenance

* fix(desktop): preserve inspection facts after cleanup retry

* refactor(process)!: migrate desktop capture with inspection producers

* fix(desktop)!: preserve capture failure origins and cleanup owners (#10115)

* fix(desktop): preserve capture failure origins and cleanup

* fix(desktop): type keyring cleanup cancellation

* fix(kuna)!: retain copy cleanup before retiring workspaces (#10114)

* fix(kuna): own copy cancellation and descriptor cleanup

* fix(kuna): close copy descriptors before workspace removal

* test(kuna): type copy lifecycle gates for project library

* docs(kuna): explain retained copy-file cleanup and staging ownership

* docs(kuna): separate cache publication, integrity and cleanup guidance

* fix(desktop): retain sampling inspection owners before release

* fix(process): preserve capture file read cancellation origin

* fix(process): retain capture inspection cancellation and cleanup owners

* fix(process): retain sampling inspection failures in capture scope

* fix(process): retain inspection failures through mandatory cleanup

* fix(process): distinguish sampler shutdown from capture cancellation

* fix(browser): retain startup EOF before later cancellation

* fix(process): bind capture inspection cleanup to the owning run

* test(process): migrate cancellation fixtures to typed owners

Record explicit preparation and command cancellation facts, retain exact opaque reasons and physical causes, and guard the mocked version probe from native preparation. Keep the live Wakaru command control while adding a recording equivalent for bounded source verification.

* fix(process): own sampling reads and settle inspection peers

* fix(errors): retain authored recovery through cleanup composition

* fix(process): retain all sampling inspection leaves for final cleanup

* test(browser): report recording cleanup signal outcome

* fix(process): keep inspection error context out of implicit JSON

* fix(desktop): retain normalized sampling cleanup ownership

* test(process): assert tagged startup cancellation
2026-11-11 16:37:48 -05:00

404 lines
15 KiB
TypeScript

import type { ProcessInspectionFailure } from "../process/ProcessInspectionCleanup.js";
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,
processInspectionCancellationError,
processInspectionFailure,
bindProcessInspectionCancellations,
} from "../providers/OwnedCommandCleanup.js";
import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js";
import { jadxOperationTimeoutGuidance } from "./JadxOperationTimeout.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 {
/** Actual preparation record; retained without changing timeout or fallback policy. */
readonly inspectionFailure?: ProcessInspectionFailure;
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 = processInspectionFailure(cause);
const output =
command?.snapshot == null
? preparation instanceof DarwinProcessOwnershipInspectionError
? preparation.capturedOutput
: undefined
: capturedOutput(command.snapshot);
const inspectionCancellation =
command === undefined || command.preparationFailure !== undefined
? processInspectionCancellationError(cause, operation)
: undefined;
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,
),
},
)
: (inspectionCancellation ??
new AnalysisCancelledError(operation, errorOptions));
} else if (inspectionCancellation !== undefined) {
primary = inspectionCancellation;
} 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 && command?.reason !== "timeout") {
primary = new ProviderAdapterError("jadx", operation, {
cause,
...(output === undefined ? {} : { capturedOutput: output }),
diagnostics: {
stage: "ownership_preparation",
...(!(
preparation instanceof DarwinProcessOwnershipInspectionError
) || 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 (preparation !== undefined)
Object.defineProperty(failure, "inspectionFailure", {
value: preparation,
});
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 bindProcessInspectionCancellations(cause, primary, operation);
}
});
} 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 });
};