pyre-cli 10.3.0 → 10.4.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/README.md +41 -0
- package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
- package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
- package/dist/benchGitCmd-HIXUC5EK.js +87 -0
- package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
- package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
- package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
- package/dist/budgetCmd-TGO3RYLN.js +11 -0
- package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
- package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
- package/dist/chunk-JH7P6KLN.js +143 -0
- package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
- package/dist/chunk-NMGQVRCA.js +111 -0
- package/dist/chunk-OCL7P7N7.js +936 -0
- package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
- package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
- package/dist/chunk-UF3XQ3R3.js +3800 -0
- package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
- package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
- package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
- package/dist/containersCmd-FSMYOBG2.js +84 -0
- package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
- package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
- package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
- package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
- package/dist/flightRecorder-GJIAQOXC.js +162 -0
- package/dist/index.js +736 -4125
- package/dist/inspectCmd-5FXJBJTK.js +137 -0
- package/dist/menu-AJFGDLBP.js +1233 -0
- package/dist/monitors/platform/windows-ui.ps1 +31 -29
- package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
- package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
- package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
- package/dist/playbookCmd-FRROIEXR.js +145 -0
- package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
- package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
- package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
- package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
- package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
- package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
- package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
- package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
- package/dist/statuslineCmd-EUKYRR2G.js +75 -0
- package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
- package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
- package/dist/topoCmd-RTXQJ6XA.js +189 -0
- package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
- package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
- package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
- package/package.json +2 -2
- package/src/benchGitCmd.ts +110 -0
- package/src/budgetCmd.ts +199 -0
- package/src/completions.ts +42 -3
- package/src/containersCmd.ts +106 -0
- package/src/doctor.ts +86 -6
- package/src/easy_features.test.ts +83 -0
- package/src/flightRecorder.ts +214 -0
- package/src/index.ts +181 -8
- package/src/inspectCmd.ts +165 -0
- package/src/live/input.ts +70 -3
- package/src/live/render.ts +1 -1
- package/src/live/state.ts +2 -1
- package/src/menu/commands.ts +754 -0
- package/src/menu/index.ts +358 -0
- package/src/menu/render.ts +272 -0
- package/src/menu/types.ts +63 -0
- package/src/menu.test.ts +110 -0
- package/src/monitors/blender.ts +49 -19
- package/src/monitors/buildTracker.ts +72 -54
- package/src/monitors/platform/windows-ui.ps1 +31 -29
- package/src/monitors/platform/windows.ts +613 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +135 -0
- package/src/playbookCmd.ts +156 -0
- package/src/power_features.test.ts +251 -0
- package/src/profileProcCmd.ts +68 -21
- package/src/replayCmd.ts +15 -6
- package/src/smartCmd.ts +149 -10
- package/src/state/config.ts +13 -0
- package/src/statuslineCmd.ts +87 -0
- package/src/syncCmd.ts +12 -0
- package/src/topoCmd.ts +83 -50
- package/src/watchdogCmd.ts +40 -3
- package/src/xbar.ts +195 -17
- package/dist/chunk-M73HCQD2.js +0 -95
- package/dist/topoCmd-DZ3YS5IL.js +0 -148
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Gathers metrics on Windows (win32) using Node.js standard APIs,
|
|
5
5
|
* PowerShell CIM/WMI, Windows Performance Counters, tasklist, and netstat.
|
|
6
|
+
*
|
|
7
|
+
* Fully supports Intel, AMD, and NVIDIA processors and graphics cards.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'node:fs';
|
|
9
13
|
import { run, runPowerShell } from '../run.js';
|
|
10
14
|
import type {
|
|
11
15
|
CpuData,
|
|
@@ -21,9 +25,13 @@ import type {
|
|
|
21
25
|
DisplayInfo,
|
|
22
26
|
PowerData,
|
|
23
27
|
ConnectionStateStats,
|
|
28
|
+
RemoteHostInfo,
|
|
29
|
+
NetworkProcess,
|
|
24
30
|
} from '../types.js';
|
|
25
31
|
|
|
26
32
|
let prevNetSample: { rxBytes: number; txBytes: number; ts: number } | null = null;
|
|
33
|
+
let emaLoadAvg: [number, number, number] = [0, 0, 0];
|
|
34
|
+
let lastLoadTime = 0;
|
|
27
35
|
|
|
28
36
|
function safeParseFloat(s: string | undefined): number {
|
|
29
37
|
if (!s) return 0;
|
|
@@ -37,15 +45,7 @@ function safeParseInt(s: string | undefined, def = 0): number {
|
|
|
37
45
|
return Number.isFinite(n) ? n : def;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
function
|
|
41
|
-
return raw
|
|
42
|
-
.split('\n')
|
|
43
|
-
.map(l => l.trim())
|
|
44
|
-
.filter(l => l.length > 0)
|
|
45
|
-
.map(l => l.split('|').map(s => s.trim()));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function getCimProperty(className: string, property: string): Promise<string> {
|
|
48
|
+
export async function getCimProperty(className: string, property: string): Promise<string> {
|
|
49
49
|
const raw = await runPowerShell(
|
|
50
50
|
`(Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ${property}) -join '|'`,
|
|
51
51
|
''
|
|
@@ -53,7 +53,7 @@ async function getCimProperty(className: string, property: string): Promise<stri
|
|
|
53
53
|
return raw.split('|')[0] || '';
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async function getCimRow(className: string): Promise<Record<string, string>> {
|
|
56
|
+
export async function getCimRow(className: string): Promise<Record<string, string>> {
|
|
57
57
|
const raw = await runPowerShell(
|
|
58
58
|
`Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -Property * -ExcludeProperty CIM* | ConvertTo-Csv -NoTypeInformation`,
|
|
59
59
|
''
|
|
@@ -93,6 +93,82 @@ export async function collectWindowsSystem(): Promise<{ hostname: string; os: st
|
|
|
93
93
|
return { hostname, os: osName, uptime };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ─── CPU & Architecture ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export function parseWindowsCpuBrand(rawName: string, fallback: string): string {
|
|
99
|
+
if (!rawName) return fallback;
|
|
100
|
+
return rawName
|
|
101
|
+
.replace(/\s+/g, ' ')
|
|
102
|
+
.replace(/,\s*$/, '')
|
|
103
|
+
.trim() || fallback;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function detectProcessorVendor(brand: string): 'intel' | 'amd' | 'other' {
|
|
107
|
+
const b = brand.toLowerCase();
|
|
108
|
+
if (b.includes('amd') || b.includes('ryzen') || b.includes('threadripper') || b.includes('epyc') || b.includes('athlon')) {
|
|
109
|
+
return 'amd';
|
|
110
|
+
}
|
|
111
|
+
if (b.includes('intel') || b.includes('xeon') || /\bcore\s*(i[3579]|ultra|2)/i.test(brand)) {
|
|
112
|
+
return 'intel';
|
|
113
|
+
}
|
|
114
|
+
return 'other';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function estimateCpuTdp(brand: string): number {
|
|
118
|
+
const b = brand.toLowerCase();
|
|
119
|
+
const isAmd = b.includes('amd') || b.includes('ryzen');
|
|
120
|
+
const isIntel = b.includes('intel') || b.includes('core');
|
|
121
|
+
|
|
122
|
+
if (isAmd) {
|
|
123
|
+
if (b.includes('threadripper') || b.includes('epyc')) return 280;
|
|
124
|
+
if (b.includes('ryzen 9') || b.includes('7950') || b.includes('7900') || b.includes('5950') || b.includes('5900') || b.includes('9950') || b.includes('9900')) return 170;
|
|
125
|
+
if (b.includes('ryzen 7') || b.includes('7700') || b.includes('7800x3d') || b.includes('5800') || b.includes('9700')) return 105;
|
|
126
|
+
if (b.includes('ryzen 5') || b.includes('7600') || b.includes('5600')) return 65;
|
|
127
|
+
if (b.includes('u') || b.includes('hs') || b.includes('mobile')) return 28;
|
|
128
|
+
return 65;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isIntel) {
|
|
132
|
+
if (b.includes('xeon')) return 205;
|
|
133
|
+
if (b.includes('i9') || b.includes('14900') || b.includes('13900') || b.includes('12900')) return 125;
|
|
134
|
+
if (b.includes('i7') || b.includes('14700') || b.includes('13700') || b.includes('12700')) return 125;
|
|
135
|
+
if (b.includes('i5')) return 65;
|
|
136
|
+
if (b.includes('i3')) return 60;
|
|
137
|
+
if (b.includes('ultra')) return 45;
|
|
138
|
+
if (b.includes('h') || b.includes('hx')) return 45;
|
|
139
|
+
if (b.includes('u') || b.includes('p')) return 28;
|
|
140
|
+
return 65;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return 65;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function updateEmaLoadAvg(currentUsage: number, cores: number): [number, number, number] {
|
|
147
|
+
const activeLoad = Math.round((currentUsage / 100) * cores * 100) / 100;
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
if (lastLoadTime === 0) {
|
|
150
|
+
lastLoadTime = now;
|
|
151
|
+
emaLoadAvg = [activeLoad, activeLoad, activeLoad];
|
|
152
|
+
return emaLoadAvg;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const dtSec = Math.max((now - lastLoadTime) / 1000, 0.1);
|
|
156
|
+
lastLoadTime = now;
|
|
157
|
+
|
|
158
|
+
// Exponential moving average constants for 1m, 5m, 15m
|
|
159
|
+
const exp1 = Math.exp(-dtSec / 60);
|
|
160
|
+
const exp5 = Math.exp(-dtSec / 300);
|
|
161
|
+
const exp15 = Math.exp(-dtSec / 900);
|
|
162
|
+
|
|
163
|
+
emaLoadAvg = [
|
|
164
|
+
Math.round((activeLoad * (1 - exp1) + emaLoadAvg[0] * exp1) * 100) / 100,
|
|
165
|
+
Math.round((activeLoad * (1 - exp5) + emaLoadAvg[1] * exp5) * 100) / 100,
|
|
166
|
+
Math.round((activeLoad * (1 - exp15) + emaLoadAvg[2] * exp15) * 100) / 100,
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
return emaLoadAvg;
|
|
170
|
+
}
|
|
171
|
+
|
|
96
172
|
export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
|
|
97
173
|
const cpus = os.cpus();
|
|
98
174
|
const cores = cpus.length || 1;
|
|
@@ -103,7 +179,7 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
103
179
|
|
|
104
180
|
try {
|
|
105
181
|
const cpuRow = await getCimRow('Win32_Processor');
|
|
106
|
-
if (cpuRow.Name) brand = cpuRow.Name
|
|
182
|
+
if (cpuRow.Name) brand = parseWindowsCpuBrand(cpuRow.Name, brand);
|
|
107
183
|
if (cpuRow.NumberOfCores) physicalCores = safeParseInt(cpuRow.NumberOfCores, cores);
|
|
108
184
|
if (cpuRow.MaxClockSpeed) frequency = safeParseInt(cpuRow.MaxClockSpeed, frequency);
|
|
109
185
|
} catch {
|
|
@@ -132,37 +208,10 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
132
208
|
}
|
|
133
209
|
prevCpuTimesRef.current = current;
|
|
134
210
|
|
|
135
|
-
const
|
|
136
|
-
const loadAvg = [activeLoad, activeLoad, activeLoad];
|
|
211
|
+
const loadAvg = updateEmaLoadAvg(usage, cores);
|
|
137
212
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const raw = await runPowerShell(
|
|
142
|
-
`(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
|
|
143
|
-
''
|
|
144
|
-
).catch(() => '');
|
|
145
|
-
const tempK = parseFloat(raw);
|
|
146
|
-
if (tempK > 200 && tempK < 400) {
|
|
147
|
-
temperature = Math.round((tempK - 273.15) * 10) / 10;
|
|
148
|
-
}
|
|
149
|
-
} catch {
|
|
150
|
-
// ignore
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (temperature === undefined) {
|
|
154
|
-
try {
|
|
155
|
-
const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
|
|
156
|
-
const match = rawWmi.match(/(\d{3,4})/);
|
|
157
|
-
if (match) {
|
|
158
|
-
const kelvinTenths = parseInt(match[1], 10);
|
|
159
|
-
const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
|
|
160
|
-
if (c >= 15 && c <= 115) temperature = c;
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
// ignore
|
|
164
|
-
}
|
|
165
|
-
}
|
|
213
|
+
// Temperature reading across modern CIM and Perf Counters
|
|
214
|
+
let temperature = await getWindowsCpuTemperature();
|
|
166
215
|
|
|
167
216
|
return {
|
|
168
217
|
brand,
|
|
@@ -176,6 +225,56 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
176
225
|
};
|
|
177
226
|
}
|
|
178
227
|
|
|
228
|
+
export async function getWindowsCpuTemperature(): Promise<number | undefined> {
|
|
229
|
+
// 1. Try LibreHardwareMonitor or OpenHardwareMonitor WMI (widely supported on AMD Ryzen and Intel desktops)
|
|
230
|
+
try {
|
|
231
|
+
const lhmRaw = await runPowerShell(
|
|
232
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%CPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
233
|
+
''
|
|
234
|
+
);
|
|
235
|
+
const lhmVal = safeParseFloat(lhmRaw);
|
|
236
|
+
if (lhmVal >= 15 && lhmVal <= 115) return Math.round(lhmVal * 10) / 10;
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const ohmRaw = await runPowerShell(
|
|
241
|
+
`Get-CimInstance -Namespace root/OpenHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%CPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
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 {}
|
|
260
|
+
|
|
261
|
+
// 3. Try Windows Performance Counter
|
|
262
|
+
try {
|
|
263
|
+
const raw = await runPowerShell(
|
|
264
|
+
`(Get-Counter '\\Thermal Zone Information(*)\\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
|
|
265
|
+
''
|
|
266
|
+
);
|
|
267
|
+
const tempK = safeParseFloat(raw);
|
|
268
|
+
if (tempK > 200 && tempK < 400) {
|
|
269
|
+
return Math.round((tempK - 273.15) * 10) / 10;
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Memory ───────────────────────────────────────────────────────────────────
|
|
277
|
+
|
|
179
278
|
export async function collectWindowsMemory(): Promise<MemoryData> {
|
|
180
279
|
const total = os.totalmem();
|
|
181
280
|
const free = os.freemem();
|
|
@@ -221,6 +320,8 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
|
|
|
221
320
|
};
|
|
222
321
|
}
|
|
223
322
|
|
|
323
|
+
// ─── Disks ─────────────────────────────────────────────────────────────────────
|
|
324
|
+
|
|
224
325
|
export async function collectWindowsDisk(): Promise<DiskData[]> {
|
|
225
326
|
try {
|
|
226
327
|
const raw = await runPowerShell(
|
|
@@ -278,6 +379,8 @@ function formatBytesStr(bytes: number): string {
|
|
|
278
379
|
return (bytes / 1024).toFixed(0) + 'K';
|
|
279
380
|
}
|
|
280
381
|
|
|
382
|
+
// ─── Battery ───────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
281
384
|
export async function collectWindowsBattery(): Promise<BatteryData | null> {
|
|
282
385
|
try {
|
|
283
386
|
const raw = await runPowerShell(
|
|
@@ -317,67 +420,53 @@ export async function collectWindowsBattery(): Promise<BatteryData | null> {
|
|
|
317
420
|
}
|
|
318
421
|
}
|
|
319
422
|
|
|
423
|
+
// ─── Thermal & Power ───────────────────────────────────────────────────────────
|
|
424
|
+
|
|
320
425
|
export async function collectWindowsThermal(): Promise<ThermalData> {
|
|
321
426
|
const temperatures: Record<string, number | null> = {};
|
|
322
427
|
|
|
323
|
-
|
|
428
|
+
const cpuTemp = await getWindowsCpuTemperature();
|
|
429
|
+
if (cpuTemp !== undefined) {
|
|
430
|
+
temperatures['cpu'] = cpuTemp;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// GPU temperature across NVIDIA / AMD / Intel
|
|
434
|
+
let gpuTemp: number | undefined;
|
|
324
435
|
|
|
436
|
+
// Try nvidia-smi
|
|
325
437
|
try {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const tempK = parseFloat(raw);
|
|
331
|
-
if (tempK > 200 && tempK < 400) {
|
|
332
|
-
tempC = Math.round((tempK - 273.15) * 10) / 10;
|
|
333
|
-
temperatures['cpu'] = tempC;
|
|
334
|
-
}
|
|
335
|
-
} catch {
|
|
336
|
-
// ignore
|
|
337
|
-
}
|
|
438
|
+
const nvRaw = await runNvidiaSmiQuery('temperature.gpu');
|
|
439
|
+
const t = safeParseFloat(nvRaw);
|
|
440
|
+
if (t > 0 && t < 125) gpuTemp = t;
|
|
441
|
+
} catch {}
|
|
338
442
|
|
|
339
|
-
|
|
443
|
+
// Try LibreHardwareMonitor GPU temperature
|
|
444
|
+
if (gpuTemp === undefined) {
|
|
340
445
|
try {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
temperatures['cpu'] = c;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
} catch {
|
|
352
|
-
// ignore
|
|
353
|
-
}
|
|
446
|
+
const lhmGpu = await runPowerShell(
|
|
447
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
448
|
+
''
|
|
449
|
+
);
|
|
450
|
+
const t = safeParseFloat(lhmGpu);
|
|
451
|
+
if (t > 0 && t < 125) gpuTemp = t;
|
|
452
|
+
} catch {}
|
|
354
453
|
}
|
|
355
454
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const gpuRaw = await runPowerShell(
|
|
359
|
-
`(Get-Counter '\GPU(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CounterName -match 'Temperature' -and $_.CookedValue -gt 0 } | Select-Object -First 1 -ExpandProperty CookedValue`,
|
|
360
|
-
''
|
|
361
|
-
).catch(() => '');
|
|
362
|
-
gpuTemp = parseFloat(gpuRaw);
|
|
363
|
-
if (gpuTemp > 0 && gpuTemp < 150) {
|
|
364
|
-
temperatures['gpu'] = Math.round(gpuTemp * 10) / 10;
|
|
365
|
-
}
|
|
366
|
-
} catch {
|
|
367
|
-
// ignore
|
|
455
|
+
if (gpuTemp !== undefined) {
|
|
456
|
+
temperatures['gpu'] = gpuTemp;
|
|
368
457
|
}
|
|
369
458
|
|
|
370
|
-
const
|
|
459
|
+
const primaryTemp = cpuTemp ?? gpuTemp;
|
|
371
460
|
let state = 'Nominal';
|
|
372
461
|
let pressureLevel = 0;
|
|
373
|
-
if (
|
|
374
|
-
if (
|
|
462
|
+
if (primaryTemp !== undefined) {
|
|
463
|
+
if (primaryTemp >= 100) {
|
|
375
464
|
state = 'Critical';
|
|
376
465
|
pressureLevel = 3;
|
|
377
|
-
} else if (
|
|
466
|
+
} else if (primaryTemp >= 90) {
|
|
378
467
|
state = 'Serious';
|
|
379
468
|
pressureLevel = 2;
|
|
380
|
-
} else if (
|
|
469
|
+
} else if (primaryTemp >= 80) {
|
|
381
470
|
state = 'Fair';
|
|
382
471
|
pressureLevel = 1;
|
|
383
472
|
}
|
|
@@ -385,7 +474,7 @@ export async function collectWindowsThermal(): Promise<ThermalData> {
|
|
|
385
474
|
|
|
386
475
|
return {
|
|
387
476
|
state,
|
|
388
|
-
detail:
|
|
477
|
+
detail: primaryTemp !== undefined ? `${state} (${primaryTemp}°C)` : state,
|
|
389
478
|
pressureLevel,
|
|
390
479
|
temperatures: Object.keys(temperatures).length ? temperatures : undefined,
|
|
391
480
|
};
|
|
@@ -394,6 +483,7 @@ export async function collectWindowsThermal(): Promise<ThermalData> {
|
|
|
394
483
|
export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
395
484
|
const power: PowerData = {};
|
|
396
485
|
|
|
486
|
+
// 1. Try battery discharge wattage
|
|
397
487
|
try {
|
|
398
488
|
const batRaw = await runPowerShell(
|
|
399
489
|
`$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.DischargeRate)|$($bat.BatteryStatus)" } else { '|0' }`,
|
|
@@ -406,36 +496,58 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
|
406
496
|
if (dischargeRate > 0 && (statusCode === 1 || statusCode === 2)) {
|
|
407
497
|
power.combinedWatts = Math.round((dischargeRate / 1000) * 100) / 100;
|
|
408
498
|
}
|
|
409
|
-
} catch {
|
|
410
|
-
// ignore
|
|
411
|
-
}
|
|
499
|
+
} catch {}
|
|
412
500
|
|
|
501
|
+
// 2. Try LibreHardwareMonitor exact power readings for CPU & GPU
|
|
413
502
|
try {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
503
|
+
const lhmCpu = await runPowerShell(
|
|
504
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Power' and Name like '%CPU Package%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
505
|
+
''
|
|
506
|
+
);
|
|
507
|
+
const cpuP = safeParseFloat(lhmCpu);
|
|
508
|
+
if (cpuP > 0) power.cpuWatts = Math.round(cpuP * 100) / 100;
|
|
509
|
+
|
|
510
|
+
const lhmGpu = await runPowerShell(
|
|
511
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Power' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
512
|
+
''
|
|
513
|
+
);
|
|
514
|
+
const gpuP = safeParseFloat(lhmGpu);
|
|
515
|
+
if (gpuP > 0) power.gpuWatts = Math.round(gpuP * 100) / 100;
|
|
516
|
+
} catch {}
|
|
517
|
+
|
|
518
|
+
// 3. NVIDIA SMI power draw fallback
|
|
519
|
+
if (!power.gpuWatts) {
|
|
520
|
+
try {
|
|
521
|
+
const nvPower = await runNvidiaSmiQuery('power.draw');
|
|
522
|
+
const p = safeParseFloat(nvPower);
|
|
523
|
+
if (p > 0) power.gpuWatts = Math.round(p * 100) / 100;
|
|
524
|
+
} catch {}
|
|
422
525
|
}
|
|
423
526
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
527
|
+
// 4. CPU usage TDP estimation fallback if direct sensor unavailable
|
|
528
|
+
if (!power.cpuWatts) {
|
|
529
|
+
try {
|
|
530
|
+
const cpu = os.cpus();
|
|
531
|
+
const brand = cpu[0]?.model || '';
|
|
532
|
+
const tdpWatts = estimateCpuTdp(brand);
|
|
533
|
+
const usage = cpu.length > 0
|
|
534
|
+
? Math.round(
|
|
535
|
+
(cpu.reduce(
|
|
536
|
+
(a, c) =>
|
|
537
|
+
a +
|
|
538
|
+
(c.times.user + c.times.nice + c.times.sys + c.times.irq - c.times.idle) /
|
|
539
|
+
(c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq),
|
|
540
|
+
0
|
|
541
|
+
) /
|
|
542
|
+
cpu.length) *
|
|
543
|
+
100
|
|
544
|
+
)
|
|
545
|
+
: 0;
|
|
546
|
+
|
|
547
|
+
if (usage > 0) {
|
|
548
|
+
power.cpuWatts = Math.round((usage / 100) * tdpWatts * 100) / 100;
|
|
435
549
|
}
|
|
436
|
-
}
|
|
437
|
-
} catch {
|
|
438
|
-
// ignore
|
|
550
|
+
} catch {}
|
|
439
551
|
}
|
|
440
552
|
|
|
441
553
|
if (power.combinedWatts || power.cpuWatts || power.gpuWatts) {
|
|
@@ -448,117 +560,240 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
|
448
560
|
return null;
|
|
449
561
|
}
|
|
450
562
|
|
|
451
|
-
|
|
563
|
+
// ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
|
|
564
|
+
|
|
565
|
+
async function findNvidiaSmiPath(): Promise<string> {
|
|
566
|
+
const commonPaths = [
|
|
567
|
+
'nvidia-smi',
|
|
568
|
+
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
|
|
569
|
+
path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'nvidia-smi.exe'),
|
|
570
|
+
];
|
|
571
|
+
for (const p of commonPaths) {
|
|
572
|
+
try {
|
|
573
|
+
const test = await run(`"${p}" -L 2>nul`, '');
|
|
574
|
+
if (test.includes('GPU 0:')) return p;
|
|
575
|
+
} catch {}
|
|
576
|
+
}
|
|
577
|
+
return 'nvidia-smi';
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function runNvidiaSmiQuery(query: string): Promise<string> {
|
|
581
|
+
const smiPath = await findNvidiaSmiPath();
|
|
582
|
+
return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function parseNvidiaSmiOutput(csv: string): GpuData | null {
|
|
586
|
+
const lines = csv.split('\n').filter(l => l.trim().length > 0);
|
|
587
|
+
if (lines.length === 0) return null;
|
|
588
|
+
const parts = lines[0].split(',').map(s => s.trim());
|
|
589
|
+
if (parts.length < 3) return null;
|
|
590
|
+
|
|
591
|
+
const model = parts[0];
|
|
592
|
+
const memory = (safeParseFloat(parts[1])) * 1024 * 1024;
|
|
593
|
+
const utilization = Math.min(100, Math.max(0, Math.round(safeParseFloat(parts[2]))));
|
|
594
|
+
const temperature = parts[3] ? safeParseFloat(parts[3]) : undefined;
|
|
595
|
+
const powerDraw = parts[4] ? safeParseFloat(parts[4]) : undefined;
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
model,
|
|
599
|
+
memory,
|
|
600
|
+
utilization,
|
|
601
|
+
temperature: Number.isFinite(temperature) && (temperature as number) > 0 ? temperature : undefined,
|
|
602
|
+
processes: 0,
|
|
603
|
+
powerDraw: Number.isFinite(powerDraw) && (powerDraw as number) > 0 ? powerDraw : undefined,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export async function getNvidiaGpu(): Promise<GpuData | null> {
|
|
452
608
|
try {
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
model,
|
|
465
|
-
memory,
|
|
466
|
-
utilization,
|
|
467
|
-
temperature: Number.isFinite(temperature as number) ? temperature : undefined,
|
|
468
|
-
processes: 0,
|
|
469
|
-
powerDraw: Number.isFinite(powerDraw as number) ? powerDraw : undefined,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
609
|
+
const raw = await runNvidiaSmiQuery('gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw');
|
|
610
|
+
const parsed = parseNvidiaSmiOutput(raw);
|
|
611
|
+
if (parsed) {
|
|
612
|
+
// Query active compute / graphics process count
|
|
613
|
+
try {
|
|
614
|
+
const smiPath = await findNvidiaSmiPath();
|
|
615
|
+
const procList = await run(`"${smiPath}" --query-compute-apps=pid --format=csv,noheader 2>nul`, '');
|
|
616
|
+
const pids = procList.split('\n').filter(l => l.trim().length > 0);
|
|
617
|
+
parsed.processes = pids.length;
|
|
618
|
+
} catch {}
|
|
619
|
+
return parsed;
|
|
472
620
|
}
|
|
473
|
-
} catch {
|
|
474
|
-
// ignore
|
|
475
|
-
}
|
|
621
|
+
} catch {}
|
|
476
622
|
return null;
|
|
477
623
|
}
|
|
478
624
|
|
|
479
|
-
|
|
625
|
+
export interface WindowsVideoControllerInfo {
|
|
626
|
+
name: string;
|
|
627
|
+
adapterRam: number;
|
|
628
|
+
driverVersion: string;
|
|
629
|
+
videoProcessor: string;
|
|
630
|
+
pnpDeviceId: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoControllerInfo[] {
|
|
634
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
635
|
+
if (lines.length < 2) return [];
|
|
636
|
+
|
|
637
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
638
|
+
const controllers: WindowsVideoControllerInfo[] = [];
|
|
639
|
+
|
|
640
|
+
for (const line of lines.slice(1)) {
|
|
641
|
+
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
642
|
+
const row: Record<string, string> = {};
|
|
643
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
644
|
+
|
|
645
|
+
const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
|
|
646
|
+
const ram = safeParseInt(row.AdapterRAM, 0);
|
|
647
|
+
controllers.push({
|
|
648
|
+
name,
|
|
649
|
+
adapterRam: ram,
|
|
650
|
+
driverVersion: row.DriverVersion || '',
|
|
651
|
+
videoProcessor: row.VideoProcessor || '',
|
|
652
|
+
pnpDeviceId: row.PNPDeviceID || '',
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return controllers;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
|
|
660
|
+
// Sort discrete cards (NVIDIA, AMD Radeon RX, Intel Arc) before integrated adapters (Intel UHD, AMD Radeon(TM) Graphics)
|
|
661
|
+
return [...controllers].sort((a, b) => {
|
|
662
|
+
const aName = a.name.toLowerCase();
|
|
663
|
+
const bName = b.name.toLowerCase();
|
|
664
|
+
const aIsDiscrete = aName.includes('rtx') || aName.includes('gtx') || aName.includes('geforce') ||
|
|
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;
|
|
670
|
+
return b.adapterRam - a.adapterRam;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export async function getWindowsGpu64BitVram(): Promise<number> {
|
|
675
|
+
// Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
|
|
676
|
+
// Read exact 64-bit VRAM from Registry Display Class hardware information:
|
|
480
677
|
try {
|
|
481
678
|
const raw = await runPowerShell(
|
|
482
|
-
`Get-
|
|
679
|
+
`Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\0*' -ErrorAction SilentlyContinue | Where-Object { $_.HardwareInformation.qwMemorySize -gt 0 } | Select-Object -First 1 -ExpandProperty HardwareInformation.qwMemorySize`,
|
|
483
680
|
''
|
|
484
681
|
);
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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`,
|
|
690
|
+
''
|
|
691
|
+
);
|
|
692
|
+
const bytes = safeParseFloat(rawDedicated);
|
|
693
|
+
if (bytes > 0) return bytes;
|
|
694
|
+
} catch {}
|
|
695
|
+
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
|
|
700
|
+
try {
|
|
701
|
+
const raw = await runPowerShell(
|
|
702
|
+
`$samples = (Get-Counter '\\GPU Engine(*engtype_3D*)\\Utilization Percentage' -ErrorAction SilentlyContinue).CounterSamples; if ($samples) { $activePids = $samples | Where-Object { $_.CookedValue -gt 0 } | ForEach-Object { if ($_.InstanceName -match 'pid_(\\d+)') { $matches[1] } } | Select-Object -Unique; $total = ($samples | Measure-Object -Property CookedValue -Sum).Sum; "$total|$($activePids.Count)" } else { '0|0' }`,
|
|
703
|
+
'0|0'
|
|
704
|
+
);
|
|
705
|
+
const [utilStr, procCountStr] = raw.split('|');
|
|
706
|
+
const util = Math.min(100, Math.max(0, Math.round(safeParseFloat(utilStr))));
|
|
707
|
+
const processCount = safeParseInt(procCountStr, 0);
|
|
708
|
+
return { utilization: util, processCount };
|
|
501
709
|
} catch {
|
|
502
|
-
|
|
710
|
+
return { utilization: 0, processCount: 0 };
|
|
503
711
|
}
|
|
504
|
-
return null;
|
|
505
712
|
}
|
|
506
713
|
|
|
507
|
-
async function
|
|
714
|
+
export async function collectWindowsGpu(): Promise<GpuData | null> {
|
|
715
|
+
// 1. NVIDIA via nvidia-smi (first-class support when available)
|
|
716
|
+
const nv = await getNvidiaGpu();
|
|
717
|
+
if (nv) return nv;
|
|
718
|
+
|
|
719
|
+
// 2. AMD / Intel / General Windows GPUs
|
|
508
720
|
try {
|
|
509
|
-
const
|
|
510
|
-
|
|
721
|
+
const rawControllers = await runPowerShell(
|
|
722
|
+
`Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor, PNPDeviceID | ConvertTo-Csv -NoTypeInformation`,
|
|
511
723
|
''
|
|
512
|
-
)
|
|
513
|
-
|
|
724
|
+
);
|
|
725
|
+
const controllers = parseWindowsVideoControllers(rawControllers);
|
|
726
|
+
if (controllers.length === 0) return null;
|
|
514
727
|
|
|
515
|
-
const
|
|
516
|
-
|
|
728
|
+
const prioritized = prioritizeGpuControllers(controllers);
|
|
729
|
+
const bestController = prioritized[0];
|
|
517
730
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (!gpuGroups.has(baseName)) {
|
|
523
|
-
gpuGroups.set(baseName, { name: baseName, values: [] });
|
|
524
|
-
}
|
|
525
|
-
gpuGroups.get(baseName)!.values.push(parseFloat(valueStr) || 0);
|
|
731
|
+
// Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
|
|
732
|
+
let vramBytes = await getWindowsGpu64BitVram();
|
|
733
|
+
if (vramBytes === 0 && bestController.adapterRam > 0) {
|
|
734
|
+
vramBytes = bestController.adapterRam;
|
|
526
735
|
}
|
|
527
736
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const total = group.values.reduce((a, b) => a + b, 0);
|
|
531
|
-
const util = Math.min(100, Math.round(total));
|
|
532
|
-
if (!bestGpu || util > bestGpu.util) {
|
|
533
|
-
bestGpu = { name: group.name, util };
|
|
534
|
-
}
|
|
535
|
-
}
|
|
737
|
+
// Read real 3D engine utilization and process count
|
|
738
|
+
const { utilization, processCount } = await getWindowsGpu3dUtilization();
|
|
536
739
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
740
|
+
// Query temperature for AMD / Intel via LibreHardwareMonitor or Windows Thermal
|
|
741
|
+
let temperature: number | undefined;
|
|
742
|
+
try {
|
|
743
|
+
const rawTemp = await runPowerShell(
|
|
744
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
745
|
+
''
|
|
746
|
+
);
|
|
747
|
+
const t = safeParseFloat(rawTemp);
|
|
748
|
+
if (t > 0 && t < 125) temperature = t;
|
|
749
|
+
} catch {}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
model: bestController.name,
|
|
753
|
+
memory: vramBytes,
|
|
754
|
+
utilization,
|
|
755
|
+
temperature,
|
|
756
|
+
processes: processCount,
|
|
757
|
+
};
|
|
545
758
|
} catch {
|
|
546
|
-
|
|
759
|
+
return null;
|
|
547
760
|
}
|
|
548
|
-
return null;
|
|
549
761
|
}
|
|
550
762
|
|
|
551
|
-
|
|
552
|
-
const nv = await getNvidiaGpu();
|
|
553
|
-
if (nv) return nv;
|
|
763
|
+
// ─── Network & Socket Monitoring (`pyre netusage`) ─────────────────────────────
|
|
554
764
|
|
|
555
|
-
|
|
556
|
-
|
|
765
|
+
export interface WindowsTcpConnection {
|
|
766
|
+
localPort: number;
|
|
767
|
+
remoteAddress: string;
|
|
768
|
+
remotePort: number;
|
|
769
|
+
state: string;
|
|
770
|
+
owningPid: number;
|
|
771
|
+
}
|
|
557
772
|
|
|
558
|
-
|
|
559
|
-
|
|
773
|
+
export function parseWindowsTcpConnections(csvRaw: string): WindowsTcpConnection[] {
|
|
774
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
775
|
+
if (lines.length < 2) return [];
|
|
560
776
|
|
|
561
|
-
|
|
777
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
778
|
+
const connections: WindowsTcpConnection[] = [];
|
|
779
|
+
|
|
780
|
+
for (const line of lines.slice(1)) {
|
|
781
|
+
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
782
|
+
const row: Record<string, string> = {};
|
|
783
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
784
|
+
|
|
785
|
+
const localPort = safeParseInt(row.LocalPort, 0);
|
|
786
|
+
const remoteAddress = row.RemoteAddress || '';
|
|
787
|
+
const remotePort = safeParseInt(row.RemotePort, 0);
|
|
788
|
+
const state = row.State || '';
|
|
789
|
+
const owningPid = safeParseInt(row.OwningProcess, 0);
|
|
790
|
+
|
|
791
|
+
if (owningPid > 0) {
|
|
792
|
+
connections.push({ localPort, remoteAddress, remotePort, state, owningPid });
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return connections;
|
|
562
797
|
}
|
|
563
798
|
|
|
564
799
|
export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
@@ -585,9 +820,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
585
820
|
''
|
|
586
821
|
);
|
|
587
822
|
if (raw) iface = raw;
|
|
588
|
-
} catch {
|
|
589
|
-
// ignore
|
|
590
|
-
}
|
|
823
|
+
} catch {}
|
|
591
824
|
|
|
592
825
|
try {
|
|
593
826
|
const netstatE = await run('netstat -e 2>nul', '', 1500);
|
|
@@ -602,9 +835,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
602
835
|
txPackets += parseInt(parts[2], 10) || 0;
|
|
603
836
|
}
|
|
604
837
|
}
|
|
605
|
-
} catch {
|
|
606
|
-
// ignore
|
|
607
|
-
}
|
|
838
|
+
} catch {}
|
|
608
839
|
|
|
609
840
|
const now = Date.now();
|
|
610
841
|
let rxRate = 0;
|
|
@@ -618,6 +849,9 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
618
849
|
|
|
619
850
|
const connectionStates = await getWindowsConnectionStates();
|
|
620
851
|
|
|
852
|
+
// Populate listening ports and remote hosts
|
|
853
|
+
const { listeningPorts, topRemoteHosts } = await getWindowsSocketDetails();
|
|
854
|
+
|
|
621
855
|
return {
|
|
622
856
|
interface: iface,
|
|
623
857
|
ip,
|
|
@@ -633,42 +867,98 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
633
867
|
udp: { connections: 0, rxBytes: 0, txBytes: 0 },
|
|
634
868
|
},
|
|
635
869
|
connectionStates,
|
|
636
|
-
listeningPorts
|
|
870
|
+
listeningPorts,
|
|
637
871
|
establishedConnections: connectionStates.established,
|
|
638
|
-
topRemoteHosts
|
|
872
|
+
topRemoteHosts,
|
|
639
873
|
};
|
|
640
874
|
}
|
|
641
875
|
|
|
642
|
-
async function
|
|
643
|
-
const
|
|
876
|
+
async function getWindowsSocketDetails(): Promise<{ listeningPorts: number[]; topRemoteHosts: RemoteHostInfo[] }> {
|
|
877
|
+
const listeningPorts: number[] = [];
|
|
878
|
+
const topRemoteHosts: RemoteHostInfo[] = [];
|
|
879
|
+
|
|
644
880
|
try {
|
|
645
881
|
const raw = await runPowerShell(
|
|
646
|
-
|
|
647
|
-
'
|
|
882
|
+
`$conns = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($conns) { $ports = ($conns | Where-Object { $_.State -eq 'Listen' } | Select-Object -ExpandProperty LocalPort -Unique) -join ','; $hosts = ($conns | Where-Object { $_.State -eq 'Established' -and $_.RemoteAddress -ne '127.0.0.1' -and $_.RemoteAddress -ne '0.0.0.0' } | Group-Object RemoteAddress | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object { "$($_.Name):$($_.Count)" }) -join '|'; "$ports#$hosts" } else { '#' }`,
|
|
883
|
+
'#'
|
|
648
884
|
);
|
|
649
|
-
|
|
885
|
+
const [portsPart, hostsPart] = raw.split('#');
|
|
886
|
+
if (portsPart) {
|
|
887
|
+
portsPart.split(',').forEach(p => {
|
|
888
|
+
const portNum = safeParseInt(p);
|
|
889
|
+
if (portNum > 0 && !listeningPorts.includes(portNum)) listeningPorts.push(portNum);
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (hostsPart) {
|
|
893
|
+
hostsPart.split('|').forEach(h => {
|
|
894
|
+
const [ip, countStr] = h.split(':');
|
|
895
|
+
if (ip) {
|
|
896
|
+
topRemoteHosts.push({
|
|
897
|
+
ip,
|
|
898
|
+
connections: safeParseInt(countStr, 1),
|
|
899
|
+
activePids: [],
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
} catch {}
|
|
650
905
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
'0'
|
|
654
|
-
);
|
|
655
|
-
stats.listening = safeParseInt(listenRaw, 0);
|
|
906
|
+
return { listeningPorts, topRemoteHosts };
|
|
907
|
+
}
|
|
656
908
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
909
|
+
async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
|
|
910
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
911
|
+
try {
|
|
912
|
+
const raw = await runPowerShell(
|
|
913
|
+
`$c = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($c) { $groups = $c | Group-Object State; ($groups | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join '|' } else { '' }`,
|
|
914
|
+
''
|
|
660
915
|
);
|
|
661
|
-
|
|
916
|
+
for (const pair of raw.split('|')) {
|
|
917
|
+
const [state, countStr] = pair.split('=');
|
|
918
|
+
const count = safeParseInt(countStr, 0);
|
|
919
|
+
if (state === 'Established') stats.established = count;
|
|
920
|
+
else if (state === 'Listen') stats.listening = count;
|
|
921
|
+
else if (state === 'TimeWait') stats.timeWait = count;
|
|
922
|
+
else if (state === 'CloseWait') stats.closeWait = count;
|
|
923
|
+
else if (count > 0) stats.other += count;
|
|
924
|
+
}
|
|
925
|
+
} catch {}
|
|
926
|
+
return stats;
|
|
927
|
+
}
|
|
662
928
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
929
|
+
export function buildWindowsNetworkProcesses(
|
|
930
|
+
conns: WindowsTcpConnection[],
|
|
931
|
+
procMap: Map<number, string>
|
|
932
|
+
): NetworkProcess[] {
|
|
933
|
+
const agg = new Map<number, NetworkProcess>();
|
|
934
|
+
|
|
935
|
+
for (const c of conns) {
|
|
936
|
+
const pid = c.owningPid;
|
|
937
|
+
const name = procMap.get(pid) || `pid-${pid}`;
|
|
938
|
+
let item = agg.get(pid);
|
|
939
|
+
if (!item) {
|
|
940
|
+
item = {
|
|
941
|
+
pid,
|
|
942
|
+
command: name,
|
|
943
|
+
rxBytes: 0,
|
|
944
|
+
txBytes: 0,
|
|
945
|
+
rxPackets: 0,
|
|
946
|
+
txPackets: 0,
|
|
947
|
+
connections: [],
|
|
948
|
+
};
|
|
949
|
+
agg.set(pid, item);
|
|
950
|
+
}
|
|
951
|
+
if (c.remoteAddress && c.remoteAddress !== '0.0.0.0') {
|
|
952
|
+
item.connections.push({
|
|
953
|
+
proto: 'tcp',
|
|
954
|
+
localAddress: `127.0.0.1:${c.localPort}`,
|
|
955
|
+
remoteAddress: `${c.remoteAddress}:${c.remotePort}`,
|
|
956
|
+
state: c.state,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
670
959
|
}
|
|
671
|
-
|
|
960
|
+
|
|
961
|
+
return Array.from(agg.values()).sort((a, b) => b.connections.length - a.connections.length);
|
|
672
962
|
}
|
|
673
963
|
|
|
674
964
|
export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
@@ -690,11 +980,34 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
690
980
|
txPackets += parseInt(parts[2], 10) || 0;
|
|
691
981
|
}
|
|
692
982
|
}
|
|
693
|
-
} catch {
|
|
694
|
-
// ignore
|
|
695
|
-
}
|
|
983
|
+
} catch {}
|
|
696
984
|
|
|
697
985
|
const connectionStates = await getWindowsConnectionStates();
|
|
986
|
+
const { listeningPorts } = await getWindowsSocketDetails();
|
|
987
|
+
|
|
988
|
+
// Populate per-process network socket metrics for `pyre netusage` and packet panel
|
|
989
|
+
let allProcesses: NetworkProcess[] = [];
|
|
990
|
+
try {
|
|
991
|
+
const rawConns = await runPowerShell(
|
|
992
|
+
`Get-NetTCPConnection -State Established,Listen -ErrorAction SilentlyContinue | Select-Object LocalPort, RemoteAddress, RemotePort, State, OwningProcess | ConvertTo-Csv -NoTypeInformation`,
|
|
993
|
+
''
|
|
994
|
+
);
|
|
995
|
+
const conns = parseWindowsTcpConnections(rawConns);
|
|
996
|
+
|
|
997
|
+
// Build PID to process command name mapping
|
|
998
|
+
const rawProcs = await runPowerShell(
|
|
999
|
+
`Get-Process -ErrorAction SilentlyContinue | Select-Object Id, ProcessName | ForEach-Object { "$($_.Id)|$($_.ProcessName)" }`,
|
|
1000
|
+
''
|
|
1001
|
+
);
|
|
1002
|
+
const procMap = new Map<number, string>();
|
|
1003
|
+
for (const line of rawProcs.split('\n')) {
|
|
1004
|
+
const [idStr, name] = line.trim().split('|');
|
|
1005
|
+
const pid = safeParseInt(idStr);
|
|
1006
|
+
if (pid > 0 && name) procMap.set(pid, name);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
allProcesses = buildWindowsNetworkProcesses(conns, procMap);
|
|
1010
|
+
} catch {}
|
|
698
1011
|
|
|
699
1012
|
return {
|
|
700
1013
|
totalPackets: rxPackets + txPackets,
|
|
@@ -703,73 +1016,91 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
703
1016
|
rxRate: 0,
|
|
704
1017
|
txRate: 0,
|
|
705
1018
|
connections: connectionStates.established,
|
|
706
|
-
topProcesses:
|
|
1019
|
+
topProcesses: allProcesses.slice(0, 10),
|
|
707
1020
|
interfaces: [{ iface: 'Primary', rxPackets, txPackets, rxBytes, txBytes }],
|
|
708
|
-
allProcesses
|
|
1021
|
+
allProcesses,
|
|
709
1022
|
connectionStates,
|
|
710
|
-
listeningPorts
|
|
1023
|
+
listeningPorts,
|
|
711
1024
|
};
|
|
712
1025
|
}
|
|
713
1026
|
|
|
1027
|
+
// ─── Process Collection with ParentProcessId (`ppid`) for Tree View ─────────────
|
|
1028
|
+
|
|
714
1029
|
const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
|
|
715
1030
|
|
|
1031
|
+
export function parseWindowsProcessCsv(
|
|
1032
|
+
csvRaw: string,
|
|
1033
|
+
totalMem: number,
|
|
1034
|
+
cache: Map<number, { user: number; kernel: number; ts: number }>,
|
|
1035
|
+
now: number
|
|
1036
|
+
): ProcessData[] {
|
|
1037
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
|
|
1038
|
+
const procs: ProcessData[] = [];
|
|
1039
|
+
|
|
1040
|
+
for (const line of lines) {
|
|
1041
|
+
const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
|
|
1042
|
+
if (parts.length < 6) continue;
|
|
1043
|
+
|
|
1044
|
+
const name = parts[0];
|
|
1045
|
+
const pid = safeParseInt(parts[1], 0);
|
|
1046
|
+
const ppid = safeParseInt(parts[2], 0);
|
|
1047
|
+
const threads = safeParseInt(parts[3], 0);
|
|
1048
|
+
const memBytes = safeParseInt(parts[4], 0);
|
|
1049
|
+
const userTime = safeParseInt(parts[5], 0);
|
|
1050
|
+
const kernelTime = safeParseInt(parts[6], 0);
|
|
1051
|
+
const commandLine = parts[7] || name;
|
|
1052
|
+
|
|
1053
|
+
if (!pid || pid <= 0) continue;
|
|
1054
|
+
|
|
1055
|
+
const totalTime = userTime + kernelTime;
|
|
1056
|
+
let cpu = 0;
|
|
1057
|
+
|
|
1058
|
+
const prev = cache.get(pid);
|
|
1059
|
+
if (prev) {
|
|
1060
|
+
const dt = now - prev.ts;
|
|
1061
|
+
if (dt > 0) {
|
|
1062
|
+
const timeDelta = totalTime - (prev.user + prev.kernel);
|
|
1063
|
+
if (timeDelta > 0) {
|
|
1064
|
+
const cpuSeconds = timeDelta / 10_000_000;
|
|
1065
|
+
cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
cache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
|
|
1070
|
+
|
|
1071
|
+
const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
|
|
1072
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|directx|game/i.test(name) ||
|
|
1073
|
+
/python|node|ollama/i.test(commandLine);
|
|
1074
|
+
|
|
1075
|
+
procs.push({
|
|
1076
|
+
pid,
|
|
1077
|
+
ppid,
|
|
1078
|
+
user: 'SYSTEM',
|
|
1079
|
+
cpu,
|
|
1080
|
+
mem: memPct,
|
|
1081
|
+
state: 'R',
|
|
1082
|
+
threads,
|
|
1083
|
+
runtime: 0,
|
|
1084
|
+
command: name,
|
|
1085
|
+
isGpuOrMlAttributed: isGpuOrMl,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
|
|
1090
|
+
return procs;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
716
1093
|
export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
|
|
717
1094
|
try {
|
|
1095
|
+
// Explicitly query ParentProcessId for process tree view (`pyre --tree`)
|
|
718
1096
|
const raw = await runPowerShell(
|
|
719
|
-
`Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
|
|
1097
|
+
`Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ParentProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime, CommandLine | ConvertTo-Csv -NoTypeInformation`,
|
|
720
1098
|
''
|
|
721
1099
|
);
|
|
722
|
-
const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
|
|
723
1100
|
const now = Date.now();
|
|
724
|
-
const procs: ProcessData[] = [];
|
|
725
1101
|
const totalMem = os.totalmem();
|
|
1102
|
+
const procs = parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
|
|
726
1103
|
|
|
727
|
-
for (const line of lines) {
|
|
728
|
-
const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
|
|
729
|
-
if (parts.length < 6) continue;
|
|
730
|
-
const name = parts[0];
|
|
731
|
-
const pid = safeParseInt(parts[1], 0);
|
|
732
|
-
const threads = safeParseInt(parts[2], 0);
|
|
733
|
-
const memBytes = safeParseInt(parts[3], 0);
|
|
734
|
-
const userTime = safeParseInt(parts[4], 0);
|
|
735
|
-
const kernelTime = safeParseInt(parts[5], 0);
|
|
736
|
-
|
|
737
|
-
if (!pid || pid <= 0) continue;
|
|
738
|
-
|
|
739
|
-
const totalTime = userTime + kernelTime;
|
|
740
|
-
let cpu = 0;
|
|
741
|
-
|
|
742
|
-
const prev = winProcCache.get(pid);
|
|
743
|
-
if (prev) {
|
|
744
|
-
const dt = now - prev.ts;
|
|
745
|
-
if (dt > 0) {
|
|
746
|
-
const timeDelta = totalTime - (prev.user + prev.kernel);
|
|
747
|
-
if (timeDelta > 0) {
|
|
748
|
-
const cpuSeconds = timeDelta / 10_000_000;
|
|
749
|
-
cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
winProcCache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
|
|
754
|
-
|
|
755
|
-
const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
|
|
756
|
-
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|intel.*graphics/i.test(name);
|
|
757
|
-
|
|
758
|
-
procs.push({
|
|
759
|
-
pid,
|
|
760
|
-
ppid: 0,
|
|
761
|
-
user: 'SYSTEM',
|
|
762
|
-
cpu,
|
|
763
|
-
mem: memPct,
|
|
764
|
-
state: 'R',
|
|
765
|
-
threads,
|
|
766
|
-
runtime: 0,
|
|
767
|
-
command: name,
|
|
768
|
-
isGpuOrMlAttributed: isGpuOrMl,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
|
|
773
1104
|
if (limit && limit > 0) return procs.slice(0, limit);
|
|
774
1105
|
return procs;
|
|
775
1106
|
} catch {
|
|
@@ -790,6 +1121,8 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
|
|
|
790
1121
|
}));
|
|
791
1122
|
}
|
|
792
1123
|
|
|
1124
|
+
// ─── Displays ──────────────────────────────────────────────────────────────────
|
|
1125
|
+
|
|
793
1126
|
export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
|
|
794
1127
|
try {
|
|
795
1128
|
const raw = await runPowerShell(
|