mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00:00
A taken port now says which port is in use instead of Node's EADDRINUSE text; a listening PHP Remote Debug session shows "Listening for Xdebug on port N" instead of "Running…"; clearing the only field of a debug-only remote type keeps the form and shows the field as required; php-debug leaves the generic Debug adapter list until it has a launch template (#1623). Co-authored-by: DanielADK <18214958+DanielADK@users.noreply.github.com>
403 lines
13 KiB
TypeScript
403 lines
13 KiB
TypeScript
/**
|
||
* Remote debug run configurations: the debugger attaches to a process started
|
||
* elsewhere (another machine, a container) over host:port, like IDEA's Remote JVM
|
||
* Debug, GoLand's Go Remote, PyCharm's remote debugpy and IDEA's Attach to Node.js.
|
||
* PHP Remote Debug is the exception: its php-debug listens and Xdebug connects to it.
|
||
* Only Debug applies to them (`debugOnly`); they launch no process.
|
||
*/
|
||
import { tx } from "@/platform/i18n";
|
||
import type { LaunchConfig, RunConfig } from "@/platform/ipc";
|
||
import { workflowSettings } from "@/platform/workflowSettings";
|
||
import type { RunConfigTypeContribution, RunConfigTypeField } from "@/workbench/contributions";
|
||
|
||
export const REMOTE_JVM = "remote-jvm";
|
||
export const GO_REMOTE = "go-remote";
|
||
export const PYTHON_REMOTE = "python-remote";
|
||
export const NODE_REMOTE = "node-remote";
|
||
export const GODOT_ATTACH = "godot-attach";
|
||
export const PHP_REMOTE = "php-remote";
|
||
|
||
type Params = Readonly<Record<string, string>>;
|
||
|
||
/** A TCP port, or null. */
|
||
export function parsePort(value: string | undefined): number | null {
|
||
const v = (value ?? "").trim();
|
||
if (!/^\d+$/.test(v)) return null;
|
||
const n = Number(v);
|
||
return n >= 1 && n <= 65535 ? n : null;
|
||
}
|
||
|
||
const hostField: RunConfigTypeField = { key: "host", label: "Host", mono: true, required: true };
|
||
const portField: RunConfigTypeField = {
|
||
key: "port",
|
||
label: "Port",
|
||
mono: true,
|
||
required: true,
|
||
validate: (v) => (parsePort(v) == null ? "Port must be a number from 1 to 65535" : null),
|
||
};
|
||
|
||
const hostOf = (p: Params) => p.host?.trim() || "localhost";
|
||
const portOf = (p: Params, fallback: number) => parsePort(p.port) ?? fallback;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Remote JVM Debug
|
||
|
||
export type JdwpMode = "attach" | "listen";
|
||
export type JdkVersion = "9" | "5" | "1.4";
|
||
|
||
/**
|
||
* The agent the remote JVM is started with, as IDEA shows it ("Command line arguments
|
||
* for remote JVM"). Attach: the JVM listens (`server=y`); JDK 9+ binds only to localhost
|
||
* unless the address names a host, hence `*:`. Listen: the JVM connects to the IDE.
|
||
*/
|
||
export function jdwpAgentArgs(p: Params): string {
|
||
const mode: JdwpMode = p.mode === "listen" ? "listen" : "attach";
|
||
const jdk = (p.jdk || "9") as JdkVersion;
|
||
const port = portOf(p, 5005);
|
||
const opts =
|
||
mode === "listen"
|
||
? `transport=dt_socket,server=n,address=${hostOf(p)}:${port},suspend=y`
|
||
: `transport=dt_socket,server=y,suspend=n,address=${jdk === "9" ? "*:" : ""}${port}`;
|
||
return jdk === "1.4" ? `-Xdebug -Xrunjdwp:${opts}` : `-agentlib:jdwp=${opts}`;
|
||
}
|
||
|
||
const JVM_KINDS = new Set(["application", "spring-boot", "maven", "gradle", "java-file"]);
|
||
|
||
export const remoteJvmType: RunConfigTypeContribution = {
|
||
id: REMOTE_JVM,
|
||
label: "Remote JVM Debug",
|
||
icon: "attach-to-process",
|
||
description: "Attach the debugger to a JVM started with a JDWP agent, here or on another machine",
|
||
defaults: { mode: "attach", host: "localhost", port: "5005", jdk: "9" },
|
||
debugOnly: true,
|
||
debugAdapter: "java-debug",
|
||
fields: [
|
||
{
|
||
key: "mode",
|
||
label: "Debugger mode",
|
||
control: "select",
|
||
options: [
|
||
{ value: "attach", label: "Attach to remote JVM" },
|
||
{ value: "listen", label: "Listen to remote JVM" },
|
||
],
|
||
},
|
||
hostField,
|
||
portField,
|
||
{
|
||
key: "jdk",
|
||
label: "JDK version of the remote JVM",
|
||
control: "select",
|
||
options: [
|
||
{ value: "9", label: "JDK 9 or later" },
|
||
{ value: "5", label: "JDK 5 - 8" },
|
||
{ value: "1.4", label: "JDK 1.4.x" },
|
||
],
|
||
},
|
||
{
|
||
key: "args",
|
||
label: "Command line arguments for remote JVM",
|
||
control: "output",
|
||
mono: true,
|
||
value: jdwpAgentArgs,
|
||
hint: "Add to the remote JVM's command line (or JAVA_TOOL_OPTIONS)",
|
||
},
|
||
{
|
||
key: "module",
|
||
label: "Use module classpath",
|
||
mono: true,
|
||
placeholder: "Project root",
|
||
hint: "Module directory whose sources the debugger shows",
|
||
suggestions: (configs) => [...new Set(configs.filter((c) => JVM_KINDS.has(c.kind) && c.cwd).map((c) => c.cwd!))],
|
||
},
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Go Remote (Delve headless server)
|
||
|
||
/** How to start the remote side, as GoLand shows it. */
|
||
export function delveServerCommand(p: Params): string {
|
||
return `dlv debug --headless --listen=:${portOf(p, 2345)} --api-version=2 --accept-multiclient`;
|
||
}
|
||
|
||
const localRootField: RunConfigTypeField = {
|
||
key: "localRoot",
|
||
label: "Local root",
|
||
mono: true,
|
||
placeholder: "Project root",
|
||
};
|
||
|
||
const remoteRootField = (placeholder: string): RunConfigTypeField => ({
|
||
key: "remoteRoot",
|
||
label: "Remote root",
|
||
mono: true,
|
||
placeholder,
|
||
hint: "Where the project's sources are on the remote machine, when the paths differ",
|
||
});
|
||
|
||
export const goRemoteType: RunConfigTypeContribution = {
|
||
id: GO_REMOTE,
|
||
label: "Go Remote",
|
||
icon: "file-go",
|
||
description: "Attach to a Delve server (dlv --headless) here or on another machine",
|
||
defaults: { host: "localhost", port: "2345" },
|
||
debugOnly: true,
|
||
debugAdapter: "delve",
|
||
fields: [
|
||
hostField,
|
||
portField,
|
||
{
|
||
key: "hint",
|
||
label: "Start the remote side with",
|
||
control: "output",
|
||
mono: true,
|
||
value: delveServerCommand,
|
||
hint: "Delve 1.7.3 or later; dlv exec ./app or dlv attach <pid> take the same flags",
|
||
},
|
||
localRootField,
|
||
remoteRootField("e.g. /go/src/app"),
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Python remote (debugpy --listen)
|
||
|
||
export function debugpyServerCommand(p: Params): string {
|
||
return `python -m debugpy --listen 0.0.0.0:${portOf(p, 5678)} --wait-for-client app.py`;
|
||
}
|
||
|
||
export const pythonRemoteType: RunConfigTypeContribution = {
|
||
id: PYTHON_REMOTE,
|
||
label: "Python Remote Debug (debugpy)",
|
||
icon: "file-python",
|
||
description: "Attach to a Python process listening with debugpy here or on another machine",
|
||
defaults: { host: "localhost", port: "5678" },
|
||
debugOnly: true,
|
||
debugAdapter: "debugpy",
|
||
fields: [
|
||
hostField,
|
||
portField,
|
||
{
|
||
key: "hint",
|
||
label: "Start the remote side with",
|
||
control: "output",
|
||
mono: true,
|
||
value: debugpyServerCommand,
|
||
hint: "pip install debugpy on the remote machine",
|
||
},
|
||
localRootField,
|
||
remoteRootField("e.g. /app"),
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Attach to Node.js (inspector)
|
||
|
||
export function nodeInspectCommand(p: Params): string {
|
||
return `node --inspect=0.0.0.0:${portOf(p, 9229)} app.js`;
|
||
}
|
||
|
||
export const nodeRemoteType: RunConfigTypeContribution = {
|
||
id: NODE_REMOTE,
|
||
label: "Attach to Node.js",
|
||
icon: "file-js",
|
||
description: "Attach to a Node.js process started with --inspect here or on another machine",
|
||
defaults: { host: "localhost", port: "9229" },
|
||
debugOnly: true,
|
||
debugAdapter: "js-debug",
|
||
fields: [
|
||
hostField,
|
||
portField,
|
||
{
|
||
key: "hint",
|
||
label: "Start the remote side with",
|
||
control: "output",
|
||
mono: true,
|
||
value: nodeInspectCommand,
|
||
},
|
||
localRootField,
|
||
remoteRootField("e.g. /usr/src/app"),
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// PHP Remote Debug (Xdebug): php-debug listens, the remote PHP connects to it
|
||
|
||
/** How to start the remote side, so Xdebug connects back to the listening php-debug. */
|
||
export function xdebugCommand(p: Params): string {
|
||
return `php -dxdebug.mode=debug -dxdebug.start_with_request=yes -dxdebug.client_port=${portOf(p, 9003)} script.php`;
|
||
}
|
||
|
||
export const phpRemoteType: RunConfigTypeContribution = {
|
||
id: PHP_REMOTE,
|
||
label: "PHP Remote Debug (Xdebug)",
|
||
icon: "file-php",
|
||
description: "Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)",
|
||
defaults: { port: "9003" },
|
||
debugOnly: true,
|
||
debugAdapter: "php-debug",
|
||
fields: [
|
||
portField,
|
||
{
|
||
key: "hint",
|
||
label: "Start the remote side with",
|
||
control: "output",
|
||
mono: true,
|
||
value: xdebugCommand,
|
||
hint: "For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container",
|
||
},
|
||
localRootField,
|
||
remoteRootField("e.g. /var/www/html"),
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Attach to Godot (the editor's debug adapter, Editor Settings › Network › Debug Adapter)
|
||
|
||
export const godotAttachType: RunConfigTypeContribution = {
|
||
id: GODOT_ATTACH,
|
||
label: "Attach to Godot",
|
||
icon: "file-gdscript",
|
||
description: "Debug the game the Godot editor is running, through the editor's debug adapter",
|
||
defaults: { host: "localhost", port: "6006" },
|
||
debugOnly: true,
|
||
debugAdapter: "godot",
|
||
fields: [
|
||
hostField,
|
||
portField,
|
||
{
|
||
key: "hint",
|
||
label: "Before attaching",
|
||
control: "output",
|
||
value: () => "Open the project in the Godot editor and run a scene (F5 / F6)",
|
||
},
|
||
],
|
||
commandLine: () => null,
|
||
};
|
||
|
||
export const REMOTE_TYPES: readonly RunConfigTypeContribution[] = [
|
||
remoteJvmType,
|
||
goRemoteType,
|
||
pythonRemoteType,
|
||
nodeRemoteType,
|
||
phpRemoteType,
|
||
godotAttachType,
|
||
];
|
||
|
||
export const isRemoteConfig = (c: Pick<RunConfig, "kind">) => REMOTE_TYPES.some((t) => t.id === c.kind);
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Attach / listen requests
|
||
|
||
function isAbsolute(p: string) {
|
||
return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
|
||
}
|
||
|
||
function join(base: string, p: string) {
|
||
if (isAbsolute(p)) return p;
|
||
const rel = p.replace(/^\.\//, "");
|
||
return rel === "." || rel === "" ? base : `${base.replace(/[\\/]$/, "")}/${rel}`;
|
||
}
|
||
|
||
/** Local root (absolute) ↔ remote root, or null when the paths are the same on both sides. */
|
||
function rootMapping(p: Params, root: string): { local: string; remote: string } | null {
|
||
const remote = p.remoteRoot?.trim();
|
||
if (!remote) return null;
|
||
const local = p.localRoot?.trim() ? join(root, p.localRoot.trim()) : root;
|
||
return { local, remote };
|
||
}
|
||
|
||
/** `host:port` for `debugServer` (IPv6 literals in brackets). */
|
||
export function serverAddress(host: string, port: number): string {
|
||
return `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
|
||
}
|
||
|
||
function requestBase(
|
||
rc: RunConfig,
|
||
adapter: string,
|
||
root: string,
|
||
request: "attach" | "launch",
|
||
extra: Record<string, unknown>,
|
||
): LaunchConfig {
|
||
return {
|
||
name: rc.name,
|
||
adapter,
|
||
request,
|
||
program: null,
|
||
args: [],
|
||
cwd: root,
|
||
env: {},
|
||
extraJson: JSON.stringify(extra),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* The request of a Go / Python / Node.js / PHP remote configuration. Delve's headless
|
||
* server and `debugpy --listen` speak DAP themselves (`debugServer` connects there);
|
||
* js-debug runs here and connects to the inspector. PHP Remote Debug is the one
|
||
* `launch`: php-debug is the side that listens, and each Xdebug connection becomes
|
||
* a thread, so nothing attaches and no program is named.
|
||
*/
|
||
export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig | null {
|
||
const p = rc.params ?? {};
|
||
const host = hostOf(p);
|
||
const map = rootMapping(p, root);
|
||
const skip = workflowSettings().debugger.skipLibraryCode;
|
||
switch (rc.kind) {
|
||
case GO_REMOTE:
|
||
return requestBase(rc, "delve", root, "attach", {
|
||
debugServer: serverAddress(host, portOf(p, 2345)),
|
||
mode: "remote",
|
||
...(map ? { substitutePath: [{ from: map.local, to: map.remote }] } : {}),
|
||
});
|
||
case PYTHON_REMOTE:
|
||
return requestBase(rc, "debugpy", root, "attach", {
|
||
debugServer: serverAddress(host, portOf(p, 5678)),
|
||
justMyCode: skip,
|
||
...(map ? { pathMappings: [{ localRoot: map.local, remoteRoot: map.remote }] } : {}),
|
||
});
|
||
case GODOT_ATTACH:
|
||
return requestBase(rc, "godot", root, "attach", {
|
||
debugServer: serverAddress(host, portOf(p, 6006)),
|
||
project: root,
|
||
});
|
||
case NODE_REMOTE:
|
||
return requestBase(rc, "js-debug", root, "attach", {
|
||
type: "pwa-node",
|
||
address: host,
|
||
port: portOf(p, 9229),
|
||
...(map ? { localRoot: map.local, remoteRoot: map.remote } : {}),
|
||
skipFiles: skip ? ["<node_internals>/**", "**/node_modules/**"] : [],
|
||
});
|
||
case PHP_REMOTE:
|
||
return requestBase(rc, "php-debug", root, "launch", {
|
||
port: portOf(p, 9003),
|
||
...(map ? { pathMappings: { [map.remote]: map.local } } : {}),
|
||
});
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** What a session of `rc` shows while nothing is suspended, when it waits for the debuggee to connect. */
|
||
export function remoteWaitingNote(rc: Pick<RunConfig, "kind" | "params"> | undefined): string | null {
|
||
if (rc?.kind !== PHP_REMOTE) return null;
|
||
return tx`Listening for Xdebug on port ${portOf(rc.params ?? {}, 9003)}`;
|
||
}
|
||
|
||
/** What a Remote JVM Debug configuration attaches to (before the listen mode relay). */
|
||
export function remoteJvmTarget(rc: RunConfig): { mode: JdwpMode; host: string; port: number; module: string } {
|
||
const p = rc.params ?? {};
|
||
return {
|
||
mode: p.mode === "listen" ? "listen" : "attach",
|
||
host: hostOf(p),
|
||
port: portOf(p, 5005),
|
||
module: p.module?.trim() ?? "",
|
||
};
|
||
}
|