Skip to content

Commit ea254b5

Browse files
authored
Attach to process quick pick - Windows support (#9249)
* Renaming functions and classes * Windows support * Typo * Refactoring pt 1 * Temp tests * Move ps perf comment to ps file * Temp commit to test ti on windows * Revert "Temp commit to test it on windows" * Windows unit tests * More tests + some cleanup
1 parent 4237a55 commit ea254b5

File tree

15 files changed

+677
-462
lines changed

15 files changed

+677
-462
lines changed

src/client/debugger/extension/adapter/activator.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ export class DebugAdapterActivator implements IExtensionSingleActivationService
2424
) { }
2525
public async activate(): Promise<void> {
2626
if (this.experimentsManager.inExperiment(DebugAdapterDescriptorFactory.experiment)) {
27-
const attachProcessProvider = this.attachProcessProviderFactory.getProvider();
28-
attachProcessProvider.registerCommands();
27+
this.attachProcessProviderFactory.registerCommands();
2928

3029
this.disposables.push(this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debugSessionLoggingFactory));
3130
this.disposables.push(this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory));

src/client/debugger/extension/attachQuickPick/baseProvider.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/client/debugger/extension/attachQuickPick/factory.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55

66
import { inject, injectable } from 'inversify';
77
import { IApplicationShell, ICommandManager } from '../../../common/application/types';
8+
import { Commands } from '../../../common/constants';
89
import { IPlatformService } from '../../../common/platform/types';
910
import { IProcessServiceFactory } from '../../../common/process/types';
1011
import { IDisposableRegistry } from '../../../common/types';
11-
import { PsAttachProcessProvider } from './psProvider';
12+
import { AttachPicker } from './picker';
13+
import { AttachProcessProvider } from './provider';
1214
import { IAttachProcessProviderFactory } from './types';
1315

1416
@injectable()
@@ -21,14 +23,13 @@ export class AttachProcessProviderFactory implements IAttachProcessProviderFacto
2123
@inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry
2224
) { }
2325

