@cfour-hi/dsh-plugin-devicex 0.0.0-stage → 1.7.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/LICENSE +21 -0
- package/README.md +38 -2
- package/client.js +1008 -0
- package/cordis.patch.yml +3 -0
- package/host/actions-darwin.js +1261 -0
- package/host/actions-win32.js +1333 -0
- package/host/actions.js +45 -0
- package/host/benchmark.js +413 -0
- package/host/cleanup-darwin.js +715 -0
- package/host/cleanup-win32.js +760 -0
- package/host/cleanup.js +62 -0
- package/host/collector-darwin.js +2995 -0
- package/host/collector-win32.js +2060 -0
- package/host/collector.js +71 -0
- package/host/footprint-core.js +399 -0
- package/host/footprint-darwin.js +663 -0
- package/host/footprint-win32.js +411 -0
- package/host/footprint.js +284 -0
- package/host/proc.js +232 -0
- package/host/tool.js +254 -0
- package/host/transport.js +316 -0
- package/host/uninstall.js +244 -0
- package/host/win/hardware.ps1 +501 -0
- package/host/win/metrics.ps1 +535 -0
- package/host/win/network.ps1 +322 -0
- package/host/win/processes.ps1 +169 -0
- package/host/win/security.ps1 +197 -0
- package/host/win/software.ps1 +119 -0
- package/host/win/system.ps1 +279 -0
- package/host/win.js +187 -0
- package/icon.svg +18 -0
- package/index.js +67 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +60 -4
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
# Device X - Windows fast metrics snapshot.
|
|
2
|
+
#
|
|
3
|
+
# Emits exactly one JSON document on stdout (no other stdout text).
|
|
4
|
+
# Read-only: no admin rights, no network calls, no external modules.
|
|
5
|
+
# CPU usage percent is intentionally NOT computed here: the Node mapper derives
|
|
6
|
+
# it from successive os.cpus() snapshots (same algorithm as the macOS collector).
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$errors = @()
|
|
10
|
+
|
|
11
|
+
function To-EpochMs($value) {
|
|
12
|
+
if ($null -eq $value) { return $null }
|
|
13
|
+
try { return ([DateTimeOffset]$value).ToUnixTimeMilliseconds() } catch { return $null }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function To-LongOrNull($value) {
|
|
17
|
+
if ($null -eq $value) { return $null }
|
|
18
|
+
try { return [long]$value } catch { return $null }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function Invoke-NativeOem([scriptblock]$action) {
|
|
22
|
+
# Native console tools (netsh/tasklist/manage-bde/winget) write OEM/ANSI bytes
|
|
23
|
+
# on a non-English Windows. The bridge sets the console to UTF-8 for the JSON
|
|
24
|
+
# document; temporarily decode native output with the OEM code page so SSIDs,
|
|
25
|
+
# user names and device names are not mojibake, then restore in finally.
|
|
26
|
+
$originalEncoding = [Console]::OutputEncoding
|
|
27
|
+
$oemCodePage = 437
|
|
28
|
+
try {
|
|
29
|
+
$cultureCodePage = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.OEMCodePage
|
|
30
|
+
if ($cultureCodePage -gt 0) { $oemCodePage = $cultureCodePage }
|
|
31
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::GetEncoding($oemCodePage)
|
|
32
|
+
return & $action
|
|
33
|
+
} finally {
|
|
34
|
+
[Console]::OutputEncoding = $originalEncoding
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------- host / OS --
|
|
39
|
+
$hostName = $env:COMPUTERNAME
|
|
40
|
+
if (-not $hostName) {
|
|
41
|
+
try { $hostName = [System.Net.Dns]::GetHostName() } catch { $hostName = $null }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$osInfo = $null
|
|
45
|
+
$memoryInfo = $null
|
|
46
|
+
try {
|
|
47
|
+
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop
|
|
48
|
+
$bootMs = To-EpochMs $os.LastBootUpTime
|
|
49
|
+
$displayVersion = $null
|
|
50
|
+
try {
|
|
51
|
+
$winVersionKey = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction Stop
|
|
52
|
+
if ($winVersionKey.DisplayVersion) { $displayVersion = [string]$winVersionKey.DisplayVersion }
|
|
53
|
+
} catch { $displayVersion = $null }
|
|
54
|
+
$osInfo = [ordered]@{
|
|
55
|
+
caption = [string]$os.Caption
|
|
56
|
+
displayVersion = $displayVersion
|
|
57
|
+
version = [string]$os.Version
|
|
58
|
+
build = [string]$os.BuildNumber
|
|
59
|
+
lastBootUpTimeMs = $bootMs
|
|
60
|
+
osArchitecture = [string]$os.OSArchitecture
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$totalBytes = $null
|
|
64
|
+
$freeBytes = $null
|
|
65
|
+
if ($null -ne $os.TotalVisibleMemorySize) { $totalBytes = To-LongOrNull ([long]$os.TotalVisibleMemorySize * 1024) }
|
|
66
|
+
if ($null -ne $os.FreePhysicalMemory) { $freeBytes = To-LongOrNull ([long]$os.FreePhysicalMemory * 1024) }
|
|
67
|
+
|
|
68
|
+
$cacheBytes = $null
|
|
69
|
+
$availableBytes = $null
|
|
70
|
+
$committedBytes = $null
|
|
71
|
+
$commitLimitBytes = $null
|
|
72
|
+
$poolPagedBytes = $null
|
|
73
|
+
$poolNonPagedBytes = $null
|
|
74
|
+
try {
|
|
75
|
+
$memoryPerf = Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Memory -ErrorAction Stop
|
|
76
|
+
if ($memoryPerf) {
|
|
77
|
+
$cacheBytes = To-LongOrNull $memoryPerf.CacheBytes
|
|
78
|
+
$availableBytes = To-LongOrNull $memoryPerf.AvailableBytes
|
|
79
|
+
$committedBytes = To-LongOrNull $memoryPerf.CommittedBytes
|
|
80
|
+
$commitLimitBytes = To-LongOrNull $memoryPerf.CommitLimit
|
|
81
|
+
$poolPagedBytes = To-LongOrNull $memoryPerf.PoolPagedBytes
|
|
82
|
+
$poolNonPagedBytes = To-LongOrNull $memoryPerf.PoolNonPagedBytes
|
|
83
|
+
}
|
|
84
|
+
} catch { $errors += ('Win32_PerfFormattedData_PerfOS_Memory: ' + $_.Exception.Message) }
|
|
85
|
+
|
|
86
|
+
$pageTotalBytes = $null
|
|
87
|
+
$pageUsedBytes = $null
|
|
88
|
+
try {
|
|
89
|
+
$pageFiles = @(Get-CimInstance -ClassName Win32_PageFileUsage -ErrorAction Stop)
|
|
90
|
+
$allocatedMb = 0
|
|
91
|
+
$usedMb = 0
|
|
92
|
+
$pageFileSeen = $false
|
|
93
|
+
foreach ($pageFile in $pageFiles) {
|
|
94
|
+
if ($null -ne $pageFile.AllocatedBaseSize) { $allocatedMb += [double]$pageFile.AllocatedBaseSize; $pageFileSeen = $true }
|
|
95
|
+
if ($null -ne $pageFile.CurrentUsage) { $usedMb += [double]$pageFile.CurrentUsage }
|
|
96
|
+
}
|
|
97
|
+
if ($pageFileSeen) {
|
|
98
|
+
$pageTotalBytes = [long][math]::Round($allocatedMb * 1MB)
|
|
99
|
+
$pageUsedBytes = [long][math]::Round($usedMb * 1MB)
|
|
100
|
+
}
|
|
101
|
+
} catch { $errors += ('Win32_PageFileUsage: ' + $_.Exception.Message) }
|
|
102
|
+
|
|
103
|
+
$memoryInfo = [ordered]@{
|
|
104
|
+
totalBytes = $totalBytes
|
|
105
|
+
freeBytes = $freeBytes
|
|
106
|
+
cacheBytes = $cacheBytes
|
|
107
|
+
availableBytes = $availableBytes
|
|
108
|
+
committedBytes = $committedBytes
|
|
109
|
+
commitLimitBytes = $commitLimitBytes
|
|
110
|
+
poolPagedBytes = $poolPagedBytes
|
|
111
|
+
poolNonPagedBytes = $poolNonPagedBytes
|
|
112
|
+
pageFileTotalBytes = $pageTotalBytes
|
|
113
|
+
pageFileUsedBytes = $pageUsedBytes
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
$errors += ('Win32_OperatingSystem: ' + $_.Exception.Message)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# --------------------------------------------------------------------- CPU --
|
|
120
|
+
$cpuInfo = $null
|
|
121
|
+
try {
|
|
122
|
+
$processors = @(Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop)
|
|
123
|
+
if ($processors.Count -gt 0) {
|
|
124
|
+
$first = $processors[0]
|
|
125
|
+
$physicalCores = 0
|
|
126
|
+
$logicalCores = 0
|
|
127
|
+
foreach ($processor in $processors) {
|
|
128
|
+
if ($null -ne $processor.NumberOfCores) { $physicalCores += [int]$processor.NumberOfCores }
|
|
129
|
+
if ($null -ne $processor.NumberOfLogicalProcessors) { $logicalCores += [int]$processor.NumberOfLogicalProcessors }
|
|
130
|
+
}
|
|
131
|
+
$cpuInfo = [ordered]@{
|
|
132
|
+
name = [string]$first.Name
|
|
133
|
+
manufacturer = [string]$first.Manufacturer
|
|
134
|
+
physicalCores = $physicalCores
|
|
135
|
+
logicalCores = $logicalCores
|
|
136
|
+
maxClockSpeedMHz = To-LongOrNull $first.MaxClockSpeed
|
|
137
|
+
architecture = To-LongOrNull $first.Architecture
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch { $errors += ('Win32_Processor: ' + $_.Exception.Message) }
|
|
141
|
+
|
|
142
|
+
# -------------------------------------------------------------------- disk --
|
|
143
|
+
$diskInfo = $null
|
|
144
|
+
try {
|
|
145
|
+
$drives = @(Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction Stop)
|
|
146
|
+
$totalDisk = [long]0
|
|
147
|
+
$freeDisk = [long]0
|
|
148
|
+
$diskSeen = $false
|
|
149
|
+
foreach ($drive in $drives) {
|
|
150
|
+
if ($null -ne $drive.Size) { $totalDisk += [long]$drive.Size; $diskSeen = $true }
|
|
151
|
+
if ($null -ne $drive.FreeSpace) { $freeDisk += [long]$drive.FreeSpace }
|
|
152
|
+
}
|
|
153
|
+
if ($diskSeen) {
|
|
154
|
+
$diskInfo = [ordered]@{
|
|
155
|
+
totalBytes = $totalDisk
|
|
156
|
+
freeBytes = $freeDisk
|
|
157
|
+
usedBytes = [math]::Max([long]0, $totalDisk - $freeDisk)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch { $errors += ('Win32_LogicalDisk: ' + $_.Exception.Message) }
|
|
161
|
+
|
|
162
|
+
# Cumulative disk byte counters: the Node side turns these into per-second rates.
|
|
163
|
+
$diskIo = $null
|
|
164
|
+
try {
|
|
165
|
+
$diskPerf = @(Get-CimInstance -ClassName Win32_PerfRawData_PerfDisk_PhysicalDisk -ErrorAction Stop)
|
|
166
|
+
$readBytes = [long]0
|
|
167
|
+
$writeBytes = [long]0
|
|
168
|
+
$perfSeen = $false
|
|
169
|
+
$totalRow = $null
|
|
170
|
+
foreach ($row in $diskPerf) {
|
|
171
|
+
if ([string]$row.Name -eq '_Total') { $totalRow = $row; break }
|
|
172
|
+
}
|
|
173
|
+
if ($null -ne $totalRow) {
|
|
174
|
+
$readBytes = To-LongOrNull $totalRow.DiskReadBytesPersec
|
|
175
|
+
$writeBytes = To-LongOrNull $totalRow.DiskWriteBytesPersec
|
|
176
|
+
if ($null -ne $readBytes) { $perfSeen = $true }
|
|
177
|
+
} else {
|
|
178
|
+
foreach ($row in $diskPerf) {
|
|
179
|
+
$readValue = To-LongOrNull $row.DiskReadBytesPersec
|
|
180
|
+
$writeValue = To-LongOrNull $row.DiskWriteBytesPersec
|
|
181
|
+
if ($null -ne $readValue) { $readBytes += $readValue; $perfSeen = $true }
|
|
182
|
+
if ($null -ne $writeValue) { $writeBytes += $writeValue }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if ($perfSeen) {
|
|
186
|
+
$diskIo = [ordered]@{ read = $readBytes; write = $writeBytes }
|
|
187
|
+
}
|
|
188
|
+
} catch { $errors += ('Win32_PerfRawData_PerfDisk_PhysicalDisk: ' + $_.Exception.Message) }
|
|
189
|
+
|
|
190
|
+
# ----------------------------------------------------------------- network --
|
|
191
|
+
$networkInfo = $null
|
|
192
|
+
try {
|
|
193
|
+
$defaultInterface = $null
|
|
194
|
+
$defaultGateway = $null
|
|
195
|
+
try {
|
|
196
|
+
# The effective Windows route metric is RouteMetric + InterfaceMetric (a
|
|
197
|
+
# two-key Sort-Object compares lexicographically, not by the sum), so sort
|
|
198
|
+
# by the numeric sum to report the real egress adapter (network.setDns uses
|
|
199
|
+
# this alias).
|
|
200
|
+
$routes = @(Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction Stop | Sort-Object -Property @{Expression = { [long]$_.RouteMetric + [long]$_.InterfaceMetric }})
|
|
201
|
+
if ($routes.Count -gt 0) {
|
|
202
|
+
$defaultInterface = [string]$routes[0].InterfaceAlias
|
|
203
|
+
$defaultGateway = [string]$routes[0].NextHop
|
|
204
|
+
}
|
|
205
|
+
} catch { $errors += ('Get-NetRoute: ' + $_.Exception.Message) }
|
|
206
|
+
|
|
207
|
+
$primaryIpv4 = @()
|
|
208
|
+
if ($defaultInterface) {
|
|
209
|
+
try {
|
|
210
|
+
$addresses = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop)
|
|
211
|
+
foreach ($address in $addresses) {
|
|
212
|
+
if ([string]$address.InterfaceAlias -eq $defaultInterface -and [string]$address.IPAddress -ne '127.0.0.1') {
|
|
213
|
+
$primaryIpv4 += [string]$address.IPAddress
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch { $errors += ('Get-NetIPAddress: ' + $_.Exception.Message) }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
# Cumulative adapter byte counters (loopback excluded) for delta-based rates.
|
|
220
|
+
$counters = $null
|
|
221
|
+
try {
|
|
222
|
+
$statistics = @(Get-NetAdapterStatistics -ErrorAction Stop)
|
|
223
|
+
$receivedBytes = [long]0
|
|
224
|
+
$sentBytes = [long]0
|
|
225
|
+
$counterSeen = $false
|
|
226
|
+
foreach ($statistic in $statistics) {
|
|
227
|
+
$name = [string]$statistic.Name
|
|
228
|
+
if ($name -match 'Loopback') { continue }
|
|
229
|
+
$received = To-LongOrNull $statistic.ReceivedBytes
|
|
230
|
+
$sent = To-LongOrNull $statistic.SentBytes
|
|
231
|
+
if ($null -ne $received) { $receivedBytes += $received; $counterSeen = $true }
|
|
232
|
+
if ($null -ne $sent) { $sentBytes += $sent }
|
|
233
|
+
}
|
|
234
|
+
if ($counterSeen) {
|
|
235
|
+
$counters = [ordered]@{ read = $receivedBytes; write = $sentBytes }
|
|
236
|
+
}
|
|
237
|
+
} catch { $errors += ('Get-NetAdapterStatistics: ' + $_.Exception.Message) }
|
|
238
|
+
|
|
239
|
+
# Wi-Fi via netsh. Field labels are localized by Windows, so the parser keys
|
|
240
|
+
# off the untranslated tokens that netsh always prints ("SSID", "BSSID",
|
|
241
|
+
# "Mbps", "...%") and falls back to positional/value heuristics.
|
|
242
|
+
$wifi = $null
|
|
243
|
+
try {
|
|
244
|
+
$wlanLines = @(Invoke-NativeOem { & netsh wlan show interfaces 2>$null })
|
|
245
|
+
if ($wlanLines.Count -gt 0) {
|
|
246
|
+
$parsedWifi = [ordered]@{}
|
|
247
|
+
$macValues = @()
|
|
248
|
+
$values = @()
|
|
249
|
+
foreach ($line in $wlanLines) {
|
|
250
|
+
$text = [string]$line
|
|
251
|
+
if ($text -match '^\s*([^:]+?)\s*:\s*(.*?)\s*$') {
|
|
252
|
+
$label = $matches[1]
|
|
253
|
+
$value = $matches[2]
|
|
254
|
+
$values += [pscustomobject]@{ label = $label; value = $value }
|
|
255
|
+
if ($value -match '^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$') { $macValues += $value }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
$firstValue = $null
|
|
259
|
+
if ($values.Count -gt 0) { $firstValue = [string]$values[0].value }
|
|
260
|
+
if ($firstValue -and -not $parsedWifi.name) { $parsedWifi.name = $firstValue }
|
|
261
|
+
foreach ($entry in $values) {
|
|
262
|
+
$label = [string]$entry.label
|
|
263
|
+
$value = [string]$entry.value
|
|
264
|
+
if ($label -eq 'SSID') { $parsedWifi.ssid = $value; continue }
|
|
265
|
+
if ($label -eq 'BSSID' -or $label -eq 'AP BSSID') { $parsedWifi.bssid = $value; continue }
|
|
266
|
+
if ($label -eq 'Name') { $parsedWifi.name = $value; continue }
|
|
267
|
+
if ($label -eq 'State') { $parsedWifi.state = $value; continue }
|
|
268
|
+
if ($label -eq 'Authentication') { $parsedWifi.authentication = $value; continue }
|
|
269
|
+
if ($label -eq 'Radio type') { $parsedWifi.radioType = $value; continue }
|
|
270
|
+
if ($value -match '^(\d{1,3})%$') { $parsedWifi.signalPercent = [int]$matches[1]; continue }
|
|
271
|
+
if ($label -match 'Mbps') {
|
|
272
|
+
if (-not $parsedWifi.receiveRateMbps) { $parsedWifi.receiveRateMbps = [double]$value }
|
|
273
|
+
else { $parsedWifi.transmitRateMbps = [double]$value }
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if ($value -match '^\d{1,3}$') {
|
|
277
|
+
$number = [int]$value
|
|
278
|
+
if ($number -ge 1 -and $number -le 233 -and -not $parsedWifi.channel) { $parsedWifi.channel = $number }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (-not $parsedWifi.bssid -and $macValues.Count -gt 1) { $parsedWifi.bssid = [string]$macValues[$macValues.Count - 1] }
|
|
282
|
+
if ($parsedWifi.ssid -and $parsedWifi.ssid.Length -gt 0) {
|
|
283
|
+
if (-not $parsedWifi.state) { $parsedWifi.state = 'connected' }
|
|
284
|
+
$wifi = $parsedWifi
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch { $errors += ('netsh wlan show interfaces: ' + $_.Exception.Message) }
|
|
288
|
+
|
|
289
|
+
$networkInfo = [ordered]@{
|
|
290
|
+
defaultInterface = $defaultInterface
|
|
291
|
+
defaultGateway = $defaultGateway
|
|
292
|
+
primaryIpv4 = $primaryIpv4
|
|
293
|
+
counters = $counters
|
|
294
|
+
wifi = $wifi
|
|
295
|
+
}
|
|
296
|
+
} catch { $errors += ('network metrics: ' + $_.Exception.Message) }
|
|
297
|
+
|
|
298
|
+
# ----------------------------------------------------------------- battery --
|
|
299
|
+
$battery = $null
|
|
300
|
+
try {
|
|
301
|
+
$batteries = @(Get-CimInstance -ClassName Win32_Battery -ErrorAction Stop)
|
|
302
|
+
if ($batteries.Count -gt 0) {
|
|
303
|
+
$batteryDevice = $batteries[0]
|
|
304
|
+
$percent = $null
|
|
305
|
+
if ($null -ne $batteryDevice.EstimatedChargeRemaining) { $percent = [int]$batteryDevice.EstimatedChargeRemaining }
|
|
306
|
+
$status = $null
|
|
307
|
+
if ($null -ne $batteryDevice.BatteryStatus) { $status = [int]$batteryDevice.BatteryStatus }
|
|
308
|
+
$powerOnline = $null
|
|
309
|
+
if ($null -ne $batteryDevice.PowerOnline) { $powerOnline = [bool]$batteryDevice.PowerOnline }
|
|
310
|
+
$charging = $false
|
|
311
|
+
if ($status -eq 6 -or $status -eq 7 -or $status -eq 8 -or $status -eq 9) { $charging = $true }
|
|
312
|
+
$plugged = $false
|
|
313
|
+
if ($powerOnline -eq $true) { $plugged = $true }
|
|
314
|
+
if ($status -eq 3 -or $charging) { $plugged = $true }
|
|
315
|
+
$powerSource = 'Battery Power'
|
|
316
|
+
if ($plugged) { $powerSource = 'AC Power' }
|
|
317
|
+
$remaining = $null
|
|
318
|
+
if (-not $plugged -and $null -ne $batteryDevice.EstimatedRunTime) {
|
|
319
|
+
$runTime = [int]$batteryDevice.EstimatedRunTime
|
|
320
|
+
if ($runTime -gt 0 -and $runTime -lt 6000) { $remaining = $runTime }
|
|
321
|
+
}
|
|
322
|
+
$temperatureC = $null
|
|
323
|
+
try {
|
|
324
|
+
$wmib = Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryStatus -ErrorAction Stop
|
|
325
|
+
if ($wmib -and $null -ne $wmib.Temperature) {
|
|
326
|
+
$temperatureC = [math]::Round(([double]$wmib.Temperature / 10.0) - 273.15, 1)
|
|
327
|
+
}
|
|
328
|
+
} catch { $temperatureC = $null }
|
|
329
|
+
$designCapacityMah = $null
|
|
330
|
+
$maxCapacityMah = $null
|
|
331
|
+
$healthPercent = $null
|
|
332
|
+
$designMwh = To-LongOrNull $batteryDevice.DesignCapacity
|
|
333
|
+
$fullMwh = To-LongOrNull $batteryDevice.FullChargeCapacity
|
|
334
|
+
$voltageMv = To-LongOrNull $batteryDevice.DesignVoltage
|
|
335
|
+
# Win32_Battery usually exposes both capacities; fall back to the root/wmi
|
|
336
|
+
# battery classes when a driver only publishes them there (both in mWh).
|
|
337
|
+
if ($null -eq $designMwh -or $null -eq $fullMwh) {
|
|
338
|
+
try {
|
|
339
|
+
$staticBattery = @(Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryStaticData -ErrorAction Stop)
|
|
340
|
+
if ($staticBattery.Count -gt 0) {
|
|
341
|
+
if ($null -eq $designMwh -and $null -ne $staticBattery[0].DesignedCapacity) {
|
|
342
|
+
$designMwh = [double]$staticBattery[0].DesignedCapacity
|
|
343
|
+
}
|
|
344
|
+
if ($null -eq $voltageMv -and $null -ne $staticBattery[0].DesignedVoltage) {
|
|
345
|
+
$voltageMv = [double]$staticBattery[0].DesignedVoltage
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
$fullBattery = @(Get-CimInstance -Namespace 'root/wmi' -ClassName BatteryFullChargedCapacity -ErrorAction Stop)
|
|
349
|
+
if ($fullBattery.Count -gt 0 -and $null -eq $fullMwh -and $null -ne $fullBattery[0].BatteryFullChargedCapacity) {
|
|
350
|
+
$fullMwh = [double]$fullBattery[0].BatteryFullChargedCapacity
|
|
351
|
+
}
|
|
352
|
+
} catch { }
|
|
353
|
+
}
|
|
354
|
+
if ($null -ne $designMwh -and $null -ne $voltageMv -and $voltageMv -gt 0) {
|
|
355
|
+
$designCapacityMah = [int][math]::Round(([double]$designMwh * 1000.0) / [double]$voltageMv)
|
|
356
|
+
}
|
|
357
|
+
if ($null -ne $fullMwh -and $null -ne $voltageMv -and $voltageMv -gt 0) {
|
|
358
|
+
$maxCapacityMah = [int][math]::Round(([double]$fullMwh * 1000.0) / [double]$voltageMv)
|
|
359
|
+
}
|
|
360
|
+
if ($null -ne $designMwh -and $null -ne $fullMwh -and $designMwh -gt 0) {
|
|
361
|
+
$healthPercent = [int][math]::Round(([double]$fullMwh / [double]$designMwh) * 100.0)
|
|
362
|
+
}
|
|
363
|
+
$battery = [ordered]@{
|
|
364
|
+
present = $true
|
|
365
|
+
percent = $percent
|
|
366
|
+
charging = $charging
|
|
367
|
+
plugged = $plugged
|
|
368
|
+
powerSource = $powerSource
|
|
369
|
+
timeRemainingMin = $remaining
|
|
370
|
+
temperatureC = $temperatureC
|
|
371
|
+
cycleCount = $null
|
|
372
|
+
healthPercent = $healthPercent
|
|
373
|
+
designCapacityMah = $designCapacityMah
|
|
374
|
+
maxCapacityMah = $maxCapacityMah
|
|
375
|
+
status = $status
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch { $errors += ('Win32_Battery: ' + $_.Exception.Message) }
|
|
379
|
+
|
|
380
|
+
# ----------------------------------------------------------------- thermal --
|
|
381
|
+
# Windows exposes ACPI thermal zones through root/wmi MSAcpi_ThermalZoneTemperature
|
|
382
|
+
# (tenths of Kelvin). Many desktops/VMs simply do not publish any zone, which is
|
|
383
|
+
# reported as a null cpuCelsius plus the note 'wmi-unsupported' - never guessed.
|
|
384
|
+
# nvidia-smi is optional: absent/again-failing tooling just leaves gpuCelsius
|
|
385
|
+
# null. The Node mapper mirrors batteryCelsius from the battery block above.
|
|
386
|
+
$thermal = $null
|
|
387
|
+
try {
|
|
388
|
+
$thermalSources = @()
|
|
389
|
+
$thermalZones = @()
|
|
390
|
+
$cpuCelsius = $null
|
|
391
|
+
try {
|
|
392
|
+
$rawZones = @(Get-CimInstance -Namespace 'root/wmi' -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction Stop)
|
|
393
|
+
foreach ($zone in $rawZones) {
|
|
394
|
+
if ($null -eq $zone.CurrentTemperature) { continue }
|
|
395
|
+
$zoneCelsius = [math]::Round(([double]$zone.CurrentTemperature / 10.0) - 273.15, 1)
|
|
396
|
+
if ($zoneCelsius -lt 0 -or $zoneCelsius -gt 120) { continue }
|
|
397
|
+
$thermalZones += [ordered]@{
|
|
398
|
+
name = [string]$zone.InstanceName
|
|
399
|
+
celsius = $zoneCelsius
|
|
400
|
+
}
|
|
401
|
+
if ($null -eq $cpuCelsius -or $zoneCelsius -gt $cpuCelsius) { $cpuCelsius = $zoneCelsius }
|
|
402
|
+
}
|
|
403
|
+
if ($null -ne $cpuCelsius) { $thermalSources += 'wmi-thermal-zone' }
|
|
404
|
+
} catch { $errors += ('MSAcpi_ThermalZoneTemperature: ' + $_.Exception.Message) }
|
|
405
|
+
|
|
406
|
+
$gpuCelsius = $null
|
|
407
|
+
$thermalFans = @()
|
|
408
|
+
try {
|
|
409
|
+
$nvidiaSmi = Get-Command -Name 'nvidia-smi' -ErrorAction SilentlyContinue
|
|
410
|
+
if ($null -ne $nvidiaSmi -and $nvidiaSmi.Source) {
|
|
411
|
+
$nvidiaSmiPath = [string]$nvidiaSmi.Source
|
|
412
|
+
$gpuRows = @(& $nvidiaSmiPath '--query-gpu=temperature.gpu,fan.speed' '--format=csv,noheader,nounits' 2>$null)
|
|
413
|
+
if ($gpuRows.Count -gt 0) {
|
|
414
|
+
$gpuFields = [string]$gpuRows[0] -split ','
|
|
415
|
+
$gpuTemp = $null
|
|
416
|
+
if ($gpuFields.Count -ge 1) {
|
|
417
|
+
$parsedTemp = 0.0
|
|
418
|
+
if ([double]::TryParse($gpuFields[0].Trim(), [ref]$parsedTemp)) {
|
|
419
|
+
if ($parsedTemp -ge -20 -and $parsedTemp -le 150) { $gpuTemp = [math]::Round($parsedTemp, 1) }
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
$fanPercent = $null
|
|
423
|
+
if ($gpuFields.Count -ge 2) {
|
|
424
|
+
$parsedFan = 0.0
|
|
425
|
+
if ([double]::TryParse($gpuFields[1].Trim(), [ref]$parsedFan)) { $fanPercent = [int][math]::Round($parsedFan) }
|
|
426
|
+
}
|
|
427
|
+
if ($null -ne $gpuTemp) {
|
|
428
|
+
$gpuCelsius = $gpuTemp
|
|
429
|
+
# nvidia-smi reports fan speed as a percentage; RPM is not available,
|
|
430
|
+
# so the mapper keeps name/rpm and drops the percent detail.
|
|
431
|
+
$thermalFans += [ordered]@{ name = 'GPU'; rpm = $null; percent = $fanPercent }
|
|
432
|
+
$thermalSources += 'nvidia-smi'
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch { $errors += ('nvidia-smi: ' + $_.Exception.Message) }
|
|
437
|
+
|
|
438
|
+
$thermalNote = $null
|
|
439
|
+
if ($null -eq $cpuCelsius) { $thermalNote = 'wmi-unsupported' }
|
|
440
|
+
$thermal = [ordered]@{
|
|
441
|
+
zones = $thermalZones
|
|
442
|
+
cpuCelsius = $cpuCelsius
|
|
443
|
+
gpuCelsius = $gpuCelsius
|
|
444
|
+
fans = $thermalFans
|
|
445
|
+
sources = $thermalSources
|
|
446
|
+
note = $thermalNote
|
|
447
|
+
}
|
|
448
|
+
} catch { $errors += ('thermal metrics: ' + $_.Exception.Message) }
|
|
449
|
+
|
|
450
|
+
# --------------------------------------------------------------- processes --
|
|
451
|
+
$processInfo = $null
|
|
452
|
+
try {
|
|
453
|
+
$processNames = @{}
|
|
454
|
+
$processCount = 0
|
|
455
|
+
try {
|
|
456
|
+
$running = @(Get-Process -ErrorAction SilentlyContinue)
|
|
457
|
+
$processCount = $running.Count
|
|
458
|
+
foreach ($runningProcess in $running) {
|
|
459
|
+
try { $processNames[[int]$runningProcess.Id] = [string]$runningProcess.ProcessName } catch { }
|
|
460
|
+
}
|
|
461
|
+
} catch { $processCount = 0 }
|
|
462
|
+
|
|
463
|
+
$candidates = @()
|
|
464
|
+
try {
|
|
465
|
+
$processPerf = @(Get-CimInstance -ClassName Win32_PerfFormattedData_PerfProc_Process -ErrorAction Stop)
|
|
466
|
+
foreach ($row in $processPerf) {
|
|
467
|
+
$rawName = [string]$row.Name
|
|
468
|
+
if (-not $rawName) { continue }
|
|
469
|
+
if ($rawName -eq '_Total' -or $rawName -eq 'Idle') { continue }
|
|
470
|
+
$processId = 0
|
|
471
|
+
if ($null -ne $row.IDProcess) { $processId = [int]$row.IDProcess }
|
|
472
|
+
if ($processId -le 0) { continue }
|
|
473
|
+
$cpuPercent = 0.0
|
|
474
|
+
if ($null -ne $row.PercentProcessorTime) { $cpuPercent = [double]$row.PercentProcessorTime }
|
|
475
|
+
$workingSet = $null
|
|
476
|
+
if ($null -ne $row.WorkingSetPrivate) { $workingSet = To-LongOrNull $row.WorkingSetPrivate }
|
|
477
|
+
if ($null -eq $workingSet -and $null -ne $row.WorkingSet) { $workingSet = To-LongOrNull $row.WorkingSet }
|
|
478
|
+
$name = $rawName -replace '#\d+$', ''
|
|
479
|
+
if ($processNames.ContainsKey($processId)) { $name = $processNames[$processId] }
|
|
480
|
+
$candidates += [pscustomobject]@{
|
|
481
|
+
pid = $processId
|
|
482
|
+
name = $name
|
|
483
|
+
cpuPercent = $cpuPercent
|
|
484
|
+
workingSetBytes = $workingSet
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
} catch { $errors += ('Win32_PerfFormattedData_PerfProc_Process: ' + $_.Exception.Message) }
|
|
488
|
+
|
|
489
|
+
# The mapper only consumes metrics.processes.top[0..4]; asking for five rows
|
|
490
|
+
# avoids five unnecessary Win32_Process GetOwner round-trips per poll.
|
|
491
|
+
$top = @($candidates | Sort-Object -Property cpuPercent -Descending | Select-Object -First 5)
|
|
492
|
+
$topList = @()
|
|
493
|
+
foreach ($entry in $top) {
|
|
494
|
+
$userName = $null
|
|
495
|
+
try {
|
|
496
|
+
$ownerProcess = Get-CimInstance -ClassName Win32_Process -Filter ('ProcessId = ' + [string]$entry.pid) -ErrorAction Stop
|
|
497
|
+
if ($ownerProcess) {
|
|
498
|
+
$owner = Invoke-CimMethod -InputObject $ownerProcess -MethodName GetOwner -ErrorAction Stop
|
|
499
|
+
if ($owner -and $owner.User) {
|
|
500
|
+
if ($owner.Domain) { $userName = [string]$owner.Domain + '\' + [string]$owner.User }
|
|
501
|
+
else { $userName = [string]$owner.User }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch { $userName = $null }
|
|
505
|
+
$topList += [ordered]@{
|
|
506
|
+
pid = $entry.pid
|
|
507
|
+
name = $entry.name
|
|
508
|
+
cpuPercent = $entry.cpuPercent
|
|
509
|
+
workingSetBytes = $entry.workingSetBytes
|
|
510
|
+
user = $userName
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
$processInfo = [ordered]@{
|
|
514
|
+
total = $processCount
|
|
515
|
+
top = $topList
|
|
516
|
+
}
|
|
517
|
+
} catch { $errors += ('process metrics: ' + $_.Exception.Message) }
|
|
518
|
+
|
|
519
|
+
# -------------------------------------------------------------------- emit --
|
|
520
|
+
$result = [ordered]@{
|
|
521
|
+
generatedAt = ([DateTimeOffset]::UtcNow).ToUnixTimeMilliseconds()
|
|
522
|
+
hostname = $hostName
|
|
523
|
+
os = $osInfo
|
|
524
|
+
cpu = $cpuInfo
|
|
525
|
+
memory = $memoryInfo
|
|
526
|
+
disk = $diskInfo
|
|
527
|
+
diskIo = $diskIo
|
|
528
|
+
network = $networkInfo
|
|
529
|
+
battery = $battery
|
|
530
|
+
thermal = $thermal
|
|
531
|
+
processes = $processInfo
|
|
532
|
+
errors = $errors
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
$result | ConvertTo-Json -Depth 8 -Compress
|