@daimazun/hardware-info 1.0.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.
Files changed (47) hide show
  1. package/README.md +907 -0
  2. package/bin/OpenHardwareMonitorLib.dll +0 -0
  3. package/index.js +192 -0
  4. package/lib/allhardware.js +35 -0
  5. package/lib/backend-usage.js +103 -0
  6. package/lib/battery.js +76 -0
  7. package/lib/disk-watcher.js +156 -0
  8. package/lib/disk.js +77 -0
  9. package/lib/gpu.js +70 -0
  10. package/lib/hardware-service.js +284 -0
  11. package/lib/memory.js +59 -0
  12. package/lib/monitor.js +176 -0
  13. package/lib/network.js +75 -0
  14. package/lib/ohm-daemon.js +208 -0
  15. package/lib/ohm.js +41 -0
  16. package/lib/perfctr.js +43 -0
  17. package/lib/process-icon.js +76 -0
  18. package/lib/process-ops.js +218 -0
  19. package/lib/processes.js +232 -0
  20. package/lib/ps.js +93 -0
  21. package/lib/public-ip.js +124 -0
  22. package/lib/services.js +43 -0
  23. package/lib/sysinfo-daemon.js +188 -0
  24. package/lib/system.js +77 -0
  25. package/lib/usb.js +49 -0
  26. package/lib/win32-procs.js +241 -0
  27. package/package.json +50 -0
  28. package/scripts/get-all-hardware.ps1 +98 -0
  29. package/scripts/get-lhm-temp.ps1 +119 -0
  30. package/scripts/ohm-daemon.ps1 +141 -0
  31. package/scripts/sysinfo-daemon.ps1 +386 -0
  32. package/test/check-bugs.js +182 -0
  33. package/test/public/gj.pay.ali.jpg +0 -0
  34. package/test/public/gj.pay.wx.jpg +0 -0
  35. package/test/public/index.html +996 -0
  36. package/test/public/zdl.pay.ali.jpg +0 -0
  37. package/test/public/zdl.pay.wx.jpg +0 -0
  38. package/test/scan-encoding.js +77 -0
  39. package/test/server.js +255 -0
  40. package/test/test-all.js +180 -0
  41. package/test/test-daemon.js +51 -0
  42. package/test/test-kill-name.js +30 -0
  43. package/test/test-monitor.js +78 -0
  44. package/test/test-new-features.js +93 -0
  45. package/test/test-service.js +91 -0
  46. package/test/test-sysinfo-daemon.js +95 -0
  47. package/test/test.js +63 -0
