mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
perf(android): validate prerequisites once per retained JADX session
Resolve current paths and JVM options and rehash source identities for each request. Probe Java modules and engine capabilities only when creating a new session; remove the superseded full configuration resolver. Focused configuration, session retirement, cancellation, cleanup and Android MCP boundaries passed. The 34-operation retained session now performs one prerequisite probe. Protocol fixtures do not establish real JADX or JDK performance.
This commit is contained in:
@@ -52,7 +52,11 @@ export const inspectJadxAvailability = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<ProviderAvailability> => {
|
||||
try {
|
||||
const configuration = await readJadxConfiguration(environment, signal);
|
||||
const configuration = await readJadxConfigurationInputs(
|
||||
environment,
|
||||
signal,
|
||||
);
|
||||
await validateJadxConfigurationContents(configuration, environment, signal);
|
||||
return {
|
||||
status: "available",
|
||||
code: null,
|
||||
@@ -77,52 +81,83 @@ export const inspectJadxAvailability = async (
|
||||
}
|
||||
};
|
||||
|
||||
/** Resolve caller-selected tools and preserve the same prerequisite diagnostics. */
|
||||
export const resolveJadxConfiguration = async (
|
||||
/** Resolve caller-selected paths and JVM options without probing their contents. */
|
||||
export const resolveJadxConfigurationInputs = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
operation: AndroidOperation,
|
||||
signal?: AbortSignal,
|
||||
): Promise<JadxConfiguration> => {
|
||||
try {
|
||||
return await readJadxConfiguration(environment, signal);
|
||||
return await readJadxConfigurationInputs(environment, signal);
|
||||
} catch (cause) {
|
||||
if (signal?.aborted === true) throw cause;
|
||||
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
|
||||
const output = cause.diagnostics;
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
operation,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
userMessage: cause.message,
|
||||
...(typeof output.stdout === "string" &&
|
||||
typeof output.stderr === "string"
|
||||
? {
|
||||
capturedOutput: {
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
truncated: output.output_truncated === true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
throw projectConfigurationFailure(operation, cause);
|
||||
}
|
||||
};
|
||||
|
||||
const readJadxConfiguration = async (
|
||||
/** Probe Java and archive prerequisites before starting a new engine session. */
|
||||
export const validateJadxConfiguration = async (
|
||||
configuration: JadxConfiguration,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
operation: AndroidOperation,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> => {
|
||||
try {
|
||||
await validateJadxConfigurationContents(configuration, environment, signal);
|
||||
} catch (cause) {
|
||||
if (signal?.aborted === true) throw cause;
|
||||
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
|
||||
throw projectConfigurationFailure(operation, cause);
|
||||
}
|
||||
};
|
||||
|
||||
const readJadxConfigurationInputs = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<JadxConfiguration> => {
|
||||
signal?.throwIfAborted();
|
||||
requireSupportedHost();
|
||||
const jar = await resolveJar(environment);
|
||||
const java = await resolveJava(environment, signal);
|
||||
await requireJavaHomeExecutable(environment.JAVA_HOME, java);
|
||||
const jvmArguments = resolveJvmArguments(environment);
|
||||
await inspectJavaRuntime(java, environment, signal);
|
||||
await requireEngineArchive(jar, signal);
|
||||
return { jar, java, jvmArguments };
|
||||
signal?.throwIfAborted();
|
||||
return { jar, java, jvmArguments: resolveJvmArguments(environment) };
|
||||
};
|
||||
|
||||
const validateJadxConfigurationContents = async (
|
||||
configuration: JadxConfiguration,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> => {
|
||||
signal?.throwIfAborted();
|
||||
await inspectJavaRuntime(configuration.java, environment, signal);
|
||||
await requireEngineArchive(configuration.jar, signal);
|
||||
};
|
||||
|
||||
const projectConfigurationFailure = (
|
||||
operation: AndroidOperation,
|
||||
cause: JadxConfigurationFailure,
|
||||
): AnalysisCapabilityUnavailableError => {
|
||||
const output = cause.diagnostics;
|
||||
return new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
operation,
|
||||
cause.message,
|
||||
{
|
||||
cause,
|
||||
userMessage: cause.message,
|
||||
...(typeof output.stdout === "string" && typeof output.stderr === "string"
|
||||
? {
|
||||
capturedOutput: {
|
||||
stdout: output.stdout,
|
||||
stderr: output.stderr,
|
||||
truncated: output.output_truncated === true,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const requireEngineArchive = async (
|
||||
|
||||
@@ -27,7 +27,8 @@ import { hashStableFile } from "../filesystem/StableFileHash.js";
|
||||
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
|
||||
import {
|
||||
inspectJadxAvailability,
|
||||
resolveJadxConfiguration,
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "./JadxConfiguration.js";
|
||||
import type { JadxLauncher } from "./JadxMcpTransport.js";
|
||||
|
||||
@@ -333,7 +334,7 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
request.operation,
|
||||
`Target ${target.path} is ${target.format}; provide one standalone APK.`,
|
||||
);
|
||||
const configuration = await resolveJadxConfiguration(
|
||||
const configuration = await resolveJadxConfigurationInputs(
|
||||
this.environment,
|
||||
request.operation,
|
||||
signal,
|
||||
@@ -382,6 +383,12 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
}
|
||||
let retained = this.#retained;
|
||||
if (retained === undefined) {
|
||||
await validateJadxConfiguration(
|
||||
configuration,
|
||||
this.environment,
|
||||
request.operation,
|
||||
signal,
|
||||
);
|
||||
root = await PrivateRuntimeRoot.create({ prefix: "rea-android-" });
|
||||
const engine = await snapshotAndroidEngine(
|
||||
configuration.jar,
|
||||
|
||||
@@ -5,7 +5,8 @@ import { expect, it as test } from "vitest";
|
||||
|
||||
import {
|
||||
inspectJadxAvailability,
|
||||
resolveJadxConfiguration,
|
||||
resolveJadxConfigurationInputs,
|
||||
validateJadxConfiguration,
|
||||
} from "../../../src/android/JadxConfiguration.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
@@ -14,6 +15,18 @@ import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
const resolveConfiguration = async (
|
||||
environment: Parameters<typeof resolveJadxConfigurationInputs>[0],
|
||||
operation: Parameters<typeof resolveJadxConfigurationInputs>[1],
|
||||
) => {
|
||||
const configuration = await resolveJadxConfigurationInputs(
|
||||
environment,
|
||||
operation,
|
||||
);
|
||||
await validateJadxConfiguration(configuration, environment, operation);
|
||||
return configuration;
|
||||
};
|
||||
|
||||
it("rejects malformed or unrelated engine archives with JAR-specific recovery", async () => {
|
||||
const root = await createTestTempDirectory("rea-jadx-invalid-jar-");
|
||||
const javaHome = await createFakeJdk(root);
|
||||
@@ -29,7 +42,7 @@ it("rejects malformed or unrelated engine archives with JAR-specific recovery",
|
||||
diagnostics: { jar_path: await realpath(jar), phase: "jar-inspection" },
|
||||
});
|
||||
await expect(
|
||||
resolveJadxConfiguration(environment, "inspect_android_package"),
|
||||
resolveConfiguration(environment, "inspect_android_package"),
|
||||
).rejects.toMatchObject({
|
||||
_tag: "AnalysisCapabilityUnavailableError",
|
||||
reason: expect.stringContaining("REA_JADX_MCP_JAR"),
|
||||
@@ -45,7 +58,7 @@ it("rejects an explicit JAVA_HOME without an executable java binary", async () =
|
||||
await writeJadxJarInventory(jar);
|
||||
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: javaHome },
|
||||
"inspect_android_package",
|
||||
),
|
||||
@@ -62,7 +75,7 @@ it("accepts a full JDK and canonicalizes the selected JAR", async () => {
|
||||
await writeJadxJarInventory(jar);
|
||||
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: javaHome },
|
||||
"inspect_android_package",
|
||||
),
|
||||
@@ -78,7 +91,7 @@ it("configures heap and visible processors independently without forcing either"
|
||||
const jar = join(root, "engine.jar");
|
||||
const javaHome = await createFakeJdk(root);
|
||||
await writeJadxJarInventory(jar);
|
||||
const configuration = await resolveJadxConfiguration(
|
||||
const configuration = await resolveConfiguration(
|
||||
{
|
||||
REA_JADX_MCP_JAR: jar,
|
||||
JAVA_HOME: javaHome,
|
||||
@@ -92,7 +105,7 @@ it("configures heap and visible processors independently without forcing either"
|
||||
"-XX:ActiveProcessorCount=4",
|
||||
]);
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{
|
||||
REA_JADX_MCP_JAR: jar,
|
||||
JAVA_HOME: javaHome,
|
||||
@@ -107,7 +120,7 @@ it("configures heap and visible processors independently without forcing either"
|
||||
for (const name of ["REA_JADX_HEAP_MIB", "REA_JADX_ACTIVE_PROCESSOR_COUNT"])
|
||||
for (const value of ["", "0", "-1", "1.5", "8g", " 4", "9007199254740992"])
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{
|
||||
REA_JADX_MCP_JAR: jar,
|
||||
JAVA_HOME: javaHome,
|
||||
@@ -127,7 +140,7 @@ it("distinguishes a runnable JRE from a missing Java executable", async () => {
|
||||
await writeJadxJarInventory(jar);
|
||||
const jreHome = await createFakeJdk(root, false);
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: jreHome },
|
||||
"inspect_android_package",
|
||||
),
|
||||
@@ -175,7 +188,7 @@ it("preserves exact Java probe output and marks max-buffer capture as truncated"
|
||||
);
|
||||
await chmod(java, 0o700);
|
||||
|
||||
const failure = await resolveJadxConfiguration(
|
||||
const failure = await resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: javaHome },
|
||||
"inspect_android_package",
|
||||
).then(
|
||||
@@ -241,7 +254,7 @@ it("distinguishes an unreported Java version from an unsupported old version", a
|
||||
);
|
||||
await chmod(join(oldHome, "bin", "java"), 0o700);
|
||||
await expect(
|
||||
resolveJadxConfiguration(
|
||||
resolveConfiguration(
|
||||
{ REA_JADX_MCP_JAR: jar, JAVA_HOME: oldHome },
|
||||
"inspect_android_package",
|
||||
),
|
||||
|
||||
@@ -91,8 +91,9 @@ it("reuses immutable engine state while keeping each Evidence observation separa
|
||||
await verifyCleanup(launches);
|
||||
});
|
||||
|
||||
it("bounds output per operation across a retained session rather than accumulating earlier captures", async () => {
|
||||
const { service, apk, launches, provider } = await setup("large-manifest");
|
||||
it("bounds output per operation without repeating prerequisite probes for a retained session", async () => {
|
||||
const { service, apk, launches, provider, javaProbeCount } =
|
||||
await setup("large-manifest");
|
||||
const expectedManifest = "x".repeat(1024 * 1024);
|
||||
for (let index = 0; index < 34; index += 1) {
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
@@ -106,12 +107,14 @@ it("bounds output per operation across a retained session rather than accumulati
|
||||
).toBe(expectedManifest);
|
||||
}
|
||||
expect(launches).toHaveLength(1);
|
||||
expect(await javaProbeCount()).toBe(1);
|
||||
await provider.close();
|
||||
await verifyCleanup(launches);
|
||||
});
|
||||
|
||||
it("retire sessions when APK bytes, engine bytes or JVM options change", async () => {
|
||||
const { service, apk, jar, launches, provider, environment } = await setup();
|
||||
const { service, apk, jar, launches, provider, environment, javaProbeCount } =
|
||||
await setup();
|
||||
const inspect = async () => {
|
||||
const result = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
@@ -133,6 +136,7 @@ it("retire sessions when APK bytes, engine bytes or JVM options change", async (
|
||||
environment._JAVA_OPTIONS = "-XX:+UseG1GC";
|
||||
await inspect();
|
||||
expect(launches).toHaveLength(4);
|
||||
expect(await javaProbeCount()).toBe(4);
|
||||
expect(launches[3]).toMatchObject({
|
||||
javaToolOptions: "-Xmx8g",
|
||||
legacyJavaOptions: "-XX:+UseG1GC",
|
||||
|
||||
Vendored
+14
-2
@@ -1,4 +1,11 @@
|
||||
import { access, chmod, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
chmod,
|
||||
mkdir,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, onTestFinished } from "vitest";
|
||||
@@ -27,10 +34,11 @@ export const createJadxProtocolFixture = async (
|
||||
const javaHome = join(root, "jdk");
|
||||
const javaBin = join(javaHome, "bin");
|
||||
const java = join(javaBin, "java");
|
||||
const javaProbeLog = join(root, "java-probes.log");
|
||||
await mkdir(javaBin, { recursive: true });
|
||||
await writeFile(
|
||||
java,
|
||||
`#!/usr/bin/env node\nif (process.argv[2] === "--list-modules") console.log("[email protected]\\[email protected]");\n`,
|
||||
`#!/usr/bin/env node\nif (process.argv[2] === "--list-modules") { require("node:fs").appendFileSync(${JSON.stringify(javaProbeLog)}, "probe\\n"); console.log("[email protected]\\[email protected]"); }\n`,
|
||||
);
|
||||
await chmod(java, 0o755);
|
||||
await writeOrderedZip(apk, ["AndroidManifest.xml", "classes.dex"]);
|
||||
@@ -115,6 +123,10 @@ export const createJadxProtocolFixture = async (
|
||||
jar,
|
||||
environment,
|
||||
launches,
|
||||
javaProbeCount: async () => {
|
||||
const contents = await readFile(javaProbeLog, "utf8");
|
||||
return contents.split("\n").filter(Boolean).length;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user