[Lldb-commits] [lldb] [lldb-dap] Add process picker command to VS Code extension (PR #128943)
Matthew Bastien via lldb-commits
lldb-commits at lists.llvm.org
Wed Feb 26 12:47:13 PST 2025
https://github.com/matthewbastien created https://github.com/llvm/llvm-project/pull/128943
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:
```json
{
"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
>From b9083ea16c7b1dba70cc04acf78f5001f0fb86c6 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Wed, 26 Feb 2025 11:18:21 -0500
Subject: [PATCH 1/2] add a process picker for attaching by PID
---
lldb/tools/lldb-dap/package.json | 30 +++++-
.../lldb-dap/src-ts/commands/pick-process.ts | 37 +++++++
lldb/tools/lldb-dap/src-ts/extension.ts | 7 +-
.../src-ts/process-tree/base-process-tree.ts | 102 ++++++++++++++++++
.../lldb-dap/src-ts/process-tree/index.ts | 36 +++++++
.../platforms/darwin-process-tree.ts | 16 +++
.../platforms/linux-process-tree.ts | 38 +++++++
.../platforms/windows-process-tree.ts | 52 +++++++++
8 files changed, 315 insertions(+), 3 deletions(-)
create mode 100644 lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/process-tree/base-process-tree.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/process-tree/index.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/process-tree/platforms/darwin-process-tree.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/process-tree/platforms/linux-process-tree.ts
create mode 100644 lldb/tools/lldb-dap/src-ts/process-tree/platforms/windows-process-tree.ts
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..b83e749e7da7b
--- /dev/null
+++ b/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
@@ -0,0 +1,37 @@
+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.
+ *
+ * @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/extension.ts b/lldb/tools/lldb-dap/src-ts/extension.ts
index 71fd48298f8f5..3532a2143155b 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -1,7 +1,6 @@
-import * as path from "path";
-import * as util from "util";
import * as vscode from "vscode";
+import { pickProcess } from "./commands/pick-process";
import {
LLDBDapDescriptorFactory,
isExecutable,
@@ -38,6 +37,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 };
+ };
+ }
+}
>From b4238421d732437fd01d3ee8658f72e2a3805232 Mon Sep 17 00:00:00 2001
From: Matthew Bastien <matthew_bastien at apple.com>
Date: Wed, 26 Feb 2025 15:16:05 -0500
Subject: [PATCH 2/2] convert pid to a number so that lldb-dap can properly
consume it
---
.../lldb-dap/src-ts/commands/pick-process.ts | 4 ++
.../src-ts/debug-configuration-provider.ts | 54 +++++++++++++++++++
lldb/tools/lldb-dap/src-ts/extension.ts | 7 +++
3 files changed, 65 insertions(+)
create mode 100644 lldb/tools/lldb-dap/src-ts/debug-configuration-provider.ts
diff --git a/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts b/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
index b83e749e7da7b..355d508075080 100644
--- a/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
+++ b/lldb/tools/lldb-dap/src-ts/commands/pick-process.ts
@@ -9,6 +9,10 @@ interface ProcessQuickPick extends vscode.QuickPickItem {
/**
* 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> {
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 3532a2143155b..74815022d468a 100644
--- a/lldb/tools/lldb-dap/src-ts/extension.ts
+++ b/lldb/tools/lldb-dap/src-ts/extension.ts
@@ -6,6 +6,7 @@ import {
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
@@ -14,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",
More information about the lldb-commits
mailing list