pyre-cli 10.5.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 +176 -76
- package/src/monitors/run.ts +83 -7
- package/src/platform.test.ts +13 -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
|
-
|
|
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';
|
|
391
470
|
|
|
392
|
-
const parts = raw.split('|');
|
|
393
|
-
const level = safeParseInt(parts[0], 0);
|
|
394
|
-
const statusCode = safeParseInt(parts[1], 1);
|
|
395
|
-
const runTimeMinutes = safeParseInt(parts[2], 0);
|
|
396
|
-
const dischargeRate = safeParseInt(parts[3], 0);
|
|
397
|
-
|
|
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;
|
|
@@ -1113,7 +1203,20 @@ export function parseWindowsProcessCsv(
|
|
|
1113
1203
|
return procs;
|
|
1114
1204
|
}
|
|
1115
1205
|
|
|
1206
|
+
let procListMemo: { ts: number; promise: Promise<ProcessData[]> } | null = null;
|
|
1207
|
+
const PROC_MEMO_MS = 1500;
|
|
1208
|
+
|
|
1116
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[]> {
|
|
1117
1220
|
try {
|
|
1118
1221
|
// Explicitly query ParentProcessId for process tree view (`pyre --tree`)
|
|
1119
1222
|
const raw = await runPowerShell(
|
|
@@ -1122,10 +1225,7 @@ export async function collectWindowsProcesses(limit?: number): Promise<ProcessDa
|
|
|
1122
1225
|
);
|
|
1123
1226
|
const now = Date.now();
|
|
1124
1227
|
const totalMem = os.totalmem();
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (limit && limit > 0) return procs.slice(0, limit);
|
|
1128
|
-
return procs;
|
|
1228
|
+
return parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
|
|
1129
1229
|
} catch {
|
|
1130
1230
|
return [];
|
|
1131
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,19 @@ 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
|
+
|
|
122
135
|
it('should rank hybrid laptop adapters: discrete > integrated > virtual', async () => {
|
|
123
136
|
const { prioritizeGpuControllers } = await import('./monitors/platform/windows.js');
|
|
124
137
|
const mk = (name: string, adapterRam = 0) => ({ name, adapterRam, driverVersion: '', videoProcessor: '', pnpDeviceId: '' });
|
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
|
-
};
|