[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