Skip to content

Commit 20e7346

Browse files
add process picker
1 parent 79c1688 commit 20e7346

File tree

8 files changed

+350
-2
lines changed

8 files changed

+350
-2
lines changed

package.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@
271271
"title": "Run Until Failure...",
272272
"category": "Swift"
273273
},
274+
{
275+
"command": "swift.pickProcess",
276+
"title": "Pick Process...",
277+
"category": "Swift"
278+
},
274279
{
275280
"command": "swift.runAllTestsParallel",
276281
"title": "Run Tests in Parallel",
@@ -957,6 +962,10 @@
957962
"command": "swift.reindexProject",
958963
"when": "swift.supportsReindexing"
959964
},
965+
{
966+
"command": "swift.pickProcess",
967+
"when": "false"
968+
},
960969
{
961970
"command": "swift.runAllTestsParallel",
962971
"when": "swift.isActivated"
@@ -1359,7 +1368,7 @@
13591368
"type": "swift",
13601369
"label": "Swift Debugger",
13611370
"variables": {
1362-
"PickProcess": "lldb-dap.pickProcess"
1371+
"pickProcess": "swift.pickProcess"
13631372
},
13641373
"configurationAttributes": {
13651374
"launch": {
@@ -1586,7 +1595,7 @@
15861595
"type": "swift",
15871596
"request": "attach",
15881597
"name": "${1:Attach to Swift Executable}",
1589-
"pid": "^\"\\${command:PickProcess}\""
1598+
"pid": "^\"\\${command:pickProcess}\""
15901599
}
15911600
},
15921601
{

src/commands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { runAllTests } from "./commands/runAllTests";
4343
import { updateDependenciesViewList } from "./commands/dependencies/updateDepViewList";
4444
import { runTask } from "./commands/runTask";
4545
import { TestKind } from "./TestExplorer/TestKind";
46+
import { pickProcess } from "./commands/pickProcess";
4647

4748
/**
4849
* References:
@@ -65,6 +66,9 @@ export function registerToolchainCommands(
6566
vscode.commands.registerCommand("swift.selectToolchain", () =>
6667
showToolchainSelectionQuickPick(toolchain)
6768
),
69+
vscode.commands.registerCommand("swift.pickProcess", configuration =>
70+
pickProcess(configuration)
71+
),
6872
];
6973
}
7074

src/commands/pickProcess.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as path from "path";
16+
import * as vscode from "vscode";
17+
import { createProcessTree } from "../process-tree";
18+
19+
interface ProcessQuickPick extends vscode.QuickPickItem {
20+
processId?: number;
21+
}
22+
23+
/**
24+
* Prompts the user to select a running process.
25+
*
26+
* The return value must be a string so that it is compatible with VS Code's
27+
* string substitution infrastructure. The value will eventually be converted
28+
* to a number by the debug configuration provider.
29+
*
30+
* @param configuration The related debug configuration, if any
31+
* @returns The pid of the process as a string or undefined if cancelled.
32+
*/
33+
export async function pickProcess(
34+
configuration?: vscode.DebugConfiguration
35+
): Promise<string | undefined> {
36+
const processTree = createProcessTree();
37+
const selectedProcess = await vscode.window.showQuickPick<ProcessQuickPick>(
38+
processTree.listAllProcesses().then((processes): ProcessQuickPick[] => {
39+
// Sort by start date in descending order
40+
processes.sort((a, b) => b.start - a.start);
41+
// Filter by program if requested
42+
if (typeof configuration?.program === "string") {
43+
const program = configuration.program;
44+
const programBaseName = path.basename(program);
45+
processes = processes
46+
.filter(proc => path.basename(proc.command) === programBaseName)
47+
.sort((a, b) => {
48+
// Bring exact command matches to the top
49+
const aIsExactMatch = a.command === program ? 1 : 0;
50+
const bIsExactMatch = b.command === program ? 1 : 0;
51+
return bIsExactMatch - aIsExactMatch;
52+
});
53+
// Show a better message if all processes were filtered out
54+
if (processes.length === 0) {
55+
return [
56+
{
57+
label: "No processes matched the debug configuration's program",
58+
},
59+
];
60+
}
61+
}
62+
// Convert to a QuickPickItem
63+
return processes.map(proc => {
64+
return {
65+
processId: proc.id,
66+
label: path.basename(proc.command),
67+
description: proc.id.toString(),
68+
detail: proc.arguments,
69+
} satisfies ProcessQuickPick;
70+
});
71+
}),
72+
{
73+
placeHolder: "Select a process to attach the debugger to",
74+
matchOnDetail: true,
75+
matchOnDescription: true,
76+
}
77+
);
78+
return selectedProcess?.processId?.toString();
79+
}

src/process-tree/BaseProcessTree.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as util from "util";
16+
import * as child_process from "child_process";
17+
import { Process, ProcessTree } from ".";
18+
19+
const exec = util.promisify(child_process.execFile);
20+
21+
/** Parses process information from a given line of process output. */
22+
export type ProcessTreeParser = (line: string) => Process | undefined;
23+
24+
/**
25+
* Implements common behavior between the different {@link ProcessTree} implementations.
26+
*/
27+
export abstract class BaseProcessTree implements ProcessTree {
28+
/**
29+
* Get the command responsible for collecting all processes on the system.
30+
*/
31+
protected abstract getCommand(): string;
32+
33+
/**
34+
* Get the list of arguments used to launch the command.
35+
*/
36+
protected abstract getCommandArguments(): string[];
37+
38+
/**
39+
* Create a new parser that can read the process information from stdout of the process
40+
* spawned by {@link spawnProcess spawnProcess()}.
41+
*/
42+
protected abstract createParser(): ProcessTreeParser;
43+
44+
async listAllProcesses(): Promise<Process[]> {
45+
const execCommand = exec(this.getCommand(), this.getCommandArguments());
46+
const parser = this.createParser();
47+
return (await execCommand).stdout.split("\n").flatMap(line => {
48+
const process = parser(line.toString());
49+
if (!process || process.id === execCommand.child.pid) {
50+
return [];
51+
}
52+
return [process];
53+
});
54+
}
55+
}

src/process-tree/index.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { DarwinProcessTree } from "./platforms/DarwinProcessTree";
16+
import { LinuxProcessTree } from "./platforms/LinuxProcessTree";
17+
import { WindowsProcessTree } from "./platforms/WindowsProcessTree";
18+
19+
/**
20+
* Represents a single process running on the system.
21+
*/
22+
export interface Process {
23+
/** Process ID */
24+
id: number;
25+
26+
/** Command that was used to start the process */
27+
command: string;
28+
29+
/** The full command including arguments that was used to start the process */
30+
arguments: string;
31+
32+
/** The date when the process was started */
33+
start: number;
34+
}
35+
36+
export interface ProcessTree {
37+
listAllProcesses(): Promise<Process[]>;
38+
}
39+
40+
/** Returns a {@link ProcessTree} based on the current platform. */
41+
export function createProcessTree(): ProcessTree {
42+
switch (process.platform) {
43+
case "darwin":
44+
return new DarwinProcessTree();
45+
case "win32":
46+
return new WindowsProcessTree();
47+
default:
48+
return new LinuxProcessTree();
49+
}
50+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { LinuxProcessTree } from "./LinuxProcessTree";
16+
17+
export class DarwinProcessTree extends LinuxProcessTree {
18+
protected override getCommandArguments(): string[] {
19+
return [
20+
"-axo",
21+
// The length of comm must be large enough or data will be truncated.
22+
`pid=PID,state=STATE,lstart=START,comm=${"COMMAND".padEnd(256, "-")},args=ARGUMENTS`,
23+
];
24+
}
25+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { BaseProcessTree, ProcessTreeParser } from "../BaseProcessTree";
16+
17+
export class LinuxProcessTree extends BaseProcessTree {
18+
protected override getCommand(): string {
19+
return "ps";
20+
}
21+
22+
protected override getCommandArguments(): string[] {
23+
return [
24+
"-axo",
25+
// The length of exe must be large enough or data will be truncated.
26+
`pid=PID,state=STATE,lstart=START,exe:128=COMMAND,args=ARGUMENTS`,
27+
];
28+
}
29+
30+
protected override createParser(): ProcessTreeParser {
31+
let commandOffset: number | undefined;
32+
let argumentsOffset: number | undefined;
33+
return line => {
34+
if (!commandOffset || !argumentsOffset) {
35+
commandOffset = line.indexOf("COMMAND");
36+
argumentsOffset = line.indexOf("ARGUMENTS");
37+
return;
38+
}
39+
40+
const pidAndState = /^\s*([0-9]+)\s+([a-zA-Z<>+]+)\s+/.exec(line);
41+
if (!pidAndState) {
42+
return;
43+
}
44+
45+
// Make sure the process isn't in a trace/debug or zombie state as we cannot attach to them
46+
const state = pidAndState[2];
47+
if (state.includes("X") || state.includes("Z")) {
48+
return;
49+
}
50+
51+
// ps will list "-" as the command if it does not know where the executable is located
52+
const command = line.slice(commandOffset, argumentsOffset).trim();
53+
if (command === "-") {
54+
return;
55+
}
56+
57+
return {
58+
id: Number(pidAndState[1]),
59+
command,
60+
arguments: line.slice(argumentsOffset).trim(),
61+
start: Date.parse(line.slice(pidAndState[0].length, commandOffset).trim()),
62+
};
63+
};
64+
}
65+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2025 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import { BaseProcessTree, ProcessTreeParser } from "../BaseProcessTree";
16+
17+
export class WindowsProcessTree extends BaseProcessTree {
18+
protected override getCommand(): string {
19+
return "PowerShell";
20+
}
21+
22+
protected override getCommandArguments(): string[] {
23+
return [
24+
"-Command",
25+
'Get-CimInstance -ClassName Win32_Process | Format-Table ProcessId, @{Label="CreationDate";Expression={"{0:yyyyMddHHmmss}" -f $_.CreationDate}}, CommandLine | Out-String -width 9999',
26+
];
27+
}
28+
29+
protected override createParser(): ProcessTreeParser {
30+
const lineRegex = /^([0-9]+)\s+([0-9]+)\s+(.*)$/;
31+
32+
return line => {
33+
const matches = lineRegex.exec(line.trim());
34+
if (!matches || matches.length !== 4) {
35+
return;
36+
}
37+
38+
const id = Number(matches[1]);
39+
const start = Number(matches[2]);
40+
const fullCommandLine = matches[3].trim();
41+
if (isNaN(id) || !fullCommandLine) {
42+
return;
43+
}
44+
// Extract the command from the full command line
45+
let command = fullCommandLine;
46+
if (fullCommandLine[0] === '"') {
47+
const end = fullCommandLine.indexOf('"', 1);
48+
if (end > 0) {
49+
command = fullCommandLine.slice(1, end);
50+
}
51+
} else {
52+
const end = fullCommandLine.indexOf(" ");
53+
if (end > 0) {
54+
command = fullCommandLine.slice(0, end);
55+
}
56+
}
57+
58+
return { id, command, arguments: fullCommandLine, start };
59+
};
60+
}
61+
}

0 commit comments

Comments
 (0)