[Lldb-commits] [lldb] [lldb-dap] Allow providing debug adapter arguments in the extension (PR #129262)
Matthew Bastien via lldb-commits
lldb-commits at lists.llvm.org
Thu Mar 13 13:56:04 PDT 2025
https://github.com/matthewbastien updated https://github.com/llvm/llvm-project/pull/129262
>From f687fdfeda083587bd8f2a874d34c0f9dba1e31a Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 28 Feb 2025 11:08:25 -0500
Subject: [PATCH 1/8] allow providing debug adapter arguments
---
lldb/tools/lldb-dap/package.json | 29 +++++--
.../lldb-dap/src-ts/debug-adapter-factory.ts | 78 ++++++++++++-------
2 files changed, 76 insertions(+), 31 deletions(-)
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cd450a614b3f7..a0c8847d72535 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -75,6 +75,15 @@
"type": "string",
"description": "The path to the lldb-dap binary."
},
+ "lldb-dap.arguments": {
+ "scope": "resource",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ },
+ "description": "The arguments provided to the lldb-dap process."
+ },
"lldb-dap.log-path": {
"scope": "resource",
"type": "string",
@@ -148,10 +157,6 @@
{
"type": "lldb-dap",
"label": "LLDB DAP Debugger",
- "program": "./bin/lldb-dap",
- "windows": {
- "program": "./bin/lldb-dap.exe"
- },
"configurationAttributes": {
"launch": {
"required": [
@@ -162,6 +167,13 @@
"type": "string",
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
},
+ "debugAdapterArgs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+ },
"program": {
"type": "string",
"description": "Path to the program to debug."
@@ -352,6 +364,13 @@
"type": "string",
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
},
+ "debugAdapterArgs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+ },
"program": {
"type": "string",
"description": "Path to the program to attach to."
@@ -549,4 +568,4 @@
}
]
}
-}
\ No newline at end of file
+}
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index c2244dcbde8f2..85c34b773e047 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -25,7 +25,7 @@ async function findWithXcrun(executable: string): Promise<string | undefined> {
if (stdout) {
return stdout.toString().trimEnd();
}
- } catch (error) { }
+ } catch (error) {}
}
return undefined;
}
@@ -93,8 +93,23 @@ async function getDAPExecutable(
return undefined;
}
+function getDAPArguments(session: vscode.DebugSession): string[] {
+ // Check the debug configuration for arguments first
+ const debugConfigArgs = session.configuration.debugAdapterArgs;
+ if (
+ Array.isArray(debugConfigArgs) &&
+ debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
+ ) {
+ return debugConfigArgs;
+ }
+ // Fall back on the workspace configuration
+ return vscode.workspace
+ .getConfiguration("lldb-dap")
+ .get<string[]>("arguments", []);
+}
+
async function isServerModeSupported(exe: string): Promise<boolean> {
- const { stdout } = await exec(exe, ['--help']);
+ const { stdout } = await exec(exe, ["--help"]);
return /--connection/.test(stdout);
}
@@ -103,8 +118,13 @@ async function isServerModeSupported(exe: string): Promise<boolean> {
* depending on the session configuration.
*/
export class LLDBDapDescriptorFactory
- implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable {
- private server?: Promise<{ process: child_process.ChildProcess, host: string, port: number }>;
+ implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable
+{
+ private server?: Promise<{
+ process: child_process.ChildProcess;
+ host: string;
+ port: number;
+ }>;
dispose() {
this.server?.then(({ process }) => {
@@ -114,7 +134,7 @@ export class LLDBDapDescriptorFactory
async createDebugAdapterDescriptor(
session: vscode.DebugSession,
- executable: vscode.DebugAdapterExecutable | undefined,
+ _executable: vscode.DebugAdapterExecutable | undefined,
): Promise<vscode.DebugAdapterDescriptor | undefined> {
const config = vscode.workspace.getConfiguration(
"lldb-dap",
@@ -128,7 +148,7 @@ export class LLDBDapDescriptorFactory
}
const configEnvironment =
config.get<{ [key: string]: string }>("environment") || {};
- const dapPath = (await getDAPExecutable(session)) ?? executable?.command;
+ const dapPath = await getDAPExecutable(session);
if (!dapPath) {
LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
@@ -142,32 +162,38 @@ export class LLDBDapDescriptorFactory
const dbgOptions = {
env: {
- ...executable?.options?.env,
...configEnvironment,
...env,
},
};
- const dbgArgs = executable?.args ?? [];
-
- const serverMode = config.get<boolean>('serverMode', false);
- if (serverMode && await isServerModeSupported(dapPath)) {
- const { host, port } = await this.startServer(dapPath, dbgArgs, dbgOptions);
+ const dbgArgs = getDAPArguments(session);
+
+ const serverMode = config.get<boolean>("serverMode", false);
+ if (serverMode && (await isServerModeSupported(dapPath))) {
+ const { host, port } = await this.startServer(
+ dapPath,
+ dbgArgs,
+ dbgOptions,
+ );
return new vscode.DebugAdapterServer(port, host);
}
return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
}
- startServer(dapPath: string, args: string[], options: child_process.CommonSpawnOptions): Promise<{ host: string, port: number }> {
- if (this.server) return this.server;
+ startServer(
+ dapPath: string,
+ args: string[],
+ options: child_process.CommonSpawnOptions,
+ ): Promise<{ host: string; port: number }> {
+ if (this.server) {
+ return this.server;
+ }
- this.server = new Promise(resolve => {
- args.push(
- '--connection',
- 'connect://localhost:0'
- );
+ this.server = new Promise((resolve) => {
+ args.push("--connection", "connect://localhost:0");
const server = child_process.spawn(dapPath, args, options);
- server.stdout!.setEncoding('utf8').once('data', (data: string) => {
+ server.stdout!.setEncoding("utf8").once("data", (data: string) => {
const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
if (connection) {
const host = connection[1];
@@ -175,9 +201,9 @@ export class LLDBDapDescriptorFactory
resolve({ process: server, host, port });
}
});
- server.on('exit', () => {
+ server.on("exit", () => {
this.server = undefined;
- })
+ });
});
return this.server;
}
@@ -185,11 +211,11 @@ export class LLDBDapDescriptorFactory
/**
* Shows a message box when the debug adapter's path is not found
*/
- static async showLLDBDapNotFoundMessage(path?: string) {
+ static async showLLDBDapNotFoundMessage(path?: string | undefined) {
const message =
- path
- ? `Debug adapter path: ${path} is not a valid file.`
- : "Unable to find the path to the LLDB debug adapter executable.";
+ path !== undefined
+ ? `Debug adapter path: ${path} is not a valid file`
+ : "Unable to find the LLDB debug adapter executable.";
const openSettingsAction = "Open Settings";
const callbackValue = await vscode.window.showErrorMessage(
message,
>From 8d422880164bd1861642d1f09f149c0a1dd1fabb Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 28 Feb 2025 11:22:41 -0500
Subject: [PATCH 2/8] update wording
---
lldb/tools/lldb-dap/package.json | 6 +++---
lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index a0c8847d72535..cd96483691d8a 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -82,7 +82,7 @@
"items": {
"type": "string"
},
- "description": "The arguments provided to the lldb-dap process."
+ "description": "The list of additional arguments used to launch the debug adapter executable."
},
"lldb-dap.log-path": {
"scope": "resource",
@@ -172,7 +172,7 @@
"items": {
"type": "string"
},
- "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+ "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
},
"program": {
"type": "string",
@@ -369,7 +369,7 @@
"items": {
"type": "string"
},
- "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+ "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
},
"program": {
"type": "string",
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 85c34b773e047..e36fde9b61473 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -215,7 +215,7 @@ export class LLDBDapDescriptorFactory
const message =
path !== undefined
? `Debug adapter path: ${path} is not a valid file`
- : "Unable to find the LLDB debug adapter executable.";
+ : "Unable to find the path to the LLDB debug adapter executable.";
const openSettingsAction = "Open Settings";
const callbackValue = await vscode.window.showErrorMessage(
message,
>From 8b42ee72e28aa01a283c517b9d7c30ba2281aed8 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 17:45:19 -0500
Subject: [PATCH 3/8] prompt the user to restart the server if the executable
or arguments change
---
lldb/tools/lldb-dap/package.json | 16 ++
.../lldb-dap/src-ts/debug-adapter-factory.ts | 208 ++++++++----------
.../src-ts/debug-configuration-provider.ts | 106 +++++++++
lldb/tools/lldb-dap/src-ts/extension.ts | 38 ++--
lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts | 130 +++++++++++
5 files changed, 362 insertions(+), 136 deletions(-)
create mode 100644 lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cd96483691d8a..99d95ec72093e 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -163,6 +163,14 @@
"program"
],
"properties": {
+ "debugAdapterHostname": {
+ "type": "string",
+ "markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
+ },
+ "debugAdapterPort": {
+ "type": "number",
+ "markdownDescription": "The port that an existing lldb-dap executable is listening on."
+ },
"debugAdapterExecutable": {
"type": "string",
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
@@ -360,6 +368,14 @@
},
"attach": {
"properties": {
+ "debugAdapterHostname": {
+ "type": "string",
+ "markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
+ },
+ "debugAdapterPort": {
+ "type": "number",
+ "markdownDescription": "The port that an existing lldb-dap executable is listening on."
+ },
"debugAdapterExecutable": {
"type": "string",
"markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index e36fde9b61473..61c4b95efb8a7 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -6,7 +6,7 @@ import * as fs from "node:fs/promises";
const exec = util.promisify(child_process.execFile);
-export async function isExecutable(path: string): Promise<Boolean> {
+async function isExecutable(path: string): Promise<Boolean> {
try {
await fs.access(path, fs.constants.X_OK);
} catch {
@@ -66,19 +66,17 @@ async function findDAPExecutable(): Promise<string | undefined> {
}
async function getDAPExecutable(
- session: vscode.DebugSession,
+ folder: vscode.WorkspaceFolder | undefined,
+ configuration: vscode.DebugConfiguration,
): Promise<string | undefined> {
// Check if the executable was provided in the launch configuration.
- const launchConfigPath = session.configuration["debugAdapterExecutable"];
+ const launchConfigPath = configuration["debugAdapterExecutable"];
if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {
return launchConfigPath;
}
// Check if the executable was provided in the extension's configuration.
- const config = vscode.workspace.getConfiguration(
- "lldb-dap",
- session.workspaceFolder,
- );
+ const config = vscode.workspace.getConfiguration("lldb-dap", folder);
const configPath = config.get<string>("executable-path");
if (configPath && configPath.length !== 0) {
return configPath;
@@ -93,9 +91,12 @@ async function getDAPExecutable(
return undefined;
}
-function getDAPArguments(session: vscode.DebugSession): string[] {
+function getDAPArguments(
+ folder: vscode.WorkspaceFolder | undefined,
+ configuration: vscode.DebugConfiguration,
+): string[] {
// Check the debug configuration for arguments first
- const debugConfigArgs = session.configuration.debugAdapterArgs;
+ const debugConfigArgs = configuration.debugAdapterArgs;
if (
Array.isArray(debugConfigArgs) &&
debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
@@ -104,129 +105,110 @@ function getDAPArguments(session: vscode.DebugSession): string[] {
}
// Fall back on the workspace configuration
return vscode.workspace
- .getConfiguration("lldb-dap")
+ .getConfiguration("lldb-dap", folder)
.get<string[]>("arguments", []);
}
-async function isServerModeSupported(exe: string): Promise<boolean> {
- const { stdout } = await exec(exe, ["--help"]);
- return /--connection/.test(stdout);
+/**
+ * Shows a modal when the debug adapter's path is not found
+ */
+async function showLLDBDapNotFoundMessage(path?: string) {
+ const message =
+ path !== undefined
+ ? `Debug adapter path: ${path} is not a valid file`
+ : "Unable to find the path to the LLDB debug adapter executable.";
+ const openSettingsAction = "Open Settings";
+ const callbackValue = await vscode.window.showErrorMessage(
+ message,
+ { modal: true },
+ openSettingsAction,
+ );
+
+ if (openSettingsAction === callbackValue) {
+ vscode.commands.executeCommand(
+ "workbench.action.openSettings",
+ "lldb-dap.executable-path",
+ );
+ }
}
/**
- * This class defines a factory used to find the lldb-dap binary to use
- * depending on the session configuration.
+ * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
+ * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
+ *
+ * @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration}
+ * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
+ * @returns
*/
-export class LLDBDapDescriptorFactory
- implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable
-{
- private server?: Promise<{
- process: child_process.ChildProcess;
- host: string;
- port: number;
- }>;
-
- dispose() {
- this.server?.then(({ process }) => {
- process.kill();
- });
+export async function createDebugAdapterExecutable(
+ folder: vscode.WorkspaceFolder | undefined,
+ configuration: vscode.DebugConfiguration,
+ userInteractive?: boolean,
+): Promise<vscode.DebugAdapterExecutable | undefined> {
+ const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ const log_path = config.get<string>("log-path");
+ let env: { [key: string]: string } = {};
+ if (log_path) {
+ env["LLDBDAP_LOG"] = log_path;
}
+ const configEnvironment =
+ config.get<{ [key: string]: string }>("environment") || {};
+ const dapPath = await getDAPExecutable(folder, configuration);
- async createDebugAdapterDescriptor(
- session: vscode.DebugSession,
- _executable: vscode.DebugAdapterExecutable | undefined,
- ): Promise<vscode.DebugAdapterDescriptor | undefined> {
- const config = vscode.workspace.getConfiguration(
- "lldb-dap",
- session.workspaceFolder,
- );
-
- const log_path = config.get<string>("log-path");
- let env: { [key: string]: string } = {};
- if (log_path) {
- env["LLDBDAP_LOG"] = log_path;
+ if (!dapPath) {
+ if (userInteractive) {
+ showLLDBDapNotFoundMessage();
}
- const configEnvironment =
- config.get<{ [key: string]: string }>("environment") || {};
- const dapPath = await getDAPExecutable(session);
+ return undefined;
+ }
- if (!dapPath) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
- return undefined;
+ if (!(await isExecutable(dapPath))) {
+ if (userInteractive) {
+ showLLDBDapNotFoundMessage(dapPath);
}
+ return undefined;
+ }
- if (!(await isExecutable(dapPath))) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
- return;
- }
+ const dbgOptions = {
+ env: {
+ ...configEnvironment,
+ ...env,
+ },
+ };
+ const dbgArgs = getDAPArguments(folder, configuration);
+
+ return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
+}
- const dbgOptions = {
- env: {
- ...configEnvironment,
- ...env,
- },
- };
- const dbgArgs = getDAPArguments(session);
-
- const serverMode = config.get<boolean>("serverMode", false);
- if (serverMode && (await isServerModeSupported(dapPath))) {
- const { host, port } = await this.startServer(
- dapPath,
- dbgArgs,
- dbgOptions,
+/**
+ * This class defines a factory used to find the lldb-dap binary to use
+ * depending on the session configuration.
+ */
+export class LLDBDapDescriptorFactory
+ implements vscode.DebugAdapterDescriptorFactory
+{
+ async createDebugAdapterDescriptor(
+ session: vscode.DebugSession,
+ executable: vscode.DebugAdapterExecutable | undefined,
+ ): Promise<vscode.DebugAdapterDescriptor | undefined> {
+ if (executable) {
+ throw new Error(
+ "Setting the debug adapter executable in the package.json is not supported.",
);
- return new vscode.DebugAdapterServer(port, host);
}
- return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
- }
-
- startServer(
- dapPath: string,
- args: string[],
- options: child_process.CommonSpawnOptions,
- ): Promise<{ host: string; port: number }> {
- if (this.server) {
- return this.server;
+ // Use a server connection if the debugAdapterPort is provided
+ if (session.configuration.debugAdapterPort) {
+ return new vscode.DebugAdapterServer(
+ session.configuration.debugAdapterPort,
+ session.configuration.debugAdapterHost,
+ );
}
- this.server = new Promise((resolve) => {
- args.push("--connection", "connect://localhost:0");
- const server = child_process.spawn(dapPath, args, options);
- server.stdout!.setEncoding("utf8").once("data", (data: string) => {
- const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
- if (connection) {
- const host = connection[1];
- const port = Number(connection[2]);
- resolve({ process: server, host, port });
- }
- });
- server.on("exit", () => {
- this.server = undefined;
- });
- });
- return this.server;
- }
-
- /**
- * Shows a message box when the debug adapter's path is not found
- */
- static async showLLDBDapNotFoundMessage(path?: string | undefined) {
- const message =
- path !== undefined
- ? `Debug adapter path: ${path} is not a valid file`
- : "Unable to find the path to the LLDB debug adapter executable.";
- const openSettingsAction = "Open Settings";
- const callbackValue = await vscode.window.showErrorMessage(
- message,
- openSettingsAction,
+ return createDebugAdapterExecutable(
+ session.workspaceFolder,
+ session.configuration,
);
-
- if (openSettingsAction === callbackValue) {
- vscode.commands.executeCommand(
- "workbench.action.openSettings",
- "lldb-dap.executable-path",
- );
- }
}
}
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
new file mode 100644
index 0000000000000..aa8c1af6d1e00
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -0,0 +1,106 @@
+import * as vscode from "vscode";
+import * as child_process from "child_process";
+import * as util from "util";
+import { LLDBDapServer } from "./lldb-dap-server";
+import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+
+const exec = util.promisify(child_process.execFile);
+
+/**
+ * Shows an error message to the user that optionally allows them to open their
+ * launch.json to configure it.
+ *
+ * @param message The error message to display to the user
+ * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
+ */
+async function showErrorWithConfigureButton(
+ message: string,
+): Promise<null | undefined> {
+ const userSelection = await vscode.window.showErrorMessage(
+ message,
+ { modal: true },
+ "Configure",
+ );
+
+ if (userSelection === "Configure") {
+ return null; // Stops the debug session and opens the launch.json for editing
+ }
+
+ return undefined; // Only stops the debug session
+}
+
+/**
+ * Determines whether or not the given lldb-dap executable supports executing
+ * in server mode.
+ *
+ * @param exe the path to the lldb-dap executable
+ * @returns a boolean indicating whether or not lldb-dap supports server mode
+ */
+async function isServerModeSupported(exe: string): Promise<boolean> {
+ const { stdout } = await exec(exe, ["--help"]);
+ return /--connection/.test(stdout);
+}
+
+export class LLDBDapConfigurationProvider
+ implements vscode.DebugConfigurationProvider
+{
+ constructor(private readonly server: LLDBDapServer) {}
+
+ async resolveDebugConfiguration(
+ folder: vscode.WorkspaceFolder | undefined,
+ debugConfiguration: vscode.DebugConfiguration,
+ _token?: vscode.CancellationToken,
+ ): Promise<vscode.DebugConfiguration | null | undefined> {
+ if (
+ "debugAdapterHost" in debugConfiguration &&
+ !("debugAdapterPort" in debugConfiguration)
+ ) {
+ return showErrorWithConfigureButton(
+ "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
+ );
+ }
+
+ if (
+ "debugAdapterPort" in debugConfiguration &&
+ ("debugAdapterExecutable" in debugConfiguration ||
+ "debugAdapterArgs" in debugConfiguration)
+ ) {
+ return showErrorWithConfigureButton(
+ "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+ );
+ }
+
+ // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+ // will show an unhelpful error if it returns undefined. We'd rather show a
+ // nicer error message here and allow stopping the debug session gracefully.
+ const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ if (config.get<boolean>("serverMode", false)) {
+ const executable = await createDebugAdapterExecutable(
+ folder,
+ debugConfiguration,
+ /* userInteractive */ true,
+ );
+ if (!executable) {
+ return undefined;
+ }
+ if (await isServerModeSupported(executable.command)) {
+ const serverInfo = await this.server.start(
+ executable.command,
+ executable.args,
+ executable.options,
+ );
+ if (!serverInfo) {
+ return undefined;
+ }
+ // Use a debug adapter host and port combination rather than an executable
+ // and list of arguments.
+ delete debugConfiguration.debugAdapterExecutable;
+ delete debugConfiguration.debugAdapterArgs;
+ debugConfiguration.debugAdapterHost = serverInfo.host;
+ debugConfiguration.debugAdapterPort = serverInfo.port;
+ }
+ }
+
+ return debugConfiguration;
+ }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index a07bcdebcb68b..e29e2bee2f1fa 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,10 +1,9 @@
import * as vscode from "vscode";
-import {
- LLDBDapDescriptorFactory,
- isExecutable,
-} from "./debug-adapter-factory";
+import { LLDBDapDescriptorFactory } from "./debug-adapter-factory";
import { DisposableContext } from "./disposable-context";
+import { LLDBDapConfigurationProvider } from "./debug-configuration-provider";
+import { LLDBDapServer } from "./lldb-dap-server";
/**
* This class represents the extension and manages its life cycle. Other extensions
@@ -13,29 +12,22 @@ import { DisposableContext } from "./disposable-context";
export class LLDBDapExtension extends DisposableContext {
constructor() {
super();
- const factory = new LLDBDapDescriptorFactory();
- this.pushSubscription(factory);
+
+ const lldbDapServer = new LLDBDapServer();
+ this.pushSubscription(lldbDapServer);
+
this.pushSubscription(
- vscode.debug.registerDebugAdapterDescriptorFactory(
+ vscode.debug.registerDebugConfigurationProvider(
"lldb-dap",
- factory,
- )
+ new LLDBDapConfigurationProvider(lldbDapServer),
+ ),
);
- this.pushSubscription(
- vscode.workspace.onDidChangeConfiguration(async (event) => {
- if (event.affectsConfiguration("lldb-dap.executable-path")) {
- const dapPath = vscode.workspace
- .getConfiguration("lldb-dap")
- .get<string>("executable-path");
- if (dapPath) {
- if (await isExecutable(dapPath)) {
- return;
- }
- }
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath || "");
- }
- }),
+ this.pushSubscription(
+ vscode.debug.registerDebugAdapterDescriptorFactory(
+ "lldb-dap",
+ new LLDBDapDescriptorFactory(),
+ ),
);
}
}
diff --git a/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
new file mode 100644
index 0000000000000..2241a8676e46f
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
@@ -0,0 +1,130 @@
+import * as child_process from "node:child_process";
+import * as vscode from "vscode";
+
+function areArraysEqual<T>(lhs: T[], rhs: T[]): boolean {
+ if (lhs.length !== rhs.length) {
+ return false;
+ }
+ for (let i = 0; i < lhs.length; i++) {
+ if (lhs[i] !== rhs[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Represents a running lldb-dap process that is accepting connections (i.e. in "server mode").
+ *
+ * Handles startup of the process if it isn't running already as well as prompting the user
+ * to restart when arguments have changed.
+ */
+export class LLDBDapServer implements vscode.Disposable {
+ private serverProcess?: child_process.ChildProcessWithoutNullStreams;
+ private serverInfo?: Promise<{ host: string; port: number }>;
+
+ /**
+ * Starts the server with the provided options. The server will be restarted or reused as
+ * necessary.
+ *
+ * @param dapPath the path to the debug adapter executable
+ * @param args the list of arguments to provide to the debug adapter
+ * @param options the options to provide to the debug adapter process
+ * @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server.
+ */
+ async start(
+ dapPath: string,
+ args: string[],
+ options?: child_process.SpawnOptionsWithoutStdio,
+ ): Promise<{ host: string; port: number } | undefined> {
+ const dapArgs = [...args, "--connection", "connect://localhost:0"];
+ if (!(await this.shouldContinueStartup(dapPath, dapArgs))) {
+ return undefined;
+ }
+
+ if (this.serverInfo) {
+ return this.serverInfo;
+ }
+
+ this.serverInfo = new Promise((resolve, reject) => {
+ const process = child_process.spawn(dapPath, dapArgs, options);
+ process.on("error", (error) => {
+ reject(error);
+ this.serverProcess = undefined;
+ this.serverInfo = undefined;
+ });
+ process.on("exit", (code, signal) => {
+ let errorMessage = "Server process exited early";
+ if (code !== undefined) {
+ errorMessage += ` with code ${code}`;
+ } else if (signal !== undefined) {
+ errorMessage += ` due to signal ${signal}`;
+ }
+ reject(new Error(errorMessage));
+ this.serverProcess = undefined;
+ this.serverInfo = undefined;
+ });
+ process.stdout.setEncoding("utf8").on("data", (data) => {
+ const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(
+ data.toString(),
+ );
+ if (connection) {
+ const host = connection[1];
+ const port = Number(connection[2]);
+ resolve({ host, port });
+ process.stdout.removeAllListeners();
+ }
+ });
+ this.serverProcess = process;
+ });
+ return this.serverInfo;
+ }
+
+ /**
+ * Checks to see if the server needs to be restarted. If so, it will prompt the user
+ * to ask if they wish to restart.
+ *
+ * @param dapPath the path to the debug adapter
+ * @param args the arguments for the debug adapter
+ * @returns whether or not startup should continue depending on user input
+ */
+ private async shouldContinueStartup(
+ dapPath: string,
+ args: string[],
+ ): Promise<boolean> {
+ if (!this.serverProcess || !this.serverInfo) {
+ return true;
+ }
+
+ if (areArraysEqual(this.serverProcess.spawnargs, [dapPath, ...args])) {
+ return true;
+ }
+
+ const userInput = await vscode.window.showInformationMessage(
+ "A server mode instance of lldb-dap is already running, but the arguments are different from what is requested in your debug configuration or settings. Would you like to restart the server?",
+ { modal: true },
+ "Restart",
+ "Use Existing",
+ );
+ switch (userInput) {
+ case "Restart":
+ this.serverProcess.kill();
+ this.serverProcess = undefined;
+ this.serverInfo = undefined;
+ return true;
+ case "Use Existing":
+ return true;
+ case undefined:
+ return false;
+ }
+ }
+
+ dispose() {
+ if (!this.serverProcess) {
+ return;
+ }
+ this.serverProcess.kill();
+ this.serverProcess = undefined;
+ this.serverInfo = undefined;
+ }
+}
>From 8e78be264d1e2514cfa30bd2875bf1d2a7c75264 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:00:03 -0500
Subject: [PATCH 4/8] add more checks and error messages
---
.../lldb-dap/src-ts/debug-adapter-factory.ts | 51 +++++++++----------
.../src-ts/debug-configuration-provider.ts | 24 +--------
.../lldb-dap/src-ts/ui/error-messages.ts | 49 ++++++++++++++++++
3 files changed, 75 insertions(+), 49 deletions(-)
create mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 61c4b95efb8a7..11a1cb776b0a3 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -3,6 +3,10 @@ import * as util from "util";
import * as vscode from "vscode";
import * as child_process from "child_process";
import * as fs from "node:fs/promises";
+import {
+ showErrorWithConfigureButton,
+ showLLDBDapNotFoundMessage,
+} from "./ui/error-messages";
const exec = util.promisify(child_process.execFile);
@@ -91,12 +95,27 @@ async function getDAPExecutable(
return undefined;
}
-function getDAPArguments(
+async function getDAPArguments(
folder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
-): string[] {
+ userInteractive?: boolean,
+): Promise<string[] | null | undefined> {
// Check the debug configuration for arguments first
const debugConfigArgs = configuration.debugAdapterArgs;
+ if (debugConfigArgs) {
+ if (
+ !Array.isArray(debugConfigArgs) ||
+ debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1
+ ) {
+ if (!userInteractive) {
+ return undefined;
+ }
+ return showErrorWithConfigureButton(
+ "The debugAdapterArgs property must be an array of string values.",
+ );
+ }
+ return debugConfigArgs;
+ }
if (
Array.isArray(debugConfigArgs) &&
debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
@@ -109,29 +128,6 @@ function getDAPArguments(
.get<string[]>("arguments", []);
}
-/**
- * Shows a modal when the debug adapter's path is not found
- */
-async function showLLDBDapNotFoundMessage(path?: string) {
- const message =
- path !== undefined
- ? `Debug adapter path: ${path} is not a valid file`
- : "Unable to find the path to the LLDB debug adapter executable.";
- const openSettingsAction = "Open Settings";
- const callbackValue = await vscode.window.showErrorMessage(
- message,
- { modal: true },
- openSettingsAction,
- );
-
- if (openSettingsAction === callbackValue) {
- vscode.commands.executeCommand(
- "workbench.action.openSettings",
- "lldb-dap.executable-path",
- );
- }
-}
-
/**
* Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
* debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
@@ -176,7 +172,10 @@ export async function createDebugAdapterExecutable(
...env,
},
};
- const dbgArgs = getDAPArguments(folder, configuration);
+ const dbgArgs = await getDAPArguments(folder, configuration, userInteractive);
+ if (!dbgArgs) {
+ return undefined;
+ }
return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
}
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index aa8c1af6d1e00..b2eef56726f22 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -3,32 +3,10 @@ import * as child_process from "child_process";
import * as util from "util";
import { LLDBDapServer } from "./lldb-dap-server";
import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+import { showErrorWithConfigureButton } from "./ui/error-messages";
const exec = util.promisify(child_process.execFile);
-/**
- * Shows an error message to the user that optionally allows them to open their
- * launch.json to configure it.
- *
- * @param message The error message to display to the user
- * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
- */
-async function showErrorWithConfigureButton(
- message: string,
-): Promise<null | undefined> {
- const userSelection = await vscode.window.showErrorMessage(
- message,
- { modal: true },
- "Configure",
- );
-
- if (userSelection === "Configure") {
- return null; // Stops the debug session and opens the launch.json for editing
- }
-
- return undefined; // Only stops the debug session
-}
-
/**
* Determines whether or not the given lldb-dap executable supports executing
* in server mode.
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
new file mode 100644
index 0000000000000..0127ca5e288cc
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
@@ -0,0 +1,49 @@
+import * as vscode from "vscode";
+
+/**
+ * Shows a modal when the debug adapter's path is not found
+ */
+export async function showLLDBDapNotFoundMessage(path?: string) {
+ const message =
+ path !== undefined
+ ? `Debug adapter path: ${path} is not a valid file`
+ : "Unable to find the path to the LLDB debug adapter executable.";
+ const openSettingsAction = "Open Settings";
+ const callbackValue = await vscode.window.showErrorMessage(
+ message,
+ { modal: true },
+ openSettingsAction,
+ );
+
+ if (openSettingsAction === callbackValue) {
+ vscode.commands.executeCommand(
+ "workbench.action.openSettings",
+ "lldb-dap.executable-path",
+ );
+ }
+}
+
+/**
+ * Shows an error message to the user that optionally allows them to open their
+ * launch.json to configure it.
+ *
+ * Expected to be used in the context of a {@link vscode.DebugConfigurationProvider}.
+ *
+ * @param message The error message to display to the user
+ * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
+ */
+export async function showErrorWithConfigureButton(
+ message: string,
+): Promise<null | undefined> {
+ const userSelection = await vscode.window.showErrorMessage(
+ message,
+ { modal: true },
+ "Configure",
+ );
+
+ if (userSelection === "Configure") {
+ return null; // Stops the debug session and opens the launch.json for editing
+ }
+
+ return undefined; // Only stops the debug session
+}
>From 7d64c2114eef2c5c1516a1455b904a124becbea4 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:09:15 -0500
Subject: [PATCH 5/8] mention that debug configuration properties override VS
Code settings
---
lldb/tools/lldb-dap/package.json | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index 99d95ec72093e..1e099a7067e2f 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -173,14 +173,14 @@
},
"debugAdapterExecutable": {
"type": "string",
- "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
+ "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings."
},
"debugAdapterArgs": {
"type": "array",
"items": {
"type": "string"
},
- "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
+ "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings."
},
"program": {
"type": "string",
@@ -378,14 +378,14 @@
},
"debugAdapterExecutable": {
"type": "string",
- "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
+ "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings."
},
"debugAdapterArgs": {
"type": "array",
"items": {
"type": "string"
},
- "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
+ "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings."
},
"program": {
"type": "string",
>From bfdcd9416482b796ec12f8dc5996c624aa292506 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:32:38 -0500
Subject: [PATCH 6/8] tell the user if lldb-dap doesn't exist when a debug
session is started
---
.../src-ts/debug-configuration-provider.ts | 38 +++++++++++--------
1 file changed, 22 insertions(+), 16 deletions(-)
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index b2eef56726f22..3f76610f09752 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -38,21 +38,19 @@ export class LLDBDapConfigurationProvider
);
}
- if (
- "debugAdapterPort" in debugConfiguration &&
- ("debugAdapterExecutable" in debugConfiguration ||
- "debugAdapterArgs" in debugConfiguration)
- ) {
- return showErrorWithConfigureButton(
- "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
- );
- }
-
- // Server mode needs to be handled here since DebugAdapterDescriptorFactory
- // will show an unhelpful error if it returns undefined. We'd rather show a
- // nicer error message here and allow stopping the debug session gracefully.
- const config = vscode.workspace.getConfiguration("lldb-dap", folder);
- if (config.get<boolean>("serverMode", false)) {
+ // Check if we're going to launch a debug session or use an existing process
+ if ("debugAdapterPort" in debugConfiguration) {
+ if (
+ "debugAdapterExecutable" in debugConfiguration ||
+ "debugAdapterArgs" in debugConfiguration
+ ) {
+ return showErrorWithConfigureButton(
+ "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+ );
+ }
+ } else {
+ // Always try to create the debug adapter executable as this will show the user errors
+ // if there are any.
const executable = await createDebugAdapterExecutable(
folder,
debugConfiguration,
@@ -61,7 +59,15 @@ export class LLDBDapConfigurationProvider
if (!executable) {
return undefined;
}
- if (await isServerModeSupported(executable.command)) {
+
+ // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+ // will show an unhelpful error if it returns undefined. We'd rather show a
+ // nicer error message here and allow stopping the debug session gracefully.
+ const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ if (
+ config.get<boolean>("serverMode", false) &&
+ (await isServerModeSupported(executable.command))
+ ) {
const serverInfo = await this.server.start(
executable.command,
executable.args,
>From 3890c176ef1cdf2a44909a3cd817148c17424c54 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Tue, 11 Mar 2025 13:14:38 -0400
Subject: [PATCH 7/8] change `folder` arguments to `workspaceFolder`
---
.../lldb-dap/src-ts/debug-adapter-factory.ts | 22 +++++++++++--------
1 file changed, 13 insertions(+), 9 deletions(-)
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 11a1cb776b0a3..8cc78fe93c83d 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -70,7 +70,7 @@ async function findDAPExecutable(): Promise<string | undefined> {
}
async function getDAPExecutable(
- folder: vscode.WorkspaceFolder | undefined,
+ workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
): Promise<string | undefined> {
// Check if the executable was provided in the launch configuration.
@@ -80,7 +80,7 @@ async function getDAPExecutable(
}
// Check if the executable was provided in the extension's configuration.
- const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
const configPath = config.get<string>("executable-path");
if (configPath && configPath.length !== 0) {
return configPath;
@@ -96,7 +96,7 @@ async function getDAPExecutable(
}
async function getDAPArguments(
- folder: vscode.WorkspaceFolder | undefined,
+ workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
userInteractive?: boolean,
): Promise<string[] | null | undefined> {
@@ -124,7 +124,7 @@ async function getDAPArguments(
}
// Fall back on the workspace configuration
return vscode.workspace
- .getConfiguration("lldb-dap", folder)
+ .getConfiguration("lldb-dap", workspaceFolder)
.get<string[]>("arguments", []);
}
@@ -132,17 +132,17 @@ async function getDAPArguments(
* Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
* debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
*
- * @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
* @param configuration The {@link vscode.DebugConfiguration}
* @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
* @returns
*/
export async function createDebugAdapterExecutable(
- folder: vscode.WorkspaceFolder | undefined,
+ workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
userInteractive?: boolean,
): Promise<vscode.DebugAdapterExecutable | undefined> {
- const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
const log_path = config.get<string>("log-path");
let env: { [key: string]: string } = {};
if (log_path) {
@@ -150,7 +150,7 @@ export async function createDebugAdapterExecutable(
}
const configEnvironment =
config.get<{ [key: string]: string }>("environment") || {};
- const dapPath = await getDAPExecutable(folder, configuration);
+ const dapPath = await getDAPExecutable(workspaceFolder, configuration);
if (!dapPath) {
if (userInteractive) {
@@ -172,7 +172,11 @@ export async function createDebugAdapterExecutable(
...env,
},
};
- const dbgArgs = await getDAPArguments(folder, configuration, userInteractive);
+ const dbgArgs = await getDAPArguments(
+ workspaceFolder,
+ configuration,
+ userInteractive,
+ );
if (!dbgArgs) {
return undefined;
}
>From 6fbe0e19cd3ea91fbfa0fe8068937eabd5f5ca20 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Thu, 13 Mar 2025 16:55:28 -0400
Subject: [PATCH 8/8] show more detailed error messages depending on what went
wrong
---
.../lldb-dap/src-ts/debug-adapter-factory.ts | 84 +++++++-------
.../src-ts/debug-configuration-provider.ts | 109 ++++++++++--------
.../lldb-dap/src-ts/ui/error-messages.ts | 49 --------
.../src-ts/ui/error-with-notification.ts | 68 +++++++++++
.../lldb-dap/src-ts/ui/show-error-message.ts | 98 ++++++++++++++++
5 files changed, 269 insertions(+), 139 deletions(-)
delete mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 8cc78fe93c83d..79fe6396416eb 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -3,10 +3,8 @@ import * as util from "util";
import * as vscode from "vscode";
import * as child_process from "child_process";
import * as fs from "node:fs/promises";
-import {
- showErrorWithConfigureButton,
- showLLDBDapNotFoundMessage,
-} from "./ui/error-messages";
+import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
const exec = util.promisify(child_process.execFile);
@@ -69,10 +67,19 @@ async function findDAPExecutable(): Promise<string | undefined> {
return undefined;
}
+/**
+ * Retrieves the lldb-dap executable path either from settings or the provided
+ * {@link vscode.DebugConfiguration}.
+ *
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The path to the lldb-dap executable
+ */
async function getDAPExecutable(
workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
-): Promise<string | undefined> {
+): Promise<string> {
// Check if the executable was provided in the launch configuration.
const launchConfigPath = configuration["debugAdapterExecutable"];
if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {
@@ -92,14 +99,25 @@ async function getDAPExecutable(
return foundPath;
}
- return undefined;
+ throw new ErrorWithNotification(
+ "Unable to find the path to the LLDB debug adapter executable.",
+ new OpenSettingsButton("lldb-dap.executable-path"),
+ );
}
+/**
+ * Retrieves the arguments that will be provided to lldb-dap either from settings or the provided
+ * {@link vscode.DebugConfiguration}.
+ *
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The arguments that will be provided to lldb-dap
+ */
async function getDAPArguments(
workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
- userInteractive?: boolean,
-): Promise<string[] | null | undefined> {
+): Promise<string[]> {
// Check the debug configuration for arguments first
const debugConfigArgs = configuration.debugAdapterArgs;
if (debugConfigArgs) {
@@ -107,21 +125,13 @@ async function getDAPArguments(
!Array.isArray(debugConfigArgs) ||
debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1
) {
- if (!userInteractive) {
- return undefined;
- }
- return showErrorWithConfigureButton(
- "The debugAdapterArgs property must be an array of string values.",
+ throw new ErrorWithNotification(
+ "The debugAdapterArgs property must be an array of string values. Please update your launch configuration",
+ new ConfigureButton(),
);
}
return debugConfigArgs;
}
- if (
- Array.isArray(debugConfigArgs) &&
- debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
- ) {
- return debugConfigArgs;
- }
// Fall back on the workspace configuration
return vscode.workspace
.getConfiguration("lldb-dap", workspaceFolder)
@@ -133,15 +143,14 @@ async function getDAPArguments(
* debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
*
* @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
- * @param configuration The {@link vscode.DebugConfiguration}
- * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
- * @returns
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The {@link vscode.DebugAdapterExecutable} that can be used to launch lldb-dap
*/
export async function createDebugAdapterExecutable(
workspaceFolder: vscode.WorkspaceFolder | undefined,
configuration: vscode.DebugConfiguration,
- userInteractive?: boolean,
-): Promise<vscode.DebugAdapterExecutable | undefined> {
+): Promise<vscode.DebugAdapterExecutable> {
const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
const log_path = config.get<string>("log-path");
let env: { [key: string]: string } = {};
@@ -152,18 +161,16 @@ export async function createDebugAdapterExecutable(
config.get<{ [key: string]: string }>("environment") || {};
const dapPath = await getDAPExecutable(workspaceFolder, configuration);
- if (!dapPath) {
- if (userInteractive) {
- showLLDBDapNotFoundMessage();
- }
- return undefined;
- }
-
if (!(await isExecutable(dapPath))) {
- if (userInteractive) {
- showLLDBDapNotFoundMessage(dapPath);
+ let message = `Debug adapter path "${dapPath}" is not a valid file.`;
+ let buttons: (OpenSettingsButton | ConfigureButton)[] = [
+ new OpenSettingsButton("lldb-dap.executable-path"),
+ ];
+ if ("debugAdapterPath" in configuration) {
+ message += " The path comes from your launch configuration.";
+ buttons = [new ConfigureButton()];
}
- return undefined;
+ throw new ErrorWithNotification(message, ...buttons);
}
const dbgOptions = {
@@ -172,14 +179,7 @@ export async function createDebugAdapterExecutable(
...env,
},
};
- const dbgArgs = await getDAPArguments(
- workspaceFolder,
- configuration,
- userInteractive,
- );
- if (!dbgArgs) {
- return undefined;
- }
+ const dbgArgs = await getDAPArguments(workspaceFolder, configuration);
return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
}
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index 3f76610f09752..46c9ea77c0d22 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -3,7 +3,8 @@ import * as child_process from "child_process";
import * as util from "util";
import { LLDBDapServer } from "./lldb-dap-server";
import { createDebugAdapterExecutable } from "./debug-adapter-factory";
-import { showErrorWithConfigureButton } from "./ui/error-messages";
+import { ConfigureButton, showErrorMessage } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
const exec = util.promisify(child_process.execFile);
@@ -29,62 +30,74 @@ export class LLDBDapConfigurationProvider
debugConfiguration: vscode.DebugConfiguration,
_token?: vscode.CancellationToken,
): Promise<vscode.DebugConfiguration | null | undefined> {
- if (
- "debugAdapterHost" in debugConfiguration &&
- !("debugAdapterPort" in debugConfiguration)
- ) {
- return showErrorWithConfigureButton(
- "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
- );
- }
-
- // Check if we're going to launch a debug session or use an existing process
- if ("debugAdapterPort" in debugConfiguration) {
+ try {
if (
- "debugAdapterExecutable" in debugConfiguration ||
- "debugAdapterArgs" in debugConfiguration
+ "debugAdapterHost" in debugConfiguration &&
+ !("debugAdapterPort" in debugConfiguration)
) {
- return showErrorWithConfigureButton(
- "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+ throw new ErrorWithNotification(
+ "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
+ new ConfigureButton(),
);
}
- } else {
- // Always try to create the debug adapter executable as this will show the user errors
- // if there are any.
- const executable = await createDebugAdapterExecutable(
- folder,
- debugConfiguration,
- /* userInteractive */ true,
- );
- if (!executable) {
- return undefined;
- }
- // Server mode needs to be handled here since DebugAdapterDescriptorFactory
- // will show an unhelpful error if it returns undefined. We'd rather show a
- // nicer error message here and allow stopping the debug session gracefully.
- const config = vscode.workspace.getConfiguration("lldb-dap", folder);
- if (
- config.get<boolean>("serverMode", false) &&
- (await isServerModeSupported(executable.command))
- ) {
- const serverInfo = await this.server.start(
- executable.command,
- executable.args,
- executable.options,
+ // Check if we're going to launch a debug session or use an existing process
+ if ("debugAdapterPort" in debugConfiguration) {
+ if (
+ "debugAdapterExecutable" in debugConfiguration ||
+ "debugAdapterArgs" in debugConfiguration
+ ) {
+ throw new ErrorWithNotification(
+ "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+ new ConfigureButton(),
+ );
+ }
+ } else {
+ // Always try to create the debug adapter executable as this will show the user errors
+ // if there are any.
+ const executable = await createDebugAdapterExecutable(
+ folder,
+ debugConfiguration,
);
- if (!serverInfo) {
+ if (!executable) {
return undefined;
}
- // Use a debug adapter host and port combination rather than an executable
- // and list of arguments.
- delete debugConfiguration.debugAdapterExecutable;
- delete debugConfiguration.debugAdapterArgs;
- debugConfiguration.debugAdapterHost = serverInfo.host;
- debugConfiguration.debugAdapterPort = serverInfo.port;
+
+ // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+ // will show an unhelpful error if it returns undefined. We'd rather show a
+ // nicer error message here and allow stopping the debug session gracefully.
+ const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+ if (
+ config.get<boolean>("serverMode", false) &&
+ (await isServerModeSupported(executable.command))
+ ) {
+ const serverInfo = await this.server.start(
+ executable.command,
+ executable.args,
+ executable.options,
+ );
+ if (!serverInfo) {
+ return undefined;
+ }
+ // Use a debug adapter host and port combination rather than an executable
+ // and list of arguments.
+ delete debugConfiguration.debugAdapterExecutable;
+ delete debugConfiguration.debugAdapterArgs;
+ debugConfiguration.debugAdapterHost = serverInfo.host;
+ debugConfiguration.debugAdapterPort = serverInfo.port;
+ }
}
- }
- return debugConfiguration;
+ return debugConfiguration;
+ } catch (error) {
+ // Show a better error message to the user if possible
+ if (!(error instanceof ErrorWithNotification)) {
+ throw error;
+ }
+ return await error.showNotification({
+ modal: true,
+ showConfigureButton: true,
+ });
+ }
}
}
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
deleted file mode 100644
index 0127ca5e288cc..0000000000000
--- a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as vscode from "vscode";
-
-/**
- * Shows a modal when the debug adapter's path is not found
- */
-export async function showLLDBDapNotFoundMessage(path?: string) {
- const message =
- path !== undefined
- ? `Debug adapter path: ${path} is not a valid file`
- : "Unable to find the path to the LLDB debug adapter executable.";
- const openSettingsAction = "Open Settings";
- const callbackValue = await vscode.window.showErrorMessage(
- message,
- { modal: true },
- openSettingsAction,
- );
-
- if (openSettingsAction === callbackValue) {
- vscode.commands.executeCommand(
- "workbench.action.openSettings",
- "lldb-dap.executable-path",
- );
- }
-}
-
-/**
- * Shows an error message to the user that optionally allows them to open their
- * launch.json to configure it.
- *
- * Expected to be used in the context of a {@link vscode.DebugConfigurationProvider}.
- *
- * @param message The error message to display to the user
- * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
- */
-export async function showErrorWithConfigureButton(
- message: string,
-): Promise<null | undefined> {
- const userSelection = await vscode.window.showErrorMessage(
- message,
- { modal: true },
- "Configure",
- );
-
- if (userSelection === "Configure") {
- return null; // Stops the debug session and opens the launch.json for editing
- }
-
- return undefined; // Only stops the debug session
-}
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts b/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
new file mode 100644
index 0000000000000..1f8676d3eb135
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
@@ -0,0 +1,68 @@
+import * as vscode from "vscode";
+import {
+ ConfigureButton,
+ NotificationButton,
+ showErrorMessage,
+} from "./show-error-message";
+
+/** Options used to configure {@link ErrorWithNotification.showNotification}. */
+export interface ShowNotificationOptions extends vscode.MessageOptions {
+ /**
+ * Whether or not to show the configure launch configuration button.
+ *
+ * **IMPORTANT**: the configure launch configuration button will do nothing if the
+ * callee isn't a {@link vscode.DebugConfigurationProvider}.
+ */
+ showConfigureButton?: boolean;
+}
+
+/**
+ * An error that is able to be displayed to the user as a notification.
+ *
+ * Used in combination with {@link showErrorMessage showErrorMessage()} when whatever caused
+ * the error was the result of a direct action by the user. E.g. launching a debug session.
+ */
+export class ErrorWithNotification extends Error {
+ private readonly buttons: NotificationButton<any, null | undefined>[];
+
+ constructor(
+ message: string,
+ ...buttons: NotificationButton<any, null | undefined>[]
+ ) {
+ super(message);
+ this.buttons = buttons;
+ }
+
+ /**
+ * Shows the notification to the user including the configure launch configuration button.
+ *
+ * **IMPORTANT**: the configure launch configuration button will do nothing if the
+ * callee isn't a {@link vscode.DebugConfigurationProvider}.
+ *
+ * @param options Configure the behavior of the notification
+ */
+ showNotification(
+ options: ShowNotificationOptions & { showConfigureButton: true },
+ ): Promise<null | undefined>;
+
+ /**
+ * Shows the notification to the user.
+ *
+ * @param options Configure the behavior of the notification
+ */
+ showNotification(options?: ShowNotificationOptions): Promise<undefined>;
+
+ // Actual implementation of showNotification()
+ async showNotification(
+ options: ShowNotificationOptions = {},
+ ): Promise<null | undefined> {
+ // Filter out the configure button unless explicitly requested
+ let buttons = this.buttons;
+ if (options.showConfigureButton !== true) {
+ buttons = buttons.filter(
+ (button) => !(button instanceof ConfigureButton),
+ );
+ }
+ return showErrorMessage(this.message, options, ...buttons);
+ }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts b/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
new file mode 100644
index 0000000000000..de7b07dc97a64
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
@@ -0,0 +1,98 @@
+import * as vscode from "vscode";
+
+/**
+ * A button with a particular label that can perform an action when clicked.
+ *
+ * Used to add buttons to {@link showErrorMessage showErrorMessage()}.
+ */
+export interface NotificationButton<T, Result> {
+ readonly label: T;
+ action(): Promise<Result>;
+}
+
+/**
+ * Represents a button that, when clicked, will open a particular VS Code setting.
+ */
+export class OpenSettingsButton
+ implements NotificationButton<"Open Settings", undefined>
+{
+ readonly label = "Open Settings";
+
+ constructor(private readonly settingId?: string) {}
+
+ async action(): Promise<undefined> {
+ await vscode.commands.executeCommand(
+ "workbench.action.openSettings",
+ this.settingId ?? "@ext:llvm-vs-code-extensions.lldb-dap ",
+ );
+ }
+}
+
+/**
+ * Represents a button that, when clicked, will return `null`.
+ *
+ * Used by a {@link vscode.DebugConfigurationProvider} to indicate that VS Code should
+ * cancel a debug session and open its launch configuration.
+ *
+ * **IMPORTANT**: this button will do nothing if the callee isn't a
+ * {@link vscode.DebugConfigurationProvider}.
+ */
+export class ConfigureButton
+ implements NotificationButton<"Configure", null | undefined>
+{
+ readonly label = "Configure";
+
+ async action(): Promise<null | undefined> {
+ return null; // Opens the launch.json if returned from a DebugConfigurationProvider
+ }
+}
+
+/** Gets the Result type from a {@link NotificationButton} or string value. */
+type ResultOf<T> = T extends string
+ ? T
+ : T extends NotificationButton<any, infer Result>
+ ? Result
+ : never;
+
+/**
+ * Shows an error message to the user with an optional array of buttons.
+ *
+ * This can be used with common buttons such as {@link OpenSettingsButton} or plain
+ * strings as would normally be accepted by {@link vscode.window.showErrorMessage}.
+ *
+ * @param message The error message to display to the user
+ * @param options Configures the behaviour of the message.
+ * @param buttons An array of {@link NotificationButton buttons} or strings that the user can click on
+ * @returns `undefined` or the result of a button's action
+ */
+export async function showErrorMessage<
+ T extends string | NotificationButton<any, any>,
+>(
+ message: string,
+ options: vscode.MessageOptions = {},
+ ...buttons: T[]
+): Promise<ResultOf<T> | undefined> {
+ const userSelection = await vscode.window.showErrorMessage(
+ message,
+ options,
+ ...buttons.map((button) => {
+ if (typeof button === "string") {
+ return button;
+ }
+ return button.label;
+ }),
+ );
+
+ for (const button of buttons) {
+ if (typeof button === "string") {
+ if (userSelection === button) {
+ // Type assertion is required to let TypeScript know that "button" isn't just any old string.
+ return button as ResultOf<T>;
+ }
+ } else if (userSelection === button.label) {
+ return await button.action();
+ }
+ }
+
+ return undefined;
+}
More information about the lldb-commits
mailing list