[Lldb-commits] [lldb] [lldb-dap] Allow providing debug adapter arguments in the extension (PR #129262)

Matthew Bastien via lldb-commits lldb-commits at lists.llvm.org
Thu Mar 13 13:56:04 PDT 2025


https://github.com/matthewbastien updated https://github.com/llvm/llvm-project/pull/129262

>From f687fdfeda083587bd8f2a874d34c0f9dba1e31a Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 28 Feb 2025 11:08:25 -0500
Subject: [PATCH 1/8] allow providing debug adapter arguments

---
 lldb/tools/lldb-dap/package.json              | 29 +++++--
 .../lldb-dap/src-ts/debug-adapter-factory.ts  | 78 ++++++++++++-------
 2 files changed, 76 insertions(+), 31 deletions(-)

diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cd450a614b3f7..a0c8847d72535 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -75,6 +75,15 @@
           "type": "string",
           "description": "The path to the lldb-dap binary."
         },
+        "lldb-dap.arguments": {
+          "scope": "resource",
+          "type": "array",
+          "default": [],
+          "items": {
+            "type": "string"
+          },
+          "description": "The arguments provided to the lldb-dap process."
+        },
         "lldb-dap.log-path": {
           "scope": "resource",
           "type": "string",
@@ -148,10 +157,6 @@
       {
         "type": "lldb-dap",
         "label": "LLDB DAP Debugger",
-        "program": "./bin/lldb-dap",
-        "windows": {
-          "program": "./bin/lldb-dap.exe"
-        },
         "configurationAttributes": {
           "launch": {
             "required": [
@@ -162,6 +167,13 @@
                 "type": "string",
                 "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
               },
+              "debugAdapterArgs": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+              },
               "program": {
                 "type": "string",
                 "description": "Path to the program to debug."
@@ -352,6 +364,13 @@
                 "type": "string",
                 "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
               },
+              "debugAdapterArgs": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                },
+                "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+              },
               "program": {
                 "type": "string",
                 "description": "Path to the program to attach to."
@@ -549,4 +568,4 @@
       }
     ]
   }
-}
\ No newline at end of file
+}
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index c2244dcbde8f2..85c34b773e047 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -25,7 +25,7 @@ async function findWithXcrun(executable: string): Promise<string | undefined> {
       if (stdout) {
         return stdout.toString().trimEnd();
       }
-    } catch (error) { }
+    } catch (error) {}
   }
   return undefined;
 }
@@ -93,8 +93,23 @@ async function getDAPExecutable(
   return undefined;
 }
 
+function getDAPArguments(session: vscode.DebugSession): string[] {
+  // Check the debug configuration for arguments first
+  const debugConfigArgs = session.configuration.debugAdapterArgs;
+  if (
+    Array.isArray(debugConfigArgs) &&
+    debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
+  ) {
+    return debugConfigArgs;
+  }
+  // Fall back on the workspace configuration
+  return vscode.workspace
+    .getConfiguration("lldb-dap")
+    .get<string[]>("arguments", []);
+}
+
 async function isServerModeSupported(exe: string): Promise<boolean> {
-  const { stdout } = await exec(exe, ['--help']);
+  const { stdout } = await exec(exe, ["--help"]);
   return /--connection/.test(stdout);
 }
 
