дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(debugger): Remote debug run configurations (Remote JVM, Go, Python, Node.js) (#50)
Root cause: run configurations could only launch a process; there was no "attach to a debugger that is already listening" type, and debugServer only connected to 127.0.0.1. Fix (IDEA / GoLand / PyCharm): debug-only Remote JVM Debug (attach / listen, live JDWP agent args per JDK version), Go Remote (dlv --headless, substitutePath), Python Remote Debug (debugpy --listen, pathMappings) and Attach to Node.js (inspector, localRoot/remoteRoot); debugServer accepts host:port and [v6]:port; JVM listen mode via a cancellable relay so java-debug can attach. Verified: ide-dap tests (host:port, relay handshake, cancel), run/debugger vitest, in-app on macOS: Python Remote Debug attached to a debugpy server, paused at app.py:3 with variables. Remote JVM/Go/Node not exercised in-app. Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
1 джерело
642b80cef0
коміт
e150101244
20 files changed
+944
-36
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { registerRunConfigType } from "@/workbench/contributions";
|
||||
|
||||
const invoked: Array<[string, unknown[]]> = [];
|
||||
vi.mock("@/platform/ipc", async (orig) => {
|
||||
const real = await orig<typeof import("@/platform/ipc")>();
|
||||
const ok = (data: unknown) => Promise.resolve({ status: "ok", data });
|
||||
return {
|
||||
...real,
|
||||
commands: {
|
||||
...real.commands,
|
||||
javaDebugServer: (...a: unknown[]) => {
|
||||
invoked.push(["javaDebugServer", a]);
|
||||
return ok(41234);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { javaAttachConfig } = await import("../java");
|
||||
const remote = await import("../remote");
|
||||
const { missingParams, newConfigOfType, isDebugOnly, terminalApplies, withParams } =
|
||||
await import("@/features/run/types");
|
||||
const { validateConfig } = await import("@/features/run/configs");
|
||||
|
||||
const {
|
||||
REMOTE_TYPES,
|
||||
remoteJvmType,
|
||||
goRemoteType,
|
||||
pythonRemoteType,
|
||||
nodeRemoteType,
|
||||
jdwpAgentArgs,
|
||||
remoteAttachConfig,
|
||||
remoteJvmTarget,
|
||||
} = remote;
|
||||
|
||||
function configOf(type: (typeof REMOTE_TYPES)[number], params: Record<string, string> = {}): RunConfig {
|
||||
const c = newConfigOfType(type, []);
|
||||
return withParams({ ...c, name: "Remote" }, { ...c.params, ...params }, type);
|
||||
}
|
||||
|
||||
let disposers: Array<() => void> = [];
|
||||
beforeEach(() => {
|
||||
invoked.length = 0;
|
||||
disposers = REMOTE_TYPES.map(registerRunConfigType);
|
||||
});
|
||||
afterEach(() => disposers.forEach((d) => d()));
|
||||
|
||||
describe("remote debug configuration types", () => {
|
||||
it.each(REMOTE_TYPES.map((t) => [t.id, t] as const))(
|
||||
"%s is debug-only, launches nothing and is valid by default",
|
||||
(_, type) => {
|
||||
const c = configOf(type);
|
||||
expect(type.debugOnly).toBe(true);
|
||||
expect(isDebugOnly(c)).toBe(true);
|
||||
expect(c.debugAdapter).toBe(type.debugAdapter);
|
||||
expect(c.command).toBe("");
|
||||
expect(terminalApplies(c, type)).toBe(false);
|
||||
expect(validateConfig(c, [c])).toEqual({});
|
||||
expect(missingParams(c, type)).toEqual({});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(REMOTE_TYPES.map((t) => [t.id, t] as const))("%s rejects a bad port and an empty host", (_, type) => {
|
||||
expect(missingParams(configOf(type, { port: "70000" }), type)).toEqual({
|
||||
port: "Port must be a number from 1 to 65535",
|
||||
});
|
||||
expect(missingParams(configOf(type, { port: "abc" }), type).port).toBeDefined();
|
||||
expect(missingParams(configOf(type, { host: "" }), type)).toEqual({ host: "Host is required" });
|
||||
});
|
||||
|
||||
it("Remote JVM Debug defaults to attaching to localhost:5005", () => {
|
||||
expect(configOf(remoteJvmType).params).toEqual({ mode: "attach", host: "localhost", port: "5005", jdk: "9" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Remote JVM Debug: command line arguments for remote JVM", () => {
|
||||
it.each([
|
||||
[{ jdk: "9" }, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"],
|
||||
[{ jdk: "5" }, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"],
|
||||
[{ jdk: "1.4" }, "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"],
|
||||
[{ jdk: "9", port: "8000" }, "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"],
|
||||
[
|
||||
{ mode: "listen", host: "dev-box", port: "5005" },
|
||||
"-agentlib:jdwp=transport=dt_socket,server=n,address=dev-box:5005,suspend=y",
|
||||
],
|
||||
])("%j → %s", (params, expected) => {
|
||||
expect(jdwpAgentArgs({ ...remoteJvmType.defaults, ...params })).toBe(expected);
|
||||
});
|
||||
|
||||
it("is shown as a read-only field that follows the form", () => {
|
||||
const field = remoteJvmType.fields.find((f) => f.key === "args")!;
|
||||
expect(field.control).toBe("output");
|
||||
expect(field.value!({ port: "6006", jdk: "5" })).toContain("address=6006");
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote attach requests", () => {
|
||||
it("Remote JVM Debug attaches java-debug to host:port with the module's sources", async () => {
|
||||
const c = configOf(remoteJvmType, { host: "10.0.0.5", port: "5006", module: "api" });
|
||||
const t = remoteJvmTarget(c);
|
||||
expect(t).toEqual({ mode: "attach", host: "10.0.0.5", port: 5006, module: "api" });
|
||||
const l = await javaAttachConfig({ name: c.name, module: t.module, hostName: t.host, port: t.port }, "/p");
|
||||
expect(invoked).toEqual([["javaDebugServer", ["api"]]]);
|
||||
expect(l.adapter).toBe("java-debug");
|
||||
expect(l.request).toBe("attach");
|
||||
expect(l.cwd).toBe("/p/api");
|
||||
expect(JSON.parse(l.extraJson!)).toMatchObject({ debugServer: 41234, hostName: "10.0.0.5", port: 5006 });
|
||||
});
|
||||
|
||||
it("Remote JVM Debug in listen mode", () => {
|
||||
expect(remoteJvmTarget(configOf(remoteJvmType, { mode: "listen" })).mode).toBe("listen");
|
||||
});
|
||||
|
||||
it("Go Remote connects to dlv --headless and maps the source root", () => {
|
||||
const l = remoteAttachConfig(configOf(goRemoteType, { host: "build", remoteRoot: "/go/src/app" }), "/p")!;
|
||||
expect(l).toMatchObject({ adapter: "delve", request: "attach", cwd: "/p" });
|
||||
expect(JSON.parse(l.extraJson!)).toEqual({
|
||||
debugServer: "build:2345",
|
||||
mode: "remote",
|
||||
substitutePath: [{ from: "/p", to: "/go/src/app" }],
|
||||
});
|
||||
const plain = JSON.parse(remoteAttachConfig(configOf(goRemoteType), "/p")!.extraJson!);
|
||||
expect(plain).toEqual({ debugServer: "localhost:2345", mode: "remote" });
|
||||
});
|
||||
|
||||
it("Python remote connects to debugpy --listen with pathMappings", () => {
|
||||
const l = remoteAttachConfig(
|
||||
configOf(pythonRemoteType, { host: "::1", localRoot: "svc", remoteRoot: "/app" }),
|
||||
"/p",
|
||||
)!;
|
||||
expect(l.adapter).toBe("debugpy");
|
||||
expect(JSON.parse(l.extraJson!)).toMatchObject({
|
||||
debugServer: "[::1]:5678",
|
||||
pathMappings: [{ localRoot: "/p/svc", remoteRoot: "/app" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("Attach to Node.js attaches js-debug to the inspector with local/remote roots", () => {
|
||||
const l = remoteAttachConfig(configOf(nodeRemoteType, { host: "web", remoteRoot: "/usr/src/app" }), "/p")!;
|
||||
expect(l.adapter).toBe("js-debug");
|
||||
expect(JSON.parse(l.extraJson!)).toMatchObject({
|
||||
type: "pwa-node",
|
||||
address: "web",
|
||||
port: 9229,
|
||||
localRoot: "/p",
|
||||
remoteRoot: "/usr/src/app",
|
||||
});
|
||||
});
|
||||
|
||||
it("start-the-remote-side hints follow the port", () => {
|
||||
expect(remote.delveServerCommand({ port: "4000" })).toBe(
|
||||
"dlv debug --headless --listen=:4000 --api-version=2 --accept-multiclient",
|
||||
);
|
||||
expect(remote.debugpyServerCommand({ port: "5679" })).toContain("--listen 0.0.0.0:5679");
|
||||
expect(remote.nodeInspectCommand({})).toBe("node --inspect=0.0.0.0:9229 app.js");
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Debugger actions (ids match `keymaps/intellij-macos.ts`). */
|
||||
import { registerActions, type Action } from "@/platform/actions";
|
||||
import { call, commands, isTauri, type RunConfig, type RunId } from "@/platform/ipc";
|
||||
import { call, commands, isCancelled, isTauri, nextRequestId, type RunConfig, type RunId } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { activeEditor } from "@/editor/api";
|
||||
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
|
||||
@@ -18,6 +18,7 @@ import { toggleBreakpointSmart } from "./breakpoints/toggle";
|
||||
import { isJavaAttachTarget, isJavaDebuggable, javaAttachConfig, javaLaunchConfig } from "./java";
|
||||
import { reloadChangedClasses } from "./hotswap";
|
||||
import { debugUnsupportedReason, launchConfigFor } from "./launch";
|
||||
import { isRemoteConfig, REMOTE_JVM, remoteAttachConfig, remoteJvmTarget } from "./remote";
|
||||
import {
|
||||
activeSession,
|
||||
isLive,
|
||||
@@ -142,6 +143,52 @@ function evaluateSeed(): string {
|
||||
return expressionAt(ed.view, sel.head)?.text ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote JVM Debug in listen mode: the IDE listens and the remote JVM connects; java-debug
|
||||
* then attaches through a loopback relay. Null when cancelled.
|
||||
*/
|
||||
async function listenForJvm(name: string, host: string, port: number): Promise<number | null> {
|
||||
const request = nextRequestId();
|
||||
const key = `debug.listen.${request}`;
|
||||
notifications().notify({
|
||||
group: "run",
|
||||
level: "info",
|
||||
key,
|
||||
sticky: true,
|
||||
title: `'${name}': listening for the remote JVM on port ${port}`,
|
||||
body: `Start the remote JVM with -agentlib:jdwp=transport=dt_socket,server=n,address=${host}:${port},suspend=y`,
|
||||
actions: [{ label: "Cancel", run: () => void commands.cancelRequest(request) }],
|
||||
});
|
||||
try {
|
||||
return await call(commands.debugListenForJvm(request, host, port));
|
||||
} catch (e) {
|
||||
if (isCancelled(e)) return null;
|
||||
throw e;
|
||||
} finally {
|
||||
notifications().expire?.(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** Remote JVM Debug, Go Remote, Python / Node.js remote: attach over host:port. */
|
||||
async function startRemote(config: RunConfig, root: string) {
|
||||
if (config.kind === REMOTE_JVM) {
|
||||
const t = remoteJvmTarget(config);
|
||||
let hostName = t.host;
|
||||
let port = t.port;
|
||||
if (t.mode === "listen") {
|
||||
const relay = await listenForJvm(config.name, t.host, t.port);
|
||||
if (relay == null) return;
|
||||
[hostName, port] = ["127.0.0.1", relay];
|
||||
}
|
||||
const launch = await javaAttachConfig({ name: config.name, module: t.module, hostName, port }, root);
|
||||
// A listen relay accepts one debugger: rerunning listens again through the configuration.
|
||||
await startSession(launch, { rerunnable: t.mode === "attach" });
|
||||
return;
|
||||
}
|
||||
const launch = remoteAttachConfig(config, root);
|
||||
if (launch) await startSession(launch);
|
||||
}
|
||||
|
||||
async function startConfig(arg: unknown) {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root) return;
|
||||
@@ -167,6 +214,10 @@ async function startConfig(arg: unknown) {
|
||||
}
|
||||
config = configs[0]!;
|
||||
}
|
||||
if (isRemoteConfig(config)) {
|
||||
await startRemote(config, root);
|
||||
return;
|
||||
}
|
||||
if (isJavaDebuggable(config)) {
|
||||
await maybeEditorService()
|
||||
?.saveAll()
|
||||
|
||||
@@ -8,7 +8,12 @@ import { onIdeEvent } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { valueTooltip } from "./editor/valueTooltip";
|
||||
import { registerEditorContribution } from "@/editor/api";
|
||||
import { registerMenuGroup, registerOverlay, registerToolWindow } from "@/workbench/contributions";
|
||||
import {
|
||||
registerMenuGroup,
|
||||
registerOverlay,
|
||||
registerRunConfigType,
|
||||
registerToolWindow,
|
||||
} from "@/workbench/contributions";
|
||||
import { registerDebugActions } from "./actions";
|
||||
import { loadBreakpoints } from "./breakpoints/store";
|
||||
import { loadExceptionBreakpoints } from "./breakpoints/exceptions";
|
||||
@@ -19,6 +24,7 @@ import { installSmartStepPicking, smartStepTargets } from "./editor/smartStep";
|
||||
import { onHotCodeReplace } from "./hotswap";
|
||||
import { handleDebugEvent, isLive, loadWatches, publishContext, resetSessions, stopSession, useDebug } from "./state";
|
||||
import { DebuggerOverlays } from "./ui/Overlays";
|
||||
import { REMOTE_TYPES } from "./remote";
|
||||
|
||||
export function activate(): () => void {
|
||||
loadWatches();
|
||||
@@ -28,6 +34,8 @@ export function activate(): () => void {
|
||||
loadSpecialBreakpoints(useProject.getState().project?.root ?? null);
|
||||
const disposers = [
|
||||
registerDebugActions(),
|
||||
// Remote JVM Debug, Go Remote, …: Run/Debug Configurations › Add New Configuration.
|
||||
...REMOTE_TYPES.map(registerRunConfigType),
|
||||
registerToolWindow({
|
||||
id: "debug",
|
||||
title: "Debug",
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* 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.
|
||||
* Only Debug applies to them (`debugOnly`); they launch no process.
|
||||
*/
|
||||
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";
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
export const REMOTE_TYPES: readonly RunConfigTypeContribution[] = [
|
||||
remoteJvmType,
|
||||
goRemoteType,
|
||||
pythonRemoteType,
|
||||
nodeRemoteType,
|
||||
];
|
||||
|
||||
export const isRemoteConfig = (c: Pick<RunConfig, "kind">) => REMOTE_TYPES.some((t) => t.id === c.kind);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attach 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 attachBase(rc: RunConfig, adapter: string, root: string, extra: Record<string, unknown>): LaunchConfig {
|
||||
return {
|
||||
name: rc.name,
|
||||
adapter,
|
||||
request: "attach",
|
||||
program: null,
|
||||
args: [],
|
||||
cwd: root,
|
||||
env: {},
|
||||
extraJson: JSON.stringify(extra),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The attach request of a Go / Python / Node.js remote configuration. Delve's headless
|
||||
* server and `debugpy --listen` speak DAP themselves (`debugServer` connects there);
|
||||
* js-debug runs here and connects to the inspector.
|
||||
*/
|
||||
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 attachBase(rc, "delve", root, {
|
||||
debugServer: serverAddress(host, portOf(p, 2345)),
|
||||
mode: "remote",
|
||||
...(map ? { substitutePath: [{ from: map.local, to: map.remote }] } : {}),
|
||||
});
|
||||
case PYTHON_REMOTE:
|
||||
return attachBase(rc, "debugpy", root, {
|
||||
debugServer: serverAddress(host, portOf(p, 5678)),
|
||||
justMyCode: skip,
|
||||
...(map ? { pathMappings: [{ localRoot: map.local, remoteRoot: map.remote }] } : {}),
|
||||
});
|
||||
case NODE_REMOTE:
|
||||
return attachBase(rc, "js-debug", root, {
|
||||
type: "pwa-node",
|
||||
address: host,
|
||||
port: portOf(p, 9229),
|
||||
...(map ? { localRoot: map.local, remoteRoot: map.remote } : {}),
|
||||
skipFiles: skip ? ["<node_internals>/**", "**/node_modules/**"] : [],
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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() ?? "",
|
||||
};
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача