mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 00:07:50 +00: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 <18214958+DanielADK@users.noreply.github.com>
This commit is contained in:
co-authored by
DanielADK
parent
1f76e71621
commit
b450f4028b
@@ -0,0 +1,140 @@
|
||||
//! Drives php-debug (vscode-php-debug) the way a PHP Remote Debug (Xdebug)
|
||||
//! configuration does: a launch without a program makes it listen, and every PHP
|
||||
//! run that Xdebug connects stops at the breakpoint set in the local copy of the
|
||||
//! file, through `pathMappings`, until the session is stopped. A second listener on
|
||||
//! the same port fails to start and names the port.
|
||||
//!
|
||||
//! Ignored by default (needs PHP with Xdebug 3, Node.js and php-debug). Run with
|
||||
//! `PHP_DEBUG_ADAPTER=<php-debug-adapter> cargo test -p ide-dap --test php_debug -- --ignored --nocapture`
|
||||
//! (`PHP_BIN` picks the PHP executable, default `php`).
|
||||
|
||||
use ide_core::event::DebugEvent;
|
||||
use ide_core::ipc_path::to_ipc;
|
||||
use ide_core::{IdeEvent, SharedSink};
|
||||
use ide_dap::*;
|
||||
use ide_language::LanguageRegistry;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const SCRIPT: &str = "<?php\n$greeting = \"hello\";\n$n = 40 + 2;\necho \"$greeting $n\\n\";\n";
|
||||
/// 0-based line of `$n = 40 + 2;`.
|
||||
const BREAK_LINE: u32 = 2;
|
||||
|
||||
async fn wait_for(events: &Mutex<Vec<IdeEvent>>, timeout: Duration, pred: impl Fn(&DebugEvent) -> bool) -> DebugEvent {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if let Some(e) = events.lock().iter().find_map(|e| match e {
|
||||
IdeEvent::Debug { event, .. } if pred(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
}) {
|
||||
return e;
|
||||
}
|
||||
assert!(start.elapsed() < timeout, "timed out; events: {:#?}", events.lock());
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn listen_config(port: u16, served: &Path, local: &Path) -> LaunchConfig {
|
||||
// As remote.ts builds it for Remote root = `served`, Local root = `local`.
|
||||
let mapping = serde_json::Map::from_iter([(to_ipc(served), to_ipc(local).into())]);
|
||||
LaunchConfig {
|
||||
name: "PHP Remote".into(),
|
||||
adapter: "php-debug".into(),
|
||||
request: "launch".into(),
|
||||
program: None,
|
||||
args: vec![],
|
||||
cwd: Some(local.to_path_buf().into()),
|
||||
env: BTreeMap::new(),
|
||||
extra_json: Some(serde_json::json!({ "port": port, "pathMappings": mapping }).to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[ignore]
|
||||
async fn listens_for_xdebug_until_stopped_with_path_mappings() {
|
||||
let Ok(adapter) = std::env::var("PHP_DEBUG_ADAPTER") else {
|
||||
eprintln!("PHP_DEBUG_ADAPTER not set; skipping");
|
||||
return;
|
||||
};
|
||||
let php = std::env::var("PHP_BIN").unwrap_or_else(|_| "php".into());
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// The project here, and the copy PHP runs from (a container's /var/www/html).
|
||||
let (local, served) = (dir.path().join("local"), dir.path().join("served"));
|
||||
for d in [&local, &served] {
|
||||
std::fs::create_dir_all(d).unwrap();
|
||||
std::fs::write(d.join("index.php"), SCRIPT).unwrap();
|
||||
}
|
||||
// Resolved like the project root the UI sends (`/private/var` on macOS, no `\\?\` on Windows).
|
||||
let resolved = |p: &Path| PathBuf::from(to_ipc(&std::fs::canonicalize(p).unwrap()));
|
||||
let (local, served) = (resolved(&local), resolved(&served));
|
||||
let port = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap().local_addr().unwrap().port();
|
||||
|
||||
let manager = DebugManager::new(Arc::new(LanguageRegistry::load(None).unwrap()));
|
||||
let events: Arc<Mutex<Vec<IdeEvent>>> = Arc::default();
|
||||
let sink: SharedSink = {
|
||||
let events = events.clone();
|
||||
Arc::new(move |e: IdeEvent| events.lock().push(e))
|
||||
};
|
||||
let local_script = local.join("index.php");
|
||||
manager
|
||||
.set_breakpoints(
|
||||
local_script.clone(),
|
||||
vec![Breakpoint {
|
||||
line: BREAK_LINE,
|
||||
condition: None,
|
||||
hit_condition: None,
|
||||
log_message: None,
|
||||
enabled: true,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let id = manager
|
||||
.start_with(listen_config(port, &served, &local), SessionOwner::new("main", sink.clone()), |_| {
|
||||
Some(adapter.clone())
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|e| panic!("start: {e:?}"));
|
||||
|
||||
// Requests in a row each stop; the session keeps listening between them.
|
||||
for request in 1..=2 {
|
||||
events.lock().clear();
|
||||
let run = std::process::Command::new(&php)
|
||||
.args([
|
||||
"-dxdebug.mode=debug",
|
||||
"-dxdebug.start_with_request=yes",
|
||||
"-dxdebug.client_host=127.0.0.1",
|
||||
&format!("-dxdebug.client_port={port}"),
|
||||
"index.php",
|
||||
])
|
||||
.current_dir(&served)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let stopped = wait_for(&events, Duration::from_secs(30), |e| matches!(e, DebugEvent::Stopped { .. })).await;
|
||||
let DebugEvent::Stopped { thread_id, .. } = stopped else { unreachable!() };
|
||||
let frames = manager.stack_trace(id, thread_id).await.unwrap();
|
||||
assert_eq!(frames[0].line, BREAK_LINE, "request {request}");
|
||||
let top = frames[0].path.as_deref().and_then(|p| p.canonicalize().ok());
|
||||
assert_eq!(top, local_script.canonicalize().ok(), "request {request}");
|
||||
manager.resume(id, thread_id).await.unwrap();
|
||||
wait_for(&events, Duration::from_secs(30), |e| matches!(e, DebugEvent::Thread { started: false, .. })).await;
|
||||
let out = run.wait_with_output().unwrap();
|
||||
assert_eq!(String::from_utf8_lossy(&out.stdout), "hello 42\n", "request {request}");
|
||||
assert!(manager.sessions(None).iter().any(|s| s.id == id), "still listening after request {request}");
|
||||
}
|
||||
|
||||
// Another listener on the port (another IDE): the start fails and says why.
|
||||
let err = manager
|
||||
.start_with(listen_config(port, &served, &local), SessionOwner::new("main", sink), |_| Some(adapter.clone()))
|
||||
.await
|
||||
.expect_err("a second listener on the same port");
|
||||
assert!(err.message.contains("EADDRINUSE") && err.message.ends_with(&format!(":{port}")), "{err:?}");
|
||||
assert_eq!(manager.sessions(None).len(), 1);
|
||||
|
||||
manager.stop(id).await.unwrap();
|
||||
assert!(manager.sessions(None).is_empty());
|
||||
}
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -49,8 +49,8 @@ import { TomcatSettings } from "./TomcatSettings";
|
||||
import styles from "./Run.module.css";
|
||||
|
||||
/**
|
||||
* Debug adapters with a real launch template (`debugger/launch.ts`, `debugger/java.ts`,
|
||||
* `debugger/remote.ts`), verified against small programs. Others (dart, …) only got
|
||||
* Debug adapters with a real launch template (`debugger/launch.ts`, `debugger/java.ts`),
|
||||
* verified against small programs. Others (dart, php-debug, …) only got
|
||||
* generic launch arguments and are left out until they have one; a configuration that
|
||||
* already names one keeps it.
|
||||
*/
|
||||
@@ -63,7 +63,6 @@ const ADAPTERS: ReadonlyArray<{ id: string; label: string }> = [
|
||||
{ id: "delve", label: "Delve (Go)" },
|
||||
{ id: "codelldb", label: "CodeLLDB (Rust, C, C++)" },
|
||||
{ id: "lldb-dap", label: "lldb-dap (C, C++, Swift)" },
|
||||
{ id: "php-debug", label: "php-debug (PHP)" },
|
||||
];
|
||||
|
||||
/** Options "Modify options" adds to the form: value fields (shown once added) and flags (chips). */
|
||||
|
||||
@@ -676,7 +676,12 @@ describe("debug-only types (Remote JVM Debug)", () => {
|
||||
debugOnly: true,
|
||||
debugAdapter: "java-debug",
|
||||
fields: [
|
||||
{ key: "port", label: "Port", validate: (v) => (/^\d+$/.test(v) ? null : "Port must be a number") },
|
||||
{
|
||||
key: "port",
|
||||
label: "Port",
|
||||
required: true,
|
||||
validate: (v) => (/^\d+$/.test(v) ? null : "Port must be a number"),
|
||||
},
|
||||
{ key: "agent", label: "Agent", control: "output", value: (p) => `address=*:${p.port ?? ""}` },
|
||||
],
|
||||
commandLine: () => null,
|
||||
@@ -703,6 +708,10 @@ describe("debug-only types (Remote JVM Debug)", () => {
|
||||
expect(screen.queryByText("Command line")).toBeNull();
|
||||
fireEvent.change(screen.getByLabelText("Port"), { target: { value: "x" } });
|
||||
expect(screen.getByRole("alert").textContent).toBe("Port must be a number");
|
||||
// Emptying the only field keeps the form: there is no command line to edit instead.
|
||||
fireEvent.change(screen.getByLabelText("Port"), { target: { value: "" } });
|
||||
expect(screen.getByRole("alert").textContent).toBe("Port is required");
|
||||
expect(screen.queryByLabelText("Command")).toBeNull();
|
||||
});
|
||||
|
||||
it("are valid without a command, get the type's adapter and are not run", async () => {
|
||||
|
||||
@@ -473,6 +473,8 @@ export function formParams(config: RunConfig, type = typeFor(config.kind)): Reco
|
||||
if (config.params && Object.keys(config.params).length && (type.id !== "go" || config.params.goKind))
|
||||
return { ...config.params };
|
||||
if (type.id === COMPOUND) return {};
|
||||
// No command line to fall back to: the form stays, and an emptied field shows as required.
|
||||
if (type.debugOnly) return { ...config.params };
|
||||
const derived = type.paramsFrom?.(config);
|
||||
if (!derived) return null;
|
||||
// Only when the form reproduces exactly what runs today.
|
||||
|
||||
@@ -33,5 +33,8 @@
|
||||
"{removed} lines deleted": "{removed} lines deleted",
|
||||
"PHP Remote Debug (Xdebug)": "PHP Remote Debug (Xdebug)",
|
||||
"Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)": "Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)",
|
||||
"For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container": "For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container"
|
||||
"For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container": "For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container",
|
||||
"e.g. /var/www/html": "e.g. /var/www/html",
|
||||
"Listening for Xdebug on port {port}": "Listening for Xdebug on port {port}",
|
||||
"Port {port} is already in use: another IDE or debug session may be listening on it. Stop it or choose another port.": "Port {port} is already in use: another IDE or debug session may be listening on it. Stop it or choose another port."
|
||||
}
|
||||
|
||||
@@ -3656,6 +3656,9 @@
|
||||
"PHP Remote Debug (Xdebug)": "PHP 远程调试 (Xdebug)",
|
||||
"Listen for Xdebug connections: debug PHP requests served elsewhere (php-fpm, a container)": "监听 Xdebug 连接:调试在其他地方(php-fpm、容器中)提供服务的 PHP 请求",
|
||||
"For web requests: the XDEBUG_SESSION cookie or query parameter; point xdebug.client_host at this machine when PHP runs in a container": "对于 Web 请求:设置 XDEBUG_SESSION cookie 或查询参数;当 PHP 运行在容器中时,将 xdebug.client_host 指向本机",
|
||||
"e.g. /var/www/html": "例如 /var/www/html",
|
||||
"Listening for Xdebug on port {port}": "正在端口 {port} 上监听 Xdebug",
|
||||
"Port {port} is already in use: another IDE or debug session may be listening on it. Stop it or choose another port.": "端口 {port} 已被占用:可能有其他 IDE 或调试会话正在监听。请停止它或选择其他端口。",
|
||||
"Copied": "已复制",
|
||||
"'{name}' can only be debugged": "'{name}' 只能调试",
|
||||
"It attaches the debugger to a process started elsewhere.": "它将调试器附加到在别处启动的进程。",
|
||||
|
||||
@@ -86,7 +86,6 @@ const BUILT_AT_RUNTIME = new Set<string>([]);
|
||||
const UNTRANSLATED = new Set<string>([
|
||||
"dotnet test",
|
||||
"netcoredbg (.NET)",
|
||||
"php-debug (PHP)",
|
||||
// Product names
|
||||
"Cargo",
|
||||
"CMake",
|
||||
|
||||
Reference in New Issue
Block a user