From cb720538bf74bf56ff214cb302c2a9e567f6f2ed Mon Sep 17 00:00:00 2001 From: Alexander Gottwald Date: Mon, 26 Oct 2026 15:45:21 +0100 Subject: [PATCH 1/2] wmic removal #1721 --- src/processTree.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/processTree.ts b/src/processTree.ts index 7a0840e0..530b37ed 100644 --- a/src/processTree.ts +++ b/src/processTree.ts @@ -73,17 +73,17 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a if (process.platform === 'win32') { // attributes columns are in alphabetic order! - const CMD_PAT = /^(.*)\s+([0-9]+)\.[0-9]+[+-][0-9]+\s+([0-9]+)\s+([0-9]+)$/; + const CMD_PAT = /^(.*)\s+([0-9]+)\s+([0-9]+)$/; - const wmic = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'wbem', 'WMIC.exe'); - proc = spawn(wmic, [ 'process', 'get', 'CommandLine,CreationDate,ParentProcessId,ProcessId' ]); + const command = 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.CommandLine) $($_.ParentProcessId) $($_.ProcessId)" }'; + const powershell = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); + proc = spawn(powershell, [ '-Command', command ]); proc.stdout.setEncoding('utf8'); proc.stdout.on('data', lines(line => { let matches = CMD_PAT.exec(line.trim()); - if (matches && matches.length === 5) { - const pid = Number(matches[4]); - const ppid = Number(matches[3]); - const date = Number(matches[2]); + if (matches && matches.length === 4) { + const pid = Number(matches[3]); + const ppid = Number(matches[2]); let args = matches[1].trim(); if (!isNaN(pid) && !isNaN(ppid) && args) { let command = args; @@ -102,7 +102,7 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a args = ''; } } - one(pid, ppid, command, args, date); + one(pid, ppid, command, args); } } })); From e62eb5c030a7a827a96cd1604248ebc11e041ae8 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Fri, 30 Oct 2026 09:28:25 +0800 Subject: [PATCH 2/2] Fix PowerShell process discovery invocation and UTF-8 output --- src/processTree.ts | 17 +++-- test/processTree.test.ts | 146 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 test/processTree.test.ts diff --git a/src/processTree.ts b/src/processTree.ts index 530b37ed..24be362f 100644 --- a/src/processTree.ts +++ b/src/processTree.ts @@ -56,9 +56,8 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a function lines(callback: (a: string) => void) { let unfinished = ''; // unfinished last line of chunk return (data: string | Buffer) => { - const lines = data.toString().split(/\r?\n/); + const lines = (unfinished + data.toString()).split(/\r?\n/); const finishedLines = lines.slice(0, lines.length - 1); - finishedLines[0] = unfinished + finishedLines[0]; // complete previous unfinished line unfinished = lines[lines.length - 1]; // remember unfinished last line of this chunk for next round for (const s of finishedLines) { callback(s); @@ -72,12 +71,20 @@ export function getProcesses(one: (pid: number, ppid: number, command: string, a if (process.platform === 'win32') { - // attributes columns are in alphabetic order! const CMD_PAT = /^(.*)\s+([0-9]+)\s+([0-9]+)$/; - const command = 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.CommandLine) $($_.ParentProcessId) $($_.ProcessId)" }'; + const command = [ + "$ErrorActionPreference = 'Stop';", + 'if ([Console]::OutputEncoding.CodePage -ne 65001) {', + "if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {", + "throw 'UTF-8 process discovery is restricted by policy. Attach using hostName and port instead.';", + '}', + '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8;', + '}', + 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.CommandLine) $($_.ParentProcessId) $($_.ProcessId)" }' + ].join(' '); const powershell = join(process.env['WINDIR'] || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); - proc = spawn(powershell, [ '-Command', command ]); + proc = spawn(powershell, [ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command ], { windowsHide: true }); proc.stdout.setEncoding('utf8'); proc.stdout.on('data', lines(line => { let matches = CMD_PAT.exec(line.trim()); diff --git a/test/processTree.test.ts b/test/processTree.test.ts new file mode 100644 index 00000000..583b4859 --- /dev/null +++ b/test/processTree.test.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; +import * as childProcess from "child_process"; +import { EventEmitter } from "events"; +import { join } from "path"; +import { PassThrough } from "stream"; +import { getProcesses } from "../src/processTree"; + +suite("Windows process discovery", () => { + const windowsTest = process.platform === "win32" ? test : test.skip; + const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!; + const spawnDescriptor = Object.getOwnPropertyDescriptor(childProcess, "spawn")!; + const commandLine = '"C:\\\u5de5\u5177\\jdk\\bin\\java.exe" -agentlib:jdwp=transport=dt_socket,address=5005 \u5e94\u7528'; + let child: EventEmitter & { stdout: PassThrough; stderr: PassThrough }; + let executable: string; + let spawnArgs: string[]; + let spawnOptions: childProcess.SpawnOptions | undefined; + + setup(() => { + child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), stderr: new PassThrough() }); + executable = ""; + spawnArgs = []; + spawnOptions = undefined; + Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" }); + Object.defineProperty(childProcess, "spawn", { + ...spawnDescriptor, + value: (command: string, args: string[], options?: childProcess.SpawnOptions) => { + executable = command; + spawnArgs = args; + spawnOptions = options; + return child; + }, + }); + }); + + teardown(() => { + Object.defineProperty(childProcess, "spawn", spawnDescriptor); + Object.defineProperty(process, "platform", platformDescriptor); + child.stdout.destroy(); + child.stderr.destroy(); + }); + + test("runs Windows PowerShell without profiles, banners, or interaction", async () => { + const discovery = getProcesses(() => { }); + child.emit("close", 0, null); + await discovery; + + assert.strictEqual(executable, join(process.env.WINDIR || "C:\\Windows", + "System32", "WindowsPowerShell", "v1.0", "powershell.exe")); + assert.deepStrictEqual(spawnArgs.slice(0, -1), ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]); + assert.deepStrictEqual(spawnOptions, { windowsHide: true }); + }); + + test("preserves Unicode command lines split across UTF-8 chunks", async () => { + const processes: { pid: number; ppid: number; command: string; args: string }[] = []; + const discovery = getProcesses((pid, ppid, command, args) => processes.push({ pid, ppid, command, args })); + const output = Buffer.from(`${commandLine} 7 42\r\n`); + child.stdout.write(output.slice(0, 5)); + child.stdout.write(output.slice(5, output.length - 1)); + child.stdout.write(output.slice(output.length - 1)); + child.emit("close", 0, null); + await discovery; + + assert.deepStrictEqual(processes, [{ + pid: 42, + ppid: 7, + command: "C:\\\u5de5\u5177\\jdk\\bin\\java.exe", + args: "-agentlib:jdwp=transport=dt_socket,address=5005 \u5e94\u7528", + }]); + }); + + test("surfaces PowerShell errors instead of reporting an empty process list", async () => { + const discovery = getProcesses(() => assert.fail("Unexpected process")); + child.stderr.write("UTF-8 process discovery is restricted by policy."); + child.emit("close", 1, null); + await assert.rejects(discovery, /UTF-8 process discovery is restricted by policy/); + }); + + test("rejects unsuccessful PowerShell exits without stderr", async () => { + const discovery = getProcesses(() => { }); + child.emit("close", 1, null); + await assert.rejects(discovery, /process terminated with exit code: 1/); + }); + + test("rejects PowerShell launch failures", async () => { + const discovery = getProcesses(() => { }); + const error = new Error("PowerShell could not be started"); + child.emit("error", error); + await assert.rejects(discovery, (actual: unknown) => actual === error); + }); + + async function runScript(prefix: string): Promise> { + const discovery = getProcesses(() => { }); + child.emit("close", 0, null); + await discovery; + const result = childProcess.spawnSync(executable, + [...spawnArgs.slice(0, -1), prefix + spawnArgs[spawnArgs.length - 1]], + { encoding: "utf8", timeout: 10000, windowsHide: true }); + assert.ifError(result.error); + return result; + } + + function fixture(codePage: number, languageMode: "FullLanguage" | "ConstrainedLanguage"): string { + return [ + `[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding(${codePage});`, + `$record = [pscustomobject]@{ CommandLine = '${commandLine}'; ParentProcessId = 7; ProcessId = 42 };`, + "function Get-CimInstance { $record };", + `$ExecutionContext.SessionState.LanguageMode = '${languageMode}'; `, + ].join(" "); + } + + windowsTest("converts a legacy code page to UTF-8 in FullLanguage mode", async function() { + this.timeout(15000); + const result = await runScript(fixture(437, "FullLanguage")); + assert.strictEqual(result.status, 0, result.stderr); + assert.strictEqual(result.stderr, ""); + assert.strictEqual(result.stdout, `${commandLine} 7 42\r\n`); + }); + + windowsTest("preserves UTF-8 output in ConstrainedLanguage mode", async function() { + this.timeout(15000); + const result = await runScript(fixture(65001, "ConstrainedLanguage")); + assert.strictEqual(result.status, 0, result.stderr); + assert.strictEqual(result.stderr, ""); + assert.strictEqual(result.stdout, `${commandLine} 7 42\r\n`); + }); + + windowsTest("rejects legacy encoding in ConstrainedLanguage mode with actionable guidance", async function() { + this.timeout(15000); + const result = await runScript(fixture(437, "ConstrainedLanguage")); + assert.strictEqual(result.status, 1); + assert.strictEqual(result.stdout, ""); + assert.ok(result.stderr.replace(/\s+/g, " ").includes( + "UTF-8 process discovery is restricted by policy. Attach using hostName and port instead."), result.stderr); + }); + + windowsTest("terminates on non-terminating CIM errors", async function() { + this.timeout(15000); + const result = await runScript("function Get-CimInstance { Write-Error 'CIM discovery failed' }; "); + assert.strictEqual(result.status, 1); + assert.strictEqual(result.stdout, ""); + assert.ok(result.stderr.includes("CIM discovery failed"), result.stderr); + }); +});