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);
+		}
+	});
 });