extension/src/utils: deprecate wmic windows process picker Replace the wmic process picker with a dependency suggested by connor4312 (a vscode maintainer) and currently used by the project microsoft/vscode-js-debug. The dependency "@vscode/win32-app-container-tokens" is different from regular deps as it is written by rust but exposed to us through 1. a typescript type, 2. a precompiled binary .node. Because of the special nature of the package, a few extra steps: - set the deps as external: esbuild inline all the functions but functions exposed by the package is not inlinable as the nature is command execution. - include !deps in .vscodeignore: inform vsce to include the precompiled binary when creating the vsix. - include deps in package.json: provide type information about the desp. Fixes golang/vscode-go#3687 Change-Id: I49b6fd0ad6e36d740ab2a5edecdcc34be2899ff7 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/816360 LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Madeline Kalil <mkalil@google.com> Auto-Submit: Hongxiang Jiang <hxjiang@golang.org>
diff --git a/extension/.vscodeignore b/extension/.vscodeignore index 2d4285d..2a344cd 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore
@@ -18,6 +18,7 @@ docs/ go.* node_modules/ +!node_modules/@vscode/win32-app-container-tokens/** out/ src/ test/
diff --git a/extension/LICENSE b/extension/LICENSE index 1444581..ef43d82 100644 --- a/extension/LICENSE +++ b/extension/LICENSE
@@ -28,6 +28,32 @@ ----- +The following software may be included in this product: @vscode/win32-app-container-tokens. This software contains the following license and notice below: + +MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +----- + The following software may be included in this product: balanced-match. A copy of the source code may be downloaded from git://github.com/juliangruber/balanced-match.git. This software contains the following license and notice below: (MIT)
diff --git a/extension/package-lock.json b/extension/package-lock.json index 34303ef..57bd60f 100644 --- a/extension/package-lock.json +++ b/extension/package-lock.json
@@ -9,6 +9,7 @@ "version": "0.58.0-dev", "license": "MIT", "dependencies": { + "@vscode/win32-app-container-tokens": "0.2.0", "diff": "8.0.4", "glob": "7.1.7", "json-rpc2": "^2.0.0", @@ -2259,6 +2260,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@vscode/win32-app-container-tokens": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@vscode/win32-app-container-tokens/-/win32-app-container-tokens-0.2.0.tgz", + "integrity": "sha512-l2Xvw0q5dPT9jNg+Nj/ohqyaqJaCC0KvZkP1wkgfFbJFAtGHFjVp++Kghni3CZEXOErOGkzlverT6kakpmY9TQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", @@ -10064,6 +10074,11 @@ "dev": true, "optional": true }, + "@vscode/win32-app-container-tokens": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@vscode/win32-app-container-tokens/-/win32-app-container-tokens-0.2.0.tgz", + "integrity": "sha512-l2Xvw0q5dPT9jNg+Nj/ohqyaqJaCC0KvZkP1wkgfFbJFAtGHFjVp++Kghni3CZEXOErOGkzlverT6kakpmY9TQ==" + }, "acorn": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
diff --git a/extension/package.json b/extension/package.json index 428d392..7d37246 100644 --- a/extension/package.json +++ b/extension/package.json
@@ -38,7 +38,7 @@ "clean": "rm -rf ./dist/* && rm -rf ./out/* && rm -rf ./bin/* && rm *.vsix", "package": "cp ../README.md ./README.md && npx vsce package --baseContentUrl https://github.com/golang/vscode-go/raw/HEAD --baseImagesUrl https://github.com/golang/vscode-go/raw/HEAD", "vscode:prepublish": "npm run compile", - "bundle": "esbuild src/goMain.ts debugAdapter=src/debugAdapter/goDebug.ts --bundle --outdir=dist --external:vscode --format=cjs --platform=node", + "bundle": "esbuild src/goMain.ts debugAdapter=src/debugAdapter/goDebug.ts --bundle --outdir=dist --external:vscode --external:@vscode/win32-app-container-tokens --format=cjs --platform=node", "bundle-dev": "npm run bundle -- --sourcemap", "bundle-watch": "npm run bundle -- --sourcemap --watch", "test-compile": "tsc -p ./", @@ -52,6 +52,7 @@ }, "extensionDependencies": [], "dependencies": { + "@vscode/win32-app-container-tokens": "0.2.0", "diff": "8.0.4", "glob": "7.1.7", "json-rpc2": "^2.0.0",
diff --git a/extension/src/pickProcess.ts b/extension/src/pickProcess.ts index ad4209c..3302ec5 100644 --- a/extension/src/pickProcess.ts +++ b/extension/src/pickProcess.ts
@@ -10,7 +10,7 @@ import { lsofDarwinCommand, parseLsofProcesses } from './utils/lsofProcessParser'; import { getEnvPath, getCurrentGoRoot } from './utils/pathUtils'; import { parsePsProcesses, psDarwinCommand, psLinuxCommand } from './utils/psProcessParser'; -import { parseWmicProcesses, wmicCommand } from './utils/wmicProcessParser'; +import { getWindowsProcesses } from './utils/windowsProcessParser'; import vscode = require('vscode'); export async function pickProcess(): Promise<string> { @@ -173,27 +173,23 @@ } async function getAllProcesses(): Promise<AttachItem[]> { - let processCmd: ProcessListCommand; switch (process.platform) { case 'win32': - processCmd = wmicCommand; - break; - case 'darwin': - processCmd = psDarwinCommand; - break; - case 'linux': - processCmd = psLinuxCommand; - break; + return await getWindowsProcesses(); + case 'darwin': { + const { stdout } = await runCommand(psDarwinCommand); + return parsePsProcesses(stdout); + } + case 'linux': { + const { stdout } = await runCommand(psLinuxCommand); + return parsePsProcesses(stdout); + } default: // Other operating systems are not supported. throw new Error( `'pickProcess' and 'pickGoProcess' are not supported for ${process.platform}. Set process id in launch.json instead.` ); } - - const { stdout } = await runCommand(processCmd); - - return process.platform === 'win32' ? parseWmicProcesses(stdout) : parsePsProcesses(stdout); } async function runCommand(
diff --git a/extension/src/utils/windowsProcessParser.ts b/extension/src/utils/windowsProcessParser.ts new file mode 100644 index 0000000..7b4228c --- /dev/null +++ b/extension/src/utils/windowsProcessParser.ts
@@ -0,0 +1,78 @@ +/*--------------------------------------------------------- + * Copyright 2026 The Go Authors. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------*/ + +import type * as Win32AppContainerTokens from '@vscode/win32-app-container-tokens'; +import path = require('path'); +import { AttachItem } from '../pickProcess'; + +export async function getWindowsProcesses(): Promise<AttachItem[]> { + const win = await getWinUtils(); + return win.getProcessInfo().map(parseWindowsProcessInfo); +} + +// Adapted from vscode-js-debug https://github.com/microsoft/vscode-js-debug/blob/a72801b175c1856138e30679624c97b3b062dc65/src/ui/processTree/windowsProcessTree.ts + +/** + * Parses a Windows ProcessInfo into an AttachItem for the process picker. + * + * Extracts the executable path from `proc.commandLine`: + * - Quoted path (e.g. `"C:\Program Files\app.exe" --arg`): parses up to `'" '` and strips the leading quote. + * - Unquoted path (e.g. `C:\app.exe --arg`): parses up to the first space `' '`. + * + * Example input: `{ processId: 1234, commandLine: '"C:\\Program Files\\Go\\bin\\go.exe" run main.go', ... }` + * Output executable: `'C:\\Program Files\\Go\\bin\\go.exe'` + */ +export function parseWindowsProcessInfo(proc: Win32AppContainerTokens.ProcessInfo): AttachItem { + let command: string; + + const quoteEnd = proc.commandLine.indexOf('" '); + if (quoteEnd === -1) { + const space = proc.commandLine.indexOf(' '); + if (space === -1) { + command = proc.commandLine; + } else { + command = proc.commandLine.slice(0, space); + } + } else { + command = proc.commandLine.slice(1, quoteEnd); + } + + const pidStr = proc.processId.toString(); + const processName = (path.win32 ? path.win32.basename(command) : path.basename(command)) || `Process ${pidStr}`; + + return { + id: pidStr, + label: processName, + processName: processName, + description: pidStr, + detail: proc.commandLine, + commandLine: proc.commandLine, + executable: command + }; +} + +// Adapted from vscode-js-debug https://github.com/microsoft/vscode-js-debug/blob/a72801b175c1856138e30679624c97b3b062dc65/src/common/win32Utils.ts + +function once<T>(fn: () => Promise<T>): () => Promise<T> { + let value: Promise<T> | undefined; + return () => { + if (!value) { + value = fn(); + } + return value; + }; +} + +const load = once((): Promise<typeof Win32AppContainerTokens> => { + return Promise.resolve(require('@vscode/win32-app-container-tokens')); +}); + +function getWinUtils(): Promise<typeof Win32AppContainerTokens> { + if (process.platform !== 'win32') { + throw new Error('Not running on Windows'); + } + + return load(); +}
diff --git a/extension/src/utils/wmicProcessParser.ts b/extension/src/utils/wmicProcessParser.ts deleted file mode 100644 index 8139f56..0000000 --- a/extension/src/utils/wmicProcessParser.ts +++ /dev/null
@@ -1,90 +0,0 @@ -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - * Modification copyright 2021 The Go Authors. All rights reserved. - * Licensed under the MIT License. See LICENSE in the project root for license information. - *--------------------------------------------------------*/ - -// Modified from: -// https://github.com/microsoft/vscode-python/blob/main/src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts. -// - Added arguments to get the ExecutablePath from wmic. - -'use strict'; - -import { AttachItem, ProcessListCommand } from '../pickProcess'; - -const wmicNameTitle = 'Name'; -const wmicCommandLineTitle = 'CommandLine'; -const wmicPidTitle = 'ProcessId'; -const wmicExecutableTitle = 'ExecutablePath'; -const defaultEmptyEntry: AttachItem = { - label: '', - description: '', - detail: '', - id: '', - processName: '', - commandLine: '' -}; - -// Perf numbers on Win10: -// | # of processes | Time (ms) | -// |----------------+-----------| -// | 309 | 413 | -// | 407 | 463 | -// | 887 | 746 | -// | 1308 | 1132 | -export const wmicCommand: ProcessListCommand = { - command: 'wmic', - args: ['process', 'get', 'Name,ProcessId,CommandLine,ExecutablePath', '/FORMAT:list'] -}; - -export function parseWmicProcesses(processes: string): AttachItem[] { - const lines: string[] = processes.split('\r\n'); - const processEntries: AttachItem[] = []; - let entry = { ...defaultEmptyEntry }; - - for (const line of lines) { - if (!line.length) { - continue; - } - - parseLineFromWmic(line, entry); - - // Each entry of processes has ProcessId as the last line - if (line.lastIndexOf(wmicPidTitle, 0) === 0) { - processEntries.push(entry); - entry = { ...defaultEmptyEntry }; - } - } - - return processEntries; -} - -function parseLineFromWmic(line: string, item: AttachItem): AttachItem { - const splitter = line.indexOf('='); - const currentItem = item; - - if (splitter > 0) { - const key = line.slice(0, splitter).trim(); - let value = line.slice(splitter + 1).trim(); - - if (key === wmicNameTitle) { - currentItem.label = value; - currentItem.processName = value; - } else if (key === wmicPidTitle) { - currentItem.description = value; - currentItem.id = value; - } else if (key === wmicCommandLineTitle) { - const dosDevicePrefix = '\\??\\'; // DOS device prefix, see https://reverseengineering.stackexchange.com/a/15178 - if (value.lastIndexOf(dosDevicePrefix, 0) === 0) { - value = value.slice(dosDevicePrefix.length); - } - - currentItem.detail = value; - currentItem.commandLine = value; - } else if (key === wmicExecutableTitle) { - currentItem.executable = value; - } - } - - return currentItem; -}
diff --git a/extension/test/integration/pickProcess.test.ts b/extension/test/integration/pickProcess.test.ts index e434580..7a5d1ac 100644 --- a/extension/test/integration/pickProcess.test.ts +++ b/extension/test/integration/pickProcess.test.ts
@@ -6,6 +6,8 @@ import assert from 'assert'; import { AttachItem, compareByProcessId, mergeExecutableAttachItem, parseGoVersionOutput } from '../../src/pickProcess'; import { parseLsofProcesses } from '../../src/utils/lsofProcessParser'; +import { parsePsProcesses } from '../../src/utils/psProcessParser'; +import { parseWindowsProcessInfo } from '../../src/utils/windowsProcessParser'; suite('Pick Process Tests', () => { test('Parse go version output', () => { @@ -162,4 +164,95 @@ } } }); + + test('Parse ps output (Linux/macOS)', () => { + const secondColumnCharacters = 50; + const commColumnTitle = ''.padStart(secondColumnCharacters, 'a'); + const commVal = 'my-go-app'.padEnd(secondColumnCharacters - 1, ' '); + const input = `PID ${commColumnTitle} ARGS\n 1234 ${commVal} /usr/local/bin/my-go-app --flag`; + const got = parsePsProcesses(input); + assert.strictEqual(got.length, 1); + assert.strictEqual(got[0].id, '1234'); + assert.strictEqual(got[0].processName, 'my-go-app'); + assert.strictEqual(got[0].commandLine, '/usr/local/bin/my-go-app --flag'); + }); + + test('Parse windows process info', () => { + const tt = [ + { + input: { + processId: 1234, + parentProcessId: 100, + creationDate: 1700000000, + commandLine: '"C:\\Program Files\\Go\\bin\\go.exe" run main.go' + }, + want: { + id: '1234', + label: 'go.exe', + processName: 'go.exe', + description: '1234', + detail: '"C:\\Program Files\\Go\\bin\\go.exe" run main.go', + executable: 'C:\\Program Files\\Go\\bin\\go.exe' + } + }, + { + input: { + processId: 5678, + parentProcessId: 100, + creationDate: 1700000000, + commandLine: 'C:\\tools\\myapp.exe --port 8080' + }, + want: { + id: '5678', + label: 'myapp.exe', + processName: 'myapp.exe', + description: '5678', + detail: 'C:\\tools\\myapp.exe --port 8080', + executable: 'C:\\tools\\myapp.exe' + } + }, + { + input: { + processId: 9012, + parentProcessId: 100, + creationDate: 1700000000, + commandLine: '\\??\\C:\\Windows\\system32\\cmd.exe' + }, + want: { + id: '9012', + label: 'cmd.exe', + processName: 'cmd.exe', + description: '9012', + detail: '\\??\\C:\\Windows\\system32\\cmd.exe', + executable: '\\??\\C:\\Windows\\system32\\cmd.exe' + } + }, + { + input: { + processId: 44, + parentProcessId: 0, + creationDate: 1700000000, + commandLine: '' + }, + want: { + id: '44', + label: 'Process 44', + processName: 'Process 44', + description: '44', + detail: '', + executable: '' + } + } + ]; + + for (const tc of tt) { + const got = parseWindowsProcessInfo(tc.input); + assert.strictEqual(got.id, tc.want.id); + assert.strictEqual(got.label, tc.want.label); + assert.strictEqual(got.processName, tc.want.processName); + assert.strictEqual(got.description, tc.want.description); + assert.strictEqual(got.detail, tc.want.detail); + assert.strictEqual(got.executable, tc.want.executable); + } + }); });