fix(services): show the service port for debug sessions like for run (#1789)

A direct debug launch had no Run session to carry the app's HTTP port, so
its Services node only showed Debugging/Starting. The debugger now parses
the same embedded web server startup line as Run, publishes the port on
DebugServiceSession, and the node shows :port as soon as the app announces
it, exactly like a running service (#1718).

Signed-off-by: Inf1mum <[email protected]>
This commit is contained in:
Vadim Lazuk authored and GitHub committed 2026-11-07 02:32:06 +08:00
1 parent d0c3b9ba34
commit b2253a8bc8
6 files changed
+84 -6

No files matched your search

@@ -53,6 +53,19 @@ it("does not republish service metadata when console output changes", () => {
expect(useDebugRunSessions.getState().serviceSessions).toBe(summaries);
});
it("announces the app's web server port once it appears on the console", () => {
adoptSession(1, { runConfig: config });
const before = useDebugRunSessions.getState().serviceSessions;
expect(before).toHaveLength(1);
appendConsole(1, "stdout", "Tomcat started on port 8080 (http) with context path '/'\n");
const withPort = useDebugRunSessions.getState().serviceSessions;
expect(withPort).not.toBe(before);
expect(withPort).toMatchObject([{ id: 1, httpPort: 8080 }]);
// Later output without a new port keeps the announced one without republishing.
appendConsole(1, "stdout", "application output\n");
expect(useDebugRunSessions.getState().serviceSessions).toBe(withPort);
});
it("stops shared-process debugger roots without stopping their process until requested", async () => {
adoptSession(1, { runConfig: config, process: 7 });
await stopSession(1, { stopProcess: false });
+29
View File
@@ -25,6 +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 { breakpointsOf, consumeTemporary, flushBreakpoints, refreshStates, resetStatuses } from "./breakpoints/store";
import { interpolateLog, logParts, suspends } from "./breakpoints/model";
import { syncSpecial, useSpecialBreakpoints } from "./breakpoints/special";
@@ -305,6 +306,7 @@ export function publishContext() {
startedAt: session.startedAt ?? 0,
status: paused ? ("paused" as const) : session.status,
processId: session.process,
httpPort: sessionHttpPort(session.id),
rerun: session.runConfig || session.launch ? () => void rerunSession(session.id) : null,
stop: (options?: { stopProcess?: boolean }) => stopSession(session.id, options),
show: () => {
@@ -351,6 +353,30 @@ export function publishContext() {
// --- console -----------------------------------------------------------------
/**
* The HTTP port the session's application announced on its console ("Tomcat started on
* port 8080"), the same embedded web server line the Run console is parsed for. Kept per
* session, so the Services node keeps the port after the line scrolls out of the buffer.
*/
const announcedPorts = new Map<DebugSessionId, number>();
export function sessionHttpPort(id: DebugSessionId): number | undefined {
let port: number | undefined;
for (const line of useDebug.getState().console[id] ?? []) {
const found = startedPort(line.text);
if (found !== null) port = found;
}
if (port !== undefined) announcedPorts.set(id, port);
else port = announcedPorts.get(id);
return port;
}
/** Republishes Services metadata only when a console announced a port it had not shown yet. */
function republishForPorts() {
const published = useDebugRunSessions.getState().serviceSessions;
if (published.some((d) => d.httpPort !== sessionHttpPort(d.id))) publishContext();
}
export function appendConsole(session: DebugSessionId, category: string, raw: string) {
if (!raw) return;
flushConsoleOutput();
@@ -364,6 +390,7 @@ export function appendConsole(session: DebugSessionId, category: string, raw: st
trimConsole(next);
return { console: { ...s.console, [session]: next } };
});
republishForPorts();
}
/**
@@ -406,6 +433,7 @@ function flushConsoleOutput() {
}
return { console: consoleBySession };
});
republishForPorts();
}
/** Split styled output at real line breaks, retaining partial lines across events. */
@@ -1078,6 +1106,7 @@ export function resetSessions() {
pendingOutput.length = 0;
cancelOutputFlush();
ansiStreams.clear();
announcedPorts.clear();
useDebug.setState({ sessions: {}, active: null, console: {}, stopSeq: 0 });
useExecutionPoint.setState({ path: null, line: 0, top: true, values: {}, variables: {} });
publishContext();
+24
View File
@@ -109,6 +109,30 @@ describe("run configurations in Services", () => {
expect(attached[0]?.children?.[0]?.detail).toBe(":8080");
});
it("shows the app port of a direct debug session like for Run", () => {
const c = config("app", "spring-boot");
const d: DebugServiceSession = {
id: 1,
config: c,
status: "running",
processId: null,
startedAt: 2,
httpPort: 8080,
stop: async () => {},
rerun: null,
show: () => {},
};
expect(runServiceItems([c], [], DEFAULT_TYPES, [], {}, [d])[0]?.children?.[0]).toMatchObject({
detail: ":8080",
icon: "debug",
dim: false,
});
// A paused debugger still wins over the announced port.
expect(
runServiceItems([c], [], DEFAULT_TYPES, [], {}, [{ ...d, status: "paused" }])[0]?.children?.[0]?.detail,
).toBe("Suspended");
});
it("applies display order to detected and saved configurations of every language", () => {
const configs = [
config("java-a", "spring-boot"),
+8 -6
View File
@@ -227,12 +227,14 @@ function executionDetail(s: ServiceSession | undefined): string | undefined {
if (!s) return undefined;
const debug = serviceDebugger(s);
if (debug?.status === "paused") return "Suspended";
if (debug)
return s.run && busy(s.run) && s.run.httpPort
? `:${s.run.httpPort}`
: debug.status === "starting"
? "Starting"
: "Debugging";
if (debug) {
// The app's HTTP port: a Run owns the console for an attached debugger, a direct
// debug launch announces it on the debug session's own console (#1718).
const run = s.run && busy(s.run) ? s.run : undefined;
const port = run?.httpPort ?? debug.httpPort;
if (port) return `:${port}`;
return debug.status === "starting" ? "Starting" : "Debugging";
}
return s.run ? sessionDetail(s.run) : "Finished";
}
@@ -101,6 +101,14 @@ it("shows debug output/state, pins actions, and does not rebuild the tree for st
expect(d.stop).toHaveBeenCalledOnce();
});
it("shows the service port announced by a debug session's application", () => {
adoptSession(1, { runConfig: config });
appendConsole(1, "stdout", "Tomcat started on port 8080 (http) with context path '/'\n");
useDebugRunSessions.setState({ serviceSessions: [summary(1, { httpPort: 8080 })] });
render(<ServicesTreeView />);
expect(within(screen.getByRole("tree")).getByText(":8080")).toBeTruthy();
});
it("keeps the other parallel instance alive and then displays it", async () => {
adoptSession(1);
adoptSession(2);
+2
View File
@@ -1470,6 +1470,8 @@ export interface DebugServiceSession {
startedAt: number;
status: "starting" | "running" | "paused" | "terminated";
processId: number | null;
/** The debuggee's HTTP port announced on its console (not the JDWP attach port), if known. */
httpPort?: number;
rerun: (() => void) | null;
stop: (options?: { stopProcess?: boolean }) => Promise<void>;
show: () => void;