дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
fix(debugger): finish PHP Remote Debug: taken port, waiting state, Port field (#1639)
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 <[email protected]>
Цей коміт міститься в:
1 джерело
1f76e71621
коміт
b450f4028b
14 files changed
+292
-8
No files matched your search
@@ -35,6 +35,7 @@ const {
|
||||
xdebugCommand,
|
||||
remoteAttachConfig,
|
||||
remoteJvmTarget,
|
||||
remoteWaitingNote,
|
||||
} = remote;
|
||||
|
||||
function configOf(type: (typeof REMOTE_TYPES)[number], params: Record<string, string> = {}): RunConfig {
|
||||
@@ -180,6 +181,25 @@ describe("remote attach requests", () => {
|
||||
expect(plain).toEqual({ port: 9003 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ remoteRoot: "/app", localRoot: "web" }, { "/app": "/p/web" }],
|
||||
[{ remoteRoot: "/app", localRoot: "/srv/site" }, { "/app": "/srv/site" }],
|
||||
])("PHP Remote Debug maps the server path %j to the local root", (params, pathMappings) => {
|
||||
const l = remoteAttachConfig(configOf(phpRemoteType, params), "/p")!;
|
||||
expect(JSON.parse(l.extraJson!)).toEqual({ port: 9003, pathMappings });
|
||||
expect(l).toMatchObject({ args: [], env: {} });
|
||||
});
|
||||
|
||||
it("PHP Remote Debug keeps its form when Port, its only value, is cleared", () => {
|
||||
const cleared = withParams(configOf(phpRemoteType), {}, phpRemoteType);
|
||||
expect(missingParams(cleared, phpRemoteType)).toEqual({ port: "Port is required" });
|
||||
});
|
||||
|
||||
it("PHP Remote Debug says which port it waits on; the attaching types don't", () => {
|
||||
expect(remoteWaitingNote(configOf(phpRemoteType, { port: "9010" }))).toBe("Listening for Xdebug on port 9010");
|
||||
for (const t of REMOTE_TYPES.filter((x) => x !== phpRemoteType)) expect(remoteWaitingNote(configOf(t))).toBeNull();
|
||||
});
|
||||
|
||||
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",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { IpcError, type LaunchConfig } from "@/platform/ipc";
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
import { startFailureMessage } from "../startFailure";
|
||||
import { startSession, useDebug } from "../state";
|
||||
|
||||
const ipc = vi.hoisted(() => ({ debugStart: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } };
|
||||
});
|
||||
|
||||
const PORT_IN_USE =
|
||||
"Port 9003 is already in use: another IDE or debug session may be listening on it. Stop it or choose another port.";
|
||||
|
||||
describe("debug start failures", () => {
|
||||
it.each([
|
||||
"launch failed: listen EADDRINUSE: address already in use :::9003",
|
||||
"launch failed: listen EADDRINUSE: address already in use 127.0.0.1:9003",
|
||||
])("names the port another listener holds: %s", (message) => {
|
||||
expect(startFailureMessage(new IpcError({ kind: "invalidInput", message }))).toBe(PORT_IN_USE);
|
||||
});
|
||||
|
||||
it("keeps other failures as they are", () => {
|
||||
expect(startFailureMessage(new Error("launch failed: Attach requests are not supported"))).toBe(
|
||||
"launch failed: Attach requests are not supported",
|
||||
);
|
||||
expect(startFailureMessage("debug adapter exited")).toBe("debug adapter exited");
|
||||
});
|
||||
|
||||
it("are what the notification of a failed start says", async () => {
|
||||
const notify = vi.fn();
|
||||
provideNotificationService({ notify });
|
||||
useDebug.setState({ sessions: {}, active: null, console: {} });
|
||||
const message = "launch failed: listen EADDRINUSE: address already in use :::9003";
|
||||
ipc.debugStart.mockResolvedValueOnce({ status: "error", error: { kind: "invalidInput", message, data: null } });
|
||||
const listen: LaunchConfig = {
|
||||
name: "Listen for Xdebug",
|
||||
adapter: "php-debug",
|
||||
request: "launch",
|
||||
program: null,
|
||||
args: [],
|
||||
cwd: "/p",
|
||||
env: {},
|
||||
extraJson: JSON.stringify({ port: 9003 }),
|
||||
};
|
||||
expect(await startSession(listen)).toBeNull();
|
||||
expect(notify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ title: "Cannot start debugger for Listen for Xdebug", body: PORT_IN_USE }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -28,6 +28,7 @@ vi.mock("@/platform/ipc", async (importOriginal) => {
|
||||
const confirmTerminate = vi.hoisted(() => vi.fn(async () => false));
|
||||
vi.mock("@/workbench/confirmTerminate", () => ({ confirmTerminate }));
|
||||
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { useDebug, type DebugSession } from "../state";
|
||||
import { DebugTabs, DebugToolWindow } from "../ui/DebugToolWindow";
|
||||
|
||||
@@ -63,6 +64,33 @@ describe("debug tool window", () => {
|
||||
expect(hot()).toEqual([true, true]);
|
||||
});
|
||||
|
||||
it("says a PHP Remote Debug session listens for Xdebug while nothing is suspended", () => {
|
||||
const runConfig = {
|
||||
id: "php-remote:web",
|
||||
name: "web",
|
||||
kind: "php-remote",
|
||||
command: "",
|
||||
args: [],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: false,
|
||||
isTest: false,
|
||||
debugAdapter: "php-debug",
|
||||
params: { port: "9010" },
|
||||
} as RunConfig;
|
||||
const waiting = { ...session, adapter: "php-debug", status: "running" as const, threads: [], frames: [] };
|
||||
useDebug.setState({ sessions: { 1: { ...waiting, runConfig } }, active: 1 });
|
||||
const { unmount } = render(<DebugToolWindow />);
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Threads & Variables" }));
|
||||
expect(screen.getByText("Listening for Xdebug on port 9010")).toBeTruthy();
|
||||
expect(screen.queryByText("Running…")).toBeNull();
|
||||
unmount();
|
||||
useDebug.setState({ sessions: { 1: waiting }, active: 1 });
|
||||
render(<DebugToolWindow />);
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Threads & Variables" }));
|
||||
expect(screen.getByText("Running…")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows preparation output, preserves a chosen tab and opens variables on pause", () => {
|
||||
useDebug.setState({ sessions: { 1: { ...session, status: "starting" } }, active: 1 });
|
||||
render(<DebugToolWindow />);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 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";
|
||||
@@ -383,6 +384,12 @@ export function remoteAttachConfig(rc: RunConfig, root: string): LaunchConfig |
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 ?? {};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Why a debug session didn't start, as the user reads it. An adapter that listens for
|
||||
* the debuggee (php-debug waiting for Xdebug) fails with Node's
|
||||
* `listen EADDRINUSE: address already in use :::9003` when another IDE holds the port.
|
||||
*/
|
||||
import { tx } from "@/platform/i18n";
|
||||
|
||||
const PORT_IN_USE = /\bEADDRINUSE\b.*:(\d+)\s*$/;
|
||||
|
||||
export function startFailureMessage(e: unknown): string {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
const port = PORT_IN_USE.exec(message)?.[1];
|
||||
return port
|
||||
? tx`Port ${port} is already in use: another IDE or debug session may be listening on it. Stop it or choose another port.`
|
||||
: message;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { interpolateLog, logParts, suspends } from "./breakpoints/model";
|
||||
import { syncSpecial, useSpecialBreakpoints } from "./breakpoints/special";
|
||||
import { refreshExceptionFilters } from "./breakpoints/exceptions";
|
||||
import { missingAdapter } from "./missingAdapter";
|
||||
import { startFailureMessage } from "./startFailure";
|
||||
|
||||
export type SessionStatus = "starting" | "running" | "paused" | "terminated";
|
||||
|
||||
@@ -607,7 +608,7 @@ export async function startSession(
|
||||
group: "run",
|
||||
level: "error",
|
||||
title: `Cannot start debugger for ${launch.name}`,
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
body: startFailureMessage(e),
|
||||
key: "debug.start",
|
||||
});
|
||||
return null;
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
useDebug,
|
||||
type DebugSession,
|
||||
} from "../state";
|
||||
import { remoteWaitingNote } from "../remote";
|
||||
import { DebugConsole } from "./DebugConsole";
|
||||
import { VariablesView, type VarRoot } from "./VariablesView";
|
||||
import { useExpressionCompletion } from "./ExpressionCompletion";
|
||||
@@ -304,7 +305,11 @@ function FramesPane({ session }: { session: DebugSession }) {
|
||||
onBlur={() => blurDebugView("frames")}
|
||||
>
|
||||
{!paused && (
|
||||
<div className={styles.note}>{session.status === "terminated" ? tx("Finished") : tx("Running…")}</div>
|
||||
<div className={styles.note}>
|
||||
{session.status === "terminated"
|
||||
? tx("Finished")
|
||||
: (remoteWaitingNote(rootOf(session).runConfig) ?? tx("Running…"))}
|
||||
</div>
|
||||
)}
|
||||
{items.map((item) =>
|
||||
item.kind === "hidden" ? (
|
||||
|
||||
Reference in new issue
Заблокувати користувача