diff --git a/package-lock.json b/package-lock.json index 3e3df566..466341f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@vscode/debugprotocol": "^1.65.0", "@vscode/extension-telemetry": "^0.8.4", "@vscode/python-extension": "^1.0.6", + "@vscode/windows-process-tree": "^0.7.0", "fs-extra": "^11.2.0", "iconv-lite": "^0.6.3", "jsonc-parser": "^3.0.0", @@ -1830,6 +1831,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@vscode/windows-process-tree": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.7.0.tgz", + "integrity": "sha512-uH58Fofu1bgiulsY2svyGOLLKAYNJ0Req4PioPd7BZzHRziuiBRw1SxyT7wvsYHvm7eKWwzwo6mZpExDfwX9iA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "7.1.0" + } + }, + "node_modules/@vscode/windows-process-tree/node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "license": "MIT", + "engines": { + "node": "^16 || ^18 || >= 20" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -9096,6 +9116,21 @@ "dev": true, "optional": true }, + "@vscode/windows-process-tree": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.7.0.tgz", + "integrity": "sha512-uH58Fofu1bgiulsY2svyGOLLKAYNJ0Req4PioPd7BZzHRziuiBRw1SxyT7wvsYHvm7eKWwzwo6mZpExDfwX9iA==", + "requires": { + "node-addon-api": "7.1.0" + }, + "dependencies": { + "node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==" + } + } + }, "@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", diff --git a/package.json b/package.json index 0167f6b5..0248804a 100644 --- a/package.json +++ b/package.json @@ -682,6 +682,7 @@ "@vscode/debugprotocol": "^1.65.0", "@vscode/extension-telemetry": "^0.8.4", "@vscode/python-extension": "^1.0.6", + "@vscode/windows-process-tree": "^0.7.0", "fs-extra": "^11.2.0", "iconv-lite": "^0.6.3", "jsonc-parser": "^3.0.0", diff --git a/src/extension/debugger/attachQuickPick/provider.ts b/src/extension/debugger/attachQuickPick/provider.ts index 8699f479..41f758d0 100644 --- a/src/extension/debugger/attachQuickPick/provider.ts +++ b/src/extension/debugger/attachQuickPick/provider.ts @@ -4,6 +4,7 @@ 'use strict'; import { l10n } from 'vscode'; +import * as wpc from '@vscode/windows-process-tree'; import { getOSType, OSType } from '../../common/platform'; import { PsProcessParser } from './psProcessParser'; import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; @@ -58,14 +59,39 @@ export class AttachProcessProvider implements IAttachProcessProvider { } public async _getInternalProcessEntries(): Promise { - let processCmd: ProcessListCommand; const osType = getOSType(); + + if (osType === OSType.Windows) { + try { + const processList = await new Promise((resolve) => { + wpc.getAllProcesses((processes) => resolve(processes), wpc.ProcessDataFlag.CommandLine); + }); + return processList.map((p) => ({ + label: p.name, + description: String(p.pid), + detail: p.commandLine || '', + id: String(p.pid), + processName: p.name, + commandLine: p.commandLine || '', + })); + } catch { + const customEnvVars = await getEnvironmentVariables(); + const output = await plainExec( + WmicProcessParser.wmicCommand.command, + WmicProcessParser.wmicCommand.args, + { throwOnStdErr: true }, + customEnvVars, + ); + logProcess(WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, { throwOnStdErr: true }); + return WmicProcessParser.parseProcesses(output.stdout); + } + } + + let processCmd: ProcessListCommand; if (osType === OSType.OSX) { processCmd = PsProcessParser.psDarwinCommand; } else if (osType === OSType.Linux) { processCmd = PsProcessParser.psLinuxCommand; - } else if (osType === OSType.Windows) { - processCmd = WmicProcessParser.wmicCommand; } else { throw new Error(l10n.t("Operating system '{0}' not supported.", osType)); } @@ -73,9 +99,6 @@ export class AttachProcessProvider implements IAttachProcessProvider { const customEnvVars = await getEnvironmentVariables(); const output = await plainExec(processCmd.command, processCmd.args, { throwOnStdErr: true }, customEnvVars); logProcess(processCmd.command, processCmd.args, { throwOnStdErr: true }); - - return osType === OSType.Windows - ? WmicProcessParser.parseProcesses(output.stdout) - : PsProcessParser.parseProcesses(output.stdout); + return PsProcessParser.parseProcesses(output.stdout); } } diff --git a/src/test/unittest/attachQuickPick/provider.unit.test.ts b/src/test/unittest/attachQuickPick/provider.unit.test.ts index 259eaa07..90b5dbec 100644 --- a/src/test/unittest/attachQuickPick/provider.unit.test.ts +++ b/src/test/unittest/attachQuickPick/provider.unit.test.ts @@ -13,6 +13,7 @@ import { IAttachItem } from '../../../extension/debugger/attachQuickPick/types'; import { WmicProcessParser } from '../../../extension/debugger/attachQuickPick/wmicProcessParser'; import * as platform from '../../../extension/common/platform'; import * as rawProcessApis from '../../../extension/common/process/rawProcessApis'; +import * as wpc from '@vscode/windows-process-tree'; use(chaiAsPromised); @@ -20,11 +21,13 @@ suite('Attach to process - process provider', () => { let provider: AttachProcessProvider; let getOSTypeStub: sinon.SinonStub; let plainExecStub: sinon.SinonStub; + let getAllProcessesStub: sinon.SinonStub; setup(() => { provider = new AttachProcessProvider(); getOSTypeStub = sinon.stub(platform, 'getOSType'); plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getAllProcessesStub = sinon.stub(wpc, 'getAllProcesses'); }); teardown(() => { @@ -128,21 +131,17 @@ suite('Attach to process - process provider', () => { assert.deepEqual(attachItems, expectedOutput); }); - test('The Windows process list command should be called if the platform is Windows', async () => { - const windowsOutput = `CommandLine=\r -Name=System\r -ProcessId=4\r -\r -\r -CommandLine=sihost.exe\r -Name=sihost.exe\r -ProcessId=5728\r -\r -\r -CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r -Name=svchost.exe\r -ProcessId=5912\r -`; + test('The Windows process list should be obtained via getAllProcesses if the platform is Windows', async () => { + const processList = [ + { pid: 4, ppid: 0, name: 'System', commandLine: '' }, + { pid: 5728, ppid: 1, name: 'sihost.exe', commandLine: 'sihost.exe' }, + { + pid: 5912, + ppid: 1, + name: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + ]; const expectedOutput: IAttachItem[] = [ { label: 'System', @@ -170,19 +169,10 @@ ProcessId=5912\r }, ]; getOSTypeStub.returns(platform.OSType.Windows); - plainExecStub - .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) - .resolves({ stdout: windowsOutput }); + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); const attachItems = await provider._getInternalProcessEntries(); - sinon.assert.calledOnceWithExactly( - plainExecStub, - WmicProcessParser.wmicCommand.command, - WmicProcessParser.wmicCommand.args, - sinon.match.any, - sinon.match.any, - ); - + sinon.assert.notCalled(plainExecStub); assert.deepEqual(attachItems, expectedOutput); }); @@ -308,6 +298,195 @@ ProcessId=5912\r }); test('Items returned by getAttachItems should be sorted alphabetically', async () => { + const processList = [ + { pid: 4, ppid: 0, name: 'System', commandLine: '' }, + { pid: 5372, ppid: 1, name: 'svchost.exe', commandLine: '' }, + { pid: 5728, ppid: 1, name: 'sihost.exe', commandLine: 'sihost.exe' }, + ]; + const expectedOutput: IAttachItem[] = [ + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems', async () => { + const processList = [ + { pid: 4, ppid: 0, name: 'System', commandLine: '' }, + { pid: 5372, ppid: 1, name: 'svchost.exe', commandLine: '' }, + { pid: 5728, ppid: 1, name: 'sihost.exe', commandLine: 'sihost.exe' }, + { + pid: 5912, + ppid: 1, + name: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + pid: 6028, + ppid: 1, + name: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + { + pid: 8026, + ppid: 1, + name: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + }, + ]; + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + }, + { + label: 'python.exe', + description: '6028', + detail: 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + }); + + suite('Windows getAttachItems - wmic fallback', () => { + setup(() => { + getOSTypeStub.returns(platform.OSType.Windows); + }); + + test('Should fall back to wmic when getAllProcesses fails', async () => { + getAllProcessesStub.callsFake((_callback: Function) => { + throw new Error('windows-process-tree unavailable'); + }); + const windowsOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +\r +\r +CommandLine=C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc\r +Name=svchost.exe\r +ProcessId=5912\r +`; + const expectedOutput: IAttachItem[] = [ + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + { + label: 'sihost.exe', + description: '5728', + detail: 'sihost.exe', + id: '5728', + processName: 'sihost.exe', + commandLine: 'sihost.exe', + }, + { + label: 'svchost.exe', + description: '5912', + detail: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + id: '5912', + processName: 'svchost.exe', + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + }, + ]; + + plainExecStub + .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) + .resolves({ stdout: windowsOutput }); + + const attachItems = await provider._getInternalProcessEntries(); + sinon.assert.calledOnce(plainExecStub); + assert.deepEqual(attachItems, expectedOutput); + }); + + test('Items returned by getAttachItems via wmic fallback should be sorted alphabetically', async () => { + getAllProcessesStub.callsFake((_callback: Function) => { + throw new Error('windows-process-tree unavailable'); + }); const windowsOutput = `CommandLine=\r Name=System\r ProcessId=4\r @@ -358,7 +537,10 @@ ProcessId=5728\r assert.deepEqual(output, expectedOutput); }); - test('Python processes should be at the top of the list returned by getAttachItems', async () => { + test('Python processes should be at the top of the list returned by getAttachItems via wmic fallback', async () => { + getAllProcessesStub.callsFake((_callback: Function) => { + throw new Error('windows-process-tree unavailable'); + }); const windowsOutput = `CommandLine=\r Name=System\r ProcessId=4\r diff --git a/webpack.config.js b/webpack.config.js index 5f815e0f..5fe62a66 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,6 +38,7 @@ const extensionConfig = { '@opentelemetry/instrumentation': 'commonjs @opentelemetry/instrumentation', // ignored because we don't ship instrumentation '@azure/opentelemetry-instrumentation-azure-sdk': 'commonjs @azure/opentelemetry-instrumentation-azure-sdk', // ignored because we don't ship instrumentation '@azure/functions-core': '@azure/functions-core', // ignored because we don't ship instrumentation + '@vscode/windows-process-tree': 'commonjs @vscode/windows-process-tree', }, resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader