[Lldb-commits] [lldb] 0d4f12e - [lldb-dap] Allow providing debug adapter arguments in the extension (#129262)
via lldb-commits
lldb-commits at lists.llvm.org
Thu Mar 27 14:09:13 PDT 2025
Author: Matthew Bastien
Date: 2025-03-27T14:09:09-07:00
New Revision: 0d4f12ee0046b83d28dbf3a8aca07a0f27b77786
URL: https://github.com/llvm/llvm-project/commit/0d4f12ee0046b83d28dbf3a8aca07a0f27b77786
DIFF: https://github.com/llvm/llvm-project/commit/0d4f12ee0046b83d28dbf3a8aca07a0f27b77786.diff
LOG: [lldb-dap] Allow providing debug adapter arguments in the extension (#129262)
Added a new setting called `lldb-dap.arguments` and a debug
configuration attribute called `debugAdapterArgs` that can be used to
set the arguments used to launch the debug adapter. Right now this is
mostly useful for debugging purposes to add the `--wait-for-debugger`
option to lldb-dap.
Additionally, the extension will now check for a changed lldb-dap
executable or arguments when launching a debug session in server mode. I
had to add a new `DebugConfigurationProvider` to do this because VSCode
will show an unhelpful error modal when the
`DebugAdapterDescriptorFactory` returns `undefined`.
In order to facilitate this, I had to add two new properties to the
launch configuration that are used by the
`DebugAdapterDescriptorFactory` to tell VS Code how to launch the debug
adapter:
- `debugAdapterHostname` - the hostname for an existing lldb-dap server
- `debugAdapterPort` - the port for an existing lldb-dap server
I've also removed the check for the `executable` argument in
`LLDBDapDescriptorFactory.createDebugAdapterDescriptor()`. This argument
is only set by VS Code when the debug adapter executable properties are
set in the `package.json`. The LLDB DAP extension does not currently do
this (and I don't think it ever will). So, this makes the debug adapter
descriptor factory a little easier to read.
The check for whether or not `lldb-dap` exists has been moved into the
new `DebugConfigurationProvider` as well. This way the extension won't
get in the user's way unless they actually try to start a debugging
session. The error will show up as a modal which will also make it more
obvious when something goes wrong, rather than popping up as a warning
at the bottom right of the screen.
Added:
lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
Modified:
lldb/tools/lldb-dap/package.json
lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
lldb/tools/lldb-dap/src-ts/extension.ts
Removed:
################################################################################
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cb4b1f1aa22ce..289e07c12682c 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -76,6 +76,15 @@
"type": "string",
"description": "The path to the lldb-dap binary."
},
+ "lldb-dap.arguments": {
+ "scope": "resource",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ },
+ "description": "The list of additional arguments used to launch the debug adapter executable."
+ },
"lldb-dap.log-path": {
"scope": "resource",
"type": "string",
@@ -149,19 +158,30 @@
{
"type": "lldb-dap",
"label": "LLDB DAP Debugger",
- "program": "./bin/lldb-dap",
- "windows": {
- "program": "./bin/lldb-dap.exe"
- },
"configurationAttributes": {
"launch": {
"required": [
"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."
+ "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. Overrides any user or workspace settings."
},
"program": {
"type": "string",
@@ -349,9 +369,24 @@
},
"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."
+ "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. Overrides any user or workspace settings."
},
"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 c2244dcbde8f2..e23d717a70101 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,12 @@ 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 { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
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 {
@@ -25,7 +27,7 @@ async function findWithXcrun(executable: string): Promise<string | undefined> {
if (stdout) {
return stdout.toString().trimEnd();
}
- } catch (error) { }
+ } catch (error) {}
}
return undefined;
}
@@ -65,37 +67,127 @@ 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(
- session: vscode.DebugSession,
-): Promise<string | undefined> {
+ workspaceFolder: vscode.WorkspaceFolder | undefined,
+ configuration: vscode.DebugConfiguration,
+): Promise<string> {
// 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) {
+ if (!(await isExecutable(launchConfigPath))) {
+ throw new ErrorWithNotification(
+ `Debug adapter path "${launchConfigPath}" is not a valid file. The path comes from your launch configuration.`,
+ new ConfigureButton(),
+ );
+ }
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", workspaceFolder);
const configPath = config.get<string>("executable-path");
if (configPath && configPath.length !== 0) {
+ if (!(await isExecutable(configPath))) {
+ throw new ErrorWithNotification(
+ `Debug adapter path "${configPath}" is not a valid file. The path comes from your settings.`,
+ new OpenSettingsButton("lldb-dap.executable-path"),
+ );
+ }
return configPath;
}
// Try finding the lldb-dap binary.
const foundPath = await findDAPExecutable();
if (foundPath) {
+ if (!(await isExecutable(foundPath))) {
+ throw new ErrorWithNotification(
+ `Found a potential debug adapter on your system at "${configPath}", but it is not a valid file.`,
+ new OpenSettingsButton("lldb-dap.executable-path"),
+ );
+ }
return foundPath;
}
- return undefined;
+ throw new ErrorWithNotification(
+ "Unable to find the path to the LLDB debug adapter executable.",
+ new OpenSettingsButton("lldb-dap.executable-path"),
+ );
}
-async function isServerModeSupported(exe: string): Promise<boolean> {
- const { stdout } = await exec(exe, ['--help']);
- return /--connection/.test(stdout);
+/**
+ * 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,
+): Promise<string[]> {
+ // Check the debug configuration for arguments first.
+ const debugConfigArgs = configuration.debugAdapterArgs;
+ if (debugConfigArgs) {
+ if (
+ !Array.isArray(debugConfigArgs) ||
+ debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1
+ ) {
+ throw new ErrorWithNotification(
+ "The debugAdapterArgs property must be an array of string values. Please update your launch configuration",
+ new ConfigureButton(),
+ );
+ }
+ return debugConfigArgs;
+ }
+ // Fall back on the workspace configuration.
+ return vscode.workspace
+ .getConfiguration("lldb-dap", workspaceFolder)
+ .get<string[]>("arguments", []);
+}
+
+/**
+ * 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 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 {@link vscode.DebugAdapterExecutable} that can be used to launch lldb-dap
+ */
+export async function createDebugAdapterExecutable(
+ workspaceFolder: vscode.WorkspaceFolder | undefined,
+ configuration: vscode.DebugConfiguration,
+): Promise<vscode.DebugAdapterExecutable> {
+ const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
+ 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(workspaceFolder, configuration);
+
+ const dbgOptions = {
+ env: {
+ ...configEnvironment,
+ ...env,
+ },
+ };
+ const dbgArgs = await getDAPArguments(workspaceFolder, configuration);
+
+ return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
}
/**
@@ -103,104 +195,29 @@ 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 }>;
-
- dispose() {
- this.server?.then(({ process }) => {
- process.kill();
- });
- }
-
+ implements vscode.DebugAdapterDescriptorFactory
+{
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;
- }
- const configEnvironment =
- config.get<{ [key: string]: string }>("environment") || {};
- const dapPath = (await getDAPExecutable(session)) ?? executable?.command;
-
- if (!dapPath) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
- return undefined;
- }
-
- if (!(await isExecutable(dapPath))) {
- LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
- return;
- }
-
- 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);
- return new vscode.DebugAdapterServer(port, host);
+ if (executable) {
+ throw new Error(
+ "Setting the debug adapter executable in the package.json is not supported.",
+ );
}
- 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;
-
- this.server = new Promise(resolve => {
- args.push(
- '--connection',
- 'connect://localhost:0'
+ // Use a server connection if the debugAdapterPort is provided
+ if (session.configuration.debugAdapterPort) {
+ return new vscode.DebugAdapterServer(
+ session.configuration.debugAdapterPort,
+ session.configuration.debugAdapterHost,
);
- 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) {
- const message =
- path
- ? `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..0272509ee55f7
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -0,0 +1,103 @@
+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";
+import { ConfigureButton, showErrorMessage } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
+
+const exec = util.promisify(child_process.execFile);
+
+/**
+ * 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 resolveDebugConfigurationWithSubstitutedVariables(
+ folder: vscode.WorkspaceFolder | undefined,
+ debugConfiguration: vscode.DebugConfiguration,
+ _token?: vscode.CancellationToken,
+ ): Promise<vscode.DebugConfiguration | null | undefined> {
+ try {
+ if (
+ "debugAdapterHost" in debugConfiguration &&
+ !("debugAdapterPort" in debugConfiguration)
+ ) {
+ throw new ErrorWithNotification(
+ "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
+ new ConfigureButton(),
+ );
+ }
+
+ // 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 (!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,
+ );
+ 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;
+ } 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/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index f0c7fb5bd1a71..0b014f953d5ba 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,11 +1,10 @@
import * as vscode from "vscode";
-import {
- LLDBDapDescriptorFactory,
- isExecutable,
-} from "./debug-adapter-factory";
+import { LLDBDapDescriptorFactory } from "./debug-adapter-factory";
import { DisposableContext } from "./disposable-context";
import { LaunchUriHandler } from "./uri-launch-handler";
+import { LLDBDapConfigurationProvider } from "./debug-configuration-provider";
+import { LLDBDapServer } from "./lldb-dap-server";
/**
* This class represents the extension and manages its life cycle. Other extensions
@@ -14,33 +13,26 @@ import { LaunchUriHandler } from "./uri-launch-handler";
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(),
+ ),
);
this.pushSubscription(
- vscode.window.registerUriHandler(new LaunchUriHandler())
+ vscode.window.registerUriHandler(new LaunchUriHandler()),
);
}
}
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..f40dbf049a4bb
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
@@ -0,0 +1,132 @@
+import * as child_process from "node:child_process";
+import { isDeepStrictEqual } from "util";
+import * as vscode from "vscode";
+
+/**
+ * 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 (isDeepStrictEqual(this.serverProcess.spawnargs, [dapPath, ...args])) {
+ return true;
+ }
+
+ const userInput = await vscode.window.showInformationMessage(
+ "The arguments to lldb-dap have changed. Would you like to restart the server?",
+ {
+ modal: true,
+ detail: `An existing lldb-dap server (${this.serverProcess.pid}) is running with
diff erent arguments.
+
+The previous lldb-dap server was started with:
+
+${this.serverProcess.spawnargs.join(" ")}
+
+The new lldb-dap server will be started with:
+
+${dapPath} ${args.join(" ")}
+
+Restarting the server will interrupt any existing debug sessions and start a new server.`,
+ },
+ "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;
+ }
+}
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