diff --git a/package.json b/package.json index ca834ebe98..2570355247 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ ], "main": "./out/omnisharpMain", "scripts": { - "postinstall": "tsc" + "postinstall": "node ./node_modules/vscode/bin/install && tsc" }, "dependencies": { "decompress": "^4.0.0", @@ -39,7 +39,7 @@ "tslint": "^3.3.0", "tslint-microsoft-contrib": "^2.0.0", "typescript": "^1.7.3", - "vscode": "^0.10.1", + "vscode": "^0.11.3", "vsce": "^1.3.0" }, "engines": { @@ -52,6 +52,7 @@ "onCommand:o.showOutput", "onCommand:dotnet.restore", "onCommand:csharp.downloadDebugger", + "onCommand:csharp.listProcess", "workspaceContains:project.json" ], "contributes": { @@ -102,6 +103,11 @@ "command": "csharp.downloadDebugger", "title": "Download .NET Core Debugger", "category": "Debug" + }, + { + "command": "csharp.listProcess", + "title": "List process for attach", + "category": "CSharp" } ], "keybindings": [ @@ -143,6 +149,9 @@ }, "runtime": "node", "runtimeArgs": [], + "variables": { + "pickProcess": "csharp.listProcess" + }, "program": "./out/coreclr-debug/proxy.js", "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "configurationAttributes": { diff --git a/src/features/commands.ts b/src/features/commands.ts index 7036e9e494..75ba5edc86 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -14,6 +14,7 @@ import * as path from 'path'; import * as protocol from '../protocol'; import * as vscode from 'vscode'; import * as dotnetTest from './dotnetTest' +import {DotNetAttachItemsProviderFactory, AttachPicker} from './processPicker' let channel = vscode.window.createOutputChannel('.NET'); @@ -29,9 +30,14 @@ export default function registerCommands(server: OmnisharpServer, extensionPath: // register two commands for running and debugging xunit tests let d6 = dotnetTest.registerDotNetTestRunCommand(server); - let d7 = dotnetTest.registerDotNetTestDebugCommand(server); - - return vscode.Disposable.from(d1, d2, d3, d4, d5, d6, d7); + let d7 = dotnetTest.registerDotNetTestDebugCommand(server); + + // register process picker for attach + let attachItemsProvider = DotNetAttachItemsProviderFactory.Get(); + let attacher = new AttachPicker(attachItemsProvider); + let d8 = vscode.commands.registerCommand('csharp.listProcess', () => attacher.ShowAttachEntries()); + + return vscode.Disposable.from(d1, d2, d3, d4, d5, d6, d7, d8); } function pickProjectAndStart(server: OmnisharpServer) { diff --git a/src/features/processPicker.ts b/src/features/processPicker.ts new file mode 100644 index 0000000000..ac2ca79ff5 --- /dev/null +++ b/src/features/processPicker.ts @@ -0,0 +1,249 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * See LICENSE.md in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import * as os from 'os'; +import * as vscode from 'vscode'; +import * as child_process from 'child_process'; + +export interface AttachItem extends vscode.QuickPickItem { + id: string; +} + +export interface AttachItemsProvider { + getAttachItems(): Promise; +} + +export class AttachPicker { + constructor(private attachItemsProvider: AttachItemsProvider) { } + + public ShowAttachEntries(): Promise { + return this.attachItemsProvider.getAttachItems() + .then(processEntries => { + let attachPickOptions: vscode.QuickPickOptions = { + matchOnDescription: true, + matchOnDetail: true, + placeHolder: "Select the process to attach to" + }; + + return vscode.window.showQuickPick(processEntries, attachPickOptions) + .then(chosenProcess => { + return chosenProcess ? chosenProcess.id : null; + }); + }); + } +} + +class Process { + constructor(public name: string, public pid: string, public commandLine: string) { } + + public toAttachItem(): AttachItem { + return { + label: this.name, + description: this.pid, + detail: this.commandLine, + id: this.pid + }; + } +} + +export class DotNetAttachItemsProviderFactory { + static Get(): AttachItemsProvider { + if (os.platform() === 'win32') { + return new WmicAttachItemsProvider(); + } + else { + return new PsAttachItemsProvider(); + } + } +} + +abstract class DotNetAttachItemsProvider implements AttachItemsProvider { + protected abstract getInternalProcessEntries(): Promise; + + getAttachItems(): Promise { + return this.getInternalProcessEntries().then(processEntries => { + // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) + // We can change to localeCompare if this becomes an issue + let dotnetProcessName = (os.platform() === 'win32') ? 'dotnet.exe' : 'dotnet'; + processEntries = processEntries.sort((a, b) => { + if (a.name.toLowerCase() === dotnetProcessName && b.name.toLowerCase() === dotnetProcessName) { + return a.commandLine.toLowerCase() < b.commandLine.toLowerCase() ? -1 : 1; + } else if (a.name.toLowerCase() === dotnetProcessName) { + return -1; + } else if (b.name.toLowerCase() === dotnetProcessName) { + return 1; + } else { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1 + } + }); + + let attachItems = processEntries.map(p => p.toAttachItem()); + return attachItems; + }); + } +} + +export class PsAttachItemsProvider extends DotNetAttachItemsProvider { + // Perf numbers: + // OS X 10.10 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 272 | 52 | + // | 296 | 49 | + // | 384 | 53 | + // | 784 | 116 | + // + // Ubuntu 16.04 + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 232 | 26 | + // | 336 | 34 | + // | 736 | 62 | + // | 1039 | 115 | + // | 1239 | 182 | + + // ps outputs as a table. With the option "ww", ps will use as much width as necessary. + // However, that only applies to the right-most column. Here we use a hack of setting + // the column header to 50 a's so that the second column will have at least that many + // characters. 50 was chosen because that's the maximum length of a "label" in the + // QuickPick UI in VSCode. + private static get secondColumnCharacters() { return 50; } + + protected getInternalProcessEntries(): Promise { + const commColumnTitle = Array(PsAttachItemsProvider.secondColumnCharacters).join("a"); + // the BSD version of ps uses '-c' to have 'comm' only output the executable name and not + // the full path. The Linux version of ps has 'comm' to only display the name of the executable + // Note that comm on Linux systems is truncated to 16 characters: + // https://bugzilla.redhat.com/show_bug.cgi?id=429565 + // Since 'args' contains the full path to the executable, even if truncated, searching will work as desired. + const psCommand = `ps -axww -o pid=,comm=${commColumnTitle},args=` + (os.platform() === 'darwin' ? ' -c' : ''); + return execChildProcess(psCommand, null).then(processes => { + return this.parseProcessFromPs(processes); + }); + } + + // Only public for tests. + public parseProcessFromPs(processes: string): Process[] { + let lines = processes.split(os.EOL); + let processEntries: Process[] = []; + + // lines[0] is the header of the table + for (let i = 1; i < lines.length; i++) { + let line = lines[i]; + if (!line) { + continue; + } + + let process = this.parseLineFromPs(line); + processEntries.push(process); + } + + return processEntries; + } + + private parseLineFromPs(line: string): Process { + // Explanation of the regex: + // - any leading whitespace + // - PID + // - whitespace + // - executable name --> this is PsAttachItemsProvider.secondColumnCharacters - 1 because ps reserves one character + // for the whitespace separator + // - whitespace + // - args (might be empty) + const psEntry = new RegExp(`^\\s*([0-9]+)\\s+(.{${PsAttachItemsProvider.secondColumnCharacters - 1}})\\s+(.*)$`); + const matches = psEntry.exec(line); + if (matches && matches.length === 4) { + const pid = matches[1].trim(); + const executable = matches[2].trim(); + const cmdline = matches[3].trim(); + return new Process(executable, pid, cmdline); + } + } +} + +export class WmicAttachItemsProvider extends DotNetAttachItemsProvider { + // Perf numbers on Win10: + // | # of processes | Time (ms) | + // |----------------+-----------| + // | 309 | 413 | + // | 407 | 463 | + // | 887 | 746 | + // | 1308 | 1132 | + + private static get wmicNameTitle() { return 'Name'; } + private static get wmicCommandLineTitle() { return 'CommandLine'; } + private static get wmicPidTitle() { return 'ProcessId'; } + + protected getInternalProcessEntries(): Promise { + const wmicCommand = 'wmic process get Name,ProcessId,CommandLine /FORMAT:list'; + return execChildProcess(wmicCommand, null).then(processes => { + return this.parseProcessFromWmic(processes); + }); + } + + // Only public for tests. + public parseProcessFromWmic(processes: string): Process[] { + let lines = processes.split(os.EOL); + let currentProcess: Process = new Process(null, null, null); + let processEntries: Process[] = []; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i]; + if (!line) { + continue; + } + + this.parseLineFromWmic(line, currentProcess); + + // Each entry of processes has ProcessId as the last line + if (line.startsWith(WmicAttachItemsProvider.wmicPidTitle)) { + processEntries.push(currentProcess); + currentProcess = new Process(null, null, null); + } + } + + return processEntries; + } + + private parseLineFromWmic(line: string, process: Process) { + let splitter = line.indexOf('='); + if (splitter >= 0) { + let key = line.slice(0, line.indexOf('=')); + let value = line.slice(line.indexOf('=') + 1); + if (key === WmicAttachItemsProvider.wmicNameTitle) { + process.name = value.trim(); + } + else if (key === WmicAttachItemsProvider.wmicPidTitle) { + process.pid = value.trim(); + } + else if (key === WmicAttachItemsProvider.wmicCommandLineTitle) { + const extendedLengthPath = '\\??\\'; + if (value.startsWith(extendedLengthPath)) { + value = value.slice(extendedLengthPath.length).trim(); + } + + process.commandLine = value.trim(); + } + } + } +} + +function execChildProcess(process: string, workingDirectory: string): Promise { + return new Promise((resolve, reject) => { + child_process.exec(process, { cwd: workingDirectory, maxBuffer: 500 * 1024 }, (error: Error, stdout: string, stderr: string) => { + if (error) { + reject(error); + return; + } + + if (stderr && stderr.length > 0) { + reject(new Error(stderr)); + return; + } + + resolve(stdout); + }); + }); +} \ No newline at end of file