pyre-cli 10.4.0 → 10.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{blender-6ZX5SPYC.js → blender-OXH3PVYM.js} +3 -3
- package/dist/{brewHealth-VFW33QB3.js → brewHealth-JE4EZ63L.js} +1 -1
- package/dist/{buildCmd-E7KUCFYG.js → buildCmd-R6RY3XZR.js} +3 -3
- package/dist/{checkCmd-QLYTP77Y.js → checkCmd-KF6G77SC.js} +5 -5
- package/dist/{chunk-RKPGPVTO.js → chunk-2SUFLPIH.js} +5 -5
- package/dist/chunk-735X2GPS.js +88 -0
- package/dist/{chunk-QEH4LR6P.js → chunk-D3HWHU3X.js} +4 -4
- package/dist/{chunk-FYCBGUTW.js → chunk-GPSU54LZ.js} +2 -2
- package/dist/{chunk-NMGQVRCA.js → chunk-KLANOKZF.js} +2 -2
- package/dist/{chunk-OCL7P7N7.js → chunk-YXZ4XREM.js} +195 -111
- package/dist/{collectors-V7SCIIDH.js → collectors-DQEVTONN.js} +5 -5
- package/dist/{doctor-O4DIAMHK.js → doctor-SCHRYF6D.js} +2 -2
- package/dist/{extensions-QQEV7U3D.js → extensions-BWXMZVMN.js} +1 -1
- package/dist/{flightRecorder-GJIAQOXC.js → flightRecorder-J4TESP5M.js} +5 -5
- package/dist/index.js +55 -28
- package/dist/monitors/platform/windows-ui.ps1 +14 -4
- package/dist/{netUsageCmd-JP6YKBPF.js → netUsageCmd-2QVRC6FX.js} +5 -5
- package/dist/{optimizeCmd-WZKLFYZU.js → optimizeCmd-ZF7U3DXR.js} +2 -2
- package/dist/{pipeCmd-CPK6FVKB.js → pipeCmd-XBWAYRRU.js} +5 -5
- package/dist/{profileProcCmd-5Y3733E7.js → profileProcCmd-33YB4RXZ.js} +1 -1
- package/dist/{prometheusCmd-G2CSEZRQ.js → prometheusCmd-6PXZG73L.js} +5 -5
- package/dist/{run-UJQYT2TO.js → run-744KZ7CE.js} +1 -1
- package/dist/{serveCmd-KDPWLVQI.js → serveCmd-R2Y7O3XC.js} +5 -5
- package/dist/{smartCmd-2BJJ37GV.js → smartCmd-C33WRXA2.js} +1 -1
- package/dist/{statuslineCmd-EUKYRR2G.js → statuslineCmd-QPBGXC2G.js} +5 -5
- package/dist/{topoCmd-RTXQJ6XA.js → topoCmd-RVWFSPHE.js} +5 -5
- package/dist/{watchdogCmd-XM6OPTKM.js → watchdogCmd-346TXWWH.js} +6 -6
- package/package.json +1 -1
- package/src/index.ts +31 -2
- package/src/monitors/platform/windows-ui.ps1 +14 -4
- package/src/monitors/platform/windows.ts +225 -102
- package/src/monitors/run.ts +83 -7
- package/src/platform.test.ts +24 -0
- package/dist/chunk-MZO5QKUR.js +0 -32
|
@@ -10,7 +10,28 @@
|
|
|
10
10
|
import os from 'node:os';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import fs from 'node:fs';
|
|
13
|
-
import { run, runPowerShell } from '../run.js';
|
|
13
|
+
import { run, runPowerShell as rawRunPowerShell } from '../run.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cache policy per query (stale-while-revalidate, see run.ts). Each powershell.exe launch is
|
|
17
|
+
* expensive, so static facts are cached for minutes and volatile ones for a few seconds.
|
|
18
|
+
* Order matters: the first matching rule wins.
|
|
19
|
+
*/
|
|
20
|
+
const PS_TTL_RULES: Array<[RegExp, number]> = [
|
|
21
|
+
[/Win32_Battery|BatteryCycleCount|BatteryStatus/, 10_000],
|
|
22
|
+
[/Win32_Processor|Win32_VideoController|HardwareInformation|Win32_OperatingSystem/, 10 * 60_000],
|
|
23
|
+
[/GPUEngine/, 3_000],
|
|
24
|
+
[/LibreHardwareMonitor|OpenHardwareMonitor|MSAcpi_ThermalZone|Thermal Zone Information/, 20_000],
|
|
25
|
+
[/Win32_PageFileUsage|Win32_LogicalDisk/, 30_000],
|
|
26
|
+
[/Get-NetIPConfiguration/, 60_000],
|
|
27
|
+
[/Get-NetTCPConnection/, 5_000],
|
|
28
|
+
[/Get-Process\b/, 5_000],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function runPowerShell(script: string, fallback = '', timeout = 10000): Promise<string> {
|
|
32
|
+
const rule = PS_TTL_RULES.find(([re]) => re.test(script));
|
|
33
|
+
return rawRunPowerShell(script, fallback, timeout, rule ? rule[1] : 0);
|
|
34
|
+
}
|
|
14
35
|
import type {
|
|
15
36
|
CpuData,
|
|
16
37
|
MemoryData,
|
|
@@ -226,48 +247,24 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
226
247
|
}
|
|
227
248
|
|
|
228
249
|
export async function getWindowsCpuTemperature(): Promise<number | undefined> {
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
''
|
|
243
|
-
);
|
|
244
|
-
const ohmVal = safeParseFloat(ohmRaw);
|
|
245
|
-
if (ohmVal >= 15 && ohmVal <= 115) return Math.round(ohmVal * 10) / 10;
|
|
246
|
-
} catch {}
|
|
247
|
-
|
|
248
|
-
// 2. Try PowerShell CIM MSAcpi_ThermalZoneTemperature (modern Windows 10/11 replacement for wmic)
|
|
249
|
-
try {
|
|
250
|
-
const cimRaw = await runPowerShell(
|
|
251
|
-
`(Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty CurrentTemperature)`,
|
|
252
|
-
''
|
|
253
|
-
);
|
|
254
|
-
const kelvinTenths = safeParseInt(cimRaw);
|
|
255
|
-
if (kelvinTenths > 2732 && kelvinTenths < 4200) {
|
|
256
|
-
const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
|
|
257
|
-
if (c >= 15 && c <= 115) return c;
|
|
258
|
-
}
|
|
259
|
-
} catch {}
|
|
250
|
+
// One PowerShell launch that tries, in order: LibreHardwareMonitor / OpenHardwareMonitor WMI
|
|
251
|
+
// (AMD Ryzen + Intel desktops), ACPI thermal zone (needs admin on many machines), then the
|
|
252
|
+
// locale-independent thermal zone performance class. Outputs Celsius or nothing.
|
|
253
|
+
const script = [
|
|
254
|
+
`$t = $null`,
|
|
255
|
+
`foreach ($ns in 'root/LibreHardwareMonitor','root/OpenHardwareMonitor') {`,
|
|
256
|
+
` if ($t) { break }`,
|
|
257
|
+
` try { $v = Get-CimInstance -Namespace $ns -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%CPU%'" -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Value; if ($v) { $t = [double]$v } } catch {}`,
|
|
258
|
+
`}`,
|
|
259
|
+
`if (-not $t) { try { $k = Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction Stop | Select-Object -First 1 -ExpandProperty CurrentTemperature; if ($k -gt 2732) { $t = ($k - 2732) / 10 } } catch {} }`,
|
|
260
|
+
`if (-not $t) { try { $k = Get-CimInstance -ClassName Win32_PerfFormattedData_Counters_ThermalZoneInformation -ErrorAction Stop | Where-Object { $_.Temperature -gt 273 -and $_.Temperature -lt 400 } | Select-Object -First 1 -ExpandProperty Temperature; if ($k) { $t = $k - 273.15 } } catch {} }`,
|
|
261
|
+
`if ($t) { [string]([math]::Round($t, 1)) }`,
|
|
262
|
+
].join('\n');
|
|
260
263
|
|
|
261
|
-
// 3. Try Windows Performance Counter
|
|
262
264
|
try {
|
|
263
|
-
const raw = await runPowerShell(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
);
|
|
267
|
-
const tempK = safeParseFloat(raw);
|
|
268
|
-
if (tempK > 200 && tempK < 400) {
|
|
269
|
-
return Math.round((tempK - 273.15) * 10) / 10;
|
|
270
|
-
}
|
|
265
|
+
const raw = await runPowerShell(script, '');
|
|
266
|
+
const val = safeParseFloat(raw.replace(',', '.'));
|
|
267
|
+
if (val >= 15 && val <= 115) return Math.round(val * 10) / 10;
|
|
271
268
|
} catch {}
|
|
272
269
|
|
|
273
270
|
return undefined;
|
|
@@ -358,18 +355,54 @@ export async function collectWindowsDisk(): Promise<DiskData[]> {
|
|
|
358
355
|
// fallback
|
|
359
356
|
}
|
|
360
357
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
358
|
+
// Fallback: real numbers from the filesystem API (no PowerShell needed)
|
|
359
|
+
const fallback: DiskData[] = [];
|
|
360
|
+
for (let code = 67; code <= 90; code++) { // C..Z
|
|
361
|
+
const drive = `${String.fromCharCode(code)}:`;
|
|
362
|
+
try {
|
|
363
|
+
const st = (fs as any).statfsSync?.(`${drive}\\`);
|
|
364
|
+
if (!st) break;
|
|
365
|
+
const totalBytes = Number(st.blocks) * Number(st.bsize);
|
|
366
|
+
const freeBytes = Number(st.bavail) * Number(st.bsize);
|
|
367
|
+
if (totalBytes > 0) {
|
|
368
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
369
|
+
fallback.push({
|
|
370
|
+
filesystem: drive,
|
|
371
|
+
size: formatBytesStr(totalBytes),
|
|
372
|
+
used: formatBytesStr(usedBytes),
|
|
373
|
+
available: formatBytesStr(freeBytes),
|
|
374
|
+
capacity: `${Math.round((usedBytes / totalBytes) * 100)}%`,
|
|
375
|
+
mountpoint: drive,
|
|
376
|
+
readBytesSec: 0,
|
|
377
|
+
writeBytesSec: 0,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
} catch {
|
|
381
|
+
// drive letter not present
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (fallback.length === 0) {
|
|
385
|
+
try {
|
|
386
|
+
const st = (fs as any).statfsSync?.(os.homedir());
|
|
387
|
+
const totalBytes = st ? Number(st.blocks) * Number(st.bsize) : 0;
|
|
388
|
+
const freeBytes = st ? Number(st.bavail) * Number(st.bsize) : 0;
|
|
389
|
+
if (totalBytes > 0) {
|
|
390
|
+
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
391
|
+
const drive = path.parse(os.homedir()).root.replace(/[\\/]+$/, '') || 'C:';
|
|
392
|
+
fallback.push({
|
|
393
|
+
filesystem: drive,
|
|
394
|
+
size: formatBytesStr(totalBytes),
|
|
395
|
+
used: formatBytesStr(usedBytes),
|
|
396
|
+
available: formatBytesStr(freeBytes),
|
|
397
|
+
capacity: `${Math.round((usedBytes / totalBytes) * 100)}%`,
|
|
398
|
+
mountpoint: drive,
|
|
399
|
+
readBytesSec: 0,
|
|
400
|
+
writeBytesSec: 0,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} catch {}
|
|
404
|
+
}
|
|
405
|
+
return fallback;
|
|
373
406
|
}
|
|
374
407
|
|
|
375
408
|
function formatBytesStr(bytes: number): string {
|
|
@@ -381,39 +414,96 @@ function formatBytesStr(bytes: number): string {
|
|
|
381
414
|
|
|
382
415
|
// ─── Battery ───────────────────────────────────────────────────────────────────
|
|
383
416
|
|
|
417
|
+
export interface WindowsBatteryRaw {
|
|
418
|
+
level: number;
|
|
419
|
+
statusCode: number;
|
|
420
|
+
runTimeMinutes: number;
|
|
421
|
+
winDischargeRate: number;
|
|
422
|
+
cycles: number;
|
|
423
|
+
fullChargeMWh: number;
|
|
424
|
+
designMWh: number;
|
|
425
|
+
rateMilliwatts: number;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function parseWindowsBatteryOutput(raw: string): WindowsBatteryRaw | null {
|
|
429
|
+
const line = raw.split('\n').map(l => l.trim()).find(l => l.includes('|'));
|
|
430
|
+
if (!line) return null;
|
|
431
|
+
const p = line.split('|');
|
|
432
|
+
return {
|
|
433
|
+
level: safeParseInt(p[0], 0),
|
|
434
|
+
statusCode: safeParseInt(p[1], 1),
|
|
435
|
+
runTimeMinutes: safeParseInt(p[2], 0),
|
|
436
|
+
winDischargeRate: safeParseInt(p[3], 0),
|
|
437
|
+
cycles: safeParseInt(p[4], 0),
|
|
438
|
+
fullChargeMWh: safeParseInt(p[5], 0),
|
|
439
|
+
designMWh: safeParseInt(p[6], 0),
|
|
440
|
+
rateMilliwatts: safeParseInt(p[7], 0),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
384
444
|
export async function collectWindowsBattery(): Promise<BatteryData | null> {
|
|
385
445
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
446
|
+
// Win32_Battery gives level/status; the root/wmi classes (supported by most modern laptops)
|
|
447
|
+
// give cycle count, design vs. full-charge capacity and instantaneous charge/discharge rate.
|
|
448
|
+
// Everything is fetched in ONE PowerShell launch. Output: level|status|runtime|discharge|cycles|full|design|rate
|
|
449
|
+
const script = [
|
|
450
|
+
`$b = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
|
451
|
+
`if ($b) {`,
|
|
452
|
+
` $cyc = (Get-CimInstance -Namespace root/wmi -ClassName BatteryCycleCount -ErrorAction SilentlyContinue | Select-Object -First 1).CycleCount`,
|
|
453
|
+
` $full = (Get-CimInstance -Namespace root/wmi -ClassName BatteryFullChargedCapacity -ErrorAction SilentlyContinue | Select-Object -First 1).FullChargedCapacity`,
|
|
454
|
+
` $des = (Get-CimInstance -Namespace root/wmi -ClassName BatteryStaticData -ErrorAction SilentlyContinue | Select-Object -First 1).DesignedCapacity`,
|
|
455
|
+
` $st = Get-CimInstance -Namespace root/wmi -ClassName BatteryStatus -ErrorAction SilentlyContinue | Select-Object -First 1`,
|
|
456
|
+
` $rate = 0; if ($st) { if ($st.Charging) { $rate = $st.ChargeRate } elseif ($st.Discharging) { $rate = $st.DischargeRate } }`,
|
|
457
|
+
` "$($b.EstimatedChargeRemaining)|$($b.BatteryStatus)|$($b.EstimatedRunTime)|$($b.DischargeRate)|$cyc|$full|$des|$rate"`,
|
|
458
|
+
`}`,
|
|
459
|
+
].join('\n');
|
|
460
|
+
|
|
461
|
+
const raw = await runPowerShell(script, '');
|
|
462
|
+
const b = parseWindowsBatteryOutput(raw);
|
|
463
|
+
if (!b) return null;
|
|
464
|
+
|
|
465
|
+
// BatteryStatus: 1 discharging, 2 on AC, 3 fully charged, 4 low, 5 critical, 6-9 charging, 11 partially charged
|
|
466
|
+
const charging = [2, 6, 7, 8, 9].includes(b.statusCode);
|
|
467
|
+
const onAc = [2, 3, 6, 7, 8, 9, 11].includes(b.statusCode);
|
|
468
|
+
const powerSource = onAc ? 'AC' : 'Battery';
|
|
469
|
+
const state = b.statusCode === 3 || (onAc && b.level >= 100) ? 'charged' : charging ? 'charging' : 'discharging';
|
|
397
470
|
|
|
398
|
-
const powerSource = statusCode === 2 || statusCode === 3 || statusCode === 6 ? 'AC' : 'Battery';
|
|
399
|
-
const state = statusCode === 3 ? 'charged' : statusCode === 6 || statusCode === 2 ? 'charging' : 'discharging';
|
|
400
471
|
let timeRemaining = 'calculating';
|
|
401
472
|
if (state === 'charged') timeRemaining = '∞';
|
|
402
|
-
else if (
|
|
403
|
-
const h = Math.floor(runTimeMinutes / 60);
|
|
404
|
-
const m = runTimeMinutes % 60;
|
|
473
|
+
else if (state === 'discharging' && b.runTimeMinutes > 0 && b.runTimeMinutes < 70000) {
|
|
474
|
+
const h = Math.floor(b.runTimeMinutes / 60);
|
|
475
|
+
const m = b.runTimeMinutes % 60;
|
|
405
476
|
timeRemaining = `${h}:${m.toString().padStart(2, '0')}`;
|
|
406
477
|
}
|
|
407
478
|
|
|
408
|
-
|
|
479
|
+
// Wattage: prefer instantaneous rate from root/wmi (mW), fall back to Win32_Battery.DischargeRate
|
|
480
|
+
const rateMw = Math.abs(b.rateMilliwatts) > 0 && Math.abs(b.rateMilliwatts) < 400_000 ? Math.abs(b.rateMilliwatts) : b.winDischargeRate;
|
|
481
|
+
const powerWatts = rateMw > 0 ? Math.round((rateMw / 1000) * 100) / 100 : undefined;
|
|
482
|
+
|
|
483
|
+
// Health = full-charge capacity / design capacity. Cycle count of 0 means "not reported".
|
|
484
|
+
let maxCapacityPercent: number | undefined;
|
|
485
|
+
if (b.designMWh > 0 && b.fullChargeMWh > 0) {
|
|
486
|
+
maxCapacityPercent = Math.min(100, Math.round((b.fullChargeMWh / b.designMWh) * 100));
|
|
487
|
+
}
|
|
488
|
+
const cycles = b.cycles > 0 && b.cycles < 100_000 ? b.cycles : undefined;
|
|
489
|
+
|
|
490
|
+
let health = 'Good';
|
|
491
|
+
let condition = 'Normal';
|
|
492
|
+
if (maxCapacityPercent !== undefined) {
|
|
493
|
+
if (maxCapacityPercent < 60) { health = 'Poor'; condition = 'Service Recommended'; }
|
|
494
|
+
else if (maxCapacityPercent < 80) { health = 'Fair'; condition = 'Reduced Capacity'; }
|
|
495
|
+
}
|
|
409
496
|
|
|
410
497
|
return {
|
|
411
|
-
level,
|
|
498
|
+
level: b.level,
|
|
412
499
|
state,
|
|
413
500
|
timeRemaining,
|
|
414
|
-
health
|
|
501
|
+
health,
|
|
415
502
|
powerSource,
|
|
416
503
|
powerWatts,
|
|
504
|
+
cycles,
|
|
505
|
+
condition,
|
|
506
|
+
maxCapacityPercent,
|
|
417
507
|
};
|
|
418
508
|
} catch {
|
|
419
509
|
return null;
|
|
@@ -562,7 +652,15 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
|
562
652
|
|
|
563
653
|
// ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
|
|
564
654
|
|
|
655
|
+
let nvidiaSmiPathCache: { path: string | null; checkedAt: number } | null = null;
|
|
656
|
+
const NVIDIA_SMI_NEGATIVE_TTL_MS = 5 * 60 * 1000;
|
|
657
|
+
|
|
565
658
|
async function findNvidiaSmiPath(): Promise<string> {
|
|
659
|
+
if (nvidiaSmiPathCache) {
|
|
660
|
+
if (nvidiaSmiPathCache.path) return nvidiaSmiPathCache.path;
|
|
661
|
+
// Not found previously: only re-probe occasionally (e.g. driver installed while running)
|
|
662
|
+
if (Date.now() - nvidiaSmiPathCache.checkedAt < NVIDIA_SMI_NEGATIVE_TTL_MS) return 'nvidia-smi';
|
|
663
|
+
}
|
|
566
664
|
const commonPaths = [
|
|
567
665
|
'nvidia-smi',
|
|
568
666
|
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
|
|
@@ -571,14 +669,19 @@ async function findNvidiaSmiPath(): Promise<string> {
|
|
|
571
669
|
for (const p of commonPaths) {
|
|
572
670
|
try {
|
|
573
671
|
const test = await run(`"${p}" -L 2>nul`, '');
|
|
574
|
-
if (test.includes('GPU 0:'))
|
|
672
|
+
if (test.includes('GPU 0:')) {
|
|
673
|
+
nvidiaSmiPathCache = { path: p, checkedAt: Date.now() };
|
|
674
|
+
return p;
|
|
675
|
+
}
|
|
575
676
|
} catch {}
|
|
576
677
|
}
|
|
678
|
+
nvidiaSmiPathCache = { path: null, checkedAt: Date.now() };
|
|
577
679
|
return 'nvidia-smi';
|
|
578
680
|
}
|
|
579
681
|
|
|
580
682
|
async function runNvidiaSmiQuery(query: string): Promise<string> {
|
|
581
683
|
const smiPath = await findNvidiaSmiPath();
|
|
684
|
+
if (nvidiaSmiPathCache && nvidiaSmiPathCache.path === null) return '';
|
|
582
685
|
return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
|
|
583
686
|
}
|
|
584
687
|
|
|
@@ -656,50 +759,60 @@ export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoContro
|
|
|
656
759
|
return controllers;
|
|
657
760
|
}
|
|
658
761
|
|
|
762
|
+
function isVirtualGpuName(name: string): boolean {
|
|
763
|
+
return /microsoft basic|remote display|hyper-v|virtual|vmware|virtualbox|parsec|citrix|indirect display|displaylink/i.test(name);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function isDiscreteGpuName(name: string): boolean {
|
|
767
|
+
const n = name.toLowerCase();
|
|
768
|
+
if (isVirtualGpuName(n)) return false;
|
|
769
|
+
return (
|
|
770
|
+
/\b(rtx|gtx|geforce|quadro|titan|tesla)\b/.test(n) ||
|
|
771
|
+
/\bnvidia\b/.test(n) ||
|
|
772
|
+
/radeon\s+(rx|pro|vii|r[5-9]|hd)\b/.test(n) ||
|
|
773
|
+
/\bfirepro\b/.test(n) ||
|
|
774
|
+
(/\barc\b/.test(n) && /\b(a\d{3}|b\d{3}|pro)\b/.test(n))
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
659
778
|
export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
|
|
660
|
-
//
|
|
779
|
+
// Order: discrete (NVIDIA, AMD Radeon RX/Pro, Intel Arc) > integrated (Intel UHD/Iris/Xe, AMD Radeon(TM) Graphics) > virtual/basic adapters
|
|
780
|
+
const rank = (c: WindowsVideoControllerInfo) =>
|
|
781
|
+
isVirtualGpuName(c.name) ? 2 : isDiscreteGpuName(c.name) ? 0 : 1;
|
|
661
782
|
return [...controllers].sort((a, b) => {
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
aName.includes('radeon rx') || aName.includes('arc');
|
|
666
|
-
const bIsDiscrete = bName.includes('rtx') || bName.includes('gtx') || bName.includes('geforce') ||
|
|
667
|
-
bName.includes('radeon rx') || bName.includes('arc');
|
|
668
|
-
if (aIsDiscrete && !bIsDiscrete) return -1;
|
|
669
|
-
if (!aIsDiscrete && bIsDiscrete) return 1;
|
|
783
|
+
const ra = rank(a);
|
|
784
|
+
const rb = rank(b);
|
|
785
|
+
if (ra !== rb) return ra - rb;
|
|
670
786
|
return b.adapterRam - a.adapterRam;
|
|
671
787
|
});
|
|
672
788
|
}
|
|
673
789
|
|
|
674
|
-
export async function getWindowsGpu64BitVram(): Promise<number> {
|
|
790
|
+
export async function getWindowsGpu64BitVram(adapterName?: string): Promise<number> {
|
|
675
791
|
// Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
|
|
676
|
-
// Read exact 64-bit VRAM from
|
|
792
|
+
// Read exact 64-bit VRAM from the Display Class registry keys. The value is literally named
|
|
793
|
+
// 'HardwareInformation.qwMemorySize' (dot is part of the name), so it must be quoted.
|
|
794
|
+
// Match by DriverDesc so hybrid systems report the VRAM of the selected adapter.
|
|
677
795
|
try {
|
|
796
|
+
const safeName = (adapterName || '').replace(/'/g, "''");
|
|
678
797
|
const raw = await runPowerShell(
|
|
679
|
-
|
|
680
|
-
''
|
|
681
|
-
);
|
|
682
|
-
const bytes = safeParseInt(raw, 0);
|
|
683
|
-
if (bytes > 0) return bytes;
|
|
684
|
-
} catch {}
|
|
685
|
-
|
|
686
|
-
// Fallback to Performance Counters Dedicated Usage + Free
|
|
687
|
-
try {
|
|
688
|
-
const rawDedicated = await runPowerShell(
|
|
689
|
-
`(Get-Counter '\\GPU Adapter Memory(*)\\Dedicated Usage' -ErrorAction SilentlyContinue).CounterSamples | Measure-Object -Property CookedValue -Sum | Select-Object -ExpandProperty Sum`,
|
|
798
|
+
`$name = '${safeName}'; $items = Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.'HardwareInformation.qwMemorySize' -gt 0 }; $pick = $items | Where-Object { $name -and $_.DriverDesc -eq $name } | Select-Object -First 1; if (-not $pick) { $pick = $items | Sort-Object -Property 'HardwareInformation.qwMemorySize' -Descending | Select-Object -First 1 }; if ($pick) { [string][uint64]$pick.'HardwareInformation.qwMemorySize' }`,
|
|
690
799
|
''
|
|
691
800
|
);
|
|
692
|
-
const bytes = safeParseFloat(
|
|
801
|
+
const bytes = safeParseFloat(raw.trim());
|
|
693
802
|
if (bytes > 0) return bytes;
|
|
694
803
|
} catch {}
|
|
695
804
|
|
|
696
805
|
return 0;
|
|
697
806
|
}
|
|
698
807
|
|
|
808
|
+
/**
|
|
809
|
+
* 3D engine utilization via CIM performance classes. Unlike Get-Counter paths these class/property
|
|
810
|
+
* names are not localized, so this works on non-English Windows. Works for NVIDIA, AMD and Intel (WDDM 2.0+).
|
|
811
|
+
*/
|
|
699
812
|
export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
|
|
700
813
|
try {
|
|
701
814
|
const raw = await runPowerShell(
|
|
702
|
-
`$samples =
|
|
815
|
+
`$samples = Get-CimInstance Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*engtype_3D*' }; if ($samples) { $activePids = $samples | Where-Object { $_.UtilizationPercentage -gt 0 } | ForEach-Object { if ($_.Name -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property UtilizationPercentage -Sum).Sum; "$total|$(@($activePids).Count)" } else { '0|0' }`,
|
|
703
816
|
'0|0'
|
|
704
817
|
);
|
|
705
818
|
const [utilStr, procCountStr] = raw.split('|');
|
|
@@ -729,7 +842,7 @@ export async function collectWindowsGpu(): Promise<GpuData | null> {
|
|
|
729
842
|
const bestController = prioritized[0];
|
|
730
843
|
|
|
731
844
|
// Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
|
|
732
|
-
let vramBytes = await getWindowsGpu64BitVram();
|
|
845
|
+
let vramBytes = await getWindowsGpu64BitVram(bestController.name);
|
|
733
846
|
if (vramBytes === 0 && bestController.adapterRam > 0) {
|
|
734
847
|
vramBytes = bestController.adapterRam;
|
|
735
848
|
}
|
|
@@ -1090,7 +1203,20 @@ export function parseWindowsProcessCsv(
|
|
|
1090
1203
|
return procs;
|
|
1091
1204
|
}
|
|
1092
1205
|
|
|
1206
|
+
let procListMemo: { ts: number; promise: Promise<ProcessData[]> } | null = null;
|
|
1207
|
+
const PROC_MEMO_MS = 1500;
|
|
1208
|
+
|
|
1093
1209
|
export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
|
|
1210
|
+
const now0 = Date.now();
|
|
1211
|
+
if (!procListMemo || now0 - procListMemo.ts > PROC_MEMO_MS) {
|
|
1212
|
+
procListMemo = { ts: now0, promise: queryWindowsProcesses() };
|
|
1213
|
+
}
|
|
1214
|
+
const procs = await procListMemo.promise;
|
|
1215
|
+
if (limit && limit > 0) return procs.slice(0, limit);
|
|
1216
|
+
return procs;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function queryWindowsProcesses(): Promise<ProcessData[]> {
|
|
1094
1220
|
try {
|
|
1095
1221
|
// Explicitly query ParentProcessId for process tree view (`pyre --tree`)
|
|
1096
1222
|
const raw = await runPowerShell(
|
|
@@ -1099,10 +1225,7 @@ export async function collectWindowsProcesses(limit?: number): Promise<ProcessDa
|
|
|
1099
1225
|
);
|
|
1100
1226
|
const now = Date.now();
|
|
1101
1227
|
const totalMem = os.totalmem();
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (limit && limit > 0) return procs.slice(0, limit);
|
|
1105
|
-
return procs;
|
|
1228
|
+
return parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
|
|
1106
1229
|
} catch {
|
|
1107
1230
|
return [];
|
|
1108
1231
|
}
|
package/src/monitors/run.ts
CHANGED
|
@@ -29,18 +29,94 @@ async function run(cmd: string, fallback: string = '', timeout = 3000): Promise<
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// ─── PowerShell execution (Windows) ──────────────────────────────────────────
|
|
33
|
+
// Every powershell.exe launch costs 0.3–1.5s of cold start. A single collection tick used to
|
|
34
|
+
// spawn ~15 of them at once; under that contention the 4s timeouts fired and results silently
|
|
35
|
+
// became empty (no battery, no GPU, ...). So we: (1) cap concurrent processes, (2) start the
|
|
36
|
+
// timeout when the process actually starts, (3) dedupe identical in-flight scripts and
|
|
37
|
+
// (4) allow stale-while-revalidate caching for data that does not change every tick.
|
|
38
|
+
|
|
39
|
+
const MAX_CONCURRENT_PS = 3;
|
|
40
|
+
let activePs = 0;
|
|
41
|
+
const psWaiters: Array<() => void> = [];
|
|
42
|
+
|
|
43
|
+
async function acquirePsSlot(): Promise<void> {
|
|
44
|
+
if (activePs < MAX_CONCURRENT_PS) {
|
|
45
|
+
activePs++;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await new Promise<void>(resolve => psWaiters.push(resolve));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function releasePsSlot(): void {
|
|
52
|
+
const next = psWaiters.shift();
|
|
53
|
+
if (next) next(); // hand the slot over directly
|
|
54
|
+
else activePs--;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PsCacheEntry {
|
|
58
|
+
value: string;
|
|
59
|
+
ts: number;
|
|
60
|
+
inflight?: Promise<string | null>;
|
|
61
|
+
}
|
|
62
|
+
const psCache = new Map<string, PsCacheEntry>();
|
|
63
|
+
const psInflight = new Map<string, Promise<string | null>>();
|
|
64
|
+
|
|
65
|
+
/** Returns stdout on success (possibly ''), or null when the process failed / timed out. */
|
|
66
|
+
async function execPowerShell(script: string, timeout: number): Promise<string | null> {
|
|
67
|
+
const existing = psInflight.get(script);
|
|
68
|
+
if (existing) return existing;
|
|
69
|
+
|
|
70
|
+
const p = (async () => {
|
|
71
|
+
await acquirePsSlot();
|
|
72
|
+
try {
|
|
73
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
74
|
+
const cmd = `powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
75
|
+
return (await execAsync(cmd, { encoding: 'utf8', timeout, windowsHide: true, maxBuffer: 16 * 1024 * 1024 })).stdout.trim();
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
} finally {
|
|
79
|
+
releasePsSlot();
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
psInflight.set(script, p);
|
|
83
|
+
p.finally(() => psInflight.delete(script)).catch(() => {});
|
|
84
|
+
return p;
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
/**
|
|
33
88
|
* Execute a PowerShell snippet safely on Windows, returning trimmed stdout or fallback.
|
|
89
|
+
*
|
|
90
|
+
* @param cacheTtlMs when > 0 the result is cached; stale values are returned immediately while
|
|
91
|
+
* a refresh runs in the background (failures never overwrite good data).
|
|
34
92
|
*/
|
|
35
|
-
async function runPowerShell(script: string, fallback: string = '', timeout =
|
|
93
|
+
async function runPowerShell(script: string, fallback: string = '', timeout = 10000, cacheTtlMs = 0): Promise<string> {
|
|
36
94
|
if (!isWindows) return fallback;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
95
|
+
|
|
96
|
+
if (cacheTtlMs > 0) {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const entry = psCache.get(script);
|
|
99
|
+
if (entry) {
|
|
100
|
+
if (now - entry.ts >= cacheTtlMs && !entry.inflight) {
|
|
101
|
+
entry.inflight = execPowerShell(script, timeout).then(v => {
|
|
102
|
+
if (v !== null) {
|
|
103
|
+
entry.value = v;
|
|
104
|
+
entry.ts = Date.now();
|
|
105
|
+
}
|
|
106
|
+
return v;
|
|
107
|
+
}).finally(() => { entry.inflight = undefined; });
|
|
108
|
+
entry.inflight.catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
return entry.value;
|
|
111
|
+
}
|
|
112
|
+
const value = await execPowerShell(script, timeout);
|
|
113
|
+
if (value === null) return fallback;
|
|
114
|
+
psCache.set(script, { value, ts: Date.now() });
|
|
115
|
+
return value;
|
|
43
116
|
}
|
|
117
|
+
|
|
118
|
+
const value = await execPowerShell(script, timeout);
|
|
119
|
+
return value === null ? fallback : value;
|
|
44
120
|
}
|
|
45
121
|
|
|
46
122
|
export { run, runPowerShell };
|
package/src/platform.test.ts
CHANGED
|
@@ -119,6 +119,30 @@ describe('Intel, AMD & NVIDIA Hardware Telemetry', () => {
|
|
|
119
119
|
assert.strictEqual(prioritized[0].name, 'Intel(R) Arc(TM) A770 Graphics');
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it('should parse Windows battery cycles, capacity and rate output', async () => {
|
|
123
|
+
const { parseWindowsBatteryOutput } = await import('./monitors/platform/windows.js');
|
|
124
|
+
const b = parseWindowsBatteryOutput('87|1|312||214|41200|50000|11500\r\n');
|
|
125
|
+
assert.ok(b);
|
|
126
|
+
assert.strictEqual(b.level, 87);
|
|
127
|
+
assert.strictEqual(b.cycles, 214);
|
|
128
|
+
assert.strictEqual(b.fullChargeMWh, 41200);
|
|
129
|
+
assert.strictEqual(b.designMWh, 50000);
|
|
130
|
+
assert.strictEqual(b.rateMilliwatts, 11500);
|
|
131
|
+
assert.strictEqual(parseWindowsBatteryOutput(''), null);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
it('should rank hybrid laptop adapters: discrete > integrated > virtual', async () => {
|
|
136
|
+
const { prioritizeGpuControllers } = await import('./monitors/platform/windows.js');
|
|
137
|
+
const mk = (name: string, adapterRam = 0) => ({ name, adapterRam, driverVersion: '', videoProcessor: '', pnpDeviceId: '' });
|
|
138
|
+
const order = (names: string[]) => prioritizeGpuControllers(names.map(n => mk(n))).map(c => c.name);
|
|
139
|
+
|
|
140
|
+
assert.strictEqual(order(['Microsoft Basic Display Adapter', 'Intel(R) Iris(R) Xe Graphics', 'NVIDIA GeForce RTX 4060 Laptop GPU'])[0], 'NVIDIA GeForce RTX 4060 Laptop GPU');
|
|
141
|
+
assert.strictEqual(order(['Intel(R) UHD Graphics', 'Microsoft Remote Display Adapter'])[0], 'Intel(R) UHD Graphics');
|
|
142
|
+
assert.strictEqual(order(['AMD Radeon(TM) Graphics', 'AMD Radeon Pro W7800'])[0], 'AMD Radeon Pro W7800');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
|
|
122
146
|
it('should detect CPU vendors and estimate architecture TDP accurately', async () => {
|
|
123
147
|
const { detectProcessorVendor, estimateCpuTdp } = await import('./monitors/platform/windows.js');
|
|
124
148
|
// AMD tests
|
package/dist/chunk-MZO5QKUR.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// src/monitors/run.ts
|
|
2
|
-
import { exec } from "child_process";
|
|
3
|
-
import { promisify } from "util";
|
|
4
|
-
var execAsync = promisify(exec);
|
|
5
|
-
var isWindows = process.platform === "win32";
|
|
6
|
-
async function run(cmd, fallback = "", timeout = 3e3) {
|
|
7
|
-
try {
|
|
8
|
-
let sanitizedCmd = cmd;
|
|
9
|
-
if (isWindows) {
|
|
10
|
-
sanitizedCmd = sanitizedCmd.replace(/2>\s*\/dev\/null/g, "2>nul");
|
|
11
|
-
sanitizedCmd = sanitizedCmd.replace(/\/dev\/null/g, "nul");
|
|
12
|
-
}
|
|
13
|
-
return (await execAsync(sanitizedCmd, { encoding: "utf8", timeout, windowsHide: true })).stdout.trim();
|
|
14
|
-
} catch {
|
|
15
|
-
return fallback;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
async function runPowerShell(script, fallback = "", timeout = 4e3) {
|
|
19
|
-
if (!isWindows) return fallback;
|
|
20
|
-
try {
|
|
21
|
-
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
22
|
-
const cmd = `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
23
|
-
return (await execAsync(cmd, { encoding: "utf8", timeout, windowsHide: true })).stdout.trim();
|
|
24
|
-
} catch {
|
|
25
|
-
return fallback;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export {
|
|
30
|
-
run,
|
|
31
|
-
runPowerShell
|
|
32
|
-
};
|