Binary file
package/index.js ADDED
@@ -0,0 +1,192 @@
1
+ const { readOhmTemp } = require('./lib/ohm');
2
+ const { readPerfCounterTemp } = require('./lib/perfctr');
3
+ const { readAllHardware } = require('./lib/allhardware');
4
+ const { createCpuTempDaemon, CpuTempDaemon } = require('./lib/ohm-daemon');
5
+ const { createCpuTempMonitor, CpuTempMonitor } = require('./lib/monitor');
6
+ const { createSysInfoDaemon, SysInfoDaemon } = require('./lib/sysinfo-daemon');
7
+ const { getSystemInfo } = require('./lib/system');
8
+ const { getMemoryInfo } = require('./lib/memory');
9
+ const { getDiskInfo } = require('./lib/disk');
10
+ const { getNetworkInfo } = require('./lib/network');
11
+ const { getBatteryInfo } = require('./lib/battery');
12
+ const { getGpuInfo } = require('./lib/gpu');
13
+ const { getServicesInfo } = require('./lib/services');
14
+ const { getUsbInfo } = require('./lib/usb');
15
+ const { getProcesses } = require('./lib/processes');
16
+ const { killProcess, showItemInFolder, findPidsByPort, findPidsByName } = require('./lib/process-ops');
17
+ const { createDiskWatcher, listDriveLetters } = require('./lib/disk-watcher');
18
+ const { getProcessIcon, getProcessIcons } = require('./lib/process-icon');
19
+ const { createHardwareService, HardwareService } = require('./lib/hardware-service');
20
+ const { getPublicIp } = require('./lib/public-ip');
21
+ const { getBackendUsage } = require('./lib/backend-usage');
22
+
23
+ /**
24
+ * 获取 CPU 温度及完整 CPU 信息(多层降级)
25
+ * 管理员 → OpenHardwareService(真实核心温度 + 负载 + 频率 + 功耗)
26
+ * 非管理员 → 性能计数器(ACPI 热区温度)
27
+ */
28
+ async function getCpuTemp(options = {}) {
29
+ const verbose = options.verbose || false;
30
+ const errors = [];
31
+
32
+ // 第一层:OpenHardwareService(真实核心温度,需管理员)
33
+ try {
34
+ const data = await readOhmTemp();
35
+ const result = {
36
+ source: 'openHardwareService',
37
+ isCoreTemp: true,
38
+ cpuName: data.cpuName,
39
+ temperature: data.temperature,
40
+ load: data.load || [],
41
+ clock: data.clock || [],
42
+ power: data.power || [],
43
+ note: 'Real CPU core temperature with load/clock/power. Requires admin privileges.'
44
+ };
45
+ if (verbose) result.errors = errors;
46
+ return result;
47
+ } catch (e) {
48
+ errors.push(`openHardwareService: ${e.message}`);
49
+ if (verbose) console.error(`[layer openHardwareService] failed: ${e.message}`);
50
+ }
51
+
52
+ // 第二层:性能计数器(热区温度,免管理员)
53
+ try {
54
+ const data = await readPerfCounterTemp();
55
+ const result = {
56
+ source: 'performance_counter',
57
+ isCoreTemp: false,
58
+ cpuName: null,
59
+ temperature: {
60
+ cores: [],
61
+ package: data.maxZone,
62
+ maxCore: data.maxZone,
63
+ zones: data.zones
64
+ },
65
+ load: [],
66
+ clock: [],
67
+ power: [],
68
+ note: 'ACPI thermal zone temperature (near CPU socket), NOT core temperature. No admin required.'
69
+ };
70
+ if (verbose) result.errors = errors;
71
+ return result;
72
+ } catch (e) {
73
+ errors.push(`performance_counter: ${e.message}`);
74
+ if (verbose) console.error(`[layer performance_counter] failed: ${e.message}`);
75
+ }
76
+
77
+ const err = new Error(`All layers failed:\n${errors.join('\n')}`);
78
+ err.errors = errors;
79
+ throw err;
80
+ }
81
+
82
+ /**
83
+ * 获取所有硬件的完整传感器信息(需管理员)
84
+ */
85
+ async function getAllHardwareInfo(options = {}) {
86
+ const data = await readAllHardware();
87
+ return {
88
+ source: 'openHardwareService',
89
+ timestamp: data.timestamp,
90
+ hardware: data.hardware,
91
+ note: 'Full hardware sensor data. Requires admin privileges.'
92
+ };
93
+ }
94
+
95
+ /**
96
+ * 获取全部系统信息(一次性汇总)
97
+ * 并行获取所有模块,单个模块失败不影响其他
98
+ *
99
+ * @param {Object} options
100
+ * @param {boolean} options.includeProcesses - 是否包含进程列表,默认 false
101
+ * @param {boolean} options.includeServices - 是否包含服务列表,默认 false
102
+ * @param {boolean} options.includeOhmHardware - 是否包含 OHM 全硬件传感器(需管理员),默认 false
103
+ * @param {number} options.processTop - 进程前 N 个,默认 20
104
+ * @returns {Promise<Object>}
105
+ */
106
+ async function getAllInfo(options = {}) {
107
+ const includeProcesses = options.includeProcesses || false;
108
+ const includeServices = options.includeServices || false;
109
+ const includeOhmHardware = options.includeOhmHardware || false;
110
+ const processTop = options.processTop || 20;
111
+
112
+ const tasks = {
113
+ system: getSystemInfo(),
114
+ cpu: getCpuTemp(),
115
+ memory: getMemoryInfo(),
116
+ disk: getDiskInfo(),
117
+ network: getNetworkInfo(),
118
+ battery: getBatteryInfo(),
119
+ gpu: getGpuInfo(),
120
+ usb: getUsbInfo()
121
+ };
122
+
123
+ if (includeProcesses) {
124
+ tasks.processes = getProcesses({ top: processTop, sortBy: 'memory' });
125
+ }
126
+ if (includeServices) {
127
+ tasks.services = getServicesInfo();
128
+ }
129
+ if (includeOhmHardware) {
130
+ tasks.ohmHardware = getAllHardwareInfo();
131
+ }
132
+
133
+ const entries = Object.entries(tasks);
134
+ const results = await Promise.allSettled(entries.map(([, p]) => p));
135
+
136
+ const info = {};
137
+ const errors = {};
138
+ entries.forEach(([key], i) => {
139
+ if (results[i].status === 'fulfilled') {
140
+ info[key] = results[i].value;
141
+ } else {
142
+ info[key] = null;
143
+ errors[key] = results[i].reason?.message || 'Unknown error';
144
+ }
145
+ });
146
+
147
+ return {
148
+ timestamp: new Date().toISOString(),
149
+ ...info,
150
+ errors: Object.keys(errors).length > 0 ? errors : null
151
+ };
152
+ }
153
+
154
+ module.exports = {
155
+ // CPU 温度(原有)
156
+ getCpuTemp,
157
+ getAllHardwareInfo,
158
+ createCpuTempDaemon,
159
+ CpuTempDaemon,
160
+ createCpuTempMonitor,
161
+ CpuTempMonitor,
162
+ // 系统信息(新增)
163
+ getSystemInfo,
164
+ getMemoryInfo,
165
+ getDiskInfo,
166
+ getNetworkInfo,
167
+ getBatteryInfo,
168
+ getGpuInfo,
169
+ getServicesInfo,
170
+ getUsbInfo,
171
+ getProcesses,
172
+ getAllInfo,
173
+ getPublicIp,
174
+ createSysInfoDaemon,
175
+ SysInfoDaemon,
176
+ // 进程操作
177
+ killProcess,
178
+ showItemInFolder,
179
+ findPidsByPort,
180
+ findPidsByName,
181
+ // 磁盘插拔监听
182
+ createDiskWatcher,
183
+ listDriveLetters,
184
+ // 进程图标
185
+ getProcessIcon,
186
+ getProcessIcons,
187
+ // 统一常驻服务
188
+ createHardwareService,
189
+ HardwareService,
190
+ // 后端资源占用
191
+ getBackendUsage
192
+ };
@@ -0,0 +1,35 @@
1
+ const { spawn } = require('child_process');
2
+ const path = require('path');
3
+
4
+ const SCRIPT = path.join(__dirname, '..', 'scripts', 'get-all-hardware.ps1');
5
+
6
+ function readAllHardware() {
7
+ return new Promise((resolve, reject) => {
8
+ const ps = spawn('powershell', [
9
+ '-NoProfile',
10
+ '-ExecutionPolicy', 'Bypass',
11
+ '-File', SCRIPT
12
+ ]);
13
+
14
+ let out = '';
15
+ let err = '';
16
+ ps.stdout.on('data', d => (out += d));
17
+ ps.stderr.on('data', d => (err += d));
18
+ ps.on('close', code => {
19
+ if (code !== 0) {
20
+ return reject(new Error(`AllHardware exited ${code}: ${err || out}`));
21
+ }
22
+ try {
23
+ const data = JSON.parse(out.trim());
24
+ if (!data.hardware || data.hardware.length === 0) {
25
+ return reject(new Error('AllHardware: no hardware found'));
26
+ }
27
+ resolve(data);
28
+ } catch (e) {
29
+ reject(new Error(`AllHardware parse error: ${e.message}`));
30
+ }
31
+ });
32
+ });
33
+ }
34
+
35
+ module.exports = { readAllHardware };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * backend-usage.js — 获取当前后端服务(Node.js + 所有子进程)的内存和 CPU 占用
3
+ *
4
+ * 性能:同步调用,~5-10ms(只枚举当前进程树,不遍历全部进程)
5
+ * 高频友好:进程内 koffi 调用,无子进程开销
6
+ *
7
+ * 返回结构:
8
+ * {
9
+ * timestamp, node: { pid, memory: {...}, cpuPercent, threads },
10
+ * children: [{ pid, name, memoryMB, cpuPercent, threads, exePath }],
11
+ * total: { processCount, memoryMB, cpuPercent, threads }
12
+ * }
13
+ */
14
+
15
+ const { sampleProcesses, computeCpuPercent } = require('./win32-procs');
16
+
17
+ /**
18
+ * 递归查找进程树中的所有后代 PID
19
+ * @param {number} rootPid - 根进程 PID
20
+ * @param {Array} allProcs - sampleProcesses() 的全量结果
21
+ * @returns {Set<number>} 所有后代 PID(不含根进程)
22
+ */
23
+ function findDescendantPids(rootPid, allProcs) {
24
+ const descendants = new Set();
25
+ const queue = [rootPid];
26
+ while (queue.length) {
27
+ const current = queue.shift();
28
+ for (const p of allProcs) {
29
+ if (p.parentPid === current && p.pid !== current && !descendants.has(p.pid)) {
30
+ descendants.add(p.pid);
31
+ queue.push(p.pid);
32
+ }
33
+ }
34
+ }
35
+ return descendants;
36
+ }
37
+
38
+ /**
39
+ * 获取后端服务资源占用(同步,高频安全)
40
+ * @returns {Object} 占用信息
41
+ */
42
+ function getBackendUsage() {
43
+ const nodePid = process.pid;
44
+ const allProcs = sampleProcesses();
45
+ const cpuMap = computeCpuPercent(allProcs);
46
+
47
+ // 找到 Node.js 进程本身
48
+ const nodeProc = allProcs.find((p) => p.pid === nodePid);
49
+
50
+ // 递归找到所有子进程
51
+ const childPids = findDescendantPids(nodePid, allProcs);
52
+ const childProcs = allProcs.filter((p) => childPids.has(p.pid));
53
+
54
+ // Node.js 详细内存(process.memoryUsage)
55
+ const nodeMem = process.memoryUsage();
56
+
57
+ const nodeMemoryMB = nodeProc ? nodeProc.workingSet / 1024 / 1024 : nodeMem.rss / 1024 / 1024;
58
+ const nodeCpuPercent = cpuMap.get(nodePid) || 0;
59
+
60
+ // 子进程列表
61
+ const children = childProcs.map((p) => ({
62
+ pid: p.pid,
63
+ name: p.name,
64
+ memoryMB: Math.round((p.workingSet / 1024 / 1024) * 10) / 10,
65
+ privateMB: Math.round((p.privateUsage / 1024 / 1024) * 10) / 10,
66
+ cpuPercent: Math.round((cpuMap.get(p.pid) || 0) * 10) / 10,
67
+ threads: p.threads,
68
+ exePath: p.exePath || ''
69
+ }));
70
+
71
+ // 汇总
72
+ const totalMemoryMB = Math.round((nodeMemoryMB + children.reduce((s, c) => s + c.memoryMB, 0)) * 10) / 10;
73
+ const totalCpuPercent = Math.round((nodeCpuPercent + children.reduce((s, c) => s + c.cpuPercent, 0)) * 10) / 10;
74
+ const totalThreads = (nodeProc?.threads || 0) + children.reduce((s, c) => s + c.threads, 0);
75
+
76
+ return {
77
+ timestamp: Date.now(),
78
+ node: {
79
+ pid: nodePid,
80
+ name: 'node',
81
+ memory: {
82
+ rss: Math.round(nodeMem.rss),
83
+ rssMB: Math.round((nodeMem.rss / 1024 / 1024) * 10) / 10,
84
+ heapTotalMB: Math.round((nodeMem.heapTotal / 1024 / 1024) * 10) / 10,
85
+ heapUsedMB: Math.round((nodeMem.heapUsed / 1024 / 1024) * 10) / 10,
86
+ externalMB: Math.round((nodeMem.external / 1024 / 1024) * 10) / 10,
87
+ workingSetMB: Math.round(nodeMemoryMB * 10) / 10,
88
+ privateMB: nodeProc ? Math.round((nodeProc.privateUsage / 1024 / 1024) * 10) / 10 : 0
89
+ },
90
+ cpuPercent: Math.round(nodeCpuPercent * 10) / 10,
91
+ threads: nodeProc?.threads || 0
92
+ },
93
+ children,
94
+ total: {
95
+ processCount: 1 + children.length,
96
+ memoryMB: totalMemoryMB,
97
+ cpuPercent: totalCpuPercent,
98
+ threads: totalThreads
99
+ }
100
+ };
101
+ }
102
+
103
+ module.exports = { getBackendUsage };
package/lib/battery.js ADDED
@@ -0,0 +1,76 @@
1
+ const { runPsJson } = require('./ps');
2
+
3
+ /**
4
+ * 获取电池信息:健康度、充电状态、容量、电源计划
5
+ */
6
+ async function getBatteryInfo() {
7
+ const cmd = `
8
+ $batteries = Get-CimInstance Win32_Battery
9
+ $portable = Get-CimInstance Win32_PortableBattery
10
+ $powerPlan = powercfg /getactivescheme | ForEach-Object { ($_ -replace '.*\\(|\\).*', '') }
11
+
12
+ if (-not $batteries -or $batteries.Count -eq 0) {
13
+ [PSCustomObject]@{
14
+ hasBattery = $false
15
+ batteries = @()
16
+ powerPlan = $powerPlan
17
+ } | ConvertTo-Json -Depth 5 -Compress
18
+ exit
19
+ }
20
+
21
+ $batteryList = @()
22
+ foreach ($b in $batteries) {
23
+ $p = $portable | Where-Object { $_.DeviceID -eq $b.DeviceID } | Select-Object -First 1
24
+
25
+ $designCapacity = if ($p -and $p.DesignCapacity) { $p.DesignCapacity } else { $null }
26
+ $fullChargeCapacity = if ($p -and $p.FullChargeCapacity) { $p.FullChargeCapacity } else { $null }
27
+ $healthPercent = if ($designCapacity -and $fullChargeCapacity -and $designCapacity -gt 0) {
28
+ [math]::Round(($fullChargeCapacity / $designCapacity) * 100, 2)
29
+ } else { $null }
30
+
31
+ $batteryList += [PSCustomObject]@{
32
+ name = $b.Name
33
+ deviceId = $b.DeviceID
34
+ status = $b.Status
35
+ availability = $b.Availability
36
+ batteryStatusValue = $b.BatteryStatus
37
+ batteryStatus = switch ($b.BatteryStatus) {
38
+ 1 { 'Discharging' }
39
+ 2 { 'AC Power (not charging)' }
40
+ 3 { 'Fully Charged' }
41
+ 4 { 'Low' }
42
+ 5 { 'Critical' }
43
+ 6 { 'Charging' }
44
+ 7 { 'Charging and High' }
45
+ 8 { 'Charging and Low' }
46
+ 9 { 'Charging and Critical' }
47
+ 10 { 'Undefined' }
48
+ 11 { 'Partially Charged' }
49
+ default { 'Unknown' }
50
+ }
51
+ chargePercent = $b.EstimatedChargeRemaining
52
+ estimatedRunTimeMinutes = if ($b.EstimatedRunTime -lt 10000) { $b.EstimatedRunTime } else { $null }
53
+ voltagemV = $b.DesignVoltage
54
+ designCapacitymWh = $designCapacity
55
+ fullChargeCapacitymWh = $fullChargeCapacity
56
+ healthPercent = $healthPercent
57
+ cycleCount = if ($p) { $p.CycleCount } else { $null }
58
+ manufacturer = if ($p) { $p.Manufacturer } else { $null }
59
+ manufactureDate = if ($p -and $p.ManufactureDate) { $p.ManufactureDate.ToString('o') } else { $null }
60
+ serialNumber = if ($p) { $p.SerialNumber } else { $null }
61
+ chemistry = if ($p) { $p.Chemistry } else { $null }
62
+ }
63
+ }
64
+
65
+ [PSCustomObject]@{
66
+ hasBattery = $true
67
+ batteries = $batteryList
68
+ powerPlan = $powerPlan
69
+ isCharging = @($batteryList | Where-Object { $_.batteryStatusValue -in 6,7,8,9 }).Count -gt 0
70
+ overallChargePercent = ($batteryList | Measure-Object -Property chargePercent -Average).Average
71
+ } | ConvertTo-Json -Depth 5 -Compress
72
+ `;
73
+ return runPsJson(cmd);
74
+ }
75
+
76
+ module.exports = { getBatteryInfo };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * disk-watcher.js — 磁盘插拔监听
3
+ *
4
+ * 双模式:
5
+ * 1. WMI 事件监听(Win32_VolumeChangeEvent)— 插拔瞬间触发,几乎零延迟
6
+ * 2. 低频轮询兜底(默认 5s)— 防止 WMI 事件丢失,同时校准当前盘符列表
7
+ *
8
+ * 用法:
9
+ * const watcher = createDiskWatcher();
10
+ * watcher.on('change', ({ added, removed, disks }) => { ... });
11
+ * watcher.start();
12
+ * watcher.stop();
13
+ */
14
+
15
+ const { EventEmitter } = require('events');
16
+ const { existsSync } = require('fs');
17
+ const { spawn } = require('child_process');
18
+ const path = require('path');
19
+
20
+ const ALL_LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
21
+
22
+ function listDriveLetters() {
23
+ return ALL_LETTERS.filter((l) => {
24
+ try { return existsSync(`${l}:/`); } catch { return false; }
25
+ });
26
+ }
27
+
28
+ // WMI 事件监听脚本:注册 Win32_VolumeChangeEvent,有事件时输出一行 JSON
29
+ // EventType: 2=插入, 3=拔出 DriveName: 盘符如 "D:"
30
+ const WMI_SCRIPT = `
31
+ $ErrorActionPreference = 'Stop'
32
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
33
+ Register-CimIndicationEvent -ClassName Win32_VolumeChangeEvent -SourceIdentifier DiskChange | Out-Null
34
+ Write-Output 'READY'
35
+ while ($true) {
36
+ $e = Wait-Event -SourceIdentifier DiskChange -Timeout 1
37
+ if ($e) {
38
+ try {
39
+ $evt = $e.SourceEventArgs.NewEvent
40
+ if ($evt.EventType -eq 2 -or $evt.EventType -eq 3) {
41
+ $drive = ($evt.DriveName -replace ':$', '').ToUpper()
42
+ $obj = @{ type = $(if ($evt.EventType -eq 2) { 'add' } else { 'remove' }); drive = $drive }
43
+ Write-Output (ConvertTo-Json -InputObject $obj -Compress)
44
+ }
45
+ } catch { }
46
+ Remove-Event -SourceIdentifier DiskChange -ErrorAction SilentlyContinue
47
+ }
48
+ }
49
+ `;
50
+
51
+ function createDiskWatcher(options = {}) {
52
+ const pollInterval = options.pollInterval || 5000; // 兜底轮询间隔
53
+ const emitter = new EventEmitter();
54
+ let current = listDriveLetters();
55
+ let pollTimer = null;
56
+ let wmiChild = null;
57
+ let wmiBuf = '';
58
+ let started = false;
59
+
60
+ // 根据 WMI 事件计算 added/removed
61
+ function handleWmiEvent(evt) {
62
+ try {
63
+ const now = listDriveLetters();
64
+ const nowSet = new Set(now);
65
+ const curSet = new Set(current);
66
+ const added = now.filter((d) => !curSet.has(d));
67
+ const removed = current.filter((d) => !nowSet.has(d));
68
+ // 如果 WMI 报告的盘符不在计算结果里,手动加上
69
+ if (evt.type === 'add' && !added.includes(evt.drive) && nowSet.has(evt.drive)) {
70
+ added.push(evt.drive);
71
+ }
72
+ if (evt.type === 'remove' && !removed.includes(evt.drive) && !nowSet.has(evt.drive)) {
73
+ removed.push(evt.drive);
74
+ }
75
+ if (added.length || removed.length) {
76
+ current = now;
77
+ emitter.emit('change', { added, removed, disks: now });
78
+ }
79
+ } catch (e) { /* 忽略 */ }
80
+ }
81
+
82
+ // 兜底轮询:校准盘符列表,发现 WMI 漏掉的变化
83
+ function pollTick() {
84
+ try {
85
+ const now = listDriveLetters();
86
+ const nowSet = new Set(now);
87
+ const curSet = new Set(current);
88
+ const added = now.filter((d) => !curSet.has(d));
89
+ const removed = current.filter((d) => !nowSet.has(d));
90
+ if (added.length || removed.length) {
91
+ current = now;
92
+ emitter.emit('change', { added, removed, disks: now });
93
+ }
94
+ } catch (e) { /* 忽略 */ }
95
+ }
96
+
97
+ // 启动 WMI 事件监听子进程
98
+ function startWmiListener() {
99
+ if (process.platform !== 'win32') return;
100
+ const psExe = path.join(
101
+ process.env.SystemRoot || 'C:\\Windows',
102
+ 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'
103
+ );
104
+ wmiBuf = '';
105
+ wmiChild = spawn(psExe, ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', WMI_SCRIPT], {
106
+ stdio: ['ignore', 'pipe', 'ignore'],
107
+ windowsHide: true
108
+ });
109
+ wmiChild.stdout.setEncoding('utf8');
110
+ wmiChild.stdout.on('data', (chunk) => {
111
+ wmiBuf += chunk;
112
+ let idx;
113
+ while ((idx = wmiBuf.indexOf('\n')) >= 0) {
114
+ const line = wmiBuf.slice(0, idx).trim();
115
+ wmiBuf = wmiBuf.slice(idx + 1);
116
+ if (!line || line === 'READY') continue;
117
+ try {
118
+ const evt = JSON.parse(line);
119
+ if (evt.type && evt.drive) handleWmiEvent(evt);
120
+ } catch { /* 脏数据忽略 */ }
121
+ }
122
+ });
123
+ wmiChild.on('exit', () => {
124
+ wmiChild = null;
125
+ if (started) {
126
+ // 崩溃后 3 秒重启
127
+ setTimeout(startWmiListener, 3000);
128
+ }
129
+ });
130
+ }
131
+
132
+ emitter.start = function () {
133
+ if (started) return;
134
+ started = true;
135
+ current = listDriveLetters();
136
+ startWmiListener();
137
+ pollTimer = setInterval(pollTick, pollInterval);
138
+ };
139
+
140
+ emitter.stop = function () {
141
+ started = false;
142
+ if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
143
+ if (wmiChild) {
144
+ wmiChild.kill();
145
+ wmiChild = null;
146
+ }
147
+ };
148
+
149
+ emitter.getDisks = function () {
150
+ return [...current];
151
+ };
152
+
153
+ return emitter;
154
+ }
155
+
156
+ module.exports = { createDiskWatcher, listDriveLetters };
package/lib/disk.js ADDED
@@ -0,0 +1,77 @@
1
+ const { runPsJson } = require('./ps');
2
+
3
+ /**
4
+ * 获取硬盘详细信息:物理磁盘、分区、SMART、读写速度
5
+ */
6
+ async function getDiskInfo() {
7
+ const cmd = `
8
+ $disks = Get-CimInstance Win32_DiskDrive
9
+ $logical = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3"
10
+ $partitions = Get-CimInstance Win32_DiskPartition
11
+
12
+ # 物理磁盘读写性能
13
+ $diskPerf = Get-CimInstance Win32_PerfFormattedData_PerfDisk_PhysicalDisk | Where-Object { $_.Name -ne '_Total' }
14
+
15
+ $diskList = @()
16
+ foreach ($d in $disks) {
17
+ $partitionsForDisk = $partitions | Where-Object { $_.DiskIndex -eq $d.Index }
18
+ $driveLetters = @()
19
+ foreach ($p in $partitionsForDisk) {
20
+ $ld = Get-CimInstance -Query "ASSOCIATORS OF {Win32_DiskPartition.DeviceID='$($p.DeviceID)'} WHERE AssocClass=Win32_LogicalDiskToPartition"
21
+ foreach ($l in $ld) { $driveLetters += $l.DeviceID }
22
+ }
23
+
24
+ $perf = $diskPerf | Where-Object { $_.Name -like "$($d.Index) *" } | Select-Object -First 1
25
+
26
+ $diskList += [PSCustomObject]@{
27
+ index = $d.Index
28
+ model = $d.Model
29
+ manufacturer = $d.Manufacturer
30
+ interfaceType = $d.InterfaceType
31
+ mediaType = $d.MediaType
32
+ serialNumber = if ($d.SerialNumber) { $d.SerialNumber.Trim() } else { $null }
33
+ firmwareRevision = $d.FirmwareRevision
34
+ sizeGB = [math]::Round($d.Size / 1GB, 2)
35
+ totalCylinders = $d.TotalCylinders
36
+ totalHeads = $d.TotalHeads
37
+ totalSectors = $d.TotalSectors
38
+ bytesPerSector = $d.BytesPerSector
39
+ partitions = $partitionsForDisk.Count
40
+ driveLetters = $driveLetters
41
+ readBytesPerSec = if ($perf) { $perf.DiskReadBytesPerSec } else { $null }
42
+ writeBytesPerSec = if ($perf) { $perf.DiskWriteBytesPerSec } else { $null }
43
+ readQueueLength = if ($perf) { $perf.AvgDiskReadQueueLength } else { $null }
44
+ writeQueueLength = if ($perf) { $perf.AvgDiskWriteQueueLength } else { $null }
45
+ percentActive = if ($perf) { $perf.PercentDiskTime } else { $null }
46
+ }
47
+ }
48
+
49
+ $partitionList = @()
50
+ foreach ($ld in $logical) {
51
+ $sizeGB = [math]::Round($ld.Size / 1GB, 2)
52
+ $freeGB = [math]::Round($ld.FreeSpace / 1GB, 2)
53
+ $usedGB = [math]::Round($sizeGB - $freeGB, 2)
54
+ $partitionList += [PSCustomObject]@{
55
+ drive = $ld.DeviceID
56
+ volumeName = $ld.VolumeName
57
+ fileSystem = $ld.FileSystem
58
+ sizeGB = $sizeGB
59
+ usedGB = $usedGB
60
+ freeGB = $freeGB
61
+ usedPercent = if ($sizeGB -gt 0) { [math]::Round(($usedGB / $sizeGB) * 100, 2) } else { 0 }
62
+ volumeSerialNumber = $ld.VolumeSerialNumber
63
+ compressed = $ld.Compressed
64
+ }
65
+ }
66
+
67
+ [PSCustomObject]@{
68
+ physicalDisks = $diskList
69
+ logicalDrives = $partitionList
70
+ diskCount = $diskList.Count
71
+ totalSizeGB = [math]::Round(($diskList | Measure-Object -Property sizeGB -Sum).Sum, 2)
72
+ } | ConvertTo-Json -Depth 6 -Compress
73
+ `;
74
+ return runPsJson(cmd, { timeout: 20000 });
75
+ }
76
+
77
+ module.exports = { getDiskInfo };