дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
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.
133 рядки
7.0 KiB
TypeScript
133 рядки
7.0 KiB
TypeScript
/**
|
|
* Debugging Maven / Gradle run configurations (IDEA debugs those too): the
|
|
* build tool starts the JVM with a JDWP agent that waits for a debugger
|
|
* (`spring-boot:run` → `-Dspring-boot.run.jvmArguments`, Surefire →
|
|
* `-Dmaven.surefire.debug`, Gradle → `--debug-jvm`); when the agent reports
|
|
* its port, the Java debugger attaches. Output and the test tree stay in the Run tab.
|
|
* Maven goals that run the application inside Maven's JVM (`tomcat7:run`, `jetty:run`,
|
|
* `exec:java`) get the agent through `MAVEN_OPTS`; any other configuration set to the Java
|
|
* debugger (a script such as `catalina.sh run`) through `JAVA_TOOL_OPTIONS`, which every JVM reads.
|
|
*/
|
|
import { executeAction } from "@/platform/actions";
|
|
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";
|
|
import { DELEGATED_DEBUG, DELEGATED_MAIN } from "./gradleRun";
|
|
|
|
const isMavenTestGoal = (a: string) =>
|
|
/^(test|verify|integration-test|surefire:test|failsafe:integration-test)$/.test(a);
|
|
const isGradleJvmTask = (a: string) => /(^|:)(test|bootRun|run|integrationTest|check)$/.test(a);
|
|
/** A plugin goal that runs the application in Maven's own JVM (`tomcat7:run`, `jetty:run-war`, `exec:java`). */
|
|
const isMavenInProcessGoal = (a: string) => /^[\w.-]+(:[\w.-]+)*:(run|run-war|run-exploded|java)$/.test(a);
|
|
const JVM_ADAPTERS = ["java-debug", "kotlin-debug-adapter"];
|
|
|
|
function withOption(env: Record<string, string>, key: string, value: string): Record<string, string> {
|
|
const current = env[key]?.trim();
|
|
return { ...env, [key]: current ? `${current} ${value}` : value };
|
|
}
|
|
|
|
function withProperty(args: readonly string[], key: string, value: string): string[] {
|
|
const prefix = `-D${key}=`;
|
|
const i = args.findIndex((a) => a.startsWith(prefix));
|
|
if (i < 0) return [...args, prefix + value];
|
|
const current = args[i]!.slice(prefix.length).trim();
|
|
const out = [...args];
|
|
out[i] = prefix + (current ? `${current} ${value}` : value);
|
|
return out;
|
|
}
|
|
|
|
/** 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;
|
|
// A single source file (`java Hi.java`): the agent goes before the file. Compiled first
|
|
// (`javac … && java -cp …`): through `JDK_JAVA_OPTIONS`, which only the `java` launcher reads.
|
|
if (config.kind === "java-file")
|
|
return args.length
|
|
? { ...config, args: [JDWP_AGENT, ...args] }
|
|
: { ...config, env: withOption(config.env, "JDK_JAVA_OPTIONS", JDWP_AGENT) };
|
|
if (config.kind === "maven" || /(^|\/)mvnw?(\.cmd)?$/.test(config.command)) {
|
|
if (args.some((a) => a === "spring-boot:run" || /:spring-boot-maven-plugin:[^:]*:run$/.test(a)))
|
|
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));
|
|
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))
|
|
return { ...config, env: withOption(config.env, "MAVEN_OPTS", JDWP_AGENT) };
|
|
return null;
|
|
}
|
|
if (config.kind === "gradle" || /(^|\/)gradlew?(\.bat)?$/.test(config.command)) {
|
|
// 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"] };
|
|
}
|
|
// Application / Spring Boot configurations are launched by java-debug itself.
|
|
if (JVM_ADAPTERS.includes(config.debugAdapter ?? "") && !isJvmApplication(config))
|
|
return { ...config, env: withOption(config.env, "JAVA_TOOL_OPTIONS", JDWP_AGENT) };
|
|
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 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,
|
|
// The application's module (a delegated run's working directory is the Gradle build).
|
|
module: config.module ?? config.cwd ?? "",
|
|
hostName: "127.0.0.1",
|
|
port,
|
|
process: runId,
|
|
runConfig: config,
|
|
show,
|
|
});
|
|
};
|
|
const session = await startRun(config, undefined, undefined, {
|
|
launch,
|
|
show,
|
|
onLine: (line) => {
|
|
const port = jdwpListeningPort(line);
|
|
if (port === null) return;
|
|
if (sessionId) attach(port);
|
|
else waiting.push(port);
|
|
},
|
|
});
|
|
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;
|
|
}
|