fix(run): pin the JDWP port for Maven test forks (#2215) (#2268)

A Surefire/Failsafe fork started with suspend=y never reaches the console
with its "Listening for transport" line: surefire buffers the fork's
output until the tests run, and the tests wait for the debugger. The IDE
found no port, never attached, and Stop left an orphan debug session.

Pick a free port up front (new debug_free_port IPC), pass it to the
-Dmaven.surefire.debug/-Dmaven.failsafe.debug agent as
address=127.0.0.1:<port>, and attach right away — java-debug retries the
connect while the fork starts, so no output line is needed. A duplicated
line that does arrive later is deduplicated by port.
Цей коміт міститься в:
arookieofc authored and GitHub committed 2026-11-09 12:50:40 +08:00
1 джерело 92b527ab0e
коміт 0ccc1e687d
6 files changed
+49 -6

No files matched your search

+22 -6
Переглянути файл
@@ -9,7 +9,7 @@
* debugger (a script such as `catalina.sh run`) through `JAVA_TOOL_OPTIONS`, which every JVM reads.
*/
import { executeAction } from "@/platform/actions";
import type { RunConfig } from "@/platform/ipc";
import { call, commands, type RunConfig } from "@/platform/ipc";
import { isJvmApplication, JDWP_AGENT, jdwpListeningPort } from "@/platform/javaLaunch";
import { applyBuildPolicyToCommand } from "./buildCommand";
import { startRun, useRun } from "./store";
@@ -37,8 +37,11 @@ function withProperty(args: readonly string[], key: string, value: string): stri
return out;
}
/** The configuration with a waiting JDWP agent, or null when it starts no debuggable JVM. */
export function jdwpLaunch(config: RunConfig): RunConfig | null {
/** The configuration with a waiting JDWP agent, or null when it starts no debuggable JVM.
* `jdwpPort`: pin a Surefire / Failsafe fork to this port — its "Listening for transport"
* line stays buffered until the tests run, which `suspend=y` prevents, so the port must be
* known without it. */
export function jdwpLaunch(config: RunConfig, jdwpPort?: number): RunConfig | null {
config = applyBuildPolicyToCommand(config);
if (config.kind === "tomcat") return { ...config, params: { ...config.params, tomcatDebug: "true" } };
const args = config.args;
@@ -53,8 +56,9 @@ export function jdwpLaunch(config: RunConfig): RunConfig | null {
return { ...config, args: withProperty(args, "spring-boot.run.jvmArguments", JDWP_AGENT) };
if (config.isTest || args.some(isMavenTestGoal)) {
const failsafe = args.some((a) => /integration-test|verify|failsafe/.test(a));
let out = withProperty(args, "maven.surefire.debug", JDWP_AGENT);
if (failsafe) out = withProperty(out, "maven.failsafe.debug", JDWP_AGENT);
const agent = jdwpPort ? JDWP_AGENT.replace("address=127.0.0.1:0", `address=127.0.0.1:${jdwpPort}`) : JDWP_AGENT;
let out = withProperty(args, "maven.surefire.debug", agent);
if (failsafe) out = withProperty(out, "maven.failsafe.debug", agent);
return { ...config, args: out };
}
if (JVM_ADAPTERS.includes(config.debugAdapter ?? "") || args.some(isMavenInProcessGoal))
@@ -80,13 +84,23 @@ export function jdwpLaunch(config: RunConfig): RunConfig | null {
return null;
}
/** A Maven test launch: its Surefire / Failsafe fork holds the "Listening" line, so pin its JDWP port. */
function needsPinnedPort(config: RunConfig): boolean {
if (config.kind !== "maven" && !/(^|\/)mvnw?(\.cmd)?$/.test(config.command)) return false;
return config.isTest || config.args.some(isMavenTestGoal);
}
/** Runs `config` with a JDWP agent and attaches the Java debugger to every JVM it starts. */
export async function debugWithAgent(config: RunConfig, show?: boolean): Promise<boolean> {
const launch = jdwpLaunch(config);
const pinned = needsPinnedPort(config) ? await call(commands.debugFreePort()).catch(() => null) : null;
const launch = jdwpLaunch(config, pinned ?? undefined);
if (!launch) return false;
let sessionId: string | null = null;
const waiting: number[] = [];
const attached = new Set<number>();
const attach = (port: number) => {
if (attached.has(port)) return;
attached.add(port);
const runId = useRun.getState().sessions.find((s) => s.id === sessionId)?.runId ?? null;
void executeAction("debug.attach", {
name: config.name,
@@ -111,6 +125,8 @@ export async function debugWithAgent(config: RunConfig, show?: boolean): Promise
});
if (!session) return true;
sessionId = session.id;
// The pinned port needs no output line: java-debug retries the attach while the fork starts.
if (pinned !== null) attach(pinned);
waiting.splice(0).forEach(attach);
return true;
}