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_bast...@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_bast...@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", _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits