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 { DEFAULT_JADX_OPERATION_TIMEOUT_MS } from "../domain/analysisResourceLimits.js";
|
||||||
import {
|
import {
|
||||||
jadxOperationTimeoutGuidance,
|
jadxOperationTimeoutGuidance,
|
||||||
@@ -600,30 +603,43 @@ const resolveJava = async (
|
|||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const executable = process.platform === "win32" ? "java.exe" : "java";
|
const executable = process.platform === "win32" ? "java.exe" : "java";
|
||||||
if (environment.JAVA_HOME === undefined) {
|
if (environment.JAVA_HOME === undefined) {
|
||||||
if (process.platform !== "win32") return executable;
|
const windows = process.platform === "win32";
|
||||||
const path =
|
const path = windows
|
||||||
Object.entries(environment).find(
|
? (Object.entries(environment).find(
|
||||||
([key]) => key.toLowerCase() === "path",
|
([key]) => key.toLowerCase() === "path",
|
||||||
)?.[1] ?? "";
|
)?.[1] ?? "")
|
||||||
for (const directory of path
|
: (environment.PATH ?? "/usr/bin:/bin");
|
||||||
.split(delimiter)
|
// Resolve in the caller's directory once. The module probe and the engine
|
||||||
.filter((entry) => entry !== "")) {
|
// 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);
|
throwIfJadxInterrupted(signal);
|
||||||
// Windows PATH commonly quotes individual entries containing spaces.
|
// Windows PATH commonly quotes individual entries containing spaces.
|
||||||
const selected =
|
const selected =
|
||||||
directory.startsWith('"') && directory.endsWith('"')
|
windows && directory.startsWith('"') && directory.endsWith('"')
|
||||||
? directory.slice(1, -1)
|
? directory.slice(1, -1)
|
||||||
: directory;
|
: directory;
|
||||||
const candidate = resolve(selected, executable);
|
const candidate = windows
|
||||||
|
? resolve(selected, executable)
|
||||||
|
: absoluteCallerPath(appendCallerPath(selected, executable));
|
||||||
try {
|
try {
|
||||||
await access(candidate, constants.R_OK | constants.X_OK);
|
await access(
|
||||||
if ((await stat(candidate)).isFile()) return await realpath(candidate);
|
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) {
|
} catch (cause: unknown) {
|
||||||
if (
|
if (
|
||||||
typeof cause === "object" &&
|
typeof cause === "object" &&
|
||||||
cause !== null &&
|
cause !== null &&
|
||||||
"code" in cause &&
|
"code" in cause &&
|
||||||
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
|
(cause.code === "ENOENT" ||
|
||||||
|
cause.code === "ENOTDIR" ||
|
||||||
|
(!windows && (cause.code === "EACCES" || cause.code === "EPERM")))
|
||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
throw configurationFailure(
|
throw configurationFailure(
|
||||||
@@ -637,8 +653,8 @@ const resolveJava = async (
|
|||||||
}
|
}
|
||||||
throw configurationFailure(
|
throw configurationFailure(
|
||||||
"runtime_missing",
|
"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.",
|
`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 },
|
{ java_executable: executable, phase: "path-lookup" },
|
||||||
"java_runtime",
|
"java_runtime",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -579,6 +579,9 @@ export class JadxProvider implements AndroidAnalysisPort {
|
|||||||
session = new JadxSession(
|
session = new JadxSession(
|
||||||
{
|
{
|
||||||
command: configuration.java,
|
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: [
|
arguments: [
|
||||||
...configuration.jvmArguments,
|
...configuration.jvmArguments,
|
||||||
"--class-path",
|
"--class-path",
|
||||||
|
|||||||
@@ -244,12 +244,10 @@ it("distinguishes a runnable JRE from a missing Java executable", async () => {
|
|||||||
expect(availability).toMatchObject({
|
expect(availability).toMatchObject({
|
||||||
status: "unavailable",
|
status: "unavailable",
|
||||||
code: "runtime_missing",
|
code: "runtime_missing",
|
||||||
reason: expect.stringContaining("spawn java ENOENT"),
|
reason: expect.stringContaining("Cannot find an executable java"),
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
java_executable: "java",
|
java_executable: "java",
|
||||||
phase: "module-inspection",
|
phase: "path-lookup",
|
||||||
exit_code: null,
|
|
||||||
process_error_code: "ENOENT",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import { SdkErrorCode } from "@modelcontextprotocol/client";
|
import { SdkErrorCode } from "@modelcontextprotocol/client";
|
||||||
import { expect, it as test, onTestFinished, vi } from "vitest";
|
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 { fileURLToPath } from "node:url";
|
||||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||||
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
|
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
|
||||||
@@ -20,10 +29,98 @@ import {
|
|||||||
verifyJadxFixtureCleanup as verifyCleanup,
|
verifyJadxFixtureCleanup as verifyCleanup,
|
||||||
} from "../../fixtures/android/jadx.js";
|
} from "../../fixtures/android/jadx.js";
|
||||||
import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
||||||
|
import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js";
|
||||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||||
|
|
||||||
const it = test.skipIf(process.platform === "win32");
|
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.
|
// Select the provider's source hash after resolver and engine admission complete.
|
||||||
const failInputHashCloses = (path: string, failures: number) => {
|
const failInputHashCloses = (path: string, failures: number) => {
|
||||||
const originalHash = stableHash.hashStableFile;
|
const originalHash = stableHash.hashStableFile;
|
||||||
|
|||||||
Reference in new issue
Block a user