24-
public getProvider() {
25-
// Will add Windows provider in a separate PR
26-
return new PsAttachProcessProvider(
27-
this.applicationShell,
28-
this.commandManager,
29-
this.disposableRegistry,
26+
public registerCommands() {
27+
const provider = new AttachProcessProvider(
3028
this.platformService,
3129
this.processServiceFactory
3230
);
31+
const picker = new AttachPicker(this.applicationShell, provider);
32+
const disposable = this.commandManager.registerCommand(Commands.PickLocalProcess, () => picker.showQuickPick(), this);
33+
this.disposableRegistry.push(disposable);
3334
}
3435
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { inject, injectable } from 'inversify';
7+
import { IPlatformService } from '../../../common/platform/types';
8+
import { IProcessServiceFactory } from '../../../common/process/types';
9+
import { AttachProcess as AttachProcessLocalization } from '../../../common/utils/localize';
10+
import { PsProcessParser } from './psProcessParser';
11+
import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types';
12+
import { WmicProcessParser } from './wmicProcessParser';
13+
14+
@injectable()
15+
export class AttachProcessProvider implements IAttachProcessProvider {
16+
constructor(
17+
@inject(IPlatformService) private readonly platformService: IPlatformService,
18+
@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory
19+
) { }
20+
21+
public getAttachItems(): Promise<IAttachItem[]> {
22+
return this._getInternalProcessEntries().then(processEntries => {
23+
// localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements)
24+
// We can change to localeCompare if this becomes an issue
25+
processEntries.sort((a, b) => {
26+
const aLower = a.label.toLowerCase();
27+
const bLower = b.label.toLowerCase();
28+
29+
if (aLower === bLower) {
30+
return 0;
31+
}
32+
33+
return aLower < bLower ? -1 : 1;
34+
});
35+
36+
return processEntries;
37+
});
38+
}
39+
40+
public async _getInternalProcessEntries(): Promise<IAttachItem[]> {
41+
let processCmd: ProcessListCommand;
42+
if (this.platformService.isMac) {
43+
processCmd = PsProcessParser.psDarwinCommand;
44+
} else if (this.platformService.isLinux) {
45+
processCmd = PsProcessParser.psLinuxCommand;
46+
} else if (this.platformService.isWindows) {
47+
processCmd = WmicProcessParser.wmicCommand;
48+
} else {
49+
throw new Error(AttachProcessLocalization.unsupportedOS().format(this.platformService.osType));
50+
}
51+
52+
const processService = await this.processServiceFactory.create();
53+
const output = await processService.exec(processCmd.command, processCmd.args, { throwOnStdErr: true });
54+
55+
return this.platformService.isWindows ?
56+
WmicProcessParser.parseProcesses(output.stdout) : PsProcessParser.parseProcesses(output.stdout);
57+
}
58+
}

src/client/debugger/extension/attachQuickPick/psProcessParser.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@ export namespace PsProcessParser {
99
const secondColumnCharacters = 50;
1010
const commColumnTitle = ''.padStart(secondColumnCharacters, 'a');
1111

12+
// Perf numbers:
13+
// OS X 10.10
14+
// | # of processes | Time (ms) |
15+
// |----------------+-----------|
16+
// | 272 | 52 |
17+
// | 296 | 49 |
18+
// | 384 | 53 |
19+
// | 784 | 116 |
20+
//
21+
// Ubuntu 16.04
22+
// | # of processes | Time (ms) |
23+
// |----------------+-----------|
24+
// | 232 | 26 |
25+
// | 336 | 34 |
26+
// | 736 | 62 |
27+
// | 1039 | 115 |
28+
// | 1239 | 182 |
29+
30+
// ps outputs as a table. With the option "ww", ps will use as much width as necessary.
31+
// However, that only applies to the right-most column. Here we use a hack of setting
32+
// the column header to 50 a's so that the second column will have at least that many
33+
// characters. 50 was chosen because that's the maximum length of a "label" in the
34+
// QuickPick UI in VS Code.
35+
1236
// the BSD version of ps uses '-c' to have 'comm' only output the executable name and not
1337
// the full path. The Linux version of ps has 'comm' to only display the name of the executable
1438
// Note that comm on Linux systems is truncated to 16 characters:
@@ -23,7 +47,7 @@ export namespace PsProcessParser {
2347
args: ['axww', '-o', `pid=,comm=${commColumnTitle},args=`, '-c']
2448
};
2549

26-
export function parseProcessesFromPs(processes: string): IAttachItem[] {
50+
export function parseProcesses(processes: string): IAttachItem[] {
2751
const lines: string[] = processes.split('\n');
2852
return parseProcessesFromPsArray(lines);
2953
}

src/client/debugger/extension/attachQuickPick/psProvider.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

src/client/debugger/extension/attachQuickPick/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ export interface IAttachItem extends QuickPickItem {
1212
}
1313

1414
export interface IAttachProcessProvider {
15-
registerCommands(): void;
1615
getAttachItems(): Promise<IAttachItem[]>;
1716
}
1817

1918
export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory');
2019
export interface IAttachProcessProviderFactory {
21-
getProvider(): IAttachProcessProvider;
20+
registerCommands(): void;
2221
}
2322

2423
export interface IAttachPicker {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { IAttachItem, ProcessListCommand } from './types';
7+
8+
export namespace WmicProcessParser {
9+
const wmicNameTitle = 'Name';
10+
const wmicCommandLineTitle = 'CommandLine';
11+
const wmicPidTitle = 'ProcessId';
12+
const defaultEmptyEntry: IAttachItem = {
13+
label: '',
14+
description: '',
15+
detail: '',
16+
id: ''
17+
};
18+
19+
// Perf numbers on Win10:
20+
// | # of processes | Time (ms) |
21+
// |----------------+-----------|
22+
// | 309 | 413 |
23+
// | 407 | 463 |
24+
// | 887 | 746 |
25+
// | 1308 | 1132 |
26+
export const wmicCommand: ProcessListCommand = {
27+
command: 'wmic',
28+
args: ['process', 'get', 'Name,ProcessId,CommandLine', '/FORMAT:list']
29+
};
30+
31+
export function parseProcesses(processes: string): IAttachItem[] {
32+
const lines: string[] = processes.split('\r\n');
33+
const processEntries: IAttachItem[] = [];
34+
let entry = { ...defaultEmptyEntry };
35+
36+
for (const line of lines) {
37+
if (!line.length) {
38+
continue;
39+
}
40+
41+
parseLineFromWmic(line, entry);
42+
43+
// Each entry of processes has ProcessId as the last line
44+
if (line.lastIndexOf(wmicPidTitle, 0) === 0) {
45+
processEntries.push(entry);
46+
entry = { ...defaultEmptyEntry };
47+
}
48+
}
49+
50+
return processEntries;
51+
}
52+
53+
function parseLineFromWmic(line: string, item: IAttachItem): IAttachItem {
54+
const splitter = line.indexOf('=');
55+
const currentItem = item;
56+
57+
if (splitter > 0) {
58+
const key = line.slice(0, splitter).trim();
59+
let value = line.slice(splitter + 1).trim();
60+
61+
if (key === wmicNameTitle) {
62+
currentItem.label = value;
63+
} else if (key === wmicPidTitle) {
64+
currentItem.description = value;
65+
currentItem.id = value;
66+
} else if (key === wmicCommandLineTitle) {
67+
const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178
68+
if (value.lastIndexOf(dosDevicePrefix, 0) === 0) {
69+
value = value.slice(dosDevicePrefix.length);
70+
}
71+
72+
currentItem.detail = value;
73+
}
74+
}
75+
76+
return currentItem;
77+
}
78+
}

src/test/debugger/extension/adapter/activator.unit.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { DebugAdapterActivator } from '../../../../client/debugger/extension/ada
2424
import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory';
2525
import { DebugSessionLoggingFactory } from '../../../../client/debugger/extension/adapter/logging';
2626
import { AttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/factory';
27-
import { PsAttachProcessProvider } from '../../../../client/debugger/extension/attachQuickPick/psProvider';
2827
import { IAttachProcessProviderFactory } from '../../../../client/debugger/extension/attachQuickPick/types';
2928
import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory } from '../../../../client/debugger/extension/types';
3029
import { clearTelemetryReporter } from '../../../../client/telemetry';
@@ -86,8 +85,6 @@ suite('Debugging - Adapter Factory and logger Registration', () => {
8685
spiedInstance = spy(experimentsManager);
8786

8887
attachFactory = mock(AttachProcessProviderFactory);
89-
const attachProvider = mock(PsAttachProcessProvider);
90-
when(attachFactory.getProvider()).thenReturn(instance(attachProvider));
9188

9289
debugService = mock(DebugService);
9390
descriptorFactory = mock(DebugAdapterDescriptorFactory);

0 commit comments

Comments
 (0)