pyre-cli 10.3.0 → 10.5.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 +636 -280
- package/src/optimizeCmd.ts +1248 -0
- package/src/platform.test.ts +146 -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];
|
|
137
|
-
|
|
138
|
-
let temperature: number | undefined;
|
|
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
|
-
}
|
|
211
|
+
const loadAvg = updateEmaLoadAvg(usage, cores);
|
|
152
212
|
|
|
153
|
-
|
|
154
|
-
|
|
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,263 @@ export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
|
448
560
|
return null;
|
|
449
561
|
}
|
|
450
562
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
563
|
+
// ─── GPU Telemetry Across Intel, AMD, NVIDIA ─────────────────────────────────
|
|
564
|
+
|
|
565
|
+
let nvidiaSmiPathCache: { path: string | null; checkedAt: number } | null = null;
|
|
566
|
+
const NVIDIA_SMI_NEGATIVE_TTL_MS = 5 * 60 * 1000;
|
|
567
|
+
|
|
568
|
+
async function findNvidiaSmiPath(): Promise<string> {
|
|
569
|
+
if (nvidiaSmiPathCache) {
|
|
570
|
+
if (nvidiaSmiPathCache.path) return nvidiaSmiPathCache.path;
|
|
571
|
+
// Not found previously: only re-probe occasionally (e.g. driver installed while running)
|
|
572
|
+
if (Date.now() - nvidiaSmiPathCache.checkedAt < NVIDIA_SMI_NEGATIVE_TTL_MS) return 'nvidia-smi';
|
|
573
|
+
}
|
|
574
|
+
const commonPaths = [
|
|
575
|
+
'nvidia-smi',
|
|
576
|
+
path.join(process.env.ProgramFiles || 'C:\\Program Files', 'NVIDIA Corporation', 'NVSMI', 'nvidia-smi.exe'),
|
|
577
|
+
path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'nvidia-smi.exe'),
|
|
578
|
+
];
|
|
579
|
+
for (const p of commonPaths) {
|
|
580
|
+
try {
|
|
581
|
+
const test = await run(`"${p}" -L 2>nul`, '');
|
|
582
|
+
if (test.includes('GPU 0:')) {
|
|
583
|
+
nvidiaSmiPathCache = { path: p, checkedAt: Date.now() };
|
|
584
|
+
return p;
|
|
471
585
|
}
|
|
472
|
-
}
|
|
473
|
-
} catch {
|
|
474
|
-
// ignore
|
|
586
|
+
} catch {}
|
|
475
587
|
}
|
|
588
|
+
nvidiaSmiPathCache = { path: null, checkedAt: Date.now() };
|
|
589
|
+
return 'nvidia-smi';
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function runNvidiaSmiQuery(query: string): Promise<string> {
|
|
593
|
+
const smiPath = await findNvidiaSmiPath();
|
|
594
|
+
if (nvidiaSmiPathCache && nvidiaSmiPathCache.path === null) return '';
|
|
595
|
+
return (await run(`"${smiPath}" --query-gpu=${query} --format=csv,noheader,nounits 2>nul`, '')).trim();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function parseNvidiaSmiOutput(csv: string): GpuData | null {
|
|
599
|
+
const lines = csv.split('\n').filter(l => l.trim().length > 0);
|
|
600
|
+
if (lines.length === 0) return null;
|
|
601
|
+
const parts = lines[0].split(',').map(s => s.trim());
|
|
602
|
+
if (parts.length < 3) return null;
|
|
603
|
+
|
|
604
|
+
const model = parts[0];
|
|
605
|
+
const memory = (safeParseFloat(parts[1])) * 1024 * 1024;
|
|
606
|
+
const utilization = Math.min(100, Math.max(0, Math.round(safeParseFloat(parts[2]))));
|
|
607
|
+
const temperature = parts[3] ? safeParseFloat(parts[3]) : undefined;
|
|
608
|
+
const powerDraw = parts[4] ? safeParseFloat(parts[4]) : undefined;
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
model,
|
|
612
|
+
memory,
|
|
613
|
+
utilization,
|
|
614
|
+
temperature: Number.isFinite(temperature) && (temperature as number) > 0 ? temperature : undefined,
|
|
615
|
+
processes: 0,
|
|
616
|
+
powerDraw: Number.isFinite(powerDraw) && (powerDraw as number) > 0 ? powerDraw : undefined,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function getNvidiaGpu(): Promise<GpuData | null> {
|
|
621
|
+
try {
|
|
622
|
+
const raw = await runNvidiaSmiQuery('gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw');
|
|
623
|
+
const parsed = parseNvidiaSmiOutput(raw);
|
|
624
|
+
if (parsed) {
|
|
625
|
+
// Query active compute / graphics process count
|
|
626
|
+
try {
|
|
627
|
+
const smiPath = await findNvidiaSmiPath();
|
|
628
|
+
const procList = await run(`"${smiPath}" --query-compute-apps=pid --format=csv,noheader 2>nul`, '');
|
|
629
|
+
const pids = procList.split('\n').filter(l => l.trim().length > 0);
|
|
630
|
+
parsed.processes = pids.length;
|
|
631
|
+
} catch {}
|
|
632
|
+
return parsed;
|
|
633
|
+
}
|
|
634
|
+
} catch {}
|
|
476
635
|
return null;
|
|
477
636
|
}
|
|
478
637
|
|
|
479
|
-
|
|
638
|
+
export interface WindowsVideoControllerInfo {
|
|
639
|
+
name: string;
|
|
640
|
+
adapterRam: number;
|
|
641
|
+
driverVersion: string;
|
|
642
|
+
videoProcessor: string;
|
|
643
|
+
pnpDeviceId: string;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export function parseWindowsVideoControllers(csvRaw: string): WindowsVideoControllerInfo[] {
|
|
647
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
648
|
+
if (lines.length < 2) return [];
|
|
649
|
+
|
|
650
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
651
|
+
const controllers: WindowsVideoControllerInfo[] = [];
|
|
652
|
+
|
|
653
|
+
for (const line of lines.slice(1)) {
|
|
654
|
+
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
655
|
+
const row: Record<string, string> = {};
|
|
656
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
657
|
+
|
|
658
|
+
const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
|
|
659
|
+
const ram = safeParseInt(row.AdapterRAM, 0);
|
|
660
|
+
controllers.push({
|
|
661
|
+
name,
|
|
662
|
+
adapterRam: ram,
|
|
663
|
+
driverVersion: row.DriverVersion || '',
|
|
664
|
+
videoProcessor: row.VideoProcessor || '',
|
|
665
|
+
pnpDeviceId: row.PNPDeviceID || '',
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return controllers;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function isVirtualGpuName(name: string): boolean {
|
|
673
|
+
return /microsoft basic|remote display|hyper-v|virtual|vmware|virtualbox|parsec|citrix|indirect display|displaylink/i.test(name);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function isDiscreteGpuName(name: string): boolean {
|
|
677
|
+
const n = name.toLowerCase();
|
|
678
|
+
if (isVirtualGpuName(n)) return false;
|
|
679
|
+
return (
|
|
680
|
+
/\b(rtx|gtx|geforce|quadro|titan|tesla)\b/.test(n) ||
|
|
681
|
+
/\bnvidia\b/.test(n) ||
|
|
682
|
+
/radeon\s+(rx|pro|vii|r[5-9]|hd)\b/.test(n) ||
|
|
683
|
+
/\bfirepro\b/.test(n) ||
|
|
684
|
+
(/\barc\b/.test(n) && /\b(a\d{3}|b\d{3}|pro)\b/.test(n))
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function prioritizeGpuControllers(controllers: WindowsVideoControllerInfo[]): WindowsVideoControllerInfo[] {
|
|
689
|
+
// Order: discrete (NVIDIA, AMD Radeon RX/Pro, Intel Arc) > integrated (Intel UHD/Iris/Xe, AMD Radeon(TM) Graphics) > virtual/basic adapters
|
|
690
|
+
const rank = (c: WindowsVideoControllerInfo) =>
|
|
691
|
+
isVirtualGpuName(c.name) ? 2 : isDiscreteGpuName(c.name) ? 0 : 1;
|
|
692
|
+
return [...controllers].sort((a, b) => {
|
|
693
|
+
const ra = rank(a);
|
|
694
|
+
const rb = rank(b);
|
|
695
|
+
if (ra !== rb) return ra - rb;
|
|
696
|
+
return b.adapterRam - a.adapterRam;
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export async function getWindowsGpu64BitVram(adapterName?: string): Promise<number> {
|
|
701
|
+
// Win32_VideoController clamps AdapterRAM to 4GB (UInt32).
|
|
702
|
+
// Read exact 64-bit VRAM from the Display Class registry keys. The value is literally named
|
|
703
|
+
// 'HardwareInformation.qwMemorySize' (dot is part of the name), so it must be quoted.
|
|
704
|
+
// Match by DriverDesc so hybrid systems report the VRAM of the selected adapter.
|
|
480
705
|
try {
|
|
706
|
+
const safeName = (adapterName || '').replace(/'/g, "''");
|
|
481
707
|
const raw = await runPowerShell(
|
|
482
|
-
|
|
708
|
+
`$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' }`,
|
|
483
709
|
''
|
|
484
710
|
);
|
|
485
|
-
const
|
|
486
|
-
if (
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
};
|
|
500
|
-
|
|
711
|
+
const bytes = safeParseFloat(raw.trim());
|
|
712
|
+
if (bytes > 0) return bytes;
|
|
713
|
+
} catch {}
|
|
714
|
+
|
|
715
|
+
return 0;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 3D engine utilization via CIM performance classes. Unlike Get-Counter paths these class/property
|
|
720
|
+
* names are not localized, so this works on non-English Windows. Works for NVIDIA, AMD and Intel (WDDM 2.0+).
|
|
721
|
+
*/
|
|
722
|
+
export async function getWindowsGpu3dUtilization(): Promise<{ utilization: number; processCount: number }> {
|
|
723
|
+
try {
|
|
724
|
+
const raw = await runPowerShell(
|
|
725
|
+
`$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' }`,
|
|
726
|
+
'0|0'
|
|
727
|
+
);
|
|
728
|
+
const [utilStr, procCountStr] = raw.split('|');
|
|
729
|
+
const util = Math.min(100, Math.max(0, Math.round(safeParseFloat(utilStr))));
|
|
730
|
+
const processCount = safeParseInt(procCountStr, 0);
|
|
731
|
+
return { utilization: util, processCount };
|
|
501
732
|
} catch {
|
|
502
|
-
|
|
733
|
+
return { utilization: 0, processCount: 0 };
|
|
503
734
|
}
|
|
504
|
-
return null;
|
|
505
735
|
}
|
|
506
736
|
|
|
507
|
-
async function
|
|
737
|
+
export async function collectWindowsGpu(): Promise<GpuData | null> {
|
|
738
|
+
// 1. NVIDIA via nvidia-smi (first-class support when available)
|
|
739
|
+
const nv = await getNvidiaGpu();
|
|
740
|
+
if (nv) return nv;
|
|
741
|
+
|
|
742
|
+
// 2. AMD / Intel / General Windows GPUs
|
|
508
743
|
try {
|
|
509
|
-
const
|
|
510
|
-
|
|
744
|
+
const rawControllers = await runPowerShell(
|
|
745
|
+
`Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor, PNPDeviceID | ConvertTo-Csv -NoTypeInformation`,
|
|
511
746
|
''
|
|
512
|
-
)
|
|
513
|
-
|
|
747
|
+
);
|
|
748
|
+
const controllers = parseWindowsVideoControllers(rawControllers);
|
|
749
|
+
if (controllers.length === 0) return null;
|
|
514
750
|
|
|
515
|
-
const
|
|
516
|
-
|
|
751
|
+
const prioritized = prioritizeGpuControllers(controllers);
|
|
752
|
+
const bestController = prioritized[0];
|
|
517
753
|
|
|
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);
|
|
754
|
+
// Read 64-bit VRAM (avoids 4GB uint32 clamp on RX 7900, Arc A770, etc.)
|
|
755
|
+
let vramBytes = await getWindowsGpu64BitVram(bestController.name);
|
|
756
|
+
if (vramBytes === 0 && bestController.adapterRam > 0) {
|
|
757
|
+
vramBytes = bestController.adapterRam;
|
|
526
758
|
}
|
|
527
759
|
|
|
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
|
-
}
|
|
760
|
+
// Read real 3D engine utilization and process count
|
|
761
|
+
const { utilization, processCount } = await getWindowsGpu3dUtilization();
|
|
536
762
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
763
|
+
// Query temperature for AMD / Intel via LibreHardwareMonitor or Windows Thermal
|
|
764
|
+
let temperature: number | undefined;
|
|
765
|
+
try {
|
|
766
|
+
const rawTemp = await runPowerShell(
|
|
767
|
+
`Get-CimInstance -Namespace root/LibreHardwareMonitor -ClassName Sensor -Filter "SensorType = 'Temperature' and Name like '%GPU%'" -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Value`,
|
|
768
|
+
''
|
|
769
|
+
);
|
|
770
|
+
const t = safeParseFloat(rawTemp);
|
|
771
|
+
if (t > 0 && t < 125) temperature = t;
|
|
772
|
+
} catch {}
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
model: bestController.name,
|
|
776
|
+
memory: vramBytes,
|
|
777
|
+
utilization,
|
|
778
|
+
temperature,
|
|
779
|
+
processes: processCount,
|
|
780
|
+
};
|
|
545
781
|
} catch {
|
|
546
|
-
|
|
782
|
+
return null;
|
|
547
783
|
}
|
|
548
|
-
return null;
|
|
549
784
|
}
|
|
550
785
|
|
|
551
|
-
|
|
552
|
-
const nv = await getNvidiaGpu();
|
|
553
|
-
if (nv) return nv;
|
|
786
|
+
// ─── Network & Socket Monitoring (`pyre netusage`) ─────────────────────────────
|
|
554
787
|
|
|
555
|
-
|
|
556
|
-
|
|
788
|
+
export interface WindowsTcpConnection {
|
|
789
|
+
localPort: number;
|
|
790
|
+
remoteAddress: string;
|
|
791
|
+
remotePort: number;
|
|
792
|
+
state: string;
|
|
793
|
+
owningPid: number;
|
|
794
|
+
}
|
|
557
795
|
|
|
558
|
-
|
|
559
|
-
|
|
796
|
+
export function parseWindowsTcpConnections(csvRaw: string): WindowsTcpConnection[] {
|
|
797
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0);
|
|
798
|
+
if (lines.length < 2) return [];
|
|
560
799
|
|
|
561
|
-
|
|
800
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
801
|
+
const connections: WindowsTcpConnection[] = [];
|
|
802
|
+
|
|
803
|
+
for (const line of lines.slice(1)) {
|
|
804
|
+
const values = line.split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
805
|
+
const row: Record<string, string> = {};
|
|
806
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
807
|
+
|
|
808
|
+
const localPort = safeParseInt(row.LocalPort, 0);
|
|
809
|
+
const remoteAddress = row.RemoteAddress || '';
|
|
810
|
+
const remotePort = safeParseInt(row.RemotePort, 0);
|
|
811
|
+
const state = row.State || '';
|
|
812
|
+
const owningPid = safeParseInt(row.OwningProcess, 0);
|
|
813
|
+
|
|
814
|
+
if (owningPid > 0) {
|
|
815
|
+
connections.push({ localPort, remoteAddress, remotePort, state, owningPid });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return connections;
|
|
562
820
|
}
|
|
563
821
|
|
|
564
822
|
export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
@@ -585,9 +843,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
585
843
|
''
|
|
586
844
|
);
|
|
587
845
|
if (raw) iface = raw;
|
|
588
|
-
} catch {
|
|
589
|
-
// ignore
|
|
590
|
-
}
|
|
846
|
+
} catch {}
|
|
591
847
|
|
|
592
848
|
try {
|
|
593
849
|
const netstatE = await run('netstat -e 2>nul', '', 1500);
|
|
@@ -602,9 +858,7 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
602
858
|
txPackets += parseInt(parts[2], 10) || 0;
|
|
603
859
|
}
|
|
604
860
|
}
|
|
605
|
-
} catch {
|
|
606
|
-
// ignore
|
|
607
|
-
}
|
|
861
|
+
} catch {}
|
|
608
862
|
|
|
609
863
|
const now = Date.now();
|
|
610
864
|
let rxRate = 0;
|
|
@@ -618,6 +872,9 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
618
872
|
|
|
619
873
|
const connectionStates = await getWindowsConnectionStates();
|
|
620
874
|
|
|
875
|
+
// Populate listening ports and remote hosts
|
|
876
|
+
const { listeningPorts, topRemoteHosts } = await getWindowsSocketDetails();
|
|
877
|
+
|
|
621
878
|
return {
|
|
622
879
|
interface: iface,
|
|
623
880
|
ip,
|
|
@@ -633,42 +890,98 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
633
890
|
udp: { connections: 0, rxBytes: 0, txBytes: 0 },
|
|
634
891
|
},
|
|
635
892
|
connectionStates,
|
|
636
|
-
listeningPorts
|
|
893
|
+
listeningPorts,
|
|
637
894
|
establishedConnections: connectionStates.established,
|
|
638
|
-
topRemoteHosts
|
|
895
|
+
topRemoteHosts,
|
|
639
896
|
};
|
|
640
897
|
}
|
|
641
898
|
|
|
642
|
-
async function
|
|
643
|
-
const
|
|
899
|
+
async function getWindowsSocketDetails(): Promise<{ listeningPorts: number[]; topRemoteHosts: RemoteHostInfo[] }> {
|
|
900
|
+
const listeningPorts: number[] = [];
|
|
901
|
+
const topRemoteHosts: RemoteHostInfo[] = [];
|
|
902
|
+
|
|
644
903
|
try {
|
|
645
904
|
const raw = await runPowerShell(
|
|
646
|
-
|
|
647
|
-
'
|
|
905
|
+
`$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 { '#' }`,
|
|
906
|
+
'#'
|
|
648
907
|
);
|
|
649
|
-
|
|
908
|
+
const [portsPart, hostsPart] = raw.split('#');
|
|
909
|
+
if (portsPart) {
|
|
910
|
+
portsPart.split(',').forEach(p => {
|
|
911
|
+
const portNum = safeParseInt(p);
|
|
912
|
+
if (portNum > 0 && !listeningPorts.includes(portNum)) listeningPorts.push(portNum);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (hostsPart) {
|
|
916
|
+
hostsPart.split('|').forEach(h => {
|
|
917
|
+
const [ip, countStr] = h.split(':');
|
|
918
|
+
if (ip) {
|
|
919
|
+
topRemoteHosts.push({
|
|
920
|
+
ip,
|
|
921
|
+
connections: safeParseInt(countStr, 1),
|
|
922
|
+
activePids: [],
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
} catch {}
|
|
650
928
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
'0'
|
|
654
|
-
);
|
|
655
|
-
stats.listening = safeParseInt(listenRaw, 0);
|
|
929
|
+
return { listeningPorts, topRemoteHosts };
|
|
930
|
+
}
|
|
656
931
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
932
|
+
async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
|
|
933
|
+
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
934
|
+
try {
|
|
935
|
+
const raw = await runPowerShell(
|
|
936
|
+
`$c = Get-NetTCPConnection -ErrorAction SilentlyContinue; if ($c) { $groups = $c | Group-Object State; ($groups | ForEach-Object { "$($_.Name)=$($_.Count)" }) -join '|' } else { '' }`,
|
|
937
|
+
''
|
|
660
938
|
);
|
|
661
|
-
|
|
939
|
+
for (const pair of raw.split('|')) {
|
|
940
|
+
const [state, countStr] = pair.split('=');
|
|
941
|
+
const count = safeParseInt(countStr, 0);
|
|
942
|
+
if (state === 'Established') stats.established = count;
|
|
943
|
+
else if (state === 'Listen') stats.listening = count;
|
|
944
|
+
else if (state === 'TimeWait') stats.timeWait = count;
|
|
945
|
+
else if (state === 'CloseWait') stats.closeWait = count;
|
|
946
|
+
else if (count > 0) stats.other += count;
|
|
947
|
+
}
|
|
948
|
+
} catch {}
|
|
949
|
+
return stats;
|
|
950
|
+
}
|
|
662
951
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
952
|
+
export function buildWindowsNetworkProcesses(
|
|
953
|
+
conns: WindowsTcpConnection[],
|
|
954
|
+
procMap: Map<number, string>
|
|
955
|
+
): NetworkProcess[] {
|
|
956
|
+
const agg = new Map<number, NetworkProcess>();
|
|
957
|
+
|
|
958
|
+
for (const c of conns) {
|
|
959
|
+
const pid = c.owningPid;
|
|
960
|
+
const name = procMap.get(pid) || `pid-${pid}`;
|
|
961
|
+
let item = agg.get(pid);
|
|
962
|
+
if (!item) {
|
|
963
|
+
item = {
|
|
964
|
+
pid,
|
|
965
|
+
command: name,
|
|
966
|
+
rxBytes: 0,
|
|
967
|
+
txBytes: 0,
|
|
968
|
+
rxPackets: 0,
|
|
969
|
+
txPackets: 0,
|
|
970
|
+
connections: [],
|
|
971
|
+
};
|
|
972
|
+
agg.set(pid, item);
|
|
973
|
+
}
|
|
974
|
+
if (c.remoteAddress && c.remoteAddress !== '0.0.0.0') {
|
|
975
|
+
item.connections.push({
|
|
976
|
+
proto: 'tcp',
|
|
977
|
+
localAddress: `127.0.0.1:${c.localPort}`,
|
|
978
|
+
remoteAddress: `${c.remoteAddress}:${c.remotePort}`,
|
|
979
|
+
state: c.state,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
670
982
|
}
|
|
671
|
-
|
|
983
|
+
|
|
984
|
+
return Array.from(agg.values()).sort((a, b) => b.connections.length - a.connections.length);
|
|
672
985
|
}
|
|
673
986
|
|
|
674
987
|
export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
@@ -690,11 +1003,34 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
690
1003
|
txPackets += parseInt(parts[2], 10) || 0;
|
|
691
1004
|
}
|
|
692
1005
|
}
|
|
693
|
-
} catch {
|
|
694
|
-
// ignore
|
|
695
|
-
}
|
|
1006
|
+
} catch {}
|
|
696
1007
|
|
|
697
1008
|
const connectionStates = await getWindowsConnectionStates();
|
|
1009
|
+
const { listeningPorts } = await getWindowsSocketDetails();
|
|
1010
|
+
|
|
1011
|
+
// Populate per-process network socket metrics for `pyre netusage` and packet panel
|
|
1012
|
+
let allProcesses: NetworkProcess[] = [];
|
|
1013
|
+
try {
|
|
1014
|
+
const rawConns = await runPowerShell(
|
|
1015
|
+
`Get-NetTCPConnection -State Established,Listen -ErrorAction SilentlyContinue | Select-Object LocalPort, RemoteAddress, RemotePort, State, OwningProcess | ConvertTo-Csv -NoTypeInformation`,
|
|
1016
|
+
''
|
|
1017
|
+
);
|
|
1018
|
+
const conns = parseWindowsTcpConnections(rawConns);
|
|
1019
|
+
|
|
1020
|
+
// Build PID to process command name mapping
|
|
1021
|
+
const rawProcs = await runPowerShell(
|
|
1022
|
+
`Get-Process -ErrorAction SilentlyContinue | Select-Object Id, ProcessName | ForEach-Object { "$($_.Id)|$($_.ProcessName)" }`,
|
|
1023
|
+
''
|
|
1024
|
+
);
|
|
1025
|
+
const procMap = new Map<number, string>();
|
|
1026
|
+
for (const line of rawProcs.split('\n')) {
|
|
1027
|
+
const [idStr, name] = line.trim().split('|');
|
|
1028
|
+
const pid = safeParseInt(idStr);
|
|
1029
|
+
if (pid > 0 && name) procMap.set(pid, name);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
allProcesses = buildWindowsNetworkProcesses(conns, procMap);
|
|
1033
|
+
} catch {}
|
|
698
1034
|
|
|
699
1035
|
return {
|
|
700
1036
|
totalPackets: rxPackets + txPackets,
|
|
@@ -703,73 +1039,91 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
703
1039
|
rxRate: 0,
|
|
704
1040
|
txRate: 0,
|
|
705
1041
|
connections: connectionStates.established,
|
|
706
|
-
topProcesses:
|
|
1042
|
+
topProcesses: allProcesses.slice(0, 10),
|
|
707
1043
|
interfaces: [{ iface: 'Primary', rxPackets, txPackets, rxBytes, txBytes }],
|
|
708
|
-
allProcesses
|
|
1044
|
+
allProcesses,
|
|
709
1045
|
connectionStates,
|
|
710
|
-
listeningPorts
|
|
1046
|
+
listeningPorts,
|
|
711
1047
|
};
|
|
712
1048
|
}
|
|
713
1049
|
|
|
1050
|
+
// ─── Process Collection with ParentProcessId (`ppid`) for Tree View ─────────────
|
|
1051
|
+
|
|
714
1052
|
const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
|
|
715
1053
|
|
|
1054
|
+
export function parseWindowsProcessCsv(
|
|
1055
|
+
csvRaw: string,
|
|
1056
|
+
totalMem: number,
|
|
1057
|
+
cache: Map<number, { user: number; kernel: number; ts: number }>,
|
|
1058
|
+
now: number
|
|
1059
|
+
): ProcessData[] {
|
|
1060
|
+
const lines = csvRaw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
|
|
1061
|
+
const procs: ProcessData[] = [];
|
|
1062
|
+
|
|
1063
|
+
for (const line of lines) {
|
|
1064
|
+
const parts = line.split(',').map(s => s.trim().replace(/^"|"$/g, ''));
|
|
1065
|
+
if (parts.length < 6) continue;
|
|
1066
|
+
|
|
1067
|
+
const name = parts[0];
|
|
1068
|
+
const pid = safeParseInt(parts[1], 0);
|
|
1069
|
+
const ppid = safeParseInt(parts[2], 0);
|
|
1070
|
+
const threads = safeParseInt(parts[3], 0);
|
|
1071
|
+
const memBytes = safeParseInt(parts[4], 0);
|
|
1072
|
+
const userTime = safeParseInt(parts[5], 0);
|
|
1073
|
+
const kernelTime = safeParseInt(parts[6], 0);
|
|
1074
|
+
const commandLine = parts[7] || name;
|
|
1075
|
+
|
|
1076
|
+
if (!pid || pid <= 0) continue;
|
|
1077
|
+
|
|
1078
|
+
const totalTime = userTime + kernelTime;
|
|
1079
|
+
let cpu = 0;
|
|
1080
|
+
|
|
1081
|
+
const prev = cache.get(pid);
|
|
1082
|
+
if (prev) {
|
|
1083
|
+
const dt = now - prev.ts;
|
|
1084
|
+
if (dt > 0) {
|
|
1085
|
+
const timeDelta = totalTime - (prev.user + prev.kernel);
|
|
1086
|
+
if (timeDelta > 0) {
|
|
1087
|
+
const cpuSeconds = timeDelta / 10_000_000;
|
|
1088
|
+
cpu = Math.min(100, Math.round((cpuSeconds / dt) * 10000) / 100);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
cache.set(pid, { user: userTime, kernel: kernelTime, ts: now });
|
|
1093
|
+
|
|
1094
|
+
const memPct = totalMem > 0 ? Math.round((memBytes / totalMem) * 1000) / 10 : 0;
|
|
1095
|
+
const isGpuOrMl = /gpu|vulkan|cuda|ollama|llama|torch|python|nvidia|amd|radeon|directx|game/i.test(name) ||
|
|
1096
|
+
/python|node|ollama/i.test(commandLine);
|
|
1097
|
+
|
|
1098
|
+
procs.push({
|
|
1099
|
+
pid,
|
|
1100
|
+
ppid,
|
|
1101
|
+
user: 'SYSTEM',
|
|
1102
|
+
cpu,
|
|
1103
|
+
mem: memPct,
|
|
1104
|
+
state: 'R',
|
|
1105
|
+
threads,
|
|
1106
|
+
runtime: 0,
|
|
1107
|
+
command: name,
|
|
1108
|
+
isGpuOrMlAttributed: isGpuOrMl,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
|
|
1113
|
+
return procs;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
716
1116
|
export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
|
|
717
1117
|
try {
|
|
1118
|
+
// Explicitly query ParentProcessId for process tree view (`pyre --tree`)
|
|
718
1119
|
const raw = await runPowerShell(
|
|
719
|
-
`Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
|
|
1120
|
+
`Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ParentProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime, CommandLine | ConvertTo-Csv -NoTypeInformation`,
|
|
720
1121
|
''
|
|
721
1122
|
);
|
|
722
|
-
const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
|
|
723
1123
|
const now = Date.now();
|
|
724
|
-
const procs: ProcessData[] = [];
|
|
725
1124
|
const totalMem = os.totalmem();
|
|
1125
|
+
const procs = parseWindowsProcessCsv(raw, totalMem, winProcCache, now);
|
|
726
1126
|
|
|
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
1127
|
if (limit && limit > 0) return procs.slice(0, limit);
|
|
774
1128
|
return procs;
|
|
775
1129
|
} catch {
|
|
@@ -790,6 +1144,8 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
|
|
|
790
1144
|
}));
|
|
791
1145
|
}
|
|
792
1146
|
|
|
1147
|
+
// ─── Displays ──────────────────────────────────────────────────────────────────
|
|
1148
|
+
|
|
793
1149
|
export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
|
|
794
1150
|
try {
|
|
795
1151
|
const raw = await runPowerShell(
|