Skip to content

[lldb-dap] Add process picker command to VS Code extension #128943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

matthewbastien
Copy link
Contributor

@matthewbastien matthewbastien commented Feb 26, 2025

Adds a process picker command to the LLDB DAP extension that will prompt the user to select a process running on their machine. It is hidden from the command palette, but can be used in an "attach" debug configuration to select a process at the start of a debug session. I've also added a debug configuration snippet for this called "LLDB: Attach to Process" that will fill in the appropriate variable substitution. e.g:

{
    "type": "lldb-dap",
    "request": "attach",
    "name": "Attach to Process",
    "pid": "${command:pickProcess}"
}

The logic is largely the same as the process picker in the vscode-js-debug extension created by Microsoft. It will use available executables based on the current platform to find the list of available processes:

  • Linux: uses the ps executable to list processes
  • macOS: nearly identical to Linux except that the command line options passed to ps are different
  • Windows: uses the PowerShell cmdletGet-CimInstance to query for processes

I manually tested this on a MacBook Pro running macOS Sequoia, a Windows 11 VM, and an Ubuntu 22.04 VM.

Fixes #96279

@llvmbot
Copy link
Member

llvmbot commented Feb 26, 2025

@llvm/pr-subscribers-lldb

Author: Matthew Bastien (matthewbastien)

Changes

Adds a process picker command to the LLDB DAP extension that will prompt the user to select a process running on their machine. It is hidden from the command palette, but can be used in an "attach" debug configuration to select a process at the start of a debug session. I've also added a debug configuration snippet for this called "LLDB: Attach to Process" that will fill in the appropriate variable substitution. e.g:

{
    "type": "lldb-dap",
    "request": "attach",
    "name": "Attach to Process",
    "pid": "${command:PickProcess}"
}

The logic is largely the same as the process picker in the vscode-js-debug extension created by Microsoft. It will use available executables based on the current platform to find the list of available processes:

  • Linux: uses the ps executable to list processes.
  • macOS: nearly identical to Linux except that the command line options passed to ps are different
  • Windows: uses WMIC.exe to query WMI for processes

I manually tested this on a MacBook Pro running macOS Sequoia, a Windows 11 VM, and an Ubuntu 22.04 VM.

Fixes #96279


Full diff: https://github.com/llvm/llvm-project/pull/128943.diff

9 Files Affected:

  • (modified) lldb/tools/lldb-dap/package.json (+29-1)
  • (added) lldb/tools/lldb-dap/src-ts/commands/pick-process.ts (+41)
  • (added) lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts (+54)
  • (modified) lldb/tools/lldb-dap/src-ts/extension.ts (+12-2)
  • (added) lldb/tools/lldb-dap/src-ts/process-tree/base-process-tree.ts (+102)
  • (added) lldb/tools/lldb-dap/src-ts/process-tree/index.ts (+36)
  • (added) lldb/tools/lldb-dap/src-ts/process-tree/platforms/darwin-process-tree.ts (+16)
  • (added) lldb/tools/lldb-dap/src-ts/process-tree/platforms/linux-process-tree.ts (+38)
  • (added) lldb/tools/lldb-dap/src-ts/process-tree/platforms/windows-process-tree.ts (+52)
diff --git a/lldb/tools/lldb-dap/package.json b/lldb/tools/lldb-dap/package.json
index 31d808eda4c35..1bbdbf045dd1b 100644
--- a/lldb/tools/lldb-dap/package.json
+++ b/lldb/tools/lldb-dap/package.json
@@ -146,6 +146,9 @@
         "windows": {
           "program": "./bin/lldb-dap.exe"
         },
+        "variables": {
+          "PickProcess": "lldb-dap.pickProcess"
+        },
         "configurationAttributes": {
           "launch": {
             "required": [
@@ -517,6 +520,16 @@
               "cwd": "^\"\\${workspaceRoot}\""
             }
           },
+          {
+            "label": "LLDB: Attach to Process",
+            "description": "",
+            "body": {
+              "type": "lldb-dap",
+              "request": "attach",
+              "name": "${1:Attach}",
+              "pid": "^\"\\${command:PickProcess}\""
+            }
+          },
           {
             "label": "LLDB: Attach",
             "description": "",
@@ -541,6 +554,21 @@
           }
         ]
       }
