дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
feat(gradle): debug forked JVMs on a dynamic JDWP port (#2154)
Debugging a Gradle test worker or Spring Boot application attached through a fixed --debug-jvm port, so concurrent runs collided. The forked JVM now gets a dynamic JDWP address for Test tasks and BootRun, and the init scripts attach to the selected port. An agent the build configures itself (jvmArgs or debug = true) is replaced, so the JVM has one agent; a plain Run has none. Co-authored-by: misswell <[email protected]>
Цей коміт міститься в:
1 джерело
a73e3c21ad
коміт
4139cb64c2
9 files changed
+113
-6
No files matched your search
@@ -77,7 +77,7 @@ test("a slow module load succeeds before the deadline and cancels the startup ti
|
||||
await page.goto("/", { waitUntil: "commit" });
|
||||
await moduleRequest;
|
||||
await page.locator("#splash").waitFor({ state: "attached" });
|
||||
await page.clock.runFor(25_000);
|
||||
await page.clock.runFor(15_000);
|
||||
await expect(page.locator("#boot-error")).toHaveCount(0);
|
||||
release();
|
||||
await expect(page.locator("#root")).not.toBeEmpty();
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// OpenIDEA: BootRun is a JavaExec task; give its forked application JVM a dynamic
|
||||
// JDWP port instead of Gradle's fixed 5005. The property is added by jdwpLaunch.
|
||||
gradle.projectsEvaluated {
|
||||
allprojects {
|
||||
tasks.matching { it.name == 'bootRun' }.configureEach { task ->
|
||||
if (System.getProperty('openidea.run.debug') == 'true') {
|
||||
task.debugOptions.enabled = false
|
||||
task.doFirst {
|
||||
task.setJvmArgs(task.jvmArgs.findAll {
|
||||
!it.startsWith('-agentlib:jdwp=') && !it.startsWith('-Xrunjdwp:')
|
||||
})
|
||||
task.jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { GradleModel, ProjectInfo, RunConfig } from "@/platform/ipc";
|
||||
import * as ipc from "@/platform/ipc";
|
||||
import { applicationConfig } from "./application";
|
||||
import {
|
||||
DELEGATED_DEBUG,
|
||||
delegatedGradleConfig,
|
||||
gradleDelegation,
|
||||
gradleBootLaunch,
|
||||
gradleTestLaunch,
|
||||
withTestEvents,
|
||||
wrapperGradleVersion,
|
||||
} from "./gradleRun";
|
||||
import { jdwpLaunch } from "./javaDebug";
|
||||
import { isJvmApplication } from "@/platform/javaLaunch";
|
||||
import TESTS_SCRIPT from "./gradleTests.gradle?raw";
|
||||
import BOOT_SCRIPT from "./gradleBoot.gradle?raw";
|
||||
import { useBuildModel } from "@/platform/buildModel";
|
||||
import { useProject } from "@/platform/project";
|
||||
|
||||
@@ -135,6 +140,16 @@ describe("Build and run using: Gradle", () => {
|
||||
expect(run.params).toBeUndefined();
|
||||
});
|
||||
|
||||
it("gives Gradle BootRun a dynamic JDWP port when debugging", () => {
|
||||
expect(BOOT_SCRIPT).toContain("openidea.run.debug");
|
||||
expect(BOOT_SCRIPT).toContain("address=127.0.0.1:0");
|
||||
});
|
||||
|
||||
it("gives Gradle test workers a dynamic JDWP port when debugging", () => {
|
||||
expect(TESTS_SCRIPT).toContain("openidea.run.debug");
|
||||
expect(TESTS_SCRIPT).toContain("address=127.0.0.1:0");
|
||||
});
|
||||
|
||||
it("debugs the forked JVM: the init script adds the agent", () => {
|
||||
const app = applicationConfig({ className: "a.Main" }, "app/src/main/java/a/Main.java", []);
|
||||
const run = delegatedGradleConfig(app, "app", ctx)!;
|
||||
@@ -166,6 +181,40 @@ describe("Gradle test runs", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Gradle launch routing", () => {
|
||||
const boot = {
|
||||
kind: "gradle",
|
||||
command: "./gradlew",
|
||||
args: [":module:bootRun"],
|
||||
} as RunConfig;
|
||||
const test = { ...boot, args: [":module:test"], isTest: true };
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
it("leaves non-desktop launches alone", async () => {
|
||||
vi.spyOn(ipc, "isTauri").mockReturnValue(false);
|
||||
expect(await gradleBootLaunch(boot)).toBe(boot);
|
||||
expect(await gradleTestLaunch(test)).toBe(test);
|
||||
});
|
||||
|
||||
it("keeps the configuration when the host cannot write the init script", async () => {
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
vi.spyOn(ipc.commands, "runGradleInitScript").mockRejectedValue(new Error("host unavailable"));
|
||||
expect(await gradleBootLaunch(boot)).toBe(boot);
|
||||
expect(await gradleTestLaunch(test)).toBe(test);
|
||||
});
|
||||
|
||||
it("adds reporting scripts without enabling JDWP for plain Run", async () => {
|
||||
for (const config of [await gradleBootLaunch(boot), await gradleTestLaunch(test)]) {
|
||||
expect(config.args.slice(0, 2)).toEqual(["--init-script", "/tmp/openidea-gradle-run.gradle"]);
|
||||
expect(config.args).not.toContain(DELEGATED_DEBUG);
|
||||
expect(config.args).not.toContain("--debug-jvm");
|
||||
}
|
||||
expect(boot.args).toEqual([":module:bootRun"]);
|
||||
expect(test.args).toEqual([":module:test"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delegation across independent builds", () => {
|
||||
afterEach(() => {
|
||||
useProject.setState({ project: null, settings: null });
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
import RUN_SCRIPT from "./gradleRun.gradle?raw";
|
||||
import TESTS_SCRIPT from "./gradleTests.gradle?raw";
|
||||
import BOOT_SCRIPT from "./gradleBoot.gradle?raw";
|
||||
import { useBuildModel } from "@/platform/buildModel";
|
||||
import { call, commands, isTauri, type GradleModel, type RunConfig } from "@/platform/ipc";
|
||||
import { findJavaModule, isJvmApplication, parseJavaArgs } from "@/platform/javaLaunch";
|
||||
@@ -125,6 +126,17 @@ export async function gradleTestLaunch<T extends RunConfig>(config: T): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
/** Gives a direct Gradle BootRun a dynamic JDWP port for the forked application JVM. */
|
||||
export async function gradleBootLaunch<T extends RunConfig>(config: T): Promise<T> {
|
||||
if (!isTauri() || !isGradleCommand(config) || !config.args.some((a) => /(^|:)bootRun$/.test(a))) return config;
|
||||
try {
|
||||
return withTestEvents(config, await writeScript("openidea-gradle-boot", BOOT_SCRIPT));
|
||||
} catch (e) {
|
||||
console.warn("Gradle BootRun init script not written", e);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The launch of an Application / Spring Boot `config` through Gradle, when the project
|
||||
* runs with Gradle (Settings › Build Tools › Gradle) and the main class is in one of its
|
||||
|
||||
@@ -42,3 +42,20 @@ allprojects {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Replace build-configured agents only for this debug launch, after project configuration.
|
||||
gradle.projectsEvaluated {
|
||||
allprojects {
|
||||
tasks.withType(Test).configureEach { Test task ->
|
||||
if (System.getProperty('openidea.run.debug') == 'true') {
|
||||
task.debugOptions.enabled = false
|
||||
task.doFirst {
|
||||
task.setJvmArgs(task.jvmArgs.findAll {
|
||||
!it.startsWith('-agentlib:jdwp=') && !it.startsWith('-Xrunjdwp:')
|
||||
})
|
||||
task.jvmArgs('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { useProject } from "@/platform/project";
|
||||
import { applicationConfig, jvmModuleDir, jvmTestConfig } from "./application";
|
||||
import { buildRows } from "./ConfigChooser";
|
||||
import { jdwpLaunch } from "./javaDebug";
|
||||
import { DELEGATED_DEBUG } from "./gradleRun";
|
||||
import { firstTopLevelType, jvmMainCandidates, scanJvmTargets } from "./jvmTargets";
|
||||
import { adoptTemporary, MAX_TEMPORARY, saveTemporary, testReportEvents, useRun } from "./store";
|
||||
import { detectRunTargets, targetConfig } from "./targets";
|
||||
@@ -324,6 +325,8 @@ describe("java command lines", () => {
|
||||
expect(joinJavaArgs(["a", "b c", 'q"'])).toBe('a "b c" "q\\""');
|
||||
expect(jdwpListeningPort("Listening for transport dt_socket at address: 50123")).toBe(50123);
|
||||
expect(jdwpListeningPort("Listening for transport dt_socket at address: 127.0.0.1:5005")).toBe(5005);
|
||||
expect(jdwpListeningPort("[Test worker] Listening for transport dt_socket at address: 50124")).toBe(50124);
|
||||
expect(jdwpListeningPort(" Listening for transport dt_socket at address: 50125\r")).toBe(50125);
|
||||
expect(jdwpListeningPort("[INFO] Listening")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -340,7 +343,10 @@ describe("debugging build tool configurations", () => {
|
||||
expect(merged.args[1]).toMatch(/^-Dspring-boot\.run\.jvmArguments=-Xmx1g -agentlib:jdwp=/);
|
||||
const test = jdwpLaunch(cfg({ id: "t", kind: "maven", args: ["test", "-Dtest=A#b"], isTest: true }))!;
|
||||
expect(test.args.some((a) => a.startsWith("-Dmaven.surefire.debug=-agentlib:jdwp="))).toBe(true);
|
||||
expect(jdwpLaunch(cfg({ id: "g", kind: "gradle", args: ["bootRun"] }))!.args).toEqual(["bootRun", "--debug-jvm"]);
|
||||
expect(jdwpLaunch(cfg({ id: "g", kind: "gradle", args: ["bootRun"] }))!.args).toContain(DELEGATED_DEBUG);
|
||||
const gradleTest = jdwpLaunch(cfg({ id: "gt", kind: "gradle", args: ["test"], isTest: true }))!;
|
||||
expect(gradleTest.args).toContain(DELEGATED_DEBUG);
|
||||
expect(gradleTest.args).not.toContain("--debug-jvm");
|
||||
expect(jdwpLaunch(cfg({ id: "p", kind: "maven", args: ["package"] }))).toBeNull();
|
||||
expect(jdwpLaunch(cfg({ id: "n", kind: "npm", args: ["test"] }))).toBeNull();
|
||||
});
|
||||
|
||||
@@ -63,6 +63,12 @@ export function jdwpLaunch(config: RunConfig): RunConfig | null {
|
||||
// A delegated Application: its init script adds the agent (port 0) to the JavaExec JVM.
|
||||
if (args.some((a) => a.startsWith(DELEGATED_MAIN)))
|
||||
return args.includes(DELEGATED_DEBUG) ? config : { ...config, args: [...args, DELEGATED_DEBUG] };
|
||||
// Gradle Test forks test workers: the init script gives them a dynamic JDWP port.
|
||||
if (config.isTest || args.some((a) => /(^|:)(test|integrationTest)$/.test(a)))
|
||||
return args.includes(DELEGATED_DEBUG) ? config : { ...config, args: [...args, DELEGATED_DEBUG] };
|
||||
// BootRun's init script gives the forked application JVM a dynamic port.
|
||||
if (args.some((a) => /(^|:)bootRun$/.test(a)))
|
||||
return args.includes(DELEGATED_DEBUG) ? config : { ...config, args: [...args, DELEGATED_DEBUG] };
|
||||
if (!args.some(isGradleJvmTask)) return null;
|
||||
return args.includes("--debug-jvm") ? config : { ...config, args: [...args, "--debug-jvm"] };
|
||||
}
|
||||
|
||||
@@ -230,13 +230,13 @@ describe("Maven / Gradle: VM options of the forked JVM", () => {
|
||||
const launched = await gradle().prepareLaunch!(c);
|
||||
expect(scripts).toEqual([gradleJvmArgs(params)]);
|
||||
expect(launched.args).toEqual(["--init-script", "/tmp/openidea-gradle/jvm-args-1.init.gradle", "bootRun"]);
|
||||
// Debugging keeps them: `--debug-jvm` is added before the launch is prepared.
|
||||
// Debugging keeps them: the BootRun init script gives the forked JVM a dynamic port.
|
||||
const debug = await gradle().prepareLaunch!(jdwpLaunch(c)!);
|
||||
expect(debug.args).toEqual([
|
||||
"--init-script",
|
||||
"/tmp/openidea-gradle/jvm-args-1.init.gradle",
|
||||
"bootRun",
|
||||
"--debug-jvm",
|
||||
"-Dopenidea.run.debug=true",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ import { loadRunTemplates } from "./runTemplates";
|
||||
import { recordRun } from "./services";
|
||||
import { startedPort } from "@/platform/serverPort";
|
||||
import { tomcatBrowserUrl } from "./tomcat";
|
||||
import { gradleDelegation, gradleTestLaunch } from "./gradleRun";
|
||||
import { gradleBootLaunch, gradleDelegation, gradleTestLaunch } from "./gradleRun";
|
||||
import { openUrl } from "./navigate";
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
@@ -877,7 +877,7 @@ export async function startRun(
|
||||
if ((options.launch || launch.isTest) && launch.emulateTerminal) launch = { ...launch, emulateTerminal: false };
|
||||
if (launch.emulateTerminal) rt.terminal = new TerminalOutput();
|
||||
patchSession(sessionId, { terminal: !!launch.emulateTerminal });
|
||||
const started = launch.isTest ? await gradleTestLaunch(withTestReporting(launch)) : launch;
|
||||
const started = launch.isTest ? await gradleTestLaunch(withTestReporting(launch)) : await gradleBootLaunch(launch);
|
||||
if (runtimes.get(sessionId)?.generation !== generation) return getSession(sessionId) ?? null;
|
||||
const processStarted = performance.now();
|
||||
const info = await startWithRemedies(started, () => call(commands.runStart(started, channel)));
|
||||
|
||||
Reference in new issue
Заблокувати користувача