mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(android): retain selected Java across workspace launches
This commit is contained in:
1 parent
ce5373e42f
commit
1a1121c61f
4 files changed
+135
-21
No files matched your search
@@ -1,4 +1,7 @@
|
||||
import { appendCallerPath } from "../filesystem/callerPath.js";
|
||||
import {
|
||||
absoluteCallerPath,
|
||||
appendCallerPath,
|
||||
} from "../filesystem/callerPath.js";
|
||||
import { DEFAULT_JADX_OPERATION_TIMEOUT_MS } from "../domain/analysisResourceLimits.js";
|
||||
import {
|
||||
jadxOperationTimeoutGuidance,
|
||||
@@ -600,30 +603,43 @@ const resolveJava = async (
|
||||
): Promise<string> => {
|
||||
const executable = process.platform === "win32" ? "java.exe" : "java";
|
||||
if (environment.JAVA_HOME === undefined) {
|
||||
if (process.platform !== "win32") return executable;
|
||||
const path =
|
||||
Object.entries(environment).find(
|
||||
([key]) => key.toLowerCase() === "path",
|
||||
)?.[1] ?? "";
|
||||
for (const directory of path
|
||||
.split(delimiter)
|
||||
.filter((entry) => entry !== "")) {
|
||||
const windows = process.platform === "win32";
|
||||
const path = windows
|
||||
? (Object.entries(environment).find(
|
||||
([key]) => key.toLowerCase() === "path",
|
||||
)?.[1] ?? "")
|
||||
: (environment.PATH ?? "/usr/bin:/bin");
|
||||
// Resolve in the caller's directory once. The module probe and the engine
|
||||
// run in different directories, and the selected executable also owns the
|
||||
// retained session's cache identity. Empty POSIX PATH components select cwd.
|
||||
const directories = path.split(delimiter);
|
||||
for (const directory of windows
|
||||
? directories.filter((entry) => entry !== "")
|
||||
: directories) {
|
||||
throwIfJadxInterrupted(signal);
|
||||
// Windows PATH commonly quotes individual entries containing spaces.
|
||||
const selected =
|
||||
directory.startsWith('"') && directory.endsWith('"')
|
||||
windows && directory.startsWith('"') && directory.endsWith('"')
|
||||
? directory.slice(1, -1)
|
||||
: directory;
|
||||
const candidate = resolve(selected, executable);
|
||||
const candidate = windows
|
||||
? resolve(selected, executable)
|
||||
: absoluteCallerPath(appendCallerPath(selected, executable));
|
||||
try {
|
||||
await access(candidate, constants.R_OK | constants.X_OK);
|
||||
if ((await stat(candidate)).isFile()) return await realpath(candidate);
|
||||
await access(
|
||||
candidate,
|
||||
windows ? constants.R_OK | constants.X_OK : constants.X_OK,
|
||||
);
|
||||
if ((await stat(candidate)).isFile())
|
||||
return windows ? await realpath(candidate) : candidate;
|
||||
} catch (cause: unknown) {
|
||||
if (
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
||||
(cause.code === "ENOENT" ||
|
||||
cause.code === "ENOTDIR" ||
|
||||
(!windows && (cause.code === "EACCES" || cause.code === "EPERM")))
|
||||
)
|
||||
continue;
|
||||
throw configurationFailure(
|
||||
@@ -637,8 +653,8 @@ const resolveJava = async (
|
||||
}
|
||||
throw configurationFailure(
|
||||
"runtime_missing",
|
||||
"Cannot find java.exe on the selected Windows PATH. Select a full JDK with JAVA_HOME or PATH, then verify it with java --list-modules. REA does not install Java.",
|
||||
{ java_executable: executable },
|
||||
`Cannot find an executable ${executable} on the selected ${windows ? "Windows " : ""}PATH. Select a full JDK with JAVA_HOME or PATH, then verify it with java --list-modules. REA does not install Java.`,
|
||||
{ java_executable: executable, phase: "path-lookup" },
|
||||
"java_runtime",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -579,6 +579,9 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
session = new JadxSession(
|
||||
{
|
||||
command: configuration.java,
|
||||
// Java PATH shims may execute an interpreter or replace themselves.
|
||||
// Match the prerequisite probe's run-token ownership policy.
|
||||
expectedCommand: null,
|
||||
arguments: [
|
||||
...configuration.jvmArguments,
|
||||
"--class-path",
|
||||
|
||||
@@ -244,12 +244,10 @@ it("distinguishes a runnable JRE from a missing Java executable", async () => {
|
||||
expect(availability).toMatchObject({
|
||||
status: "unavailable",
|
||||
code: "runtime_missing",
|
||||
reason: expect.stringContaining("spawn java ENOENT"),
|
||||
reason: expect.stringContaining("Cannot find an executable java"),
|
||||
diagnostics: {
|
||||
java_executable: "java",
|
||||
phase: "module-inspection",
|
||||
exit_code: null,
|
||||
process_error_code: "ENOENT",
|
||||
phase: "path-lookup",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { SdkErrorCode } from "@modelcontextprotocol/client";
|
||||
import { expect, it as test, onTestFinished, vi } from "vitest";
|
||||
import { access, readFile, truncate, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
chmod,
|
||||
mkdir,
|
||||
readFile,
|
||||
symlink,
|
||||
truncate,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
|
||||
@@ -20,10 +29,98 @@ import {
|
||||
verifyJadxFixtureCleanup as verifyCleanup,
|
||||
} from "../../fixtures/android/jadx.js";
|
||||
import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
||||
import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const it = test.skipIf(process.platform === "win32");
|
||||
|
||||
it.each(["alias/../bin", ""])(
|
||||
"keeps PATH=%j Java tied to each caller directory across private-workspace launches",
|
||||
async (searchPath) => {
|
||||
const root = await createTestTempDirectory("rea-jadx-relative-java-");
|
||||
const apk = join(root, "fixture.apk"),
|
||||
jar = join(root, "fixture.jar");
|
||||
await writeOrderedZip(apk, ["AndroidManifest.xml", "classes.dex"]);
|
||||
await writeJadxJarInventory(jar);
|
||||
const fixture = new URL(
|
||||
"../../fixtures/android/jadx-mcp.mjs",
|
||||
import.meta.url,
|
||||
);
|
||||
const command = [process.execPath, fileURLToPath(fixture)]
|
||||
.map((value) => `'${value.replaceAll("'", `'"'"'`)}'`)
|
||||
.join(" ");
|
||||
const shim = join(root, "java-shim");
|
||||
await writeFile(
|
||||
shim,
|
||||
`#!/bin/sh\nselected="\${0%/*}"\n[ -f "$selected/runtime-marker" ] || exit 19\npwd >> "$selected/java.log"\nif [ "$1" = "--list-modules" ]; then\n printf 'java.base@21.0.0\\njdk.compiler@21.0.0\\n'\nelse\n exec ${command}\nfi\n`,
|
||||
);
|
||||
await chmod(shim, 0o755);
|
||||
const directories = [join(root, "first"), join(root, "second")];
|
||||
for (const directory of directories) {
|
||||
await mkdir(join(directory, "real/deep"), { recursive: true });
|
||||
await mkdir(join(directory, "real/bin"));
|
||||
await symlink(join(directory, "real/deep"), join(directory, "alias"));
|
||||
await symlink(shim, join(directory, "real/bin/java"));
|
||||
await writeFile(join(directory, "real/bin/runtime-marker"), directory);
|
||||
await symlink(shim, join(directory, "java"));
|
||||
await writeFile(join(directory, "runtime-marker"), directory);
|
||||
}
|
||||
const logPath = (directory: string) =>
|
||||
join(directory, searchPath === "" ? "java.log" : "real/bin/java.log");
|
||||
const provider = new JadxProvider({
|
||||
...process.env,
|
||||
JAVA_HOME: undefined,
|
||||
PATH: searchPath,
|
||||
REA_JADX_MCP_JAR: jar,
|
||||
REA_JADX_OPERATION_TIMEOUT_MS: "5000",
|
||||
REA_FAKE_JADX_CLASS_COUNT: "0",
|
||||
});
|
||||
const service = new AndroidAnalysisService(provider);
|
||||
const previousCwd = process.cwd();
|
||||
try {
|
||||
process.chdir(directories[0]!);
|
||||
const availability = await provider.inspectAvailability();
|
||||
expect(availability, JSON.stringify(availability)).toMatchObject({
|
||||
status: "available",
|
||||
});
|
||||
const first = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
});
|
||||
if (!first.ok) throw first.error;
|
||||
const firstStarts = (await readFile(logPath(directories[0]!), "utf8"))
|
||||
.trim()
|
||||
.split("\n");
|
||||
expect(firstStarts).toHaveLength(3);
|
||||
const firstWorkspace = firstStarts.at(-1)!;
|
||||
expect(firstWorkspace).not.toBe(directories[0]);
|
||||
process.chdir(directories[1]!);
|
||||
const second = await service.execute("inspect_android_package", {
|
||||
path: apk,
|
||||
});
|
||||
if (!second.ok) throw second.error;
|
||||
expect(second.value.subject?.digest.sha256).toBe(
|
||||
first.value.subject?.digest.sha256,
|
||||
);
|
||||
const secondStarts = (await readFile(logPath(directories[1]!), "utf8"))
|
||||
.trim()
|
||||
.split("\n");
|
||||
expect(secondStarts).toHaveLength(2);
|
||||
const secondWorkspace = secondStarts.at(-1)!;
|
||||
expect(secondWorkspace).not.toBe(firstWorkspace);
|
||||
await expect(access(firstWorkspace)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await provider.close();
|
||||
await expect(access(secondWorkspace)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
await provider.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Select the provider's source hash after resolver and engine admission complete.
|
||||
const failInputHashCloses = (path: string, failures: number) => {
|
||||
const originalHash = stableHash.hashStableFile;
|
||||
|
||||
Reference in new issue
Block a user