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() {