Skip to content

Commit b9083ea

Browse files
add a process picker for attaching by PID
1 parent a955426 commit b9083ea

File tree

8 files changed

+315
-3
lines changed

8 files changed

+315
-3
lines changed

lldb/tools/lldb-dap/package.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@
146146
"windows": {
147147
"program": "./bin/lldb-dap.exe"
148148
},
149+
"variables": {
150+
"PickProcess": "lldb-dap.pickProcess"
151+
},
149152
"configurationAttributes": {
150153
"launch": {
151154
"required": [
@@ -517,6 +520,16 @@
517520
"cwd": "^\"\\${workspaceRoot}\""
518521
}
519522
},
523+
{
524+
"label": "LLDB: Attach to Process",
525+
"description": "",
526+
"body": {
527+
"type": "lldb-dap",
528+
"request": "attach",
529+
"name": "${1:Attach}",
530+
"pid": "^\"\\${command:PickProcess}\""
531+
}
532+
},
520533
{
521534
"label": "LLDB: Attach",
522535
"description": "",
@@ -541,6 +554,21 @@
541554
}
542555
]
543556
}
544-
]
557+
],
558+
"commands": [
559+
{
560+
"command": "lldb-dap.pickProcess",
561+
"title": "Pick Process",
562+
"category": "LLDB DAP"
563+
}
564+
],
565+
"menus": {
566+
"commandPalette": [
567+
{
568+
"command": "lldb-dap.pickProcess",
569+
"when": "false"
570+
}
571+
]
572+
}
545573
}
546574
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as path from "path";
2+
import * as vscode from "vscode";
3+
import { createProcessTree } from "../process-tree";
4+
5+
interface ProcessQuickPick extends vscode.QuickPickItem {
6+
processId: number;
7+
}
8+
9+
/**
10+
* Prompts the user to select a running process.
11+
*
12+
* @returns The pid of the process as a string or undefined if cancelled.
13+
*/
14+
export async function pickProcess(): Promise<string | undefined> {
15+
const processTree = createProcessTree();
16+
const selectedProcess = await vscode.window.showQuickPick<ProcessQuickPick>(
17+
processTree.listAllProcesses().then((processes): ProcessQuickPick[] => {
18+
return processes
19+
.sort((a, b) => b.start - a.start) // Sort by start date in descending order
20+
.map((proc) => {
21+
return {
22+
processId: proc.id,
23+
label: path.basename(proc.command),
24+
description: proc.id.toString(),
25+
detail: proc.arguments,
26+
} satisfies ProcessQuickPick;
27+
});
28+
}),
29+
{
30+
placeHolder: "Select a process to attach the debugger to",
31+
},
32+
);
33+
if (!selectedProcess) {
34+
return;
35+
}
36+
return selectedProcess.processId.toString();
37+
}

lldb/tools/lldb-dap/src-ts/extension.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import * as path from "path";
2-
import * as util from "util";
31
import * as vscode from "vscode";
42