-    ]
+    ],
+    "commands": [
+      {
+        "command": "lldb-dap.pickProcess",
+        "title": "Pick Process",
+        "category": "LLDB DAP"
+      }
+    ],
+    "menus": {
+      "commandPalette": [
+        {
+          "command": "lldb-dap.pickProcess",
+          "when": "false"
+        }
+      ]
+    }
   }
 }
diff --git a/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts b/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
new file mode 100644
index 0000000000000..355d508075080
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
@@ -0,0 +1,41 @@
+import * as path from "path";
+import * as vscode from "vscode";
+import { createProcessTree } from "../process-tree";
+
+interface ProcessQuickPick extends vscode.QuickPickItem {
+  processId: number;
+}
+
+/**
+ * Prompts the user to select a running process.
+ *
+ * The return value must be a string so that it is compatible with VS Code's
+ * string substitution infrastructure. The value will eventually be converted
+ * to a number by the debug configuration provider.
+ *
+ * @returns The pid of the process as a string or undefined if cancelled.
+ */
+export async function pickProcess(): Promise<string | undefined> {
+  const processTree = createProcessTree();
+  const selectedProcess = await vscode.window.showQuickPick<ProcessQuickPick>(
+    processTree.listAllProcesses().then((processes): ProcessQuickPick[] => {
+      return processes
+        .sort((a, b) => b.start - a.start) // Sort by start date in descending order
+        .map((proc) => {
+          return {
+            processId: proc.id,
+            label: path.basename(proc.command),
+            description: proc.id.toString(),
+            detail: proc.arguments,
+          } satisfies ProcessQuickPick;
+        });
+    }),
+    {
+      placeHolder: "Select a process to attach the debugger to",
+    },
+  );
+  if (!selectedProcess) {
+    return;
+  }
+  return selectedProcess.processId.toString();
+}
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..02b8bd5aa8147
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
@@ -0,0 +1,54 @@
+import * as vscode from "vscode";
+
+/**
+ * Converts the given value to an integer if it isn't already.
+ *
+ * If the value cannot be converted then this function will return undefined.
+ *
+ * @param value the value to to be converted
+ * @returns the integer value or undefined if unable to convert
+ */
+function convertToInteger(value: any): number | undefined {
+  let result: number | undefined;
+  switch (typeof value) {
+    case "number":
+      result = value;
+      break;
+    case "string":
+      result = Number(value);
+      break;
+    default:
+      return undefined;
+  }
+  if (!Number.isInteger(result)) {
+    return undefined;
+  }
+  return result;
+}
+
+/**
+ * A {@link vscode.DebugConfigurationProvider} used to resolve LLDB DAP debug configurations.
+ *
+ * Performs checks on the debug configuration before launching a debug session.
+ */
+export class LLDBDapConfigurationProvider
+  implements vscode.DebugConfigurationProvider
+{
+  resolveDebugConfigurationWithSubstitutedVariables(
+    _folder: vscode.WorkspaceFolder | undefined,
+    debugConfiguration: vscode.DebugConfiguration,
+  ): vscode.ProviderResult<vscode.DebugConfiguration> {
+    // Convert the "pid" option to a number if it is a string
+    if ("pid" in debugConfiguration) {
+      const pid = convertToInteger(debugConfiguration.pid);
+      if (pid === undefined) {
+        vscode.window.showErrorMessage(
+          "Invalid debug configuration: property 'pid' must either be an integer or a string containing an integer value.",
+        );
+        return null;
+      }
+      debugConfiguration.pid = pid;
+    }
+    return debugConfiguration;
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index 71fd48298f8f5..74815022d468a 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,12 +1,12 @@
-import * as path from "path";
-import * as util from "util";
 import * as vscode from "vscode";
 
+import { pickProcess } from "./commands/pick-process";
 import {
   LLDBDapDescriptorFactory,
   isExecutable,
 } from "./debug-adapter-factory";
 import { DisposableContext } from "./disposable-context";
+import { LLDBDapConfigurationProvider } from "./debug-configuration-provider";
 
 /**
  * This class represents the extension and manages its life cycle. Other extensions
@@ -15,6 +15,12 @@ import { DisposableContext } from "./disposable-context";
 export class LLDBDapExtension extends DisposableContext {
   constructor() {
     super();
+    this.pushSubscription(
+      vscode.debug.registerDebugConfigurationProvider(
+        "lldb-dap",
+        new LLDBDapConfigurationProvider(),
+      ),
+    );
     this.pushSubscription(
       vscode.debug.registerDebugAdapterDescriptorFactory(
         "lldb-dap",
@@ -38,6 +44,10 @@ export class LLDBDapExtension extends DisposableContext {
         }
       }),
     );
+
+    this.pushSubscription(
+      vscode.commands.registerCommand("lldb-dap.pickProcess", pickProcess),
+    );
   }
 }
 
diff --git a/lldb/tools/lldb-dap/src-ts/process-tree/base-process-tree.ts b/lldb/tools/lldb-dap/src-ts/process-tree/base-process-tree.ts
new file mode 100644
index 0000000000000..3c08f49035b35
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/process-tree/base-process-tree.ts
@@ -0,0 +1,102 @@
+import { ChildProcessWithoutNullStreams } from "child_process";
+import { Process, ProcessTree } from ".";
+import { Transform } from "stream";
+
+/** Parses process information from a given line of process output. */
+export type ProcessTreeParser = (line: string) => Process | undefined;
+
+/**
+ * Implements common behavior between the different {@link ProcessTree} implementations.
+ */
+export abstract class BaseProcessTree implements ProcessTree {
+  /**
+   * Spawn the process responsible for collecting all processes on the system.
+   */
+  protected abstract spawnProcess(): ChildProcessWithoutNullStreams;
+
+  /**
+   * Create a new parser that can read the process information from stdout of the process
+   * spawned by {@link spawnProcess spawnProcess()}.
+   */
+  protected abstract createParser(): ProcessTreeParser;
+
+  listAllProcesses(): Promise<Process[]> {
+    return new Promise<Process[]>((resolve, reject) => {
+      const proc = this.spawnProcess();
+      const parser = this.createParser();
+
+      // Capture processes from stdout
+      const processes: Process[] = [];
+      proc.stdout.pipe(new LineBasedStream()).on("data", (line) => {
+        const process = parser(line.toString());
+        if (process && process.id !== proc.pid) {
+          processes.push(process);
+        }
+      });
+
+      // Resolve or reject the promise based on exit code/signal/error
+      proc.on("error", reject);
+      proc.on("exit", (code, signal) => {
+        if (code === 0) {
+          resolve(processes);
+        } else if (signal) {
+          reject(
+            new Error(
+              `Unable to list processes: process exited due to signal ${signal}`,
+            ),
+          );
+        } else {
+          reject(
+            new Error(
+              `Unable to list processes: process exited with code ${code}`,
+            ),
+          );
+        }
+      });
+    });
+  }
+}
+
+/**
+ * A stream that emits each line as a single chunk of data. The end of a line is denoted
+ * by the newline character '\n'.
+ */
+export class LineBasedStream extends Transform {
+  private readonly newline: number = "\n".charCodeAt(0);
+  private buffer: Buffer = Buffer.alloc(0);
+
+  override _transform(
+    chunk: Buffer,
+    _encoding: string,
+    callback: () => void,
+  ): void {
+    let currentIndex = 0;
+    while (currentIndex < chunk.length) {
+      const newlineIndex = chunk.indexOf(this.newline, currentIndex);
+      if (newlineIndex === -1) {
+        this.buffer = Buffer.concat([
+          this.buffer,
+          chunk.subarray(currentIndex),
+        ]);
+        break;
+      }
+
+      const newlineChunk = chunk.subarray(currentIndex, newlineIndex);
+      const line = Buffer.concat([this.buffer, newlineChunk]);
+      this.push(line);
+      this.buffer = Buffer.alloc(0);
+
+      currentIndex = newlineIndex + 1;
+    }
+
+    callback();
+  }
+
+  override _flush(callback: () => void): void {
+    if (this.buffer.length) {
+      this.push(this.buffer);
+    }
+
+    callback();
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/process-tree/index.ts b/lldb/tools/lldb-dap/src-ts/process-tree/index.ts
new file mode 100644
index 0000000000000..9c46bc92d8548
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/process-tree/index.ts
@@ -0,0 +1,36 @@
+import { DarwinProcessTree } from "./platforms/darwin-process-tree";
+import { LinuxProcessTree } from "./platforms/linux-process-tree";
+import { WindowsProcessTree } from "./platforms/windows-process-tree";
+
+/**
+ * Represents a single process running on the system.
+ */
+export interface Process {
+  /** Process ID */
+  id: number;
+
+  /** Command that was used to start the process */
+  command: string;
+
+  /** The full command including arguments that was used to start the process */
+  arguments: string;
+
+  /** The date when the process was started */
+  start: number;
+}
+
+export interface ProcessTree {
+  listAllProcesses(): Promise<Process[]>;
+}
+
+/** Returns a {@link ProcessTree} based on the current platform. */
+export function createProcessTree(): ProcessTree {
+  switch (process.platform) {
+    case "darwin":
+      return new DarwinProcessTree();
+    case "win32":
+      return new WindowsProcessTree();
+    default:
+      return new LinuxProcessTree();
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/process-tree/platforms/darwin-process-tree.ts b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/darwin-process-tree.ts
new file mode 100644
index 0000000000000..954644288869e
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/darwin-process-tree.ts
@@ -0,0 +1,16 @@
+import { ChildProcessWithoutNullStreams, spawn } from "child_process";
+import { LinuxProcessTree } from "./linux-process-tree";
+
+function fill(prefix: string, suffix: string, length: number): string {
+  return prefix + suffix.repeat(length - prefix.length);
+}
+
+export class DarwinProcessTree extends LinuxProcessTree {
+  protected override spawnProcess(): ChildProcessWithoutNullStreams {
+    return spawn("ps", [
+      "-xo",
+      // The length of comm must be large enough or data will be truncated.
+      `pid=PID,lstart=START,comm=${fill("COMMAND", "-", 256)},command=ARGUMENTS`,
+    ]);
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/process-tree/platforms/linux-process-tree.ts b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/linux-process-tree.ts
new file mode 100644
index 0000000000000..65733f6c547b3
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/linux-process-tree.ts
@@ -0,0 +1,38 @@
+import { ChildProcessWithoutNullStreams, spawn } from "child_process";
+import { BaseProcessTree, ProcessTreeParser } from "../base-process-tree";
+
+export class LinuxProcessTree extends BaseProcessTree {
+  protected override spawnProcess(): ChildProcessWithoutNullStreams {
+    return spawn(
+      "ps",
+      ["-axo", `pid=PID,lstart=START,comm:128=COMMAND,command=ARGUMENTS`],
+      {
+        stdio: "pipe",
+      },
+    );
+  }
+
+  protected override createParser(): ProcessTreeParser {
+    let commandOffset: number | undefined;
+    let argumentsOffset: number | undefined;
+    return (line) => {
+      if (!commandOffset || !argumentsOffset) {
+        commandOffset = line.indexOf("COMMAND");
+        argumentsOffset = line.indexOf("ARGUMENTS");
+        return;
+      }
+
+      const pid = /^\s*([0-9]+)\s*/.exec(line);
+      if (!pid) {
+        return;
+      }
+
+      return {
+        id: Number(pid[1]),
+        command: line.slice(commandOffset, argumentsOffset).trim(),
+        arguments: line.slice(argumentsOffset).trim(),
+        start: Date.parse(line.slice(pid[0].length, commandOffset).trim()),
+      };
+    };
+  }
+}
diff --git a/lldb/tools/lldb-dap/src-ts/process-tree/platforms/windows-process-tree.ts b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/windows-process-tree.ts
new file mode 100644
index 0000000000000..9cfbfa29ab5d3
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/process-tree/platforms/windows-process-tree.ts
@@ -0,0 +1,52 @@
+import * as path from "path";
+import { BaseProcessTree, ProcessTreeParser } from "../base-process-tree";
+import { ChildProcessWithoutNullStreams, spawn } from "child_process";
+
+export class WindowsProcessTree extends BaseProcessTree {
+  protected override spawnProcess(): ChildProcessWithoutNullStreams {
+    const wmic = path.join(
+      process.env["WINDIR"] || "C:\\Windows",
+      "System32",
+      "wbem",
+      "WMIC.exe",
+    );
+    return spawn(
+      wmic,
+      ["process", "get", "CommandLine,CreationDate,ProcessId"],
+      { stdio: "pipe" },
+    );
+  }
+
+  protected override createParser(): ProcessTreeParser {
+    const lineRegex = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)$/;
+
+    return (line) => {
+      const matches = lineRegex.exec(line.trim());
+      if (!matches || matches.length !== 4) {
+        return;
+      }
+
+      const id = Number(matches[3]);
+      const start = Number(matches[2]);
+      let fullCommandLine = matches[1].trim();
+      if (isNaN(id) || !fullCommandLine) {
+        return;
+      }
+      // Extract the command from the full command line
+      let command = fullCommandLine;
+      if (fullCommandLine[0] === '"') {
+        const end = fullCommandLine.indexOf('"', 1);
+        if (end > 0) {
+          command = fullCommandLine.slice(1, end - 1);
+        }
+      } else {
+        const end = fullCommandLine.indexOf(" ");
+        if (end > 0) {
+          command = fullCommandLine.slice(0, end);
+        }
+      }
+
+      return { id, command, arguments: fullCommandLine, start };
+    };
+  }
+}

@JDevlieghere
Copy link
Member

It's a little unfortunate that listing processes is not part of the Debug Adapter Protocol. LLDB already knows how to do this through the host platform. I guess even more unfortunate is that neither node nor VSCode provides a cross platform API to do this.

I was expecting the implementation to be more convoluted but @matthewbastien did a great job abstracting the different platforms. This is definitely better than relying on an external package that's outside of our control.

One possible alternative would be to add a flag to the lldb-dap executable to print the processes (e.g. as JSON) and then use that. The upside is that we don't need to invoke different executables depending on the platform. The downside is that this now depends on a certain version of the lldb-dap binary and that we need to resolve the binary before we do this operation. I'm just floating the idea, I'm not sure if that's actually better or not.

@matthewbastien
Copy link
Contributor Author

Yeah, the CodeLLDB extension uses lldb to print the processes for the current platform (which could be a possibility as well, though then you'd also have to have an option to specify the lldb executable alongside lldb-dap). I'd rather have a NodeJS approach if only because relying on a specific version of lldb-dap won't really work for Swift until it updates its version of lldb-dap meaning that the functionality probably won't be available until Swift 6.2. I'd prefer to have it available sooner since we'd like to switch away from CodeLLDB as soon as possible.

@matthewbastien
Copy link
Contributor Author

Though now that I think about it further: we could take a hybrid approach where this PR is merged and then the functionality to list processes is added to lldb-dap. When Swift 6.2 gets released (or whichever version contains the lldb-dap change) the extension can switch to using that. I agree with you that it would be a lot cleaner for the extension to rely on existing lldb functionality for listing processes.

Just thinking out loud.

@matthewbastien
Copy link
Contributor Author

And more out loud thinking: the other issue to overcome with using lldb-dap for process listing in Swift is that the Swift extension currently uses the debugAdapterExecutable property in the debug configuration to tell the LLDB DAP extension where to find lldb-dap. Unfortunately, the process picking command does not have access to this property and would pick the wrong lldb-dap in the best case, or fail to find lldb-dap at all at worst. I'd like to avoid having to update user settings to tell the LLDB DAP extension where to find lldb-dap.

@JDevlieghere
Copy link
Member

Thanks for the additional perspective. I was on the fence when I mentioned it and it sounds like sticking with the TS approach avoids some complications, plus it matches what other VSCode plugins are doing.

@JDevlieghere
Copy link
Member

Adding Adrian and Walter as reviewers and tagging @da-viper as they might want to take a look. To me this looks good.

@JDevlieghere JDevlieghere requested a review from ashgti February 28, 2025 15:29
@matthewbastien matthewbastien force-pushed the lldb-dap-process-picker branch 2 times, most recently from c015ed9 to 2e68d89 Compare February 28, 2025 19:33
@matthewbastien matthewbastien force-pushed the lldb-dap-process-picker branch from 2e68d89 to f4407b2 Compare February 28, 2025 19:38
@da-viper
Copy link
Contributor

da-viper commented Mar 1, 2025

you could also validate the pid input in package.json from

"pid": {
"type": [
"number",
"string"
],
"description": "System process ID to attach to."

to

"pid": {
  "anyOf": [
    {
      "type": "number",
      "description": "System process ID to attach to."
    },
    {
      "const": "${command:pickProcess}",
      "description": "Picks a process ID using the process ID picker"
    }
  ]
},

Copy link
Member

@walter-erquinigo walter-erquinigo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can go one level beyond this and provide a way to attach to programs without needing launch.json configs at all!

  1. Create a new pallete command "attach to process"
  2. Reuse the same process picker and once the user has selected one item, issue a vscode.debug.StartDebugging() call passing a minimal debug config with the selected process as PID.

@matthewbastien matthewbastien force-pushed the lldb-dap-process-picker branch from 4137b52 to 0a615f2 Compare March 6, 2025 22:06
@vogelsgesang
Copy link
Member

Is there a way we can use the LLDB platform layer to pick processes?

See discussion further up in the thread (first comment and follow-up messages). It seems the final blocker was that we didn't have access to the debugAdapterExecutable variable - which turned out to be wrong, we do actually have access to that variable, also see this comment.

That being said, I am not sure if it's worth using the lldb layer for that...

This code, IIUC is just running a local command to pick the process from the current host. But people might be debugging remotely by connecting to a remote platform.

When you SSH-connect into a remote host, the complete lldb-dap VS-Code extension is actually installed in the remote host, not on your local machine. As such, the call to ps actually happens on the remote host, and lists the process from the remote host. So at least from that point of view, I think we are fine.

However, if the user does connect remotely from lldb via gdbserver to some other host, then we would indeed be running the ps command on the wrong machine. Not sure if we even support this today, though...

Does

{
  "name": "Local Debug Server",
  "type": "lldb-dap",
  "request": "attach",
   "gdb-remote-hostname": "hostname",
  "gdb-remote-port": 2345,
  "pid": 1234
}

currently even work? Also, does the gdb-remote protocol even offer capabilities for process-picking? (Not sure, I seldomly used gdb-remote functionality so far, except for connect to rr - for which I am impatiently awaiting first-class lldb-support 🙂 )

!("pid" in debugConfiguration) &&
!("program" in debugConfiguration)
) {
debugConfiguration.pid = "${command:pickProcess}";
Copy link
Member

@vogelsgesang vogelsgesang Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we mark pid as optional in the the package.json and the README.md?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated both of these to better reflect my changes. I did a pretty major overhaul of the README section on attaching and would appreciate another thorough review there to make sure the wording sounds good.

@clayborg
Copy link
Collaborator

clayborg commented Mar 7, 2025

Is there a way we can use the LLDB platform layer to pick processes?

See discussion further up in the thread (first comment and follow-up messages). It seems the final blocker was that we didn't have access to the debugAdapterExecutable variable - which turned out to be wrong, we do actually have access to that variable, also see this comment.

That being said, I am not sure if it's worth using the lldb layer for that...

This code, IIUC is just running a local command to pick the process from the current host. But people might be debugging remotely by connecting to a remote platform.

When you SSH-connect into a remote host, the complete lldb-dap VS-Code extension is actually installed in the remote host, not on your local machine. As such, the call to ps actually happens on the remote host, and lists the process from the remote host. So at least from that point of view, I think we are fine.

However, if the user does connect remotely from lldb via gdbserver to some other host, then we would indeed be running the ps command on the wrong machine. Not sure if we even support this today, though...

Does

{
  "name": "Local Debug Server",
  "type": "lldb-dap",
  "request": "attach",
   "gdb-remote-hostname": "hostname",
  "gdb-remote-port": 2345,
  "pid": 1234
}

currently even work?

Yes it works. LLDB actually always uses GDB remote for all local debug sessions by spawning a lldb-server to debug the binary, so yes remote debugging is very well supported in lldb.

Also, does the gdb-remote protocol even offer capabilities for process-picking? (Not sure, I seldomly used gdb-remote functionality so far, except for connect to rr - for which I am impatiently awaiting first-class lldb-support 🙂 )

Our lldb-server can be run in two modes: debug or platform. When running in platform mode it does give us the connection we need to the remote server. So yes this can and does work. You can spawn a GDB remote platform on a remote linux machine with lldb-server platform ..., then you can platform connection <host>:<port> to connect with it.

@matthewbastien
Copy link
Contributor Author

matthewbastien commented Mar 7, 2025

I agree that it would be cleaner to just use LLDB's process listing functionality and not have all this extra code. My major concern about the lack of access to the launch configuration ended up not actually being an issue. However, I still have a few questions about how this would work.

lldb-dap does not currently support executing the platform process list command, but we could add that functionality. However, this would delay the support for process picking in Swift as its version of LLVM tends to lag behind the latest. I'd like to support process picking for older versions of lldb-dap because of this.

The fallback would be to launch lldb, issue the platform process list command, and parse the result (kind of like how the CodeLLDB extension does it). However, the extension only knows about the location of lldb-dap from the settings. Would we need an additional setting then for specifying the lldb executable path or just assume it to be next to lldb-dap? The Swift extension would definitely require that there be a debug configuration option for this as well as its version of lldb and lldb-dap will almost certainly not match the system's on platforms other than macOS.

I also don't actually see any explicit support for remote attaching in the LLDB DAP extension (other than specifying custom attach/launch commands, but correct me if I'm wrong). I think adding support for that is out of scope for this PR and should be its own issue entirely. Though using the lldb platform process listing in this PR will make supporting remote process listing a lot easier down the road.

edit: there is an explanation in the README about attaching to a remote debug server.

@walter-erquinigo
Copy link
Member

I have an idea for using lldb's process list logic, although it's a bit of code.

  1. Don't use "${command:pickProcess}" at all. We need to bypass the regular command logic. You could use a wildcard like ${pickProcess}.
  2. In the debug config resolution logic, which is invoked before launching the debugger, you look for this wildcard. If you see it, then you programmatically invoke lldb-dap (you know its path at this point), and then invoke it with a new flag --list-processes that will just dump the list of processes in JSON format to stdout. With this information, you can show a custom made picker and await the result.
  3. With the result, you can then modify the debug configuration and resume the debug process.

We shouldn't invoke lldb directly because we just don't know where it is, and it might be error-prone to try to find it. Instead, it might be better to simply add a flag to lldb-dap for this, which seems to be kind of appropriate for this kind of flow.

Let me know what you think.

@vogelsgesang
Copy link
Member

Don't use "${command:pickProcess}" at all. We need to bypass the regular command logic. You could use a wildcard like ${pickProcess}.

Why would we not use ${command:pickProcess}? Why bypass the regular command logic? We can already determine the lldb-dap path inside the command (also see #128943 (comment))

[...] you programmatically invoke lldb-dap (you know its path at this point), and then invoke it with a new flag --list-processes that will just dump the list of processes in JSON format to stdout.

Note that we would also need to pass through parameters like gdb-remote-hostname and gdb-remote-port such that the --list-processes would reach out to the right gdb/lldbserver.

All in all, this sounds like a good long-term direction to me.

However, this would delay the support for process picking in Swift as its version of LLVM tends to lag behind the latest. I'd like to support process picking for older versions of lldb-dap because of this.

@matthewbastien I am not sure I understand that concern. Looking at the commit log of swiftlang/llvm-project, it seems that they are picking up changes to lldb-dap very quickly. Or am I missing something?

@matthewbastien
Copy link
Contributor Author

I am not sure I understand that concern. Looking at the commit log of swiftlang/llvm-project, it seems that they are picking up changes to lldb-dap very quickly. Or am I missing something?

Swift uses specific branches of the name stable/* for its fork of llvm-project. At the moment, the Swift main branch builds use stable/20240723 which has very few of the recent changes to llvm/llvm-project in it (and anything new needs to be cherry-picked in). Additionally, release builds of Swift are locked to a swift/release/* branch. Currently, Swift 6.1 is locked except for major bug fixes and uses the swift/release/6.1 branch for builds.

That being said, I still agree that adding the process picking functionality to lldb-dap makes the most sense in the long run. Especially for cases where remote debugging is being used. I'm going to add the NodeJS process picker to the Swift extension in order to support attach requests in Swift 6.0, but will work on updating this PR with the lldb-dap platform process list approach afterwards.

Once this gets into Swift's lldb-dap then that extension can use the process picker from this PR. I think that's a happy medium in the meantime.

@matthewbastien
Copy link
Contributor Author

One more thing that crossed my mind: given that the LLDB DAP extension will automatically be updated to the latest version by VS Code, should we also have feature flags for things like this in lldb-dap? (or is there some way already to detect which features are available?) The version of lldb-dap the user has installed will most likely not be the the absolute latest. In fact, I recently encountered an issue with the new lldb-dap.serverMode setting where enabling it caused the version of lldb-dap I have installed to hang since it doesn't actually support the --connection command line option.

@vogelsgesang
Copy link
Member

should we also have feature flags for things like this in lldb-dap?

IMO, lldb-dap itself should exit with an error code in case of unsupported command line options. We might want to use a dedicated exit code for that, in order to differentiate it from other errors, where lldb-dap understands the command but execution fails for some other reason.

The VS-Code extension should be prepared to handle this exit code accordingly. For the process picker, it should probably display an error "Picking a process for attaching is not yet supported by this lldb-dap binary. Please upgrade to a newer lldb-dap binary.".

The error message is still slightly misleading, because at least for the next 6 months, i.e. until lldb-dap 21, there will not be any release of lldb-dap which is recent enough. But I think this is good still good enough, given that we still have relatively few users (as evident by comparing the download numbers of lldb dap against vscode-lldb and Microsofts C++ debugger). Long-term we might need a better compatibility story, though...

In fact, I recently encountered an issue with the new lldb-dap.serverMode setting where enabling it caused the version of lldb-dap I have installed to hang since it doesn't actually support the --connection command line option.

We probably want to update the Typescript code to check if lldb-dap actually support the --connection parameter in the onDidChangeConfiguration event for the serverMode setting 🤔 WDYT, @ashgti? Is this something we should also track under #129283?

@ashgti
Copy link
Contributor

ashgti commented Mar 11, 2025

In fact, I recently encountered an issue with the new lldb-dap.serverMode setting where enabling it caused the version of lldb-dap I have installed to hang since it doesn't actually support the --connection command line option.

We probably want to update the Typescript code to check if lldb-dap actually support the --connection parameter in the onDidChangeConfiguration event for the serverMode setting 🤔 WDYT, @ashgti? Is this something we should also track under #129283?

Thats a good catch, I added a note to #129283 that we should validate the binary supports server mode.

@ashgti
Copy link
Contributor

ashgti commented Mar 11, 2025

Created #130855 to validate the binary before we try to use server mode.

@vogelsgesang
Copy link
Member

vogelsgesang commented Mar 12, 2025

Created #130855 to validate the binary before we try to use server mode.

Looks great, thanks! The approach in that commit (using --help to check if a given option is supported by lldb-dap) is probably also how to what we should do for the --list-processes option

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[lldb-dap] impl ${command:pickMyProcess} please
8 participants