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

+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;
}
/**