3+
import { pickProcess } from "./commands/pick-process";
54
import {
65
LLDBDapDescriptorFactory,
76
isExecutable,
@@ -38,6 +37,10 @@ export class LLDBDapExtension extends DisposableContext {
3837
}
3938
}),
4039
);
40+
41+
this.pushSubscription(
42+
vscode.commands.registerCommand("lldb-dap.pickProcess", pickProcess),
43+
);
4144
}
4245
}
4346

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { ChildProcessWithoutNullStreams } from "child_process";
2+
import { Process, ProcessTree } from ".";
3+
import { Transform } from "stream";
4+
5+
/** Parses process information from a given line of process output. */
6+
export type ProcessTreeParser = (line: string) => Process | undefined;
7+
8+
/**
9+
* Implements common behavior between the different {@link ProcessTree} implementations.
10+
*/
11+
export abstract class BaseProcessTree implements ProcessTree {
12+
/**
13+
* Spawn the process responsible for collecting all processes on the system.
14+
*/
15+
protected abstract spawnProcess(): ChildProcessWithoutNullStreams;
16+
17+
/**
18+
* Create a new parser that can read the process information from stdout of the process
19+
* spawned by {@link spawnProcess spawnProcess()}.
20+
*/
21+
protected abstract createParser(): ProcessTreeParser;
22+
23+
listAllProcesses(): Promise<Process[]> {
24+
return new Promise<Process[]>((resolve, reject) => {
25+
const proc = this.spawnProcess();
26+
const parser = this.createParser();
27+
28+
// Capture processes from stdout
29+
const processes: Process[] = [];
30+
proc.stdout.pipe(new LineBasedStream()).on("data", (line) => {
31+
const process = parser(line.toString());
32+
if (process && process.id !== proc.pid) {
33+
processes.push(process);
34+
}
35+
});
36+
37+
// Resolve or reject the promise based on exit code/signal/error
38+
proc.on("error", reject);
39+
proc.on("exit", (code, signal) => {
40+
if (code === 0) {
41+
resolve(processes);
42+
} else if (signal) {
43+
reject(
44+
new Error(
45+
`Unable to list processes: process exited due to signal ${signal}`,
46+
),
47+
);
48+
} else {
49+
reject(
50+
new Error(
51+
`Unable to list processes: process exited with code ${code}`,
52+
),
53+
);
54+
}
55+
});
56+
});
57+
}
58+
}
59+
60+
/**
61+
* A stream that emits each line as a single chunk of data. The end of a line is denoted
62+
* by the newline character '\n'.
63+
*/
64+
export class LineBasedStream extends Transform {
65+
private readonly newline: number = "\n".charCodeAt(0);
66+
private buffer: Buffer = Buffer.alloc(0);
67+
68+
override _transform(
69+
chunk: Buffer,
70+
_encoding: string,
71+
callback: () => void,
72+
): void {
73+
let currentIndex = 0;
74+
while (currentIndex < chunk.length) {
75+
const newlineIndex = chunk.indexOf(this.newline, currentIndex);
76+
if (newlineIndex === -1) {
77+
this.buffer = Buffer.concat([
78+
this.buffer,
79+
chunk.subarray(currentIndex),
80+
]);
81+
break;
82+
}
83+
84+
const newlineChunk = chunk.subarray(currentIndex, newlineIndex);
85+
const line = Buffer.concat([this.buffer, newlineChunk]);
86+
this.push(line);
87+
this.buffer = Buffer.alloc(0);
88+
89+
currentIndex = newlineIndex + 1;
90+
}
91+
92+
callback();
93+
}
94+
95+
override _flush(callback: () => void): void {
96+
if (this.buffer.length) {
97+
this.push(this.buffer);
98+
}
99+
100+
callback();
101+
}
102+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { DarwinProcessTree } from "./platforms/darwin-process-tree";
2+
import { LinuxProcessTree } from "./platforms/linux-process-tree";
3+
import { WindowsProcessTree } from "./platforms/windows-process-tree";
4+
5+
/**
6+
* Represents a single process running on the system.
7+
*/
8+
export interface Process {
9+
/** Process ID */
10+
id: number;
11+
12+
/** Command that was used to start the process */
13+
command: string;
14+
15+
/** The full command including arguments that was used to start the process */
16+
arguments: string;
17+
18+
/** The date when the process was started */
19+
start: number;
20+
}
21+
22+
export interface ProcessTree {
23+
listAllProcesses(): Promise<Process[]>;
24+
}
25+
26+
/** Returns a {@link ProcessTree} based on the current platform. */
27+
export function createProcessTree(): ProcessTree {
28+
switch (process.platform) {
29+
case "darwin":
30+
return new DarwinProcessTree();
31+
case "win32":
32+
return new WindowsProcessTree();
33+
default:
34+
return new LinuxProcessTree();
35+
}
36+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
2+
import { LinuxProcessTree } from "./linux-process-tree";
3+
4+
function fill(prefix: string, suffix: string, length: number): string {
5+
return prefix + suffix.repeat(length - prefix.length);
6+
}
7+
8+
export class DarwinProcessTree extends LinuxProcessTree {
9+
protected override spawnProcess(): ChildProcessWithoutNullStreams {
10+
return spawn("ps", [
11+
"-xo",
12+
// The length of comm must be large enough or data will be truncated.
13+
`pid=PID,lstart=START,comm=${fill("COMMAND", "-", 256)},command=ARGUMENTS`,
14+
]);
15+
}
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
2+
import { BaseProcessTree, ProcessTreeParser } from "../base-process-tree";
3+
4+
export class LinuxProcessTree extends BaseProcessTree {
5+
protected override spawnProcess(): ChildProcessWithoutNullStreams {
6+
return spawn(
7+
"ps",
8+
["-axo", `pid=PID,lstart=START,comm:128=COMMAND,command=ARGUMENTS`],
9+
{
10+
stdio: "pipe",
11+
},
12+
);
13+
}
14+
15+
protected override createParser(): ProcessTreeParser {
16+
let commandOffset: number | undefined;
17+
let argumentsOffset: number | undefined;
18+
return (line) => {
19+
if (!commandOffset || !argumentsOffset) {
20+
commandOffset = line.indexOf("COMMAND");
21+
argumentsOffset = line.indexOf("ARGUMENTS");
22+
return;
23+
}
24+
25+
const pid = /^\s*([0-9]+)\s*/.exec(line);
26+
if (!pid) {
27+
return;
28+
}
29+
30+
return {
31+
id: Number(pid[1]),
32+
command: line.slice(commandOffset, argumentsOffset).trim(),
33+
arguments: line.slice(argumentsOffset).trim(),
34+
start: Date.parse(line.slice(pid[0].length, commandOffset).trim()),
35+
};
36+
};
37+
}
38+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as path from "path";
2+
import { BaseProcessTree, ProcessTreeParser } from "../base-process-tree";
3+
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
4+
5+
export class WindowsProcessTree extends BaseProcessTree {
6+
protected override spawnProcess(): ChildProcessWithoutNullStreams {
7+
const wmic = path.join(
8+
process.env["WINDIR"] || "C:\\Windows",
9+
"System32",
10+
"wbem",
11+
"WMIC.exe",
12+
);
13+
return spawn(
14+
wmic,
15+
["process", "get", "CommandLine,CreationDate,ProcessId"],
16+
{ stdio: "pipe" },
17+
);
18+
}
19+
20+
protected override createParser(): ProcessTreeParser {
21+
const lineRegex = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)$/;
22+
23+
return (line) => {
24+
const matches = lineRegex.exec(line.trim());
25+
if (!matches || matches.length !== 4) {
26+
return;
27+
}
28+
29+
const id = Number(matches[3]);
30+
const start = Number(matches[2]);
31+
let fullCommandLine = matches[1].trim();
32+
if (isNaN(id) || !fullCommandLine) {
33+
return;
34+
}
35+
// Extract the command from the full command line
36+
let command = fullCommandLine;
37+
if (fullCommandLine[0] === '"') {
38+
const end = fullCommandLine.indexOf('"', 1);
39+
if (end > 0) {
40+
command = fullCommandLine.slice(1, end - 1);
41+
}
42+
} else {
43+
const end = fullCommandLine.indexOf(" ");
44+
if (end > 0) {
45+
command = fullCommandLine.slice(0, end);
46+
}
47+
}
48+
49+
return { id, command, arguments: fullCommandLine, start };
50+
};
51+
}
52+
}

0 commit comments

Comments
 (0)