@@ -103,8 +118,13 @@ async function isServerModeSupported(exe: string): Promise<boolean> {
  * depending on the session configuration.
  */
 export class LLDBDapDescriptorFactory
-  implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable {
-  private server?: Promise<{ process: child_process.ChildProcess, host: string, port: number }>;
+  implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable
+{
+  private server?: Promise<{
+    process: child_process.ChildProcess;
+    host: string;
+    port: number;
+  }>;
 
   dispose() {
     this.server?.then(({ process }) => {
@@ -114,7 +134,7 @@ export class LLDBDapDescriptorFactory
 
   async createDebugAdapterDescriptor(
     session: vscode.DebugSession,
-    executable: vscode.DebugAdapterExecutable | undefined,
+    _executable: vscode.DebugAdapterExecutable | undefined,
   ): Promise<vscode.DebugAdapterDescriptor | undefined> {
     const config = vscode.workspace.getConfiguration(
       "lldb-dap",
@@ -128,7 +148,7 @@ export class LLDBDapDescriptorFactory
     }
     const configEnvironment =
       config.get<{ [key: string]: string }>("environment") || {};
-    const dapPath = (await getDAPExecutable(session)) ?? executable?.command;
+    const dapPath = await getDAPExecutable(session);
 
     if (!dapPath) {
       LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
@@ -142,32 +162,38 @@ export class LLDBDapDescriptorFactory
 
     const dbgOptions = {
       env: {
-        ...executable?.options?.env,
         ...configEnvironment,
         ...env,
       },
     };
-    const dbgArgs = executable?.args ?? [];
-
-    const serverMode = config.get<boolean>('serverMode', false);
-    if (serverMode && await isServerModeSupported(dapPath)) {
-      const { host, port } = await this.startServer(dapPath, dbgArgs, dbgOptions);
+    const dbgArgs = getDAPArguments(session);
+
+    const serverMode = config.get<boolean>("serverMode", false);
+    if (serverMode && (await isServerModeSupported(dapPath))) {
+      const { host, port } = await this.startServer(
+        dapPath,
+        dbgArgs,
+        dbgOptions,
+      );
       return new vscode.DebugAdapterServer(port, host);
     }
 
     return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
   }
 
-  startServer(dapPath: string, args: string[], options: child_process.CommonSpawnOptions): Promise<{ host: string, port: number }> {
-    if (this.server) return this.server;
+  startServer(
+    dapPath: string,
+    args: string[],
+    options: child_process.CommonSpawnOptions,
+  ): Promise<{ host: string; port: number }> {
+    if (this.server) {
+      return this.server;
+    }
 
-    this.server = new Promise(resolve => {
-      args.push(
-        '--connection',
-        'connect://localhost:0'
-      );
+    this.server = new Promise((resolve) => {
+      args.push("--connection", "connect://localhost:0");
       const server = child_process.spawn(dapPath, args, options);
-      server.stdout!.setEncoding('utf8').once('data', (data: string) => {
+      server.stdout!.setEncoding("utf8").once("data", (data: string) => {
         const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
         if (connection) {
           const host = connection[1];
@@ -175,9 +201,9 @@ export class LLDBDapDescriptorFactory
           resolve({ process: server, host, port });
         }
       });
-      server.on('exit', () => {
+      server.on("exit", () => {
         this.server = undefined;
-      })
+      });
     });
     return this.server;
   }
@@ -185,11 +211,11 @@ export class LLDBDapDescriptorFactory
   /**
    * Shows a message box when the debug adapter's path is not found
    */
-  static async showLLDBDapNotFoundMessage(path?: string) {
+  static async showLLDBDapNotFoundMessage(path?: string | undefined) {
     const message =
-      path
-        ? `Debug adapter path: ${path} is not a valid file.`
-        : "Unable to find the path to the LLDB debug adapter executable.";
+      path !== undefined
+        ? `Debug adapter path: ${path} is not a valid file`
+        : "Unable to find the LLDB debug adapter executable.";
     const openSettingsAction = "Open Settings";
     const callbackValue = await vscode.window.showErrorMessage(
       message,

>From 8d422880164bd1861642d1f09f149c0a1dd1fabb Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 28 Feb 2025 11:22:41 -0500
Subject: [PATCH 2/8] update wording

---
 lldb/tools/lldb-dap/package.json                    | 6 +++---
 lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index a0c8847d72535..cd96483691d8a 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -82,7 +82,7 @@
           "items": {
             "type": "string"
           },
-          "description": "The arguments provided to the lldb-dap process."
+          "description": "The list of additional arguments used to launch the debug adapter executable."
         },
         "lldb-dap.log-path": {
           "scope": "resource",
@@ -172,7 +172,7 @@
                 "items": {
                   "type": "string"
                 },
-                "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
               },
               "program": {
                 "type": "string",
@@ -369,7 +369,7 @@
                 "items": {
                   "type": "string"
                 },
-                "markdownDescription": "The list of arguments used to launch the debug adapter executable."
+                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
               },
               "program": {
                 "type": "string",
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 85c34b773e047..e36fde9b61473 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -215,7 +215,7 @@ export class LLDBDapDescriptorFactory
     const message =
       path !== undefined
         ? `Debug adapter path: ${path} is not a valid file`
-        : "Unable to find the LLDB debug adapter executable.";
+        : "Unable to find the path to the LLDB debug adapter executable.";
     const openSettingsAction = "Open Settings";
     const callbackValue = await vscode.window.showErrorMessage(
       message,

>From 8b42ee72e28aa01a283c517b9d7c30ba2281aed8 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 17:45:19 -0500
Subject: [PATCH 3/8] prompt the user to restart the server if the executable
 or arguments change

---
 lldb/tools/lldb-dap/package.json              |  16 ++
 .../lldb-dap/src-ts/debug-adapter-factory.ts  | 208 ++++++++----------
 .../src-ts/debug-configuration-provider.ts    | 106 +++++++++
 lldb/tools/lldb-dap/src-ts/extension.ts       |  38 ++--
 lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts | 130 +++++++++++
 5 files changed, 362 insertions(+), 136 deletions(-)
 create mode 100644 lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts

diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index cd96483691d8a..99d95ec72093e 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -163,6 +163,14 @@
               "program"
             ],
             "properties": {
+              "debugAdapterHostname": {
+                "type": "string",
+                "markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
+              },
+              "debugAdapterPort": {
+                "type": "number",
+                "markdownDescription": "The port that an existing lldb-dap executable is listening on."
+              },
               "debugAdapterExecutable": {
                 "type": "string",
                 "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
@@ -360,6 +368,14 @@
           },
           "attach": {
             "properties": {
+              "debugAdapterHostname": {
+                "type": "string",
+                "markdownDescription": "The hostname that an existing lldb-dap executable is listening on."
+              },
+              "debugAdapterPort": {
+                "type": "number",
+                "markdownDescription": "The port that an existing lldb-dap executable is listening on."
+              },
               "debugAdapterExecutable": {
                 "type": "string",
                 "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index e36fde9b61473..61c4b95efb8a7 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -6,7 +6,7 @@ import * as fs from "node:fs/promises";
 
 const exec = util.promisify(child_process.execFile);
 
-export async function isExecutable(path: string): Promise<Boolean> {
+async function isExecutable(path: string): Promise<Boolean> {
   try {
     await fs.access(path, fs.constants.X_OK);
   } catch {
@@ -66,19 +66,17 @@ async function findDAPExecutable(): Promise<string | undefined> {
 }
 
 async function getDAPExecutable(
-  session: vscode.DebugSession,
+  folder: vscode.WorkspaceFolder | undefined,
+  configuration: vscode.DebugConfiguration,
 ): Promise<string | undefined> {
   // Check if the executable was provided in the launch configuration.
-  const launchConfigPath = session.configuration["debugAdapterExecutable"];
+  const launchConfigPath = configuration["debugAdapterExecutable"];
   if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {
     return launchConfigPath;
   }
 
   // Check if the executable was provided in the extension's configuration.
-  const config = vscode.workspace.getConfiguration(
-    "lldb-dap",
-    session.workspaceFolder,
-  );
+  const config = vscode.workspace.getConfiguration("lldb-dap", folder);
   const configPath = config.get<string>("executable-path");
   if (configPath && configPath.length !== 0) {
     return configPath;
@@ -93,9 +91,12 @@ async function getDAPExecutable(
   return undefined;
 }
 
-function getDAPArguments(session: vscode.DebugSession): string[] {
+function getDAPArguments(
+  folder: vscode.WorkspaceFolder | undefined,
+  configuration: vscode.DebugConfiguration,
+): string[] {
   // Check the debug configuration for arguments first
-  const debugConfigArgs = session.configuration.debugAdapterArgs;
+  const debugConfigArgs = configuration.debugAdapterArgs;
   if (
     Array.isArray(debugConfigArgs) &&
     debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
@@ -104,129 +105,110 @@ function getDAPArguments(session: vscode.DebugSession): string[] {
   }
   // Fall back on the workspace configuration
   return vscode.workspace
-    .getConfiguration("lldb-dap")
+    .getConfiguration("lldb-dap", folder)
     .get<string[]>("arguments", []);
 }
 
-async function isServerModeSupported(exe: string): Promise<boolean> {
-  const { stdout } = await exec(exe, ["--help"]);
-  return /--connection/.test(stdout);
+/**
+ * Shows a modal when the debug adapter's path is not found
+ */
+async function showLLDBDapNotFoundMessage(path?: string) {
+  const message =
+    path !== undefined
+      ? `Debug adapter path: ${path} is not a valid file`
+      : "Unable to find the path to the LLDB debug adapter executable.";
+  const openSettingsAction = "Open Settings";
+  const callbackValue = await vscode.window.showErrorMessage(
+    message,
+    { modal: true },
+    openSettingsAction,
+  );
+
+  if (openSettingsAction === callbackValue) {
+    vscode.commands.executeCommand(
+      "workbench.action.openSettings",
+      "lldb-dap.executable-path",
+    );
+  }
 }
 
 /**
- * This class defines a factory used to find the lldb-dap binary to use
- * depending on the session configuration.
+ * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
+ * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
+ *
+ * @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration}
+ * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
+ * @returns
  */
-export class LLDBDapDescriptorFactory
-  implements vscode.DebugAdapterDescriptorFactory, vscode.Disposable
-{
-  private server?: Promise<{
-    process: child_process.ChildProcess;
-    host: string;
-    port: number;
-  }>;
-
-  dispose() {
-    this.server?.then(({ process }) => {
-      process.kill();
-    });
+export async function createDebugAdapterExecutable(
+  folder: vscode.WorkspaceFolder | undefined,
+  configuration: vscode.DebugConfiguration,
+  userInteractive?: boolean,
+): Promise<vscode.DebugAdapterExecutable | undefined> {
+  const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+  const log_path = config.get<string>("log-path");
+  let env: { [key: string]: string } = {};
+  if (log_path) {
+    env["LLDBDAP_LOG"] = log_path;
   }
+  const configEnvironment =
+    config.get<{ [key: string]: string }>("environment") || {};
+  const dapPath = await getDAPExecutable(folder, configuration);
 
-  async createDebugAdapterDescriptor(
-    session: vscode.DebugSession,
-    _executable: vscode.DebugAdapterExecutable | undefined,
-  ): Promise<vscode.DebugAdapterDescriptor | undefined> {
-    const config = vscode.workspace.getConfiguration(
-      "lldb-dap",
-      session.workspaceFolder,
-    );
-
-    const log_path = config.get<string>("log-path");
-    let env: { [key: string]: string } = {};
-    if (log_path) {
-      env["LLDBDAP_LOG"] = log_path;
+  if (!dapPath) {
+    if (userInteractive) {
+      showLLDBDapNotFoundMessage();
     }
-    const configEnvironment =
-      config.get<{ [key: string]: string }>("environment") || {};
-    const dapPath = await getDAPExecutable(session);
+    return undefined;
+  }
 
-    if (!dapPath) {
-      LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage();
-      return undefined;
+  if (!(await isExecutable(dapPath))) {
+    if (userInteractive) {
+      showLLDBDapNotFoundMessage(dapPath);
     }
+    return undefined;
+  }
 
-    if (!(await isExecutable(dapPath))) {
-      LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath);
-      return;
-    }
+  const dbgOptions = {
+    env: {
+      ...configEnvironment,
+      ...env,
+    },
+  };
+  const dbgArgs = getDAPArguments(folder, configuration);
+
+  return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
+}
 
-    const dbgOptions = {
-      env: {
-        ...configEnvironment,
-        ...env,
-      },
-    };
-    const dbgArgs = getDAPArguments(session);
-
-    const serverMode = config.get<boolean>("serverMode", false);
-    if (serverMode && (await isServerModeSupported(dapPath))) {
-      const { host, port } = await this.startServer(
-        dapPath,
-        dbgArgs,
-        dbgOptions,
+/**
+ * This class defines a factory used to find the lldb-dap binary to use
+ * depending on the session configuration.
+ */
+export class LLDBDapDescriptorFactory
+  implements vscode.DebugAdapterDescriptorFactory
+{
+  async createDebugAdapterDescriptor(
+    session: vscode.DebugSession,
+    executable: vscode.DebugAdapterExecutable | undefined,
+  ): Promise<vscode.DebugAdapterDescriptor | undefined> {
+    if (executable) {
+      throw new Error(
+        "Setting the debug adapter executable in the package.json is not supported.",
       );
-      return new vscode.DebugAdapterServer(port, host);
     }
 
-    return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
-  }
-
-  startServer(
-    dapPath: string,
-    args: string[],
-    options: child_process.CommonSpawnOptions,
-  ): Promise<{ host: string; port: number }> {
-    if (this.server) {
-      return this.server;
+    // Use a server connection if the debugAdapterPort is provided
+    if (session.configuration.debugAdapterPort) {
+      return new vscode.DebugAdapterServer(
+        session.configuration.debugAdapterPort,
+        session.configuration.debugAdapterHost,
+      );
     }
 
-    this.server = new Promise((resolve) => {
-      args.push("--connection", "connect://localhost:0");
-      const server = child_process.spawn(dapPath, args, options);
-      server.stdout!.setEncoding("utf8").once("data", (data: string) => {
-        const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(data);
-        if (connection) {
-          const host = connection[1];
-          const port = Number(connection[2]);
-          resolve({ process: server, host, port });
-        }
-      });
-      server.on("exit", () => {
-        this.server = undefined;
-      });
-    });
-    return this.server;
-  }
-
-  /**
-   * Shows a message box when the debug adapter's path is not found
-   */
-  static async showLLDBDapNotFoundMessage(path?: string | undefined) {
-    const message =
-      path !== undefined
-        ? `Debug adapter path: ${path} is not a valid file`
-        : "Unable to find the path to the LLDB debug adapter executable.";
-    const openSettingsAction = "Open Settings";
-    const callbackValue = await vscode.window.showErrorMessage(
-      message,
-      openSettingsAction,
+    return createDebugAdapterExecutable(
+      session.workspaceFolder,
+      session.configuration,
     );
-
-    if (openSettingsAction === callbackValue) {
-      vscode.commands.executeCommand(
-        "workbench.action.openSettings",
-        "lldb-dap.executable-path",
-      );
-    }
   }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
new file mode 100644
index 0000000000000..aa8c1af6d1e00
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -0,0 +1,106 @@
+import * as vscode from "vscode";
+import * as child_process from "child_process";
+import * as util from "util";
+import { LLDBDapServer } from "./lldb-dap-server";
+import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+
+const exec = util.promisify(child_process.execFile);
+
+/**
+ * Shows an error message to the user that optionally allows them to open their
+ * launch.json to configure it.
+ *
+ * @param message The error message to display to the user
+ * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
+ */
+async function showErrorWithConfigureButton(
+  message: string,
+): Promise<null | undefined> {
+  const userSelection = await vscode.window.showErrorMessage(
+    message,
+    { modal: true },
+    "Configure",
+  );
+
+  if (userSelection === "Configure") {
+    return null; // Stops the debug session and opens the launch.json for editing
+  }
+
+  return undefined; // Only stops the debug session
+}
+
+/**
+ * Determines whether or not the given lldb-dap executable supports executing
+ * in server mode.
+ *
+ * @param exe the path to the lldb-dap executable
+ * @returns a boolean indicating whether or not lldb-dap supports server mode
+ */
+async function isServerModeSupported(exe: string): Promise<boolean> {
+  const { stdout } = await exec(exe, ["--help"]);
+  return /--connection/.test(stdout);
+}
+
+export class LLDBDapConfigurationProvider
+  implements vscode.DebugConfigurationProvider
+{
+  constructor(private readonly server: LLDBDapServer) {}
+
+  async resolveDebugConfiguration(
+    folder: vscode.WorkspaceFolder | undefined,
+    debugConfiguration: vscode.DebugConfiguration,
+    _token?: vscode.CancellationToken,
+  ): Promise<vscode.DebugConfiguration | null | undefined> {
+    if (
+      "debugAdapterHost" in debugConfiguration &&
+      !("debugAdapterPort" in debugConfiguration)
+    ) {
+      return showErrorWithConfigureButton(
+        "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
+      );
+    }
+
+    if (
+      "debugAdapterPort" in debugConfiguration &&
+      ("debugAdapterExecutable" in debugConfiguration ||
+        "debugAdapterArgs" in debugConfiguration)
+    ) {
+      return showErrorWithConfigureButton(
+        "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+      );
+    }
+
+    // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+    // will show an unhelpful error if it returns undefined. We'd rather show a
+    // nicer error message here and allow stopping the debug session gracefully.
+    const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+    if (config.get<boolean>("serverMode", false)) {
+      const executable = await createDebugAdapterExecutable(
+        folder,
+        debugConfiguration,
+        /* userInteractive */ true,
+      );
+      if (!executable) {
+        return undefined;
+      }
+      if (await isServerModeSupported(executable.command)) {
+        const serverInfo = await this.server.start(
+          executable.command,
+          executable.args,
+          executable.options,
+        );
+        if (!serverInfo) {
+          return undefined;
+        }
+        // Use a debug adapter host and port combination rather than an executable
+        // and list of arguments.
+        delete debugConfiguration.debugAdapterExecutable;
+        delete debugConfiguration.debugAdapterArgs;
+        debugConfiguration.debugAdapterHost = serverInfo.host;
+        debugConfiguration.debugAdapterPort = serverInfo.port;
+      }
+    }
+
+    return debugConfiguration;
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index a07bcdebcb68b..e29e2bee2f1fa 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,10 +1,9 @@
 import * as vscode from "vscode";
 
-import {
-  LLDBDapDescriptorFactory,
-  isExecutable,
-} from "./debug-adapter-factory";
+import { LLDBDapDescriptorFactory } from "./debug-adapter-factory";
 import { DisposableContext } from "./disposable-context";
+import { LLDBDapConfigurationProvider } from "./debug-configuration-provider";
+import { LLDBDapServer } from "./lldb-dap-server";
 
 /**
  * This class represents the extension and manages its life cycle. Other extensions
@@ -13,29 +12,22 @@ import { DisposableContext } from "./disposable-context";
 export class LLDBDapExtension extends DisposableContext {
   constructor() {
     super();
-    const factory = new LLDBDapDescriptorFactory();
-    this.pushSubscription(factory);
+
+    const lldbDapServer = new LLDBDapServer();
+    this.pushSubscription(lldbDapServer);
+
     this.pushSubscription(
-      vscode.debug.registerDebugAdapterDescriptorFactory(
+      vscode.debug.registerDebugConfigurationProvider(
         "lldb-dap",
-        factory,
-      )
+        new LLDBDapConfigurationProvider(lldbDapServer),
+      ),
     );
-    this.pushSubscription(
-      vscode.workspace.onDidChangeConfiguration(async (event) => {
-        if (event.affectsConfiguration("lldb-dap.executable-path")) {
-          const dapPath = vscode.workspace
-            .getConfiguration("lldb-dap")
-            .get<string>("executable-path");
 
-          if (dapPath) {
-            if (await isExecutable(dapPath)) {
-              return;
-            }
-          }
-          LLDBDapDescriptorFactory.showLLDBDapNotFoundMessage(dapPath || "");
-        }
-      }),
+    this.pushSubscription(
+      vscode.debug.registerDebugAdapterDescriptorFactory(
+        "lldb-dap",
+        new LLDBDapDescriptorFactory(),
+      ),
     );
   }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
new file mode 100644
index 0000000000000..2241a8676e46f
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/lldb-dap-server.ts
@@ -0,0 +1,130 @@
+import * as child_process from "node:child_process";
+import * as vscode from "vscode";
+
+function areArraysEqual<T>(lhs: T[], rhs: T[]): boolean {
+  if (lhs.length !== rhs.length) {
+    return false;
+  }
+  for (let i = 0; i < lhs.length; i++) {
+    if (lhs[i] !== rhs[i]) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * Represents a running lldb-dap process that is accepting connections (i.e. in "server mode").
+ *
+ * Handles startup of the process if it isn't running already as well as prompting the user
+ * to restart when arguments have changed.
+ */
+export class LLDBDapServer implements vscode.Disposable {
+  private serverProcess?: child_process.ChildProcessWithoutNullStreams;
+  private serverInfo?: Promise<{ host: string; port: number }>;
+
+  /**
+   * Starts the server with the provided options. The server will be restarted or reused as
+   * necessary.
+   *
+   * @param dapPath the path to the debug adapter executable
+   * @param args the list of arguments to provide to the debug adapter
+   * @param options the options to provide to the debug adapter process
+   * @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server.
+   */
+  async start(
+    dapPath: string,
+    args: string[],
+    options?: child_process.SpawnOptionsWithoutStdio,
+  ): Promise<{ host: string; port: number } | undefined> {
+    const dapArgs = [...args, "--connection", "connect://localhost:0"];
+    if (!(await this.shouldContinueStartup(dapPath, dapArgs))) {
+      return undefined;
+    }
+
+    if (this.serverInfo) {
+      return this.serverInfo;
+    }
+
+    this.serverInfo = new Promise((resolve, reject) => {
+      const process = child_process.spawn(dapPath, dapArgs, options);
+      process.on("error", (error) => {
+        reject(error);
+        this.serverProcess = undefined;
+        this.serverInfo = undefined;
+      });
+      process.on("exit", (code, signal) => {
+        let errorMessage = "Server process exited early";
+        if (code !== undefined) {
+          errorMessage += ` with code ${code}`;
+        } else if (signal !== undefined) {
+          errorMessage += ` due to signal ${signal}`;
+        }
+        reject(new Error(errorMessage));
+        this.serverProcess = undefined;
+        this.serverInfo = undefined;
+      });
+      process.stdout.setEncoding("utf8").on("data", (data) => {
+        const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(
+          data.toString(),
+        );
+        if (connection) {
+          const host = connection[1];
+          const port = Number(connection[2]);
+          resolve({ host, port });
+          process.stdout.removeAllListeners();
+        }
+      });
+      this.serverProcess = process;
+    });
+    return this.serverInfo;
+  }
+
+  /**
+   * Checks to see if the server needs to be restarted. If so, it will prompt the user
+   * to ask if they wish to restart.
+   *
+   * @param dapPath the path to the debug adapter
+   * @param args the arguments for the debug adapter
+   * @returns whether or not startup should continue depending on user input
+   */
+  private async shouldContinueStartup(
+    dapPath: string,
+    args: string[],
+  ): Promise<boolean> {
+    if (!this.serverProcess || !this.serverInfo) {
+      return true;
+    }
+
+    if (areArraysEqual(this.serverProcess.spawnargs, [dapPath, ...args])) {
+      return true;
+    }
+
+    const userInput = await vscode.window.showInformationMessage(
+      "A server mode instance of lldb-dap is already running, but the arguments are different from what is requested in your debug configuration or settings. Would you like to restart the server?",
+      { modal: true },
+      "Restart",
+      "Use Existing",
+    );
+    switch (userInput) {
+      case "Restart":
+        this.serverProcess.kill();
+        this.serverProcess = undefined;
+        this.serverInfo = undefined;
+        return true;
+      case "Use Existing":
+        return true;
+      case undefined:
+        return false;
+    }
+  }
+
+  dispose() {
+    if (!this.serverProcess) {
+      return;
+    }
+    this.serverProcess.kill();
+    this.serverProcess = undefined;
+    this.serverInfo = undefined;
+  }
+}

>From 8e78be264d1e2514cfa30bd2875bf1d2a7c75264 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:00:03 -0500
Subject: [PATCH 4/8] add more checks and error messages

---
 .../lldb-dap/src-ts/debug-adapter-factory.ts  | 51 +++++++++----------
 .../src-ts/debug-configuration-provider.ts    | 24 +--------
 .../lldb-dap/src-ts/ui/error-messages.ts      | 49 ++++++++++++++++++
 3 files changed, 75 insertions(+), 49 deletions(-)
 create mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-messages.ts

diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 61c4b95efb8a7..11a1cb776b0a3 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -3,6 +3,10 @@ import * as util from "util";
 import * as vscode from "vscode";
 import * as child_process from "child_process";
 import * as fs from "node:fs/promises";
+import {
+  showErrorWithConfigureButton,
+  showLLDBDapNotFoundMessage,
+} from "./ui/error-messages";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -91,12 +95,27 @@ async function getDAPExecutable(
   return undefined;
 }
 
-function getDAPArguments(
+async function getDAPArguments(
   folder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
-): string[] {
+  userInteractive?: boolean,
+): Promise<string[] | null | undefined> {
   // Check the debug configuration for arguments first
   const debugConfigArgs = configuration.debugAdapterArgs;
+  if (debugConfigArgs) {
+    if (
+      !Array.isArray(debugConfigArgs) ||
+      debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1
+    ) {
+      if (!userInteractive) {
+        return undefined;
+      }
+      return showErrorWithConfigureButton(
+        "The debugAdapterArgs property must be an array of string values.",
+      );
+    }
+    return debugConfigArgs;
+  }
   if (
     Array.isArray(debugConfigArgs) &&
     debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
@@ -109,29 +128,6 @@ function getDAPArguments(
     .get<string[]>("arguments", []);
 }
 
-/**
- * Shows a modal when the debug adapter's path is not found
- */
-async function showLLDBDapNotFoundMessage(path?: string) {
-  const message =
-    path !== undefined
-      ? `Debug adapter path: ${path} is not a valid file`
-      : "Unable to find the path to the LLDB debug adapter executable.";
-  const openSettingsAction = "Open Settings";
-  const callbackValue = await vscode.window.showErrorMessage(
-    message,
-    { modal: true },
-    openSettingsAction,
-  );
-
-  if (openSettingsAction === callbackValue) {
-    vscode.commands.executeCommand(
-      "workbench.action.openSettings",
-      "lldb-dap.executable-path",
-    );
-  }
-}
-
 /**
  * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
  * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
@@ -176,7 +172,10 @@ export async function createDebugAdapterExecutable(
       ...env,
     },
   };
-  const dbgArgs = getDAPArguments(folder, configuration);
+  const dbgArgs = await getDAPArguments(folder, configuration, userInteractive);
+  if (!dbgArgs) {
+    return undefined;
+  }
 
   return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
 }
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index aa8c1af6d1e00..b2eef56726f22 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -3,32 +3,10 @@ import * as child_process from "child_process";
 import * as util from "util";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { createDebugAdapterExecutable } from "./debug-adapter-factory";
+import { showErrorWithConfigureButton } from "./ui/error-messages";
 
 const exec = util.promisify(child_process.execFile);
 
-/**
- * Shows an error message to the user that optionally allows them to open their
- * launch.json to configure it.
- *
- * @param message The error message to display to the user
- * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
- */
-async function showErrorWithConfigureButton(
-  message: string,
-): Promise<null | undefined> {
-  const userSelection = await vscode.window.showErrorMessage(
-    message,
-    { modal: true },
-    "Configure",
-  );
-
-  if (userSelection === "Configure") {
-    return null; // Stops the debug session and opens the launch.json for editing
-  }
-
-  return undefined; // Only stops the debug session
-}
-
 /**
  * Determines whether or not the given lldb-dap executable supports executing
  * in server mode.
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
new file mode 100644
index 0000000000000..0127ca5e288cc
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
@@ -0,0 +1,49 @@
+import * as vscode from "vscode";
+
+/**
+ * Shows a modal when the debug adapter's path is not found
+ */
+export async function showLLDBDapNotFoundMessage(path?: string) {
+  const message =
+    path !== undefined
+      ? `Debug adapter path: ${path} is not a valid file`
+      : "Unable to find the path to the LLDB debug adapter executable.";
+  const openSettingsAction = "Open Settings";
+  const callbackValue = await vscode.window.showErrorMessage(
+    message,
+    { modal: true },
+    openSettingsAction,
+  );
+
+  if (openSettingsAction === callbackValue) {
+    vscode.commands.executeCommand(
+      "workbench.action.openSettings",
+      "lldb-dap.executable-path",
+    );
+  }
+}
+
+/**
+ * Shows an error message to the user that optionally allows them to open their
+ * launch.json to configure it.
+ *
+ * Expected to be used in the context of a {@link vscode.DebugConfigurationProvider}.
+ *
+ * @param message The error message to display to the user
+ * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
+ */
+export async function showErrorWithConfigureButton(
+  message: string,
+): Promise<null | undefined> {
+  const userSelection = await vscode.window.showErrorMessage(
+    message,
+    { modal: true },
+    "Configure",
+  );
+
+  if (userSelection === "Configure") {
+    return null; // Stops the debug session and opens the launch.json for editing
+  }
+
+  return undefined; // Only stops the debug session
+}

>From 7d64c2114eef2c5c1516a1455b904a124becbea4 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:09:15 -0500
Subject: [PATCH 5/8] mention that debug configuration properties override VS
 Code settings

---
 lldb/tools/lldb-dap/package.json | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index 99d95ec72093e..1e099a7067e2f 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -173,14 +173,14 @@
               },
               "debugAdapterExecutable": {
                 "type": "string",
-                "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
+                "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings."
               },
               "debugAdapterArgs": {
                 "type": "array",
                 "items": {
                   "type": "string"
                 },
-                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
+                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings."
               },
               "program": {
                 "type": "string",
@@ -378,14 +378,14 @@
               },
               "debugAdapterExecutable": {
                 "type": "string",
-                "markdownDescription": "The absolute path to the LLDB debug adapter executable to use."
+                "markdownDescription": "The absolute path to the LLDB debug adapter executable to use. Overrides any user or workspace settings."
               },
               "debugAdapterArgs": {
                 "type": "array",
                 "items": {
                   "type": "string"
                 },
-                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable."
+                "markdownDescription": "The list of additional arguments used to launch the debug adapter executable. Overrides any user or workspace settings."
               },
               "program": {
                 "type": "string",

>From bfdcd9416482b796ec12f8dc5996c624aa292506 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Fri, 7 Mar 2025 18:32:38 -0500
Subject: [PATCH 6/8] tell the user if lldb-dap doesn't exist when a debug
 session is started

---
 .../src-ts/debug-configuration-provider.ts    | 38 +++++++++++--------
 1 file changed, 22 insertions(+), 16 deletions(-)

diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index b2eef56726f22..3f76610f09752 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -38,21 +38,19 @@ export class LLDBDapConfigurationProvider
       );
     }
 
-    if (
-      "debugAdapterPort" in debugConfiguration &&
-      ("debugAdapterExecutable" in debugConfiguration ||
-        "debugAdapterArgs" in debugConfiguration)
-    ) {
-      return showErrorWithConfigureButton(
-        "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
-      );
-    }
-
-    // Server mode needs to be handled here since DebugAdapterDescriptorFactory
-    // will show an unhelpful error if it returns undefined. We'd rather show a
-    // nicer error message here and allow stopping the debug session gracefully.
-    const config = vscode.workspace.getConfiguration("lldb-dap", folder);
-    if (config.get<boolean>("serverMode", false)) {
+    // Check if we're going to launch a debug session or use an existing process
+    if ("debugAdapterPort" in debugConfiguration) {
+      if (
+        "debugAdapterExecutable" in debugConfiguration ||
+        "debugAdapterArgs" in debugConfiguration
+      ) {
+        return showErrorWithConfigureButton(
+          "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+        );
+      }
+    } else {
+      // Always try to create the debug adapter executable as this will show the user errors
+      // if there are any.
       const executable = await createDebugAdapterExecutable(
         folder,
         debugConfiguration,
@@ -61,7 +59,15 @@ export class LLDBDapConfigurationProvider
       if (!executable) {
         return undefined;
       }
-      if (await isServerModeSupported(executable.command)) {
+
+      // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+      // will show an unhelpful error if it returns undefined. We'd rather show a
+      // nicer error message here and allow stopping the debug session gracefully.
+      const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+      if (
+        config.get<boolean>("serverMode", false) &&
+        (await isServerModeSupported(executable.command))
+      ) {
         const serverInfo = await this.server.start(
           executable.command,
           executable.args,

>From 3890c176ef1cdf2a44909a3cd817148c17424c54 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Tue, 11 Mar 2025 13:14:38 -0400
Subject: [PATCH 7/8] change `folder` arguments to `workspaceFolder`

---
 .../lldb-dap/src-ts/debug-adapter-factory.ts  | 22 +++++++++++--------
 1 file changed, 13 insertions(+), 9 deletions(-)

diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 11a1cb776b0a3..8cc78fe93c83d 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -70,7 +70,7 @@ async function findDAPExecutable(): Promise<string | undefined> {
 }
 
 async function getDAPExecutable(
-  folder: vscode.WorkspaceFolder | undefined,
+  workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
 ): Promise<string | undefined> {
   // Check if the executable was provided in the launch configuration.
@@ -80,7 +80,7 @@ async function getDAPExecutable(
   }
 
   // Check if the executable was provided in the extension's configuration.
-  const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+  const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
   const configPath = config.get<string>("executable-path");
   if (configPath && configPath.length !== 0) {
     return configPath;
@@ -96,7 +96,7 @@ async function getDAPExecutable(
 }
 
 async function getDAPArguments(
-  folder: vscode.WorkspaceFolder | undefined,
+  workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
   userInteractive?: boolean,
 ): Promise<string[] | null | undefined> {
@@ -124,7 +124,7 @@ async function getDAPArguments(
   }
   // Fall back on the workspace configuration
   return vscode.workspace
-    .getConfiguration("lldb-dap", folder)
+    .getConfiguration("lldb-dap", workspaceFolder)
     .get<string[]>("arguments", []);
 }
 
@@ -132,17 +132,17 @@ async function getDAPArguments(
  * Creates a new {@link vscode.DebugAdapterExecutable} based on the provided workspace folder and
  * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
  *
- * @param folder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
  * @param configuration The {@link vscode.DebugConfiguration}
  * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
  * @returns
  */
 export async function createDebugAdapterExecutable(
-  folder: vscode.WorkspaceFolder | undefined,
+  workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
   userInteractive?: boolean,
 ): Promise<vscode.DebugAdapterExecutable | undefined> {
-  const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+  const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
   const log_path = config.get<string>("log-path");
   let env: { [key: string]: string } = {};
   if (log_path) {
@@ -150,7 +150,7 @@ export async function createDebugAdapterExecutable(
   }
   const configEnvironment =
     config.get<{ [key: string]: string }>("environment") || {};
-  const dapPath = await getDAPExecutable(folder, configuration);
+  const dapPath = await getDAPExecutable(workspaceFolder, configuration);
 
   if (!dapPath) {
     if (userInteractive) {
@@ -172,7 +172,11 @@ export async function createDebugAdapterExecutable(
       ...env,
     },
   };
-  const dbgArgs = await getDAPArguments(folder, configuration, userInteractive);
+  const dbgArgs = await getDAPArguments(
+    workspaceFolder,
+    configuration,
+    userInteractive,
+  );
   if (!dbgArgs) {
     return undefined;
   }

>From 6fbe0e19cd3ea91fbfa0fe8068937eabd5f5ca20 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Thu, 13 Mar 2025 16:55:28 -0400
Subject: [PATCH 8/8] show more detailed error messages depending on what went
 wrong

---
 .../lldb-dap/src-ts/debug-adapter-factory.ts  |  84 +++++++-------
 .../src-ts/debug-configuration-provider.ts    | 109 ++++++++++--------
 .../lldb-dap/src-ts/ui/error-messages.ts      |  49 --------
 .../src-ts/ui/error-with-notification.ts      |  68 +++++++++++
 .../lldb-dap/src-ts/ui/show-error-message.ts  |  98 ++++++++++++++++
 5 files changed, 269 insertions(+), 139 deletions(-)
 delete mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
 create mode 100644 lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts

diff --git a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
index 8cc78fe93c83d..79fe6396416eb 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-adapter-factory.ts
@@ -3,10 +3,8 @@ import * as util from "util";
 import * as vscode from "vscode";
 import * as child_process from "child_process";
 import * as fs from "node:fs/promises";
-import {
-  showErrorWithConfigureButton,
-  showLLDBDapNotFoundMessage,
-} from "./ui/error-messages";
+import { ConfigureButton, OpenSettingsButton } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -69,10 +67,19 @@ async function findDAPExecutable(): Promise<string | undefined> {
   return undefined;
 }
 
+/**
+ * Retrieves the lldb-dap executable path either from settings or the provided
+ * {@link vscode.DebugConfiguration}.
+ *
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The path to the lldb-dap executable
+ */
 async function getDAPExecutable(
   workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
-): Promise<string | undefined> {
+): Promise<string> {
   // Check if the executable was provided in the launch configuration.
   const launchConfigPath = configuration["debugAdapterExecutable"];
   if (typeof launchConfigPath === "string" && launchConfigPath.length !== 0) {
@@ -92,14 +99,25 @@ async function getDAPExecutable(
     return foundPath;
   }
 
-  return undefined;
+  throw new ErrorWithNotification(
+    "Unable to find the path to the LLDB debug adapter executable.",
+    new OpenSettingsButton("lldb-dap.executable-path"),
+  );
 }
 
+/**
+ * Retrieves the arguments that will be provided to lldb-dap either from settings or the provided
+ * {@link vscode.DebugConfiguration}.
+ *
+ * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The arguments that will be provided to lldb-dap
+ */
 async function getDAPArguments(
   workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
-  userInteractive?: boolean,
-): Promise<string[] | null | undefined> {
+): Promise<string[]> {
   // Check the debug configuration for arguments first
   const debugConfigArgs = configuration.debugAdapterArgs;
   if (debugConfigArgs) {
@@ -107,21 +125,13 @@ async function getDAPArguments(
       !Array.isArray(debugConfigArgs) ||
       debugConfigArgs.findIndex((entry) => typeof entry !== "string") !== -1
     ) {
-      if (!userInteractive) {
-        return undefined;
-      }
-      return showErrorWithConfigureButton(
-        "The debugAdapterArgs property must be an array of string values.",
+      throw new ErrorWithNotification(
+        "The debugAdapterArgs property must be an array of string values. Please update your launch configuration",
+        new ConfigureButton(),
       );
     }
     return debugConfigArgs;
   }
-  if (
-    Array.isArray(debugConfigArgs) &&
-    debugConfigArgs.findIndex((entry) => typeof entry !== "string") === -1
-  ) {
-    return debugConfigArgs;
-  }
   // Fall back on the workspace configuration
   return vscode.workspace
     .getConfiguration("lldb-dap", workspaceFolder)
@@ -133,15 +143,14 @@ async function getDAPArguments(
  * debug configuration. Assumes that the given debug configuration is for a local launch of lldb-dap.
  *
  * @param workspaceFolder The {@link vscode.WorkspaceFolder} that the debug session will be launched within
- * @param configuration The {@link vscode.DebugConfiguration}
- * @param userInteractive Whether or not this was called due to user interaction (determines if modals should be shown)
- * @returns
+ * @param configuration The {@link vscode.DebugConfiguration} that will be launched
+ * @throws An {@link ErrorWithNotification} if something went wrong
+ * @returns The {@link vscode.DebugAdapterExecutable} that can be used to launch lldb-dap
  */
 export async function createDebugAdapterExecutable(
   workspaceFolder: vscode.WorkspaceFolder | undefined,
   configuration: vscode.DebugConfiguration,
-  userInteractive?: boolean,
-): Promise<vscode.DebugAdapterExecutable | undefined> {
+): Promise<vscode.DebugAdapterExecutable> {
   const config = vscode.workspace.getConfiguration("lldb-dap", workspaceFolder);
   const log_path = config.get<string>("log-path");
   let env: { [key: string]: string } = {};
@@ -152,18 +161,16 @@ export async function createDebugAdapterExecutable(
     config.get<{ [key: string]: string }>("environment") || {};
   const dapPath = await getDAPExecutable(workspaceFolder, configuration);
 
-  if (!dapPath) {
-    if (userInteractive) {
-      showLLDBDapNotFoundMessage();
-    }
-    return undefined;
-  }
-
   if (!(await isExecutable(dapPath))) {
-    if (userInteractive) {
-      showLLDBDapNotFoundMessage(dapPath);
+    let message = `Debug adapter path "${dapPath}" is not a valid file.`;
+    let buttons: (OpenSettingsButton | ConfigureButton)[] = [
+      new OpenSettingsButton("lldb-dap.executable-path"),
+    ];
+    if ("debugAdapterPath" in configuration) {
+      message += " The path comes from your launch configuration.";
+      buttons = [new ConfigureButton()];
     }
-    return undefined;
+    throw new ErrorWithNotification(message, ...buttons);
   }
 
   const dbgOptions = {
@@ -172,14 +179,7 @@ export async function createDebugAdapterExecutable(
       ...env,
     },
   };
-  const dbgArgs = await getDAPArguments(
-    workspaceFolder,
-    configuration,
-    userInteractive,
-  );
-  if (!dbgArgs) {
-    return undefined;
-  }
+  const dbgArgs = await getDAPArguments(workspaceFolder, configuration);
 
   return new vscode.DebugAdapterExecutable(dapPath, dbgArgs, dbgOptions);
 }
diff --git a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
index 3f76610f09752..46c9ea77c0d22 100644
--- a/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -3,7 +3,8 @@ import * as child_process from "child_process";
 import * as util from "util";
 import { LLDBDapServer } from "./lldb-dap-server";
 import { createDebugAdapterExecutable } from "./debug-adapter-factory";
-import { showErrorWithConfigureButton } from "./ui/error-messages";
+import { ConfigureButton, showErrorMessage } from "./ui/show-error-message";
+import { ErrorWithNotification } from "./ui/error-with-notification";
 
 const exec = util.promisify(child_process.execFile);
 
@@ -29,62 +30,74 @@ export class LLDBDapConfigurationProvider
     debugConfiguration: vscode.DebugConfiguration,
     _token?: vscode.CancellationToken,
   ): Promise<vscode.DebugConfiguration | null | undefined> {
-    if (
-      "debugAdapterHost" in debugConfiguration &&
-      !("debugAdapterPort" in debugConfiguration)
-    ) {
-      return showErrorWithConfigureButton(
-        "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
-      );
-    }
-
-    // Check if we're going to launch a debug session or use an existing process
-    if ("debugAdapterPort" in debugConfiguration) {
+    try {
       if (
-        "debugAdapterExecutable" in debugConfiguration ||
-        "debugAdapterArgs" in debugConfiguration
+        "debugAdapterHost" in debugConfiguration &&
+        !("debugAdapterPort" in debugConfiguration)
       ) {
-        return showErrorWithConfigureButton(
-          "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+        throw new ErrorWithNotification(
+          "A debugAdapterPort must be provided when debugAdapterHost is set. Please update your launch configuration.",
+          new ConfigureButton(),
         );
       }
-    } else {
-      // Always try to create the debug adapter executable as this will show the user errors
-      // if there are any.
-      const executable = await createDebugAdapterExecutable(
-        folder,
-        debugConfiguration,
-        /* userInteractive */ true,
-      );
-      if (!executable) {
-        return undefined;
-      }
 
-      // Server mode needs to be handled here since DebugAdapterDescriptorFactory
-      // will show an unhelpful error if it returns undefined. We'd rather show a
-      // nicer error message here and allow stopping the debug session gracefully.
-      const config = vscode.workspace.getConfiguration("lldb-dap", folder);
-      if (
-        config.get<boolean>("serverMode", false) &&
-        (await isServerModeSupported(executable.command))
-      ) {
-        const serverInfo = await this.server.start(
-          executable.command,
-          executable.args,
-          executable.options,
+      // Check if we're going to launch a debug session or use an existing process
+      if ("debugAdapterPort" in debugConfiguration) {
+        if (
+          "debugAdapterExecutable" in debugConfiguration ||
+          "debugAdapterArgs" in debugConfiguration
+        ) {
+          throw new ErrorWithNotification(
+            "The debugAdapterPort property is incompatible with debugAdapterExecutable and debugAdapterArgs. Please update your launch configuration.",
+            new ConfigureButton(),
+          );
+        }
+      } else {
+        // Always try to create the debug adapter executable as this will show the user errors
+        // if there are any.
+        const executable = await createDebugAdapterExecutable(
+          folder,
+          debugConfiguration,
         );
-        if (!serverInfo) {
+        if (!executable) {
           return undefined;
         }
-        // Use a debug adapter host and port combination rather than an executable
-        // and list of arguments.
-        delete debugConfiguration.debugAdapterExecutable;
-        delete debugConfiguration.debugAdapterArgs;
-        debugConfiguration.debugAdapterHost = serverInfo.host;
-        debugConfiguration.debugAdapterPort = serverInfo.port;
+
+        // Server mode needs to be handled here since DebugAdapterDescriptorFactory
+        // will show an unhelpful error if it returns undefined. We'd rather show a
+        // nicer error message here and allow stopping the debug session gracefully.
+        const config = vscode.workspace.getConfiguration("lldb-dap", folder);
+        if (
+          config.get<boolean>("serverMode", false) &&
+          (await isServerModeSupported(executable.command))
+        ) {
+          const serverInfo = await this.server.start(
+            executable.command,
+            executable.args,
+            executable.options,
+          );
+          if (!serverInfo) {
+            return undefined;
+          }
+          // Use a debug adapter host and port combination rather than an executable
+          // and list of arguments.
+          delete debugConfiguration.debugAdapterExecutable;
+          delete debugConfiguration.debugAdapterArgs;
+          debugConfiguration.debugAdapterHost = serverInfo.host;
+          debugConfiguration.debugAdapterPort = serverInfo.port;
+        }
       }
-    }
 
-    return debugConfiguration;
+      return debugConfiguration;
+    } catch (error) {
+      // Show a better error message to the user if possible
+      if (!(error instanceof ErrorWithNotification)) {
+        throw error;
+      }
+      return await error.showNotification({
+        modal: true,
+        showConfigureButton: true,
+      });
+    }
   }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts b/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
deleted file mode 100644
index 0127ca5e288cc..0000000000000
--- a/lldb/tools/lldb-dap/src-ts/ui/error-messages.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import * as vscode from "vscode";
-
-/**
- * Shows a modal when the debug adapter's path is not found
- */
-export async function showLLDBDapNotFoundMessage(path?: string) {
-  const message =
-    path !== undefined
-      ? `Debug adapter path: ${path} is not a valid file`
-      : "Unable to find the path to the LLDB debug adapter executable.";
-  const openSettingsAction = "Open Settings";
-  const callbackValue = await vscode.window.showErrorMessage(
-    message,
-    { modal: true },
-    openSettingsAction,
-  );
-
-  if (openSettingsAction === callbackValue) {
-    vscode.commands.executeCommand(
-      "workbench.action.openSettings",
-      "lldb-dap.executable-path",
-    );
-  }
-}
-
-/**
- * Shows an error message to the user that optionally allows them to open their
- * launch.json to configure it.
- *
- * Expected to be used in the context of a {@link vscode.DebugConfigurationProvider}.
- *
- * @param message The error message to display to the user
- * @returns `undefined` if the debug session should stop or `null` if the launch.json should be opened
- */
-export async function showErrorWithConfigureButton(
-  message: string,
-): Promise<null | undefined> {
-  const userSelection = await vscode.window.showErrorMessage(
-    message,
-    { modal: true },
-    "Configure",
-  );
-
-  if (userSelection === "Configure") {
-    return null; // Stops the debug session and opens the launch.json for editing
-  }
-
-  return undefined; // Only stops the debug session
-}
diff --git a/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts b/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
new file mode 100644
index 0000000000000..1f8676d3eb135
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/error-with-notification.ts
@@ -0,0 +1,68 @@
+import * as vscode from "vscode";
+import {
+  ConfigureButton,
+  NotificationButton,
+  showErrorMessage,
+} from "./show-error-message";
+
+/** Options used to configure {@link ErrorWithNotification.showNotification}. */
+export interface ShowNotificationOptions extends vscode.MessageOptions {
+  /**
+   * Whether or not to show the configure launch configuration button.
+   *
+   * **IMPORTANT**: the configure launch configuration button will do nothing if the
+   * callee isn't a {@link vscode.DebugConfigurationProvider}.
+   */
+  showConfigureButton?: boolean;
+}
+
+/**
+ * An error that is able to be displayed to the user as a notification.
+ *
+ * Used in combination with {@link showErrorMessage showErrorMessage()} when whatever caused
+ * the error was the result of a direct action by the user. E.g. launching a debug session.
+ */
+export class ErrorWithNotification extends Error {
+  private readonly buttons: NotificationButton<any, null | undefined>[];
+
+  constructor(
+    message: string,
+    ...buttons: NotificationButton<any, null | undefined>[]
+  ) {
+    super(message);
+    this.buttons = buttons;
+  }
+
+  /**
+   * Shows the notification to the user including the configure launch configuration button.
+   *
+   * **IMPORTANT**: the configure launch configuration button will do nothing if the
+   * callee isn't a {@link vscode.DebugConfigurationProvider}.
+   *
+   * @param options Configure the behavior of the notification
+   */
+  showNotification(
+    options: ShowNotificationOptions & { showConfigureButton: true },
+  ): Promise<null | undefined>;
+
+  /**
+   * Shows the notification to the user.
+   *
+   * @param options Configure the behavior of the notification
+   */
+  showNotification(options?: ShowNotificationOptions): Promise<undefined>;
+
+  // Actual implementation of showNotification()
+  async showNotification(
+    options: ShowNotificationOptions = {},
+  ): Promise<null | undefined> {
+    // Filter out the configure button unless explicitly requested
+    let buttons = this.buttons;
+    if (options.showConfigureButton !== true) {
+      buttons = buttons.filter(
+        (button) => !(button instanceof ConfigureButton),
+      );
+    }
+    return showErrorMessage(this.message, options, ...buttons);
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts b/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
new file mode 100644
index 0000000000000..de7b07dc97a64
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/ui/show-error-message.ts
@@ -0,0 +1,98 @@
+import * as vscode from "vscode";
+
+/**
+ * A button with a particular label that can perform an action when clicked.
+ *
+ * Used to add buttons to {@link showErrorMessage showErrorMessage()}.
+ */
+export interface NotificationButton<T, Result> {
+  readonly label: T;
+  action(): Promise<Result>;
+}
+
+/**
+ * Represents a button that, when clicked, will open a particular VS Code setting.
+ */
+export class OpenSettingsButton
+  implements NotificationButton<"Open Settings", undefined>
+{
+  readonly label = "Open Settings";
+
+  constructor(private readonly settingId?: string) {}
+
+  async action(): Promise<undefined> {
+    await vscode.commands.executeCommand(
+      "workbench.action.openSettings",
+      this.settingId ?? "@ext:llvm-vs-code-extensions.lldb-dap ",
+    );
+  }
+}
+
+/**
+ * Represents a button that, when clicked, will return `null`.
+ *
+ * Used by a {@link vscode.DebugConfigurationProvider} to indicate that VS Code should
+ * cancel a debug session and open its launch configuration.
+ *
+ * **IMPORTANT**: this button will do nothing if the callee isn't a
+ * {@link vscode.DebugConfigurationProvider}.
+ */
+export class ConfigureButton
+  implements NotificationButton<"Configure", null | undefined>
+{
+  readonly label = "Configure";
+
+  async action(): Promise<null | undefined> {
+    return null; // Opens the launch.json if returned from a DebugConfigurationProvider
+  }
+}
+
+/** Gets the Result type from a {@link NotificationButton} or string value. */
+type ResultOf<T> = T extends string
+  ? T
+  : T extends NotificationButton<any, infer Result>
+    ? Result
+    : never;
+
+/**
+ * Shows an error message to the user with an optional array of buttons.
+ *
+ * This can be used with common buttons such as {@link OpenSettingsButton} or plain
+ * strings as would normally be accepted by {@link vscode.window.showErrorMessage}.
+ *
+ * @param message The error message to display to the user
+ * @param options Configures the behaviour of the message.
+ * @param buttons An array of {@link NotificationButton buttons} or strings that the user can click on
+ * @returns `undefined` or the result of a button's action
+ */
+export async function showErrorMessage<
+  T extends string | NotificationButton<any, any>,
+>(
+  message: string,
+  options: vscode.MessageOptions = {},
+  ...buttons: T[]
+): Promise<ResultOf<T> | undefined> {
+  const userSelection = await vscode.window.showErrorMessage(
+    message,
+    options,
+    ...buttons.map((button) => {
+      if (typeof button === "string") {
+        return button;
+      }
+      return button.label;
+    }),
+  );
+
+  for (const button of buttons) {
+    if (typeof button === "string") {
+      if (userSelection === button) {
+        // Type assertion is required to let TypeScript know that "button" isn't just any old string.
+        return button as ResultOf<T>;
+      }
+    } else if (userSelection === button.label) {
+      return await button.action();
+    }
+  }
+
+  return undefined;
+}



More information about the lldb-commits mailing list