Files
openidea/src/features/run/javaDebug.ts
T

79 lines
3.4 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.
*/
import { executeAction } from "@/platform/actions";
import type { RunConfig } from "@/platform/ipc";
import { JDWP_AGENT, jdwpListeningPort } from "@/platform/javaLaunch";
import { startRun, useRun } from "./store";
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);
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. */
export function jdwpLaunch(config: RunConfig): RunConfig | null {
const args = config.args;
// A single source file (`java Hi.java`): the agent goes before the file.
if (config.kind === "java-file") return { ...config, args: [JDWP_AGENT, ...args] };
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));
let out = withProperty(args, "maven.surefire.debug", JDWP_AGENT);
if (failsafe) out = withProperty(out, "maven.failsafe.debug", JDWP_AGENT);
return { ...config, args: out };
}
return null;
}
if (config.kind === "gradle" || /(^|\/)gradlew?(\.bat)?$/.test(config.command)) {
if (!args.some(isGradleJvmTask)) return null;
return args.includes("--debug-jvm") ? config : { ...config, args: [...args, "--debug-jvm"] };
}
return null;
}
/** Runs `config` with a JDWP agent and attaches the Java debugger to every JVM it starts. */
export async function debugWithAgent(config: RunConfig): Promise<boolean> {
const launch = jdwpLaunch(config);
if (!launch) return false;
let sessionId: string | null = null;
const waiting: number[] = [];
const attach = (port: number) => {
const runId = useRun.getState().sessions.find((s) => s.id === sessionId)?.runId ?? null;
void executeAction("debug.attach", {
name: config.name,
module: config.cwd ?? "",
hostName: "127.0.0.1",
port,
process: runId,
});
};
const session = await startRun(config, undefined, undefined, {
launch,
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;
waiting.splice(0).forEach(attach);
return true;
}