[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
Fri Mar 7 15:09:29 PST 2025
https://github.com/matthewbastien updated https://github.com/llvm/llvm-project/pull/129262
>From b40c3e7e4ebb154c5f231676451acbd17e1f39f7 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/5] allow providing debug adapter arguments
---
lldb/tools/lldb-dap/package.json | 23 ++++++
.../lldb-dap/src-ts/debug-adapter-factory.ts | 72 +++++++++++++------
2 files changed, 72 insertions(+), 23 deletions(-)
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cd450a614b3f7..aa11d8bcaa66e 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",
@@ -162,6 +171,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 +368,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."
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 1f76fe31b00ad..51f45f87d660a 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,13 +93,33 @@ 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", []);
+}
+
/**
* 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, 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 }) => {
@@ -109,7 +129,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",
@@ -123,7 +143,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();
@@ -137,32 +157,38 @@ export class LLDBDapDescriptorFactory
const dbgOptions = {
env: {
- ...executable?.options?.env,
...configEnvironment,
...env,
},
};
- const dbgArgs = executable?.args ?? [];
+ const dbgArgs = getDAPArguments(session);
- const serverMode = config.get<boolean>('serverMode', false);
+ const serverMode = config.get<boolean>("serverMode", false);
if (serverMode) {
- const { host, port } = await this.startServer(dapPath, dbgArgs, dbgOptions);
+ 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];
@@ -170,9 +196,9 @@ export class LLDBDapDescriptorFactory
resolve({ process: server, host, port });
}
});
- server.on('exit', () => {
+ server.on("exit", () => {
this.server = undefined;
- })
+ });
});
return this.server;
}
@@ -180,11 +206,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 057ff4c9eed5c2344f5377e9199814c55f6748b1 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/5] 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 aa11d8bcaa66e..75d52786b01e8 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",
@@ -176,7 +176,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",
@@ -373,7 +373,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 51f45f87d660a..8f1e8ad6b019a 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -210,7 +210,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 14866434337e00a077945f63ebf3005fd3f8f61e 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/5] prompt the user to restart the server if the executable
or arguments change
---
lldb/tools/lldb-dap/package.json | 18 +-
.../lldb-dap/src-ts/debug-adapter-factory.ts | 205 ++++++++----------
.../src-ts/debug-configuration-provider.ts | 88 ++++++++
lldb/tools/lldb-dap/src-ts/extension.ts | 38 ++--
lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts | 130 +++++++++++
5 files changed, 346 insertions(+), 133 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 75d52786b01e8..c3361ed3d0bf8 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -167,6 +167,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."
@@ -364,6 +372,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."
@@ -572,4 +588,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 8f1e8ad6b019a..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,124 +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", []);
}
/**
- * This class defines a factory used to find the lldb-dap binary to use
- * depending on the session configuration.
+ * Shows a modal when the debug adapter's path is not found
*/
-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();
- });
- }
+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,
+ );
- async createDebugAdapterDescriptor(
- session: vscode.DebugSession,
- _executable: vscode.DebugAdapterExecutable | undefined,
- ): Promise<vscode.DebugAdapterDescriptor | undefined> {
- const config = vscode.workspace.getConfiguration(
- "lldb-dap",
- session.workspaceFolder,
+ if (openSettingsAction === callbackValue) {
+ vscode.commands.executeCommand(
+ "workbench.action.openSettings",
+ "lldb-dap.executable-path",
);
+ }
+}
- 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(session);
-
- if (!dapPath) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
- return undefined;
- }
+/**
+ * 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 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);
- if (!(await isExecutable(dapPath))) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
- return;
+ if (!dapPath) {
+ if (userInteractive) {
+ showLLDBDapNotFoundMessage();
}
+ return undefined;
+ }
- const dbgOptions = {
- env: {
- ...configEnvironment,
- ...env,
- },
- };
- const dbgArgs = getDAPArguments(session);
-
- const serverMode = config.get<boolean>("serverMode", false);
- if (serverMode) {
- const { host, port } = await this.startServer(
- dapPath,
- dbgArgs,
- dbgOptions,
- );
- return new vscode.DebugAdapterServer(port, host);
+ if (!(await isExecutable(dapPath))) {
+ if (userInteractive) {
+ showLLDBDapNotFoundMessage(dapPath);
}
-
- return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
+ return undefined;
}
- startServer(
- dapPath: string,
- args: string[],
- options: child_process.CommonSpawnOptions,
- ): Promise<{ host: string; port: number }> {
- if (this.server) {
- return this.server;
- }
+ const dbgOptions = {
+ env: {
+ ...configEnvironment,
+ ...env,
+ },
+ };
+ const dbgArgs = getDAPArguments(folder, configuration);
- 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;
- }
+ return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
+}
- /**
- * 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,
- );
+/**
+ * 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.",
+ );
+ }
- if (openSettingsAction === callbackValue) {
- vscode.commands.executeCommand(
- "workbench.action.openSettings",
- "lldb-dap.executable-path",
+ // Use a server connection if the debugAdapterPort is provided
+ if (session.configuration.debugAdapterPort) {
+ return new vscode.DebugAdapterServer(
+ session.configuration.debugAdapterPort,
+ session.configuration.debugAdapterHost,
);
}
+
+ return createDebugAdapterExecutable(
+ session.workspaceFolder,
+ session.configuration,
+ );
}
}
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..d14393afe6658
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -0,0 +1,88 @@
+import * as vscode from "vscode";
+import { LLDBDapServer } from "./lldb-dap-server";
+import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+
+/**
+ * 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
+}
+
+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;
+ }
+ 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 4c639360892f974c1e93f37425e477387a57123a 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/5] 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 d14393afe6658..06517f05629aa 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -1,29 +1,7 @@
import * as vscode from "vscode";
import { LLDBDapServer } from "./lldb-dap-server";
import { createDebugAdapterExecutable } from "./debug-adapter-factory";
-
-/**
- * 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
-}
+import { showErrorWithConfigureButton } from "./ui/error-messages";
export class LLDBDapConfigurationProvider
implements vscode.DebugConfigurationProvider
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 34f3875ddb00d1c963154c47a5696cc1d0e761f9 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/5] 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 c3361ed3d0bf8..c2d1bceac1a07 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -177,14 +177,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",
@@ -382,14 +382,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",
More information about the lldb-commits
mailing list