pyre-cli 10.0.0 → 10.2.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 +195 -494
- package/dist/{checkCmd-RKW6QGDR.js → checkCmd-WQAHAUWO.js} +1 -1
- package/dist/{chunk-GHRDW646.js → chunk-23YWCI3O.js} +375 -135
- package/dist/{service-Q74BBILO.js → chunk-KFPTD266.js} +43 -3
- package/dist/{doctor-GYHFTNZQ.js → chunk-KVUMOA6C.js} +7 -6
- package/dist/{chunk-53O2RGSL.js → chunk-QM5L43EK.js} +19 -2
- package/dist/{chunk-6UHZP4QP.js → chunk-XTOOEKLM.js} +24 -12
- package/dist/{collectors-H43PN4PS.js → collectors-JDN5Y7EV.js} +1 -1
- package/dist/{completions-JKYEJNGR.js → completions-ZVTNE52B.js} +15 -11
- package/dist/{config-GYGJXU2Q.js → config-J4YMADEF.js} +3 -1
- package/dist/doctor-LHFW4UJP.js +10 -0
- package/dist/firebaseSync-ON3RZ6KX.js +91 -0
- package/dist/index.js +4243 -735
- package/dist/monitors/platform/windows-ui.ps1 +42 -0
- package/dist/{netUsageCmd-WKBTW2YG.js → netUsageCmd-67IBZZYN.js} +2 -2
- package/dist/{pipeCmd-7REDBLBU.js → pipeCmd-2N4JPKYE.js} +1 -1
- package/dist/{prometheusCmd-PL64BLN5.js → prometheusCmd-LEUM5WCZ.js} +1 -1
- package/dist/serveCmd-L7LVJOZN.js +353 -0
- package/dist/service-7GIX3YNC.js +16 -0
- package/dist/syncCmd-42IRF633.js +246 -0
- package/dist/{topoCmd-DU2HHZNH.js → topoCmd-DZ3YS5IL.js} +1 -1
- package/dist/{watchdogCmd-UF53JPZB.js → watchdogCmd-6GR4UN6B.js} +7 -7
- package/dist/{wizard-TD7BGX6O.js → wizard-JQMQWFAW.js} +1 -1
- package/package.json +4 -3
- package/parity.md +2 -0
- package/src/completions.ts +15 -11
- package/src/easy_features.test.ts +149 -0
- package/src/firebaseSync.ts +157 -0
- package/src/formatters/html_template.ts +3086 -598
- package/src/formatters/render.ts +25 -12
- package/src/index.ts +516 -23
- package/src/live/input.ts +501 -148
- package/src/live/render.ts +163 -18
- package/src/live/state.ts +52 -2
- package/src/live/types.ts +1 -1
- package/src/monitors/collectors.ts +3 -10
- package/src/monitors/platform/windows-ui.ps1 +42 -0
- package/src/monitors/platform/windows.ts +442 -147
- package/src/monitors/types.ts +8 -0
- package/src/serveCmd.ts +17 -2
- package/src/service.ts +49 -0
- package/src/state/config.ts +48 -0
- package/src/syncCmd.ts +159 -11
- package/src/web/dashboard.ts +205 -0
- package/dist/serveCmd-2H5VLF3P.js +0 -153
- package/dist/syncCmd-A2W3QB3S.js +0 -126
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* Windows platform telemetry collectors.
|
|
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
6
|
*/
|
|
7
7
|
|
|
8
8
|
import os from 'node:os';
|
|
9
|
-
import { run } from '../run.js';
|
|
9
|
+
import { run, runPowerShell } from '../run.js';
|
|
10
10
|
import type {
|
|
11
11
|
CpuData,
|
|
12
12
|
MemoryData,
|
|
@@ -25,6 +25,50 @@ import type {
|
|
|
25
25
|
|
|
26
26
|
let prevNetSample: { rxBytes: number; txBytes: number; ts: number } | null = null;
|
|
27
27
|
|
|
28
|
+
function safeParseFloat(s: string | undefined): number {
|
|
29
|
+
if (!s) return 0;
|
|
30
|
+
const n = parseFloat(s);
|
|
31
|
+
return Number.isFinite(n) ? n : 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeParseInt(s: string | undefined, def = 0): number {
|
|
35
|
+
if (!s) return def;
|
|
36
|
+
const n = parseInt(s, 10);
|
|
37
|
+
return Number.isFinite(n) ? n : def;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parsePipeOutput(raw: string): string[][] {
|
|
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> {
|
|
49
|
+
const raw = await runPowerShell(
|
|
50
|
+
`(Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ${property}) -join '|'`,
|
|
51
|
+
''
|
|
52
|
+
);
|
|
53
|
+
return raw.split('|')[0] || '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getCimRow(className: string): Promise<Record<string, string>> {
|
|
57
|
+
const raw = await runPowerShell(
|
|
58
|
+
`Get-CimInstance -ClassName ${className} -ErrorAction SilentlyContinue | Select-Object -Property * -ExcludeProperty CIM* | ConvertTo-Csv -NoTypeInformation`,
|
|
59
|
+
''
|
|
60
|
+
);
|
|
61
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
62
|
+
if (lines.length < 2) return {};
|
|
63
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
64
|
+
const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
65
|
+
const row: Record<string, string> = {};
|
|
66
|
+
headers.forEach((h, i) => {
|
|
67
|
+
row[h] = values[i] || '';
|
|
68
|
+
});
|
|
69
|
+
return row;
|
|
70
|
+
}
|
|
71
|
+
|
|
28
72
|
export async function collectWindowsSystem(): Promise<{ hostname: string; os: string; uptime: string }> {
|
|
29
73
|
const hostname = os.hostname();
|
|
30
74
|
const rel = os.release();
|
|
@@ -52,8 +96,19 @@ export async function collectWindowsSystem(): Promise<{ hostname: string; os: st
|
|
|
52
96
|
export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: number; idle: number }[] | null }): Promise<CpuData> {
|
|
53
97
|
const cpus = os.cpus();
|
|
54
98
|
const cores = cpus.length || 1;
|
|
55
|
-
|
|
56
|
-
|
|
99
|
+
|
|
100
|
+
let brand = cpus[0]?.model || 'Windows CPU';
|
|
101
|
+
let physicalCores = cores;
|
|
102
|
+
let frequency = cpus[0]?.speed || 0;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const cpuRow = await getCimRow('Win32_Processor');
|
|
106
|
+
if (cpuRow.Name) brand = cpuRow.Name.trim();
|
|
107
|
+
if (cpuRow.NumberOfCores) physicalCores = safeParseInt(cpuRow.NumberOfCores, cores);
|
|
108
|
+
if (cpuRow.MaxClockSpeed) frequency = safeParseInt(cpuRow.MaxClockSpeed, frequency);
|
|
109
|
+
} catch {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
57
112
|
|
|
58
113
|
const current = cpus.map(cpu => ({
|
|
59
114
|
total: cpu.times.user + cpu.times.nice + cpu.times.sys + cpu.times.idle + cpu.times.irq,
|
|
@@ -62,9 +117,10 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
62
117
|
|
|
63
118
|
let coreUsage: number[] = [];
|
|
64
119
|
let usage = 0;
|
|
65
|
-
|
|
120
|
+
const prevCpuTimes = prevCpuTimesRef.current;
|
|
121
|
+
if (prevCpuTimes && prevCpuTimes.length === current.length) {
|
|
66
122
|
coreUsage = current.map((c, i) => {
|
|
67
|
-
const prev =
|
|
123
|
+
const prev = prevCpuTimes[i];
|
|
68
124
|
const totalDelta = c.total - prev.total;
|
|
69
125
|
const idleDelta = c.idle - prev.idle;
|
|
70
126
|
if (totalDelta <= 0) return 0;
|
|
@@ -76,33 +132,42 @@ export async function collectWindowsCpu(prevCpuTimesRef: { current: { total: num
|
|
|
76
132
|
}
|
|
77
133
|
prevCpuTimesRef.current = current;
|
|
78
134
|
|
|
79
|
-
// Load avg simulation (Windows returns [0, 0, 0] from os.loadavg())
|
|
80
135
|
const activeLoad = Math.round((usage / 100) * cores * 100) / 100;
|
|
81
136
|
const loadAvg = [activeLoad, activeLoad, activeLoad];
|
|
82
137
|
|
|
83
|
-
// Temperature via WMI or estimation
|
|
84
138
|
let temperature: number | undefined;
|
|
139
|
+
|
|
85
140
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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;
|
|
93
148
|
}
|
|
94
149
|
} catch {
|
|
95
150
|
// ignore
|
|
96
151
|
}
|
|
97
152
|
|
|
98
153
|
if (temperature === undefined) {
|
|
99
|
-
|
|
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
|
+
}
|
|
100
165
|
}
|
|
101
166
|
|
|
102
167
|
return {
|
|
103
168
|
brand,
|
|
104
169
|
cores,
|
|
105
|
-
physicalCores
|
|
170
|
+
physicalCores,
|
|
106
171
|
frequency,
|
|
107
172
|
usage,
|
|
108
173
|
loadAvg,
|
|
@@ -122,18 +187,19 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
|
|
|
122
187
|
let swapFree = 0;
|
|
123
188
|
|
|
124
189
|
try {
|
|
125
|
-
const rawPage =
|
|
190
|
+
const rawPage = await runPowerShell(
|
|
191
|
+
`Get-CimInstance Win32_PageFileUsage | ForEach-Object { "$($_.AllocatedBaseSize)|$($_.CurrentUsage)" }`,
|
|
192
|
+
''
|
|
193
|
+
);
|
|
126
194
|
const lines = rawPage.split('\n').filter(l => l.trim().length > 0);
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
swapUsed = usedMb * 1024 * 1024;
|
|
134
|
-
swapFree = Math.max(0, swapTotal - swapUsed);
|
|
135
|
-
}
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const [allocStr, usedStr] = line.split('|');
|
|
197
|
+
const allocMb = safeParseInt(allocStr);
|
|
198
|
+
const usedMb = safeParseInt(usedStr);
|
|
199
|
+
swapTotal += allocMb * 1024 * 1024;
|
|
200
|
+
swapUsed += usedMb * 1024 * 1024;
|
|
136
201
|
}
|
|
202
|
+
swapFree = Math.max(0, swapTotal - swapUsed);
|
|
137
203
|
} catch {
|
|
138
204
|
// ignore
|
|
139
205
|
}
|
|
@@ -157,16 +223,18 @@ export async function collectWindowsMemory(): Promise<MemoryData> {
|
|
|
157
223
|
|
|
158
224
|
export async function collectWindowsDisk(): Promise<DiskData[]> {
|
|
159
225
|
try {
|
|
160
|
-
const raw =
|
|
161
|
-
|
|
226
|
+
const raw = await runPowerShell(
|
|
227
|
+
`Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { "$($_.DeviceID)|$($_.FileSystem)|$($_.Size)|$($_.FreeSpace)" }`,
|
|
228
|
+
''
|
|
229
|
+
);
|
|
162
230
|
const disks: DiskData[] = [];
|
|
163
231
|
|
|
164
|
-
for (const line of
|
|
165
|
-
const parts = line.trim().split(
|
|
232
|
+
for (const line of raw.split('\n')) {
|
|
233
|
+
const parts = line.trim().split('|');
|
|
166
234
|
if (parts.length >= 4) {
|
|
167
235
|
const drive = parts[0];
|
|
168
|
-
const
|
|
169
|
-
const
|
|
236
|
+
const totalBytes = safeParseInt(parts[2]);
|
|
237
|
+
const freeBytes = safeParseInt(parts[3]);
|
|
170
238
|
if (totalBytes > 0) {
|
|
171
239
|
const usedBytes = Math.max(0, totalBytes - freeBytes);
|
|
172
240
|
const capPct = Math.round((usedBytes / totalBytes) * 100);
|
|
@@ -199,7 +267,7 @@ export async function collectWindowsDisk(): Promise<DiskData[]> {
|
|
|
199
267
|
mountpoint: 'C:',
|
|
200
268
|
readBytesSec: 0,
|
|
201
269
|
writeBytesSec: 0,
|
|
202
|
-
}
|
|
270
|
+
},
|
|
203
271
|
];
|
|
204
272
|
}
|
|
205
273
|
|
|
@@ -212,117 +280,283 @@ function formatBytesStr(bytes: number): string {
|
|
|
212
280
|
|
|
213
281
|
export async function collectWindowsBattery(): Promise<BatteryData | null> {
|
|
214
282
|
try {
|
|
215
|
-
const raw =
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
level,
|
|
237
|
-
state,
|
|
238
|
-
timeRemaining,
|
|
239
|
-
health: 'Good',
|
|
240
|
-
powerSource,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
283
|
+
const raw = await runPowerShell(
|
|
284
|
+
`$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.EstimatedChargeRemaining)|$($bat.BatteryStatus)|$($bat.EstimatedRunTime)|$($bat.DischargeRate)" } else { '' }`,
|
|
285
|
+
''
|
|
286
|
+
);
|
|
287
|
+
if (!raw) return null;
|
|
288
|
+
|
|
289
|
+
const parts = raw.split('|');
|
|
290
|
+
const level = safeParseInt(parts[0], 0);
|
|
291
|
+
const statusCode = safeParseInt(parts[1], 1);
|
|
292
|
+
const runTimeMinutes = safeParseInt(parts[2], 0);
|
|
293
|
+
const dischargeRate = safeParseInt(parts[3], 0);
|
|
294
|
+
|
|
295
|
+
const powerSource = statusCode === 2 || statusCode === 3 || statusCode === 6 ? 'AC' : 'Battery';
|
|
296
|
+
const state = statusCode === 3 ? 'charged' : statusCode === 6 || statusCode === 2 ? 'charging' : 'discharging';
|
|
297
|
+
let timeRemaining = 'calculating';
|
|
298
|
+
if (state === 'charged') timeRemaining = '∞';
|
|
299
|
+
else if (runTimeMinutes && runTimeMinutes > 0 && runTimeMinutes < 70000) {
|
|
300
|
+
const h = Math.floor(runTimeMinutes / 60);
|
|
301
|
+
const m = runTimeMinutes % 60;
|
|
302
|
+
timeRemaining = `${h}:${m.toString().padStart(2, '0')}`;
|
|
243
303
|
}
|
|
304
|
+
|
|
305
|
+
const powerWatts = dischargeRate > 0 ? Math.round((dischargeRate / 1000) * 100) / 100 : undefined;
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
level,
|
|
309
|
+
state,
|
|
310
|
+
timeRemaining,
|
|
311
|
+
health: 'Good',
|
|
312
|
+
powerSource,
|
|
313
|
+
powerWatts,
|
|
314
|
+
};
|
|
244
315
|
} catch {
|
|
245
|
-
|
|
316
|
+
return null;
|
|
246
317
|
}
|
|
247
|
-
return null;
|
|
248
318
|
}
|
|
249
319
|
|
|
250
320
|
export async function collectWindowsThermal(): Promise<ThermalData> {
|
|
321
|
+
const temperatures: Record<string, number | null> = {};
|
|
322
|
+
|
|
251
323
|
let tempC: number | undefined;
|
|
324
|
+
|
|
252
325
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
326
|
+
const raw = await runPowerShell(
|
|
327
|
+
`(Get-Counter '\Thermal Zone Information(*)\*' -ErrorAction SilentlyContinue).CounterSamples | Where-Object { $_.CookedValue -gt 200 -and $_.CookedValue -lt 400 } | Select-Object -First 1 -ExpandProperty CookedValue`,
|
|
328
|
+
''
|
|
329
|
+
).catch(() => '');
|
|
330
|
+
const tempK = parseFloat(raw);
|
|
331
|
+
if (tempK > 200 && tempK < 400) {
|
|
332
|
+
tempC = Math.round((tempK - 273.15) * 10) / 10;
|
|
333
|
+
temperatures['cpu'] = tempC;
|
|
259
334
|
}
|
|
260
335
|
} catch {
|
|
261
336
|
// ignore
|
|
262
337
|
}
|
|
263
338
|
|
|
264
339
|
if (tempC === undefined) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
340
|
+
try {
|
|
341
|
+
const rawWmi = await run('wmic /namespace:\\\\root\\wmi PATH MSAcpi_ThermalZoneTemperature get CurrentTemperature 2>nul', '', 1500);
|
|
342
|
+
const match = rawWmi.match(/(\d{3,4})/);
|
|
343
|
+
if (match) {
|
|
344
|
+
const kelvinTenths = parseInt(match[1], 10);
|
|
345
|
+
const c = Math.round(((kelvinTenths - 2732) / 10) * 10) / 10;
|
|
346
|
+
if (c >= 15 && c <= 115) {
|
|
347
|
+
tempC = c;
|
|
348
|
+
temperatures['cpu'] = c;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// ignore
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let gpuTemp: number | undefined;
|
|
357
|
+
try {
|
|
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
|
|
268
368
|
}
|
|
269
369
|
|
|
370
|
+
const cpuTemp = tempC ?? gpuTemp;
|
|
270
371
|
let state = 'Nominal';
|
|
271
372
|
let pressureLevel = 0;
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
373
|
+
if (cpuTemp !== undefined) {
|
|
374
|
+
if (cpuTemp >= 100) {
|
|
375
|
+
state = 'Critical';
|
|
376
|
+
pressureLevel = 3;
|
|
377
|
+
} else if (cpuTemp >= 90) {
|
|
378
|
+
state = 'Serious';
|
|
379
|
+
pressureLevel = 2;
|
|
380
|
+
} else if (cpuTemp >= 80) {
|
|
381
|
+
state = 'Fair';
|
|
382
|
+
pressureLevel = 1;
|
|
383
|
+
}
|
|
281
384
|
}
|
|
282
385
|
|
|
283
386
|
return {
|
|
284
387
|
state,
|
|
285
|
-
detail: `${state} (${
|
|
388
|
+
detail: cpuTemp !== undefined ? `${state} (${cpuTemp}°C)` : state,
|
|
286
389
|
pressureLevel,
|
|
287
|
-
temperatures:
|
|
390
|
+
temperatures: Object.keys(temperatures).length ? temperatures : undefined,
|
|
288
391
|
};
|
|
289
392
|
}
|
|
290
393
|
|
|
291
394
|
export async function collectWindowsPower(): Promise<PowerData | null> {
|
|
395
|
+
const power: PowerData = {};
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const batRaw = await runPowerShell(
|
|
399
|
+
`$bat = Get-CimInstance Win32_Battery -ErrorAction SilentlyContinue; if ($bat) { "$($bat.DischargeRate)|$($bat.BatteryStatus)" } else { '|0' }`,
|
|
400
|
+
''
|
|
401
|
+
);
|
|
402
|
+
const [dischargeStr, statusStr] = batRaw.split('|');
|
|
403
|
+
const dischargeRate = safeParseInt(dischargeStr);
|
|
404
|
+
const statusCode = safeParseInt(statusStr, 1);
|
|
405
|
+
|
|
406
|
+
if (dischargeRate > 0 && (statusCode === 1 || statusCode === 2)) {
|
|
407
|
+
power.combinedWatts = Math.round((dischargeRate / 1000) * 100) / 100;
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
// ignore
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const cpu = os.cpus();
|
|
415
|
+
const usage = cpu.length > 0 ? Math.round(cpu.reduce((a, c) => a + (c.times.user + c.times.nice + c.times.sys + c.times.irq - c.times.idle) / (c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq), 0) / cpu.length * 100) : 0;
|
|
416
|
+
const tdpWatts = 65;
|
|
417
|
+
if (usage > 0) {
|
|
418
|
+
power.cpuWatts = Math.round((usage / 100) * tdpWatts * 100) / 100;
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
// ignore
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const nvRaw = await run('nvidia-smi --query-gpu=power.draw,utilization.gpu --format=csv,noheader,nounits 2>nul', '').catch(() => '');
|
|
426
|
+
const nvLines = nvRaw.split('\n').filter(l => l.trim());
|
|
427
|
+
for (const line of nvLines) {
|
|
428
|
+
const parts = line.trim().split(',').map(s => s.trim());
|
|
429
|
+
const pwr = parseFloat(parts[0]);
|
|
430
|
+
if (pwr > 0) {
|
|
431
|
+
power.gpuWatts = Math.round(pwr * 100) / 100;
|
|
432
|
+
if (!power.combinedWatts) power.combinedWatts = power.gpuWatts;
|
|
433
|
+
else power.combinedWatts = Math.round((power.combinedWatts + power.gpuWatts) * 100) / 100;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
// ignore
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (power.combinedWatts || power.cpuWatts || power.gpuWatts) {
|
|
442
|
+
if (!power.combinedWatts) {
|
|
443
|
+
power.combinedWatts = Math.round(((power.cpuWatts || 0) + (power.gpuWatts || 0)) * 100) / 100;
|
|
444
|
+
}
|
|
445
|
+
return power;
|
|
446
|
+
}
|
|
447
|
+
|
|
292
448
|
return null;
|
|
293
449
|
}
|
|
294
450
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
451
|
+
async function getNvidiaGpu(): Promise<GpuData | null> {
|
|
452
|
+
try {
|
|
453
|
+
const nvRaw = await run('nvidia-smi --query-gpu=gpu_name,memory.total,utilization.gpu,temperature.gpu,power.draw --format=csv,noheader,nounits 2>nul', '');
|
|
454
|
+
const nvLines = nvRaw.split('\n').filter(l => l.trim());
|
|
455
|
+
if (nvLines.length > 0) {
|
|
456
|
+
const parts = nvLines[0].trim().split(',').map(s => s.trim());
|
|
457
|
+
if (parts.length >= 3) {
|
|
458
|
+
const model = parts[0];
|
|
459
|
+
const memory = (parseFloat(parts[1]) || 0) * 1024 * 1024;
|
|
460
|
+
const utilization = Math.min(100, Math.max(0, Math.round(parseFloat(parts[2]) || 0)));
|
|
461
|
+
const temperature = parts[3] ? parseFloat(parts[3]) : undefined;
|
|
462
|
+
const powerDraw = parts[4] ? parseFloat(parts[4]) : undefined;
|
|
463
|
+
return {
|
|
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
|
+
}
|
|
306
472
|
}
|
|
473
|
+
} catch {
|
|
474
|
+
// ignore
|
|
307
475
|
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
308
478
|
|
|
309
|
-
|
|
479
|
+
async function getWmiGpu(): Promise<GpuData | null> {
|
|
310
480
|
try {
|
|
311
|
-
const raw =
|
|
481
|
+
const raw = await runPowerShell(
|
|
482
|
+
`Get-CimInstance Win32_VideoController | Select-Object Name, AdapterRAM, DriverVersion, VideoProcessor | ConvertTo-Csv -NoTypeInformation`,
|
|
483
|
+
''
|
|
484
|
+
);
|
|
312
485
|
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
313
486
|
if (lines.length >= 2) {
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
487
|
+
const headers = lines[0].split(',').map(h => h.trim().replace(/^"|"$/g, ''));
|
|
488
|
+
const values = lines[1].split(',').map(v => v.trim().replace(/^"|"$/g, ''));
|
|
489
|
+
const row: Record<string, string> = {};
|
|
490
|
+
headers.forEach((h, i) => { row[h] = values[i] || ''; });
|
|
491
|
+
|
|
492
|
+
const name = row.Name || row.VideoProcessor || 'Windows Display Adapter';
|
|
493
|
+
const ram = safeParseInt(row.AdapterRAM);
|
|
494
|
+
return {
|
|
495
|
+
model: name,
|
|
496
|
+
memory: ram,
|
|
497
|
+
utilization: 0,
|
|
498
|
+
processes: 0,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
} catch {
|
|
502
|
+
// ignore
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function getPerfCounterGpu(): Promise<GpuData | null> {
|
|
508
|
+
try {
|
|
509
|
+
const raw = await runPowerShell(
|
|
510
|
+
`$engines = Get-Counter '\GPU Engine(*)\*' -ErrorAction SilentlyContinue; if ($engines) { $engines.CounterSamples | Where-Object { $_.CounterName -match 'Utilization Percentage|GPU Time' -and $_.CookedValue -gt 0 } | ForEach-Object { "$($_.InstanceName)|$($_.CounterName)|$($_.CookedValue)" } }`,
|
|
511
|
+
''
|
|
512
|
+
).catch(() => '');
|
|
513
|
+
if (!raw) return null;
|
|
514
|
+
|
|
515
|
+
const lines = raw.split('\n').filter(l => l.trim());
|
|
516
|
+
if (lines.length === 0) return null;
|
|
517
|
+
|
|
518
|
+
const gpuGroups = new Map<string, { name: string; values: number[] }>();
|
|
519
|
+
for (const line of lines) {
|
|
520
|
+
const [instanceName, counterName, valueStr] = line.split('|');
|
|
521
|
+
const baseName = instanceName.replace(/\s*\(\d+\)\s*$/, '').trim();
|
|
522
|
+
if (!gpuGroups.has(baseName)) {
|
|
523
|
+
gpuGroups.set(baseName, { name: baseName, values: [] });
|
|
524
|
+
}
|
|
525
|
+
gpuGroups.get(baseName)!.values.push(parseFloat(valueStr) || 0);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
let bestGpu: { name: string; util: number } | null = null;
|
|
529
|
+
for (const group of gpuGroups.values()) {
|
|
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 };
|
|
321
534
|
}
|
|
322
535
|
}
|
|
536
|
+
|
|
537
|
+
if (bestGpu) {
|
|
538
|
+
return {
|
|
539
|
+
model: bestGpu.name,
|
|
540
|
+
memory: 0,
|
|
541
|
+
utilization: bestGpu.util,
|
|
542
|
+
processes: 0,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
323
545
|
} catch {
|
|
324
546
|
// ignore
|
|
325
547
|
}
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export async function collectWindowsGpu(): Promise<GpuData | null> {
|
|
552
|
+
const nv = await getNvidiaGpu();
|
|
553
|
+
if (nv) return nv;
|
|
554
|
+
|
|
555
|
+
const perf = await getPerfCounterGpu();
|
|
556
|
+
if (perf) return perf;
|
|
557
|
+
|
|
558
|
+
const wmi = await getWmiGpu();
|
|
559
|
+
if (wmi) return wmi;
|
|
326
560
|
|
|
327
561
|
return null;
|
|
328
562
|
}
|
|
@@ -346,7 +580,17 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
346
580
|
let txPackets = 0;
|
|
347
581
|
|
|
348
582
|
try {
|
|
349
|
-
const
|
|
583
|
+
const raw = await runPowerShell(
|
|
584
|
+
`Get-NetIPConfiguration -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.NetAdapter.Status -eq 'Up' } | Select-Object -First 1 -ExpandProperty NetAdapter.Name`,
|
|
585
|
+
''
|
|
586
|
+
);
|
|
587
|
+
if (raw) iface = raw;
|
|
588
|
+
} catch {
|
|
589
|
+
// ignore
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const netstatE = await run('netstat -e 2>nul', '', 1500);
|
|
350
594
|
const lines = netstatE.split('\n');
|
|
351
595
|
for (const line of lines) {
|
|
352
596
|
const parts = line.trim().split(/\s+/);
|
|
@@ -398,18 +642,29 @@ export async function collectWindowsNetwork(): Promise<NetworkData> {
|
|
|
398
642
|
async function getWindowsConnectionStates(): Promise<ConnectionStateStats> {
|
|
399
643
|
const stats: ConnectionStateStats = { established: 0, listening: 0, timeWait: 0, closeWait: 0, other: 0 };
|
|
400
644
|
try {
|
|
401
|
-
const raw =
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
645
|
+
const raw = await runPowerShell(
|
|
646
|
+
`Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
|
|
647
|
+
'0'
|
|
648
|
+
);
|
|
649
|
+
stats.established = safeParseInt(raw, 0);
|
|
650
|
+
|
|
651
|
+
const listenRaw = await runPowerShell(
|
|
652
|
+
`Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
|
|
653
|
+
'0'
|
|
654
|
+
);
|
|
655
|
+
stats.listening = safeParseInt(listenRaw, 0);
|
|
656
|
+
|
|
657
|
+
const twRaw = await runPowerShell(
|
|
658
|
+
`Get-NetTCPConnection -State TimeWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
|
|
659
|
+
'0'
|
|
660
|
+
);
|
|
661
|
+
stats.timeWait = safeParseInt(twRaw, 0);
|
|
662
|
+
|
|
663
|
+
const cwRaw = await runPowerShell(
|
|
664
|
+
`Get-NetTCPConnection -State CloseWait -ErrorAction SilentlyContinue | Measure-Object | Select-Object -ExpandProperty Count`,
|
|
665
|
+
'0'
|
|
666
|
+
);
|
|
667
|
+
stats.closeWait = safeParseInt(cwRaw, 0);
|
|
413
668
|
} catch {
|
|
414
669
|
// ignore
|
|
415
670
|
}
|
|
@@ -423,7 +678,7 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
423
678
|
let txPackets = 0;
|
|
424
679
|
|
|
425
680
|
try {
|
|
426
|
-
const netstatE =
|
|
681
|
+
const netstatE = await run('netstat -e 2>nul', '', 1500);
|
|
427
682
|
const lines = netstatE.split('\n');
|
|
428
683
|
for (const line of lines) {
|
|
429
684
|
const parts = line.trim().split(/\s+/);
|
|
@@ -456,44 +711,65 @@ export async function collectWindowsPackets(): Promise<PacketData | null> {
|
|
|
456
711
|
};
|
|
457
712
|
}
|
|
458
713
|
|
|
714
|
+
const winProcCache = new Map<number, { user: number; kernel: number; ts: number }>();
|
|
715
|
+
|
|
459
716
|
export async function collectWindowsProcesses(limit?: number): Promise<ProcessData[]> {
|
|
460
717
|
try {
|
|
461
|
-
const raw =
|
|
462
|
-
|
|
463
|
-
|
|
718
|
+
const raw = await runPowerShell(
|
|
719
|
+
`Get-CimInstance Win32_Process | Select-Object Name, ProcessId, ThreadCount, WorkingSetSize, UserModeTime, KernelModeTime | ConvertTo-Csv -NoTypeInformation`,
|
|
720
|
+
''
|
|
721
|
+
);
|
|
722
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0 && !l.startsWith('"Name"'));
|
|
723
|
+
const now = Date.now();
|
|
464
724
|
const procs: ProcessData[] = [];
|
|
465
725
|
const totalMem = os.totalmem();
|
|
466
726
|
|
|
467
727
|
for (const line of lines) {
|
|
468
|
-
const parts = line.split('
|
|
469
|
-
if (parts.length
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
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
|
+
}
|
|
491
751
|
}
|
|
492
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
|
+
});
|
|
493
770
|
}
|
|
494
771
|
|
|
495
|
-
|
|
496
|
-
procs.sort((a, b) => b.mem - a.mem);
|
|
772
|
+
procs.sort((a, b) => b.cpu - a.cpu || b.mem - a.mem);
|
|
497
773
|
if (limit && limit > 0) return procs.slice(0, limit);
|
|
498
774
|
return procs;
|
|
499
775
|
} catch {
|
|
@@ -515,5 +791,24 @@ export async function collectWindowsTasks(limit = 12): Promise<TaskData[]> {
|
|
|
515
791
|
}
|
|
516
792
|
|
|
517
793
|
export async function getWindowsDisplayInfo(): Promise<DisplayInfo[]> {
|
|
518
|
-
|
|
794
|
+
try {
|
|
795
|
+
const raw = await runPowerShell(
|
|
796
|
+
`Get-CimInstance Win32_VideoController | ForEach-Object { "$($_.Name)|$($_.VideoModeDescription)|$($_.VideoProcessor)" }`,
|
|
797
|
+
''
|
|
798
|
+
);
|
|
799
|
+
const displays: DisplayInfo[] = [];
|
|
800
|
+
for (const line of raw.split('\n')) {
|
|
801
|
+
const parts = line.trim().split('|');
|
|
802
|
+
if (parts[0]) {
|
|
803
|
+
displays.push({
|
|
804
|
+
name: parts[0],
|
|
805
|
+
resolution: parts[1] || 'Unknown',
|
|
806
|
+
isMain: displays.length === 0,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return displays.length > 0 ? displays : [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
|
|
811
|
+
} catch {
|
|
812
|
+
return [{ name: 'Default Display', resolution: '1920x1080', isMain: true }];
|
|
813
|
+
}
|
|
519
814
|
}
|