feat(run): debug Python on Docker and Compose run targets (#1757)

Refs #1083 (step A2). Debug now works on Python configurations whose run target is a Docker image or Compose service: the run starts the program in a new container under debugpy (the image's own, else the IDE's managed copy mounted read-only), publishes the debug port on 127.0.0.1 only, and the debugger attaches like a Python Remote Debug configuration with the project folder mapped. Console scripts (pytest) run as modules and `uv run` keeps the manager in front; shell command lines are refused. Container debug startup is bounded (error when the container exits early or debugpy isn't ready in 120 s) and stopping the debugger stops the container.

Co-authored-by: andreabasile <[email protected]>
Цей коміт міститься в:
Andrea Basileтаandreabasile authored and GitHub committed 2026-11-08 00:21:42 +08:00
1 джерело c3671d782c
коміт daad91777f
23 files changed
+904 -34

No files matched your search

+18
Переглянути файл
@@ -5,6 +5,7 @@ const ipc = vi.hoisted(() => {
const ok = <T>(data: T) => Promise.resolve({ status: "ok" as const, data });
return {
ok,
runStop: vi.fn(() => ok(null)),
debugStep: vi.fn(() => ok(null)),
debugResume: vi.fn(() => ok(null)),
debugForceStepOver: vi.fn(() => ok(null)),
@@ -420,3 +421,20 @@ describe("output class breakpoint restrictions", () => {
expect(ipc.debugRunTo).not.toHaveBeenCalled();
});
});
it("stops the waiting Python container if attaching fails", async () => {
useProject.setState({ project: { root: "/p" } as never });
ipc.debugStart.mockResolvedValueOnce({
status: "error",
error: { kind: "internal", message: "connection closed" },
} as never);
await executeAction("debug.attach", {
name: "container",
adapter: "debugpy",
hostName: "127.0.0.1",
port: 41234,
remoteRoot: "/app",
process: 9,
});
expect(ipc.runStop).toHaveBeenCalledWith(9);
});
+14
Переглянути файл
@@ -33,6 +33,8 @@ const {
phpRemoteType,
jdwpAgentArgs,
xdebugCommand,
isPythonAttachTarget,
pythonAttachConfig,
remoteAttachConfig,
remoteJvmTarget,
remoteWaitingNote,
@@ -130,6 +132,18 @@ describe("remote attach requests", () => {
expect(plain).toEqual({ debugServer: "localhost:2345", mode: "remote" });
});
it("a Python program in a container: the project root maps onto its folder there", () => {
const target = { name: "app", adapter: "debugpy" as const, hostName: "127.0.0.1", port: 41234, remoteRoot: "/app" };
expect(isPythonAttachTarget({ ...target, process: 3 })).toBe(true);
expect(isPythonAttachTarget({ name: "app", module: "", hostName: "h", port: 5005 })).toBe(false);
const l = pythonAttachConfig(target, "/p");
expect(l).toMatchObject({ name: "app", adapter: "debugpy", request: "attach", cwd: "/p" });
expect(JSON.parse(l.extraJson!)).toMatchObject({
debugServer: "127.0.0.1:41234",
pathMappings: [{ localRoot: "/p", remoteRoot: "/app" }],
});
});
it("Python remote connects to debugpy --listen with pathMappings", () => {
const l = remoteAttachConfig(
configOf(pythonRemoteType, { host: "::1", localRoot: "svc", remoteRoot: "/app" }),
+29
Переглянути файл
@@ -75,6 +75,35 @@ it("stops shared-process debugger roots without stopping their process until req
expect(ipc.runStop).toHaveBeenCalledWith(7);
});
it("stops associated processes through Run before disconnecting the debugger", async () => {
const order: string[] = [];
const stop = vi.fn((_process: unknown) => {
order.push("run");
});
const dispose = registerActions([
{
id: "run.stopProcess",
title: "Stop",
run: ({ arg }) => {
stop(arg);
},
},
]);
ipc.debugStop.mockImplementationOnce(async () => {
order.push("debug");
return { status: "ok", data: null };
});
try {
adoptSession(1, { runConfig: config, process: 7 });
await stopSession(1);
expect(stop).toHaveBeenCalledWith(7);
expect(ipc.runStop).not.toHaveBeenCalled();
expect(order).toEqual(["run", "debug"]);
} finally {
dispose();
}
});
it("pins Show in Debug to the configured session and clears summaries with the project", () => {
adoptSession(1, { runConfig: config });
adoptSession(2, { runConfig: { ...config, id: "other" } });
+19 -4
Переглянути файл
@@ -31,7 +31,14 @@ 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 {
isPythonAttachTarget,
isRemoteConfig,
pythonAttachConfig,
REMOTE_JVM,
remoteAttachConfig,
remoteJvmTarget,
} from "./remote";
import {
activeSession,
resumeExecution,
@@ -45,6 +52,7 @@ import {
startSession,
startPreparedSession,
stopSession,
stopAssociatedProcess,
useDebug,
type DebugSession,
} from "./state";
@@ -384,6 +392,8 @@ export function registerDebugActions(): () => void {
title: "Attach Debugger",
category: "Debug",
hidden: true,
// Also debugpy to a Python program in a run target's container
// (arg: { name, adapter: "debugpy", hostName, port, remoteRoot, process }).
description:
"Attach the Java debugger to a JVM listening for JDWP (arg: { name, module, hostName, port, process })",
run: ({ arg }) =>
@@ -391,13 +401,18 @@ export function registerDebugActions(): () => void {
tx("Cannot attach the debugger"),
(async () => {
const root = useProject.getState().project?.root;
if (!root || !isJavaAttachTarget(arg)) return;
const launch = await javaAttachConfig(arg, root);
await startSession(launch, {
if (!root) return;
let launch: LaunchConfig;
if (isPythonAttachTarget(arg)) launch = pythonAttachConfig(arg, root);
else if (isJavaAttachTarget(arg)) launch = await javaAttachConfig(arg, root);
else return;
const session = await startSession(launch, {
process: (arg.process ?? null) as RunId | null,
runConfig: arg.runConfig,
rerunnable: false,
});
if (session == null && isPythonAttachTarget(arg) && arg.process != null)
await stopAssociatedProcess(arg.process);
})(),
),
},
+40 -6
Переглянути файл
@@ -318,7 +318,7 @@ export function serverAddress(host: string, port: number): string {
}
function requestBase(
rc: RunConfig,
rc: Pick<RunConfig, "name">,
adapter: string,
root: string,
request: "attach" | "launch",
@@ -336,6 +336,44 @@ function requestBase(
};
}
/** Attach request for a process listening with `debugpy --listen`. */
function debugpyAttach(
name: string,
root: string,
host: string,
port: number,
map: { local: string; remote: string } | null,
): LaunchConfig {
return requestBase({ name }, "debugpy", root, "attach", {
debugServer: serverAddress(host, port),
justMyCode: workflowSettings().debugger.skipLibraryCode,
...(map ? { pathMappings: [{ localRoot: map.local, remoteRoot: map.remote }] } : {}),
});
}
/** A Python program debugged in a run target's container, waiting for the debugger (Run's `debugInContainer`). */
export interface PythonAttachTarget {
name: string;
adapter: "debugpy";
hostName: string;
port: number;
/** The project folder in the container. */
remoteRoot: string;
}
export function isPythonAttachTarget(
arg: unknown,
): arg is PythonAttachTarget & { process?: number | null; runConfig?: RunConfig } {
if (!arg || typeof arg !== "object") return false;
const a = arg as Partial<PythonAttachTarget>;
return a.adapter === "debugpy" && typeof a.port === "number" && typeof a.remoteRoot === "string";
}
/** Attach request of a `PythonAttachTarget`: the project root maps onto the container's folder. */
export function pythonAttachConfig(target: PythonAttachTarget, root: string): LaunchConfig {
return debugpyAttach(target.name, root, target.hostName, target.port, { local: root, remote: target.remoteRoot });
}
/**
* The request of a Go / Python / Node.js / PHP remote configuration. Delve's headless
* server and `debugpy --listen` speak DAP themselves (`debugServer` connects there);
@@ -356,11 +394,7 @@ export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig |
...(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 }] } : {}),
});
return debugpyAttach(rc.name, root, host, portOf(p, 5678), map);
case GODOT_ATTACH:
return requestBase(rc, "godot", root, "attach", {
debugServer: serverAddress(host, portOf(p, 6006)),
+11 -5
Переглянути файл
@@ -4,7 +4,7 @@
*/
import { tx } from "@/platform/i18n";
import { workflowSettings } from "@/platform/workflowSettings";
import { executeAction } from "@/platform/actions";
import { executeAction, getAction } from "@/platform/actions";
import { create } from "zustand";
import { AnsiStream, appendSpans, type Span } from "@/ui/ansi";
import { setContext } from "@/platform/context";
@@ -25,7 +25,7 @@ import {
} from "@/platform/ipc";
import { confirmTerminate } from "@/workbench/confirmTerminate";
import { maybeEditorService, notifications, toolWindowService, useDebugRunSessions } from "@/workbench/contributions";
import { startedPort } from "@/features/run/serverPort";
import { startedPort } from "@/platform/serverPort";
import { breakpointsOf, consumeTemporary, flushBreakpoints, refreshStates, resetStatuses } from "./breakpoints/store";
import { interpolateLog, logParts, suspends } from "./breakpoints/model";
import { syncSpecial, useSpecialBreakpoints } from "./breakpoints/special";
@@ -653,6 +653,12 @@ export async function startSession(
}
}
/** Stops an associated Run process through its owner, which records the user's stop intent. */
export async function stopAssociatedProcess(process: RunId): Promise<void> {
if (getAction("run.stopProcess")) await executeAction("run.stopProcess", process);
else await call(commands.runStop(process)).catch(() => undefined);
}
export async function stopSession(id: DebugSessionId, options: { stopProcess?: boolean } = {}) {
const s = useDebug.getState().sessions[id];
if (!s) return;
@@ -670,6 +676,8 @@ export async function stopSession(id: DebugSessionId, options: { stopProcess?: b
const target = useDebug.getState().sessions[sid];
if (target && isLive(target)) patch(sid, { status: "stopping" });
}
const stoppingProcess =
options.stopProcess !== false && root.process != null ? stopAssociatedProcess(root.process) : null;
try {
await call(commands.debugStop(root.id));
} catch (e) {
@@ -677,9 +685,7 @@ export async function stopSession(id: DebugSessionId, options: { stopProcess?: b
console.warn("debugStop failed", e);
markTerminated(root.id);
}
// Detaching leaves an attached JVM running: stop the process it belongs to, like IDEA.
if (options.stopProcess !== false && root.process != null)
await call(commands.runStop(root.process)).catch(() => undefined);
await stoppingProcess;
}
/**
+21
Переглянути файл
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { containerDebugLaunch, debugpyReady } from "./containerDebug";
describe("container debugging", () => {
it("reads the port and the project folder from the bootstrap's line", () => {
expect(
debugpyReady("debugpy is waiting for the debugger on port 41234, project folder /workspaces/my app\r"),
).toEqual({
port: 41234,
remoteRoot: "/workspaces/my app",
});
expect(debugpyReady("waiting for the debugger on port 41234")).toBeNull();
});
it("asks the backend for a debug run, keeping the configuration's parameters", () => {
const config = { id: "a", params: { module: "app" } } as unknown as RunConfig;
expect(containerDebugLaunch(config).params).toEqual({ module: "app", containerDebug: "true" });
expect(config.params).toEqual({ module: "app" });
});
});
+92
Переглянути файл
@@ -0,0 +1,92 @@
/**
* Debugging Python on Docker / Compose run targets: the run starts the program in a new
* container under debugpy (the bootstrap of `ide_remote::docker`), which prints its port
* once it listens; the debugger then attaches like a Python Remote Debug configuration,
* with the project folder mapped. Output stays in the Run tab; stopping the debugger
* stops the container.
*/
import { executeAction } from "@/platform/actions";
import type { RunConfig } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { notifications } from "@/workbench/contributions";
import { isLive, startRun, stopSession, useRun } from "./store";
/** The bootstrap's line (`ide_remote::docker::DEBUG_READY`). */
const READY = /^debugpy is waiting for the debugger on port (\d+), project folder (.+)$/;
interface Ready {
port: number;
remoteRoot: string;
}
/** Where the debugger attaches, from an output line of a container debug run. */
export function debugpyReady(line: string): Ready | null {
const m = READY.exec(line.trim());
return m ? { port: Number(m[1]), remoteRoot: m[2]! } : null;
}
/** `config` as the backend runs it for debugging: under debugpy, waiting for the debugger. */
export function containerDebugLaunch(config: RunConfig): RunConfig {
return { ...config, params: { ...config.params, containerDebug: "true" } };
}
/** Runs `config` in its container under debugpy and attaches the debugger. */
export async function debugInContainer(config: RunConfig): Promise<void> {
let sessionId: string | null = null;
let ready: Ready | null = null;
let done = false;
const pending: { timer?: ReturnType<typeof setTimeout>; unsubscribe?: () => void } = {};
const finish = () => {
done = true;
clearTimeout(pending.timer);
pending.unsubscribe?.();
};
const fail = (body: string) => {
finish();
notifications().notify({ group: "run", level: "error", title: tx("Cannot start container debugging"), body });
};
const attach = ({ port, remoteRoot }: Ready) => {
if (done) return;
finish();
const runId = useRun.getState().sessions.find((s) => s.id === sessionId)?.runId ?? null;
void executeAction("debug.attach", {
name: config.name,
adapter: "debugpy",
hostName: "127.0.0.1",
port,
remoteRoot,
process: runId,
runConfig: config,
});
};
const session = await startRun(config, undefined, undefined, {
launch: containerDebugLaunch(config),
onLine: (line) => {
if (done) return;
const parsed = debugpyReady(line);
if (!parsed) return;
ready = parsed;
if (sessionId) attach(ready);
},
});
if (!session) return;
sessionId = session.id;
const check = () => {
const current = useRun.getState().sessions.find((s) => s.id === sessionId);
if (!current || current.stopped || current.status === "failed") finish();
else if (!isLive(current))
fail(tx("The container exited before debugpy was ready. Check the Run console for errors."));
};
pending.unsubscribe = useRun.subscribe(check);
check();
if (done) return;
pending.timer = setTimeout(() => {
fail(
tx(
"debugpy did not become ready within 120 seconds. Check the Run console and the container's Python interpreter.",
),
);
void stopSession(session.id);
}, 120_000);
if (ready) attach(ready);
}
+20 -2
Переглянути файл
@@ -5,7 +5,7 @@
import { registerEditorContribution } from "@/editor/api";
import { executeAction, getAction, registerActions } from "@/platform/actions";
import { getContext, setContext } from "@/platform/context";
import { onIdeEvent, type RunConfig } from "@/platform/ipc";
import { call, commands, onIdeEvent, type RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { onWorkflowSettingsChange } from "@/platform/workflowSettings";
import { showContextMenu, lazyComponent, lazyWhen, type MenuItem } from "@/ui";
@@ -46,7 +46,8 @@ import { cleanupRemovedProject, recordRun, runServiceProvider } from "./services
import { liveServiceSession, serviceSessions } from "./serviceSessions";
import { runBeforeLaunch } from "./beforeLaunch";
import { compoundChildren } from "./configs";
import { remoteDebugBlocked } from "./remote";
import { containerDebuggable, remoteDebugBlocked } from "./remote";
import { debugInContainer } from "./containerDebug";
import { isDebugOnly, registerBuiltinTypes } from "./types";
import { listenForApplicationDeactivation, showApplicationUpdate, updatableSessions } from "./applicationUpdate";
import { npmGutter } from "./npmGutter";
@@ -116,6 +117,11 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = []) {
if (remoteDebugBlocked(config)) return;
if (!(await prepareLaunch(config, chain))) return;
recordRun(config.id);
// Python on a Docker / Compose target: debugpy in the container, the debugger attaches.
if (containerDebuggable(config) && getAction("debug.attach")) {
await debugInContainer(config);
return;
}
// Remote JVM Debug, Go Remote…: the debugger attaches, nothing is launched here.
if (isDebugOnly(config) && getAction("debug.startConfig")) {
await executeAction("debug.startConfig", config);
@@ -438,6 +444,18 @@ export function activate(): () => void {
when: "projectOpen",
run: ({ arg }) => stopAction(arg),
},
{
id: "run.stopProcess",
title: "Stop",
category: "Run",
hidden: true,
run: async ({ arg }) => {
if (typeof arg !== "number") return;
const session = useRun.getState().sessions.find((s) => s.runId === arg);
if (session) await stopSession(session.id);
else await call(commands.runStop(arg));
},
},
{
id: "run.rerun",
title: "Rerun",
+134 -1
Переглянути файл
@@ -3,6 +3,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
const ipc = vi.hoisted(() => ({
runStart: vi.fn(),
runStop: vi.fn(() => Promise.resolve({ status: "ok" as const, data: null })),
runSaveConfigs: vi.fn((_configs: unknown) => Promise.resolve({ status: "ok" as const, data: null })),
runConfigs: vi.fn(() => Promise.resolve({ status: "ok" as const, data: [] as unknown[] })),
sshTrustHost: vi.fn((_id: string) => Promise.resolve({ status: "ok" as const, data: null })),
@@ -59,7 +60,7 @@ import {
withTarget,
type RemedyDeps,
} from "./remote";
import { resetRuns, saveUserConfigs, startRun, useRun } from "./store";
import { onRunExited, resetRuns, saveUserConfigs, startRun, useRun } from "./store";
import { newConfigOfType } from "./types";
import { readRan } from "./services";
@@ -181,6 +182,138 @@ describe("remote debugging", () => {
);
});
describe("in containers", () => {
const container: RunTargetValid = { ...DEFAULT_RUN_TARGET, kind: "docker", id: "py", name: "Python", image: "x" };
const attach = vi.fn();
let unregisterAttach: () => void;
beforeEach(() => {
useProject.setState({ settings: { runTargets: [box, container], ssh: { configs: [] } } as never });
unregisterAttach = registerActions([{ id: "debug.attach", title: "Attach", run: ({ arg }) => attach(arg) }]);
attach.mockClear();
});
afterEach(() => unregisterAttach());
it("stops a container that never announces debugpy", async () => {
vi.useFakeTimers();
try {
const py = cfg("app", { target: "py", debugAdapter: "debugpy" });
useRun.setState({ configs: [py], loaded: true });
ipc.runStart.mockResolvedValueOnce(ok);
await executeAction("run.debugConfig", py.id);
await vi.advanceTimersByTimeAsync(120_000);
expect(ipc.runStop).toHaveBeenCalledWith(3);
expect(attach).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({
title: "Cannot start container debugging",
body: expect.stringContaining("120 seconds"),
}),
);
} finally {
vi.useRealTimers();
}
});
it("reports an early container exit and cancels the readiness timeout", async () => {
vi.useFakeTimers();
try {
const py = cfg("app", { target: "py", debugAdapter: "debugpy" });
useRun.setState({ configs: [py], loaded: true });
ipc.runStart.mockResolvedValueOnce(ok);
await executeAction("run.debugConfig", py.id);
onRunExited(3, 1);
await vi.advanceTimersByTimeAsync(120_000);
expect(attach).not.toHaveBeenCalled();
expect(ipc.runStop).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
title: "Cannot start container debugging",
body: expect.stringContaining("exited before debugpy"),
}),
);
} finally {
vi.useRealTimers();
}
});
it("cleans up the readiness wait when the user stops the run", async () => {
vi.useFakeTimers();
try {
const py = cfg("app", { target: "py", debugAdapter: "debugpy" });
useRun.setState({ configs: [py], loaded: true });
ipc.runStart.mockResolvedValueOnce(ok);
await executeAction("run.debugConfig", py.id);
useRun.setState((s) => ({ sessions: s.sessions.map((s) => ({ ...s, stopped: true })) }));
await vi.advanceTimersByTimeAsync(120_000);
expect(attach).not.toHaveBeenCalled();
expect(notify).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("records debugger-requested stops before a signalled container exit", async () => {
vi.useFakeTimers();
try {
provideToolWindowService({ show: vi.fn(), isVisible: () => false } as unknown as ToolWindowService);
const py = cfg("app", { target: "py", debugAdapter: "debugpy" });
ipc.runStart.mockResolvedValueOnce(ok);
await startRun(py);
await executeAction("run.stopProcess", 3);
expect(useRun.getState().sessions[0]?.stopped).toBe(true);
expect(ipc.runStop).toHaveBeenCalledWith(3);
onRunExited(3, 143);
await vi.advanceTimersByTimeAsync(100);
expect(notify).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it("runs Python under debugpy and attaches once it listens", async () => {
const py = cfg("app", { target: "py", debugAdapter: "debugpy" });
useRun.setState({ configs: [py], loaded: true });
ipc.runStart.mockResolvedValueOnce(ok);
await executeAction("run.debugConfig", py.id);
expect(startConfig).not.toHaveBeenCalled();
expect(ipc.runStart).toHaveBeenCalledWith(
expect.objectContaining({ id: "app", params: { containerDebug: "true" } }),
expect.anything(),
);
const channel = ipc.runStart.mock.calls.at(-1)![1] as { onmessage: (m: unknown) => void };
channel.onmessage({
kind: "stderr",
text: "debugpy is waiting for the debugger on port 41234, project folder /app\n",
});
await vi.waitFor(() => expect(attach).toHaveBeenCalled());
channel.onmessage({
kind: "stderr",
text: "debugpy is waiting for the debugger on port 41234, project folder /app\n",
});
expect(attach).toHaveBeenCalledTimes(1);
expect(attach).toHaveBeenCalledWith(
expect.objectContaining({
adapter: "debugpy",
hostName: "127.0.0.1",
port: 41234,
remoteRoot: "/app",
process: 3,
}),
);
expect(notify).not.toHaveBeenCalled();
});
it("refuses other debuggers", async () => {
const go = cfg("app", { target: "py", debugAdapter: "delve" });
useRun.setState({ configs: [go], loaded: true });
await executeAction("run.debugConfig", go.id);
expect(ipc.runStart).not.toHaveBeenCalled();
expect(notify).toHaveBeenCalledWith(
expect.objectContaining({ title: "Only Python can be debugged in containers", level: "warning" }),
);
});
});
it("still debugs local configurations", async () => {
const local = cfg("app", { debugAdapter: "debugpy" });
useRun.setState({ configs: [local], loaded: true });
+22 -6
Переглянути файл
@@ -51,18 +51,34 @@ export function withTarget(config: RunConfig, target: string): RunConfig {
return target ? { ...rest, target } : rest;
}
/** A Python configuration on a Docker / Compose target: debugged in its container (`debugInContainer`). */
export function containerDebuggable(
config: RunConfig,
targets: readonly RunTargetValid[] = workflowSettings().runTargets,
): boolean {
const target = targetOf(config, targets);
return !!target && inContainer(target) && config.debugAdapter === "debugpy";
}
/**
* Debugging on run targets isn't supported: tells so (offering Run) and returns
* true when `config` runs on a target.
* Debugging on run targets isn't supported, except Python in containers: tells so
* (offering Run) and returns true when `config` can't be debugged where it runs.
*/
export function remoteDebugBlocked(config: RunConfig): boolean {
if (!config.target) return false;
const target = targetName(config) ?? config.target;
export function remoteDebugBlocked(
config: RunConfig,
targets: readonly RunTargetValid[] = workflowSettings().runTargets,
): boolean {
if (!config.target || containerDebuggable(config, targets)) return false;
const target = targetName(config, targets) ?? config.target;
const container = targetOf(config, targets);
notifications().notify({
group: "run",
level: "warning",
key: "run.remoteDebug",
title: "Debugging on run targets is not supported",
title:
container && inContainer(container)
? "Only Python can be debugged in containers"
: "Debugging on run targets is not supported",
body: txTemplate(
`'${config.name}' runs on ${target}. Run it, or choose “Local machine” under “Run on” to debug it.`,
),
+1 -1
Переглянути файл
@@ -18,7 +18,7 @@ import {
sessionDetail,
sessionStatus,
} from "./services";
import { startedPort } from "./serverPort";
import { startedPort } from "@/platform/serverPort";
import { useRun, type RunSession } from "./store";
import type { DebugServiceSession } from "@/workbench/contributions";
+1 -1
Переглянути файл
@@ -54,7 +54,7 @@ import { TerminalOutput } from "./terminalOutput";
import { typeFor } from "./types";
import { loadRunTemplates } from "./runTemplates";
import { recordRun } from "./services";
import { startedPort } from "./serverPort";
import { startedPort } from "@/platform/serverPort";
import { tomcatBrowserUrl } from "./tomcat";
import { gradleDelegation, gradleTestLaunch } from "./gradleRun";
import { openUrl } from "./navigate";
+1 -1
Переглянути файл
@@ -553,7 +553,7 @@ function RunTargetsEditor() {
</T>{" "}
<T>
{
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends; debugging, coverage and profiling stay on this machine."
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine."
}
</T>
</div>
+5
Переглянути файл
@@ -190,6 +190,11 @@
"Cannot choose a directory": "Cannot choose a directory",
"Open in HTTP Client": "Open in HTTP Client",
"Cannot generate the HTTP request": "Cannot generate the HTTP request",
"Cannot start container debugging": "Cannot start container debugging",
"The container exited before debugpy was ready. Check the Run console for errors.": "The container exited before debugpy was ready. Check the Run console for errors.",
"debugpy did not become ready within 120 seconds. Check the Run console and the container's Python interpreter.": "debugpy did not become ready within 120 seconds. Check the Run console and the container's Python interpreter.",
"Only Python can be debugged in containers": "Only Python can be debugged in containers",
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine.": "Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine.",
"Use Project JDK": "Use Project JDK",
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence."
}
+1 -2
Переглянути файл
@@ -6296,6 +6296,5 @@
"Empty: where the service mounts the project.": "비워 두면 서비스가 프로젝트를 마운트하는 위치를 사용합니다.",
"The project is mounted here; paths and the working directory are mapped into it.": "프로젝트가 여기에 마운트되며, 경로와 작업 디렉터리가 이 위치로 매핑됩니다.",
"Published ports": "게시된 포트",
"Run as the image's user, not the project folder's owner (Linux)": "프로젝트 폴더 소유자가 아닌 이미지의 사용자로 실행(Linux)",
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends; debugging, coverage and profiling stay on this machine.": "Docker 연결은 서비스 도구 창에서 가져옵니다. 실행할 때마다 새 컨테이너가 만들어지고 실행이 끝나면 제거됩니다. 디버깅, 커버리지 및 프로파일링은 이 컴퓨터에서 실행됩니다."
"Run as the image's user, not the project folder's owner (Linux)": "프로젝트 폴더 소유자가 아닌 이미지의 사용자로 실행(Linux)"
}
+5 -1
Переглянути файл
@@ -6470,7 +6470,8 @@
"The project is mounted here; paths and the working directory are mapped into it.": "项目挂载在此处;路径和工作目录会映射到其中。",
"Published ports": "发布的端口",
"Run as the image's user, not the project folder's owner (Linux)": "以镜像的用户运行,而不是项目文件夹的所有者(Linux)",
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends; debugging, coverage and profiling stay on this machine.": "Docker 连接来自“服务”工具窗口。每次运行都会使用一个新容器,运行结束后删除;调试、覆盖率和性能分析仍在本机进行。",
"Docker connections come from the Services tool window. Each run gets a new container, removed when it ends. Python configurations can be debugged there; coverage and profiling stay on this machine.": "Docker 连接来自“服务”工具窗口。每次运行都会使用一个新容器,运行结束后删除。Python 配置可以在容器中调试;覆盖率和性能分析仍在本机进行。",
"Only Python can be debugged in containers": "容器中只能调试 Python",
"Service name, e.g. ORCLPDB1": "服务名,例如 ORCLPDB1",
"Current schema:": "当前模式:",
"HR (ALTER SESSION SET CURRENT_SCHEMA)": "HR(ALTER SESSION SET CURRENT_SCHEMA)",
@@ -6567,6 +6568,9 @@
"Java compiler output must be inside its module: {output}": "Java 编译输出必须位于所属模块目录内:{output}",
"Cannot choose a directory": "无法选择目录",
"Open in HTTP Client": "在 HTTP 客户端中打开",
"Cannot start container debugging": "无法启动容器调试",
"The container exited before debugpy was ready. Check the Run console for errors.": "容器在 debugpy 就绪前已退出。请检查运行控制台中的错误。",
"debugpy did not become ready within 120 seconds. Check the Run console and the container's Python interpreter.": "debugpy 未在 120 秒内就绪。请检查运行控制台和容器的 Python 解释器。",
"Use Project JDK": "使用项目 SDK",
"JDK used for Maven sync, builds, goals and tests. An explicit JAVA_HOME in a run configuration takes precedence.": "用于 Maven 同步、构建、目标和测试的 JDK。运行配置中显式设置的 JAVA_HOME 优先。"
}
Переглянути файл
File renamed without changes.