From 64f9d9f973a9c5e4e7e67be11610a6844112e508 Mon Sep 17 00:00:00 2001 From: Peter Schuster Date: Wed, 22 Jul 2026 20:55:52 +0200 Subject: [PATCH] fix: replace deprecated wmic process stats with CIM query wmic is removed on current Windows 11 builds, so process usage polling in ChildProcessTracker silently failed. Replace the two blocking execFileSync('wmic', ...) calls with a single async PowerShell Get-CimInstance query; Win32_PerfFormattedData_PerfProc_Process provides both cpu percentage and working set, and ConvertTo-Json keeps the output locale-independent. Also fixes memory usage being overstated 1024x: WorkingSetSize is reported in bytes, not KB, so the 100 MB warning threshold effectively fired at ~100 KB for every tracked process. --- .../src/util/processUtils.test.ts | 29 +++++++- core/aws-lsp-core/src/util/processUtils.ts | 66 ++++++++++++------- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/core/aws-lsp-core/src/util/processUtils.test.ts b/core/aws-lsp-core/src/util/processUtils.test.ts index c358cd6db1..45b9c321a7 100644 --- a/core/aws-lsp-core/src/util/processUtils.test.ts +++ b/core/aws-lsp-core/src/util/processUtils.test.ts @@ -4,7 +4,7 @@ import * as os from 'os' import * as path from 'path' import * as sinon from 'sinon' import { chmod } from 'fs/promises' -import { ChildProcess, ChildProcessResult, ChildProcessTracker, eof } from './processUtils' +import { ChildProcess, ChildProcessResult, ChildProcessTracker, eof, parseCimProcessStats } from './processUtils' import { waitUntil } from './timeoutUtils' import { TestFolder } from '../test/testFolder' import { TestFeatures } from '@aws/language-server-runtimes/testing' @@ -395,6 +395,33 @@ describe('ChildProcessTracker', function () { }) }) +describe('parseCimProcessStats', function () { + it('parses a single-object JSON result', function () { + const output = '{\r\n "PercentProcessorTime": 42,\r\n "WorkingSet": 157286400\r\n}\r\n' + assert.deepStrictEqual(parseCimProcessStats(output), { cpu: 42, memory: 157286400 }) + }) + + it('uses the first entry of an array result', function () { + const output = + '[{"PercentProcessorTime": 7, "WorkingSet": 1048576}, {"PercentProcessorTime": 99, "WorkingSet": 2097152}]' + assert.deepStrictEqual(parseCimProcessStats(output), { cpu: 7, memory: 1048576 }) + }) + + it('returns zero usage for empty output (process already exited)', function () { + assert.deepStrictEqual(parseCimProcessStats(''), { cpu: 0, memory: 0 }) + assert.deepStrictEqual(parseCimProcessStats('\r\n'), { cpu: 0, memory: 0 }) + }) + + it('returns zero usage when properties are missing', function () { + assert.deepStrictEqual(parseCimProcessStats('{}'), { cpu: 0, memory: 0 }) + assert.deepStrictEqual(parseCimProcessStats('null'), { cpu: 0, memory: 0 }) + }) + + it('throws on malformed output so the caller can log it', function () { + assert.throws(() => parseCimProcessStats('not json')) + }) +}) + async function startTestProcess( tempFolder: TestFolder, logger: Features['logging'], diff --git a/core/aws-lsp-core/src/util/processUtils.ts b/core/aws-lsp-core/src/util/processUtils.ts index eca985d548..b0edb7f08d 100644 --- a/core/aws-lsp-core/src/util/processUtils.ts +++ b/core/aws-lsp-core/src/util/processUtils.ts @@ -76,6 +76,26 @@ export interface ProcessStats { memory: number cpu: number } + +/** + * Parses the JSON emitted by the `Get-CimInstance ... | ConvertTo-Json` process-stats query. + * Empty output (process already exited) yields zero usage. Exported for testing. + */ +export function parseCimProcessStats(output: string): ProcessStats { + const trimmed = output.trim() + if (!trimmed) { + return { cpu: 0, memory: 0 } + } + const parsed = JSON.parse(trimmed) + // ConvertTo-Json emits a bare object for a single match and an array for multiple. + const stats = Array.isArray(parsed) ? parsed[0] : parsed + const cpu = Number(stats?.PercentProcessorTime) + const memory = Number(stats?.WorkingSet) + return { + cpu: isNaN(cpu) ? 0 : cpu, + memory: isNaN(memory) ? 0 : memory, + } +} export class ChildProcessTracker { static #instance: ChildProcessTracker static readonly pollingInterval: number = 10000 // Check usage every 10 seconds @@ -120,7 +140,7 @@ export class ChildProcessTracker { this.logging.warn(`Missing process with id ${pid}`) return } - const stats = this.getUsage(pid) + const stats = await this.getUsage(pid) if (stats) { this.logIfExceeds(pid, stats) } @@ -162,36 +182,32 @@ export class ChildProcessTracker { this.#processByPid.clear() } - private getUsage(pid: number): ProcessStats { + private async getUsage(pid: number): Promise { try { - return process.platform === 'win32' ? getWindowsUsage() : getUnixUsage() + return process.platform === 'win32' ? await getWindowsUsage() : getUnixUsage() } catch (e) { this.logging.warn(`Failed to get process stats for ${pid}: ${e}`) return { cpu: 0, memory: 0 } } - function getWindowsUsage() { - const cpuOutput = proc - .execFileSync('wmic', [ - 'path', - 'Win32_PerfFormattedData_PerfProc_Process', - 'where', - `IDProcess=${pid}`, - 'get', - 'PercentProcessorTime', - ]) - .toString() - const memOutput = proc - .execFileSync('wmic', ['process', 'where', `ProcessId=${pid}`, 'get', 'WorkingSetSize']) - .toString() - - const cpuPercentage = parseFloat(cpuOutput.split('\n')[1]) - const memoryBytes = parseInt(memOutput.split('\n')[1]) * 1024 - - return { - cpu: isNaN(cpuPercentage) ? 0 : cpuPercentage, - memory: memoryBytes, - } + // wmic was removed from current Windows 11 builds; query CIM through PowerShell instead. + // A single Win32_PerfFormattedData query provides both cpu percentage and working set, + // and ConvertTo-Json keeps the output locale-independent. + async function getWindowsUsage(): Promise { + const output = await new Promise((resolve, reject) => { + proc.execFile( + 'powershell.exe', + [ + '-NoProfile', + '-NonInteractive', + '-Command', + `Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -Filter 'IDProcess=${pid}' | Select-Object PercentProcessorTime,WorkingSet | ConvertTo-Json`, + ], + { windowsHide: true }, + (error, stdout) => (error ? reject(error) : resolve(stdout)) + ) + }) + return parseCimProcessStats(output) } function getUnixUsage() {