diff --git a/package-lock.json b/package-lock.json index a4315572..fe02f414 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", @@ -9016,6 +9036,21 @@ } } }, + "@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==" + } + } + }, "@vscode/vsce-sign": { "version": "2.0.9", "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", diff --git a/package.json b/package.json index e7c53f08..381f4564 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/powerShellProcessParser.ts b/src/extension/debugger/attachQuickPick/powerShellProcessParser.ts new file mode 100644 index 00000000..49dea96a --- /dev/null +++ b/src/extension/debugger/attachQuickPick/powerShellProcessParser.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// due to wmic has been deprecated create this file to replace wmicProcessParser.ts + +'use strict'; + +import { IAttachItem, ProcessListCommand } from './types'; + +export namespace PowerShellProcessParser { + export const powerShellCommand: ProcessListCommand = { + command: 'powershell', + args: [ + '-Command', + '$processes = if (Get-Command Get-CimInstance -ErrorAction SilentlyContinue) { Get-CimInstance Win32_Process } else { Get-WmiObject Win32_Process }; \ + $processes | % { @{ name = $_.Name; commandLine = $_.CommandLine; processId = $_.ProcessId } } | ConvertTo-Json', + ], // Get-WmiObject For the legacy compatibility + }; + + //for unit test with Get-WmiObject + export const powerShellWithoutCimCommand: ProcessListCommand = { + command: 'powershell', + args: [ + '-Command', + '$processes = if (Get-Command NotExistCommand-That-Will-Never-Exist -ErrorAction SilentlyContinue) { Get-CimInstance Win32_Process } else { Get-WmiObject Win32_Process }; \ + $processes | % { @{ name = $_.Name; commandLine = $_.CommandLine; processId = $_.ProcessId } } | ConvertTo-Json', + ], + }; + + export function parseProcesses(processes: string): IAttachItem[] { + const processesArray = JSON.parse(processes); + const processEntries: IAttachItem[] = []; + for (const process of processesArray) { + if (!process.processId) { + continue; + } + const entry: IAttachItem = { + label: process.name || '', + processName: process.name || '', + description: String(process.processId), + id: String(process.processId), + detail: '', + commandLine: '', + }; + if (process.commandLine) { + const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 + let commandLine = process.commandLine; + if (commandLine.startsWith(dosDevicePrefix)) { + commandLine = commandLine.slice(dosDevicePrefix.length); + } + entry.detail = commandLine; + entry.commandLine = commandLine; + } + processEntries.push(entry); + } + return processEntries; + } +} diff --git a/src/extension/debugger/attachQuickPick/provider.ts b/src/extension/debugger/attachQuickPick/provider.ts index 8699f479..a7aa4239 100644 --- a/src/extension/debugger/attachQuickPick/provider.ts +++ b/src/extension/debugger/attachQuickPick/provider.ts @@ -7,16 +7,20 @@ import { l10n } from 'vscode'; import { getOSType, OSType } from '../../common/platform'; import { PsProcessParser } from './psProcessParser'; import { IAttachItem, IAttachProcessProvider, ProcessListCommand } from './types'; -import { WmicProcessParser } from './wmicProcessParser'; +import { PowerShellProcessParser } from './powerShellProcessParser'; import { getEnvironmentVariables } from '../../common/python'; import { plainExec } from '../../common/process/rawProcessApis'; import { logProcess } from '../../common/process/logger'; +import { WmicProcessParser } from './wmicProcessParser'; +import { promisify } from 'util'; +import * as wpc from '@vscode/windows-process-tree'; +import { ProcessDataFlag } from '@vscode/windows-process-tree'; export class AttachProcessProvider implements IAttachProcessProvider { constructor() {} - public getAttachItems(): Promise { - return this._getInternalProcessEntries().then((processEntries) => { + public getAttachItems(specCommand?: ProcessListCommand): Promise { + return this._getInternalProcessEntries(specCommand).then((processEntries) => { processEntries.sort( ( { processName: aprocessName, commandLine: aCommandLine }, @@ -57,25 +61,73 @@ export class AttachProcessProvider implements IAttachProcessProvider { }); } - public async _getInternalProcessEntries(): Promise { - let processCmd: ProcessListCommand; - const osType = getOSType(); - 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)); + /** + * Get processes via wmic (fallback) + */ + private async _getProcessesViaWmic(): Promise { + 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); + } + + /** + * Get processes via Ps parser (Linux/macOS) + */ + private async _getProcessesViaPsParser(cmd: ProcessListCommand): Promise { + const customEnvVars = await getEnvironmentVariables(); + const output = await plainExec(cmd.command, cmd.args, { throwOnStdErr: true }, customEnvVars); + logProcess(cmd.command, cmd.args, { throwOnStdErr: true }); + return PsProcessParser.parseProcesses(output.stdout); + } + + public async _getInternalProcessEntries(specCommand?: ProcessListCommand): Promise { + if (specCommand === undefined) { + const osType = getOSType(); + if (osType === OSType.OSX) { + return this._getProcessesViaPsParser(PsProcessParser.psDarwinCommand); + } else if (osType === OSType.Linux) { + return this._getProcessesViaPsParser(PsProcessParser.psLinuxCommand); + } else if (osType === OSType.Windows) { + try { + const getAllProcesses = promisify(wpc.getAllProcesses) as (flags?: ProcessDataFlag) => Promise; + const processList = await getAllProcesses(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 (error) { + console.error('Failed to get processes via windows-process-tree:', error); + // 降级到 wmic + return this._getProcessesViaWmic(); + } + } else { + throw new Error(l10n.t("Operating system '{0}' not supported.", osType)); + } } + const processCmd = specCommand; 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); + if (processCmd === WmicProcessParser.wmicCommand) { + return WmicProcessParser.parseProcesses(output.stdout); + } else if ( + processCmd === PowerShellProcessParser.powerShellCommand || + processCmd === PowerShellProcessParser.powerShellWithoutCimCommand + ) { + return PowerShellProcessParser.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..ab54187c 100644 --- a/src/test/unittest/attachQuickPick/provider.unit.test.ts +++ b/src/test/unittest/attachQuickPick/provider.unit.test.ts @@ -13,6 +13,8 @@ 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 { PowerShellProcessParser } from '../../../extension/debugger/attachQuickPick/powerShellProcessParser'; +import * as windowsProcessTree from '@vscode/windows-process-tree'; use(chaiAsPromised); @@ -20,11 +22,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(windowsProcessTree, 'getAllProcesses'); }); teardown(() => { @@ -128,7 +132,7 @@ 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 () => { + test('The Windows wmic process list command should be called if the platform is Windows when powershell has not been installed', async () => { const windowsOutput = `CommandLine=\r Name=System\r ProcessId=4\r @@ -170,13 +174,25 @@ ProcessId=5912\r }, ]; getOSTypeStub.returns(platform.OSType.Windows); + const notFoundPowerShellOutput = 'INFO: Could not find files for the given pattern(s).\r\n'; + plainExecStub + .withArgs('where', ['powershell'], sinon.match.any, sinon.match.any) + .resolves({ stderr: notFoundPowerShellOutput, stdout: '' }); plainExecStub .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) .resolves({ stdout: windowsOutput }); const attachItems = await provider._getInternalProcessEntries(); - sinon.assert.calledOnceWithExactly( - plainExecStub, + sinon.assert.calledTwice(plainExecStub); + sinon.assert.calledWithExactly( + plainExecStub.firstCall, + 'where', + ['powershell'], + sinon.match.any, + sinon.match.any, + ); + sinon.assert.calledWithExactly( + plainExecStub.secondCall, WmicProcessParser.wmicCommand.command, WmicProcessParser.wmicCommand.args, sinon.match.any, @@ -301,13 +317,12 @@ ProcessId=5912\r assert.deepEqual(output, expectedOutput); }); }); - suite('Windows getAttachItems', () => { setup(() => { getOSTypeStub.returns(platform.OSType.Windows); }); - test('Items returned by getAttachItems should be sorted alphabetically', async () => { + test('Items returned by getAttachItems should be sorted alphabetically with wmic', async () => { const windowsOutput = `CommandLine=\r Name=System\r ProcessId=4\r @@ -353,12 +368,75 @@ ProcessId=5728\r .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) .resolves({ stdout: windowsOutput }); - const output = await provider.getAttachItems(); + const output = await provider.getAttachItems(WmicProcessParser.wmicCommand); assert.deepEqual(output, expectedOutput); }); - test('Python processes should be at the top of the list returned by getAttachItems', async () => { + test('Items returned by getAttachItems should be sorted alphabetically with powershell', async () => { + const windowsProcesses = [ + { + processId: 4, + commandLine: null, + name: 'System', + }, + { + processId: 5372, + commandLine: null, + name: 'svchost.exe', + }, + { + processId: 5728, + commandLine: 'sihost.exe', + name: '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: '', + }, + ]; + const foundPowerShellOutput = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\n'; + plainExecStub + .withArgs('where', ['powershell'], sinon.match.any, sinon.match.any) + .resolves({ stderr: '', stdout: foundPowerShellOutput }); + const windowsOutput = JSON.stringify(windowsProcesses, null, 4); + plainExecStub + .withArgs( + PowerShellProcessParser.powerShellCommand.command, + sinon.match.any, + sinon.match.any, + sinon.match.any, + ) + .resolves({ stdout: windowsOutput }); + + const output = await provider.getAttachItems(PowerShellProcessParser.powerShellCommand); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems with wmic', async () => { const windowsOutput = `CommandLine=\r Name=System\r ProcessId=4\r @@ -445,6 +523,489 @@ ProcessId=8026\r .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) .resolves({ stdout: windowsOutput }); + const output = await provider.getAttachItems(WmicProcessParser.wmicCommand); + + assert.deepEqual(output, expectedOutput); + }); + test('Python processes should be at the top of the list returned by getAttachItems with powershell', async () => { + const windowsProcesses = [ + { + processId: 4, + commandLine: null, + name: 'System', + }, + { + processId: 5372, + commandLine: null, + name: 'svchost.exe', + }, + { + processId: 5728, + commandLine: 'sihost.exe', + name: 'sihost.exe', + }, + { + processId: 5912, + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + name: 'svchost.exe', + }, + { + processId: 6028, + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + name: 'python.exe', + }, + { + processId: 8026, + commandLine: + 'C:\\Users\\Contoso\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + name: 'python.exe', + }, + ]; + const windowsOutput = JSON.stringify(windowsProcesses, null, 4); + 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: '', + }, + ]; + const foundPowerShellOutput = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\n'; + //const notFoundPowerShellOutput = 'INFO: Could not find files for the given pattern(s).\r\n'; + plainExecStub + .withArgs('where', ['powershell'], sinon.match.any, sinon.match.any) + .resolves({ stderr: '', stdout: foundPowerShellOutput }); + plainExecStub + .withArgs( + PowerShellProcessParser.powerShellCommand.command, + sinon.match.any, + sinon.match.any, + sinon.match.any, + ) + .resolves({ stdout: windowsOutput }); + + const output = await provider.getAttachItems(PowerShellProcessParser.powerShellCommand); + + assert.deepEqual(output, expectedOutput); + }); + + test('The Windows powershell process list command should be called if the platform is Windows when powershell has been installed', async () => { + const windowsProcesses = [ + { + processId: 4, + commandLine: null, + name: 'System', + }, + { + processId: 5372, + commandLine: null, + name: 'svchost.exe', + }, + { + processId: 5728, + commandLine: 'sihost.exe', + name: 'sihost.exe', + }, + { + processId: 5912, + commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc', + name: 'svchost.exe', + }, + { + processId: 6028, + commandLine: + 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + name: 'python.exe', + }, + { + processId: 8026, + commandLine: + 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + name: 'python.exe', + }, + ]; + const windowsOutput = JSON.stringify(windowsProcesses, null, 4); + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: + 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/foo_bar.py', + }, + { + label: 'python.exe', + description: '6028', + detail: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/Contoso/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: + 'C:\\Users\\ZA139\\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: '', + }, + ]; + getOSTypeStub.returns(platform.OSType.Windows); + const foundPowerShellOutput = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\n'; + plainExecStub + .withArgs('where', ['powershell'], sinon.match.any, sinon.match.any) + .resolves({ stderr: '', stdout: foundPowerShellOutput }); + plainExecStub + .withArgs( + PowerShellProcessParser.powerShellCommand.command, + sinon.match.any, + sinon.match.any, + sinon.match.any, + ) + .resolves({ stdout: windowsOutput }); + + const output = await provider.getAttachItems(); + sinon.assert.calledTwice(plainExecStub); + sinon.assert.calledWithExactly( + plainExecStub.firstCall, + 'where', + ['powershell'], + sinon.match.any, + sinon.match.any, + ); + sinon.assert.calledWithExactly( + plainExecStub.secondCall, + PowerShellProcessParser.powerShellCommand.command, + PowerShellProcessParser.powerShellCommand.args, + sinon.match.any, + sinon.match.any, + ); + assert.deepEqual(output, expectedOutput); + }); + + test('The Windows powershell Get-WmiObject process list command should be called when Get-CimInstance fails', async () => { + const windowsProcesses = [ + { + processId: 4, + commandLine: null, + name: 'System', + }, + { + processId: 5372, + commandLine: null, + name: 'svchost.exe', + }, + ]; + const expectedOutput: IAttachItem[] = [ + { + label: 'svchost.exe', + description: '5372', + detail: '', + id: '5372', + processName: 'svchost.exe', + commandLine: '', + }, + { + label: 'System', + description: '4', + detail: '', + id: '4', + processName: 'System', + commandLine: '', + }, + ]; + const foundPowerShellOutput = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\r\n'; + + plainExecStub + .withArgs('where', ['powershell'], sinon.match.any, sinon.match.any) + .resolves({ stderr: '', stdout: foundPowerShellOutput }); + + const windowsOutput = JSON.stringify(windowsProcesses, null, 4); + plainExecStub + .withArgs( + PowerShellProcessParser.powerShellWithoutCimCommand.command, + sinon.match.any, + sinon.match.any, + sinon.match.any, + ) + .resolves({ stdout: windowsOutput }); + + const output = await provider.getAttachItems(PowerShellProcessParser.powerShellWithoutCimCommand); + sinon.assert.calledWithExactly( + plainExecStub.firstCall, + PowerShellProcessParser.powerShellWithoutCimCommand.command, + PowerShellProcessParser.powerShellWithoutCimCommand.args, + sinon.match.any, + sinon.match.any, + ); + assert.deepEqual(output, expectedOutput); + }); + }); + + suite('Windows getAttachItems (windows-process-tree)', () => { + setup(() => { + getOSTypeStub.returns(platform.OSType.Windows); + }); + + test('getAllProcesses should be called when the platform is Windows', async () => { + const processList = [ + { pid: 4, name: 'System', commandLine: undefined }, + { pid: 5728, name: 'sihost.exe', commandLine: 'sihost.exe' }, + { pid: 5912, name: 'svchost.exe', commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' }, + ]; + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); + + const attachItems = await provider._getInternalProcessEntries(); + + sinon.assert.calledOnce(getAllProcessesStub); + sinon.assert.notCalled(plainExecStub); + assert.deepEqual(attachItems, [ + { + 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', + }, + ]); + }); + + test('Should fall back to wmic when getAllProcesses fails', async () => { + getAllProcessesStub.callsFake((_callback: Function) => { throw new Error('Native module not available'); }); + const wmicOutput = `CommandLine=\r +Name=System\r +ProcessId=4\r +\r +\r +CommandLine=sihost.exe\r +Name=sihost.exe\r +ProcessId=5728\r +`; + plainExecStub + .withArgs(WmicProcessParser.wmicCommand.command, sinon.match.any, sinon.match.any, sinon.match.any) + .resolves({ stdout: wmicOutput }); + + const attachItems = await provider._getInternalProcessEntries(); + + sinon.assert.calledOnce(getAllProcessesStub); + sinon.assert.calledOnceWithExactly( + plainExecStub, + WmicProcessParser.wmicCommand.command, + WmicProcessParser.wmicCommand.args, + sinon.match.any, + sinon.match.any, + ); + assert.deepEqual(attachItems, [ + { + 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', + }, + ]); + }); + + test('Items returned by getAttachItems should be sorted alphabetically with getAllProcesses', async () => { + const processList = [ + { pid: 4, name: 'System', commandLine: undefined }, + { pid: 5372, name: 'svchost.exe', commandLine: undefined }, + { pid: 5728, name: 'sihost.exe', commandLine: 'sihost.exe' }, + ]; + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); + + 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: '', + }, + ]; + + const output = await provider.getAttachItems(); + + assert.deepEqual(output, expectedOutput); + }); + + test('Python processes should be at the top of the list returned by getAttachItems with getAllProcesses', async () => { + const processList = [ + { pid: 4, name: 'System', commandLine: undefined }, + { pid: 5372, name: 'svchost.exe', commandLine: undefined }, + { pid: 5728, name: 'sihost.exe', commandLine: 'sihost.exe' }, + { pid: 5912, name: 'svchost.exe', commandLine: 'C:\\WINDOWS\\system32\\svchost.exe -k UnistackSvcGroup -s CDPUserSvc' }, + { pid: 6028, name: 'python.exe', commandLine: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/Documents/hello_world.py' }, + { pid: 8026, name: 'python.exe', commandLine: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/Documents/foo_bar.py' }, + ]; + getAllProcessesStub.callsFake((callback: Function) => callback(processList)); + + const expectedOutput: IAttachItem[] = [ + { + label: 'python.exe', + description: '8026', + detail: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/Documents/foo_bar.py', + id: '8026', + processName: 'python.exe', + commandLine: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/Documents/foo_bar.py', + }, + { + label: 'python.exe', + description: '6028', + detail: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/Documents/hello_world.py', + id: '6028', + processName: 'python.exe', + commandLine: 'C:\\Users\\ZA139\\AppData\\Local\\Programs\\Python\\Python37\\python.exe c:/Users/ZA139/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: '', + }, + ]; + const output = await provider.getAttachItems(); assert.deepEqual(output, expectedOutput); diff --git a/webpack.config.js b/webpack.config.js index 5f815e0f..cc4f4ed5 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', // native module, excluded from bundle }, resolve: { // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader