@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.
- package/README.md +907 -0
- package/bin/OpenHardwareMonitorLib.dll +0 -0
- package/index.js +192 -0
- package/lib/allhardware.js +35 -0
- package/lib/backend-usage.js +103 -0
- package/lib/battery.js +76 -0
- package/lib/disk-watcher.js +156 -0
- package/lib/disk.js +77 -0
- package/lib/gpu.js +70 -0
- package/lib/hardware-service.js +284 -0
- package/lib/memory.js +59 -0
- package/lib/monitor.js +176 -0
- package/lib/network.js +75 -0
- package/lib/ohm-daemon.js +208 -0
- package/lib/ohm.js +41 -0
- package/lib/perfctr.js +43 -0
- package/lib/process-icon.js +76 -0
- package/lib/process-ops.js +218 -0
- package/lib/processes.js +232 -0
- package/lib/ps.js +93 -0
- package/lib/public-ip.js +124 -0
- package/lib/services.js +43 -0
- package/lib/sysinfo-daemon.js +188 -0
- package/lib/system.js +77 -0
- package/lib/usb.js +49 -0
- package/lib/win32-procs.js +241 -0
- package/package.json +50 -0
- package/scripts/get-all-hardware.ps1 +98 -0
- package/scripts/get-lhm-temp.ps1 +119 -0
- package/scripts/ohm-daemon.ps1 +141 -0
- package/scripts/sysinfo-daemon.ps1 +386 -0
- package/test/check-bugs.js +182 -0
- package/test/public/gj.pay.ali.jpg +0 -0
- package/test/public/gj.pay.wx.jpg +0 -0
- package/test/public/index.html +996 -0
- package/test/public/zdl.pay.ali.jpg +0 -0
- package/test/public/zdl.pay.wx.jpg +0 -0
- package/test/scan-encoding.js +77 -0
- package/test/server.js +255 -0
- package/test/test-all.js +180 -0
- package/test/test-daemon.js +51 -0
- package/test/test-kill-name.js +30 -0
- package/test/test-monitor.js +78 -0
- package/test/test-new-features.js +93 -0
- package/test/test-service.js +91 -0
- package/test/test-sysinfo-daemon.js +95 -0
- package/test/test.js +63 -0
package/lib/processes.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* processes.js — 进程列表获取
|
|
3
|
+
*
|
|
4
|
+
* 双后端:
|
|
5
|
+
* - koffi(默认):直调 Win32 API,264进程 ~25ms,支持实时 CPU%
|
|
6
|
+
* - wmi:PowerShell Get-Process,~115ms,命令行信息更全
|
|
7
|
+
*
|
|
8
|
+
* 高频优化:
|
|
9
|
+
* - includeCpu=true 时启动 1s 常驻采样器,自动维护 CPU% 基线
|
|
10
|
+
* - 采样器空闲 15s 自动停止,不浪费 CPU
|
|
11
|
+
* - getProcesses 读缓存毫秒级返回
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { runPsJson } = require('./ps');
|
|
15
|
+
const { sampleProcesses, computeCpuPercent, resetCpuBaseline, getInstallTime } = require('./win32-procs');
|
|
16
|
+
const { totalmem } = require('os');
|
|
17
|
+
|
|
18
|
+
// ---- 常驻采样器(用于 CPU%)----
|
|
19
|
+
let processCache = [];
|
|
20
|
+
let processTimer = null;
|
|
21
|
+
let lastRequestAt = 0;
|
|
22
|
+
const IDLE_MS = 15000;
|
|
23
|
+
|
|
24
|
+
function tickProcessSampler() {
|
|
25
|
+
try {
|
|
26
|
+
const raw = sampleProcesses();
|
|
27
|
+
const cpuMap = computeCpuPercent(raw);
|
|
28
|
+
processCache = raw.map((p) => ({
|
|
29
|
+
pid: p.pid,
|
|
30
|
+
name: p.name.replace(/\.exe$/i, ''),
|
|
31
|
+
threads: p.threads,
|
|
32
|
+
parentPid: p.parentPid,
|
|
33
|
+
memoryMB: Math.round((p.workingSet / 1048576) * 100) / 100,
|
|
34
|
+
memoryPrivateMB: Math.round((p.privateUsage / 1048576) * 100) / 100,
|
|
35
|
+
cpuPercent: Math.max(0, Math.round((cpuMap.get(p.pid) || 0) * 10) / 10),
|
|
36
|
+
exePath: p.exePath,
|
|
37
|
+
installTime: ''
|
|
38
|
+
}));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// 采样失败保留上一帧
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ensureProcessSampler() {
|
|
45
|
+
lastRequestAt = Date.now();
|
|
46
|
+
if (processTimer) return;
|
|
47
|
+
// 立即采一帧(第一帧 CPU% 为 0,建立基线;第二帧起正常)
|
|
48
|
+
tickProcessSampler();
|
|
49
|
+
processTimer = setInterval(tickProcessSampler, 1000);
|
|
50
|
+
// 空闲自停检查
|
|
51
|
+
const idleChecker = setInterval(() => {
|
|
52
|
+
if (processTimer && Date.now() - lastRequestAt > IDLE_MS) {
|
|
53
|
+
stopProcessSampler();
|
|
54
|
+
clearInterval(idleChecker);
|
|
55
|
+
}
|
|
56
|
+
}, 5000);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stopProcessSampler() {
|
|
60
|
+
if (processTimer) {
|
|
61
|
+
clearInterval(processTimer);
|
|
62
|
+
processTimer = null;
|
|
63
|
+
}
|
|
64
|
+
resetCpuBaseline();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- 按名称聚合 ----
|
|
68
|
+
function groupByName(procs) {
|
|
69
|
+
const groups = new Map();
|
|
70
|
+
for (const p of procs) {
|
|
71
|
+
const key = p.name;
|
|
72
|
+
if (!groups.has(key)) {
|
|
73
|
+
groups.set(key, {
|
|
74
|
+
name: key,
|
|
75
|
+
pid: p.pid,
|
|
76
|
+
pids: [p.pid],
|
|
77
|
+
memoryMB: 0,
|
|
78
|
+
memoryPrivateMB: 0,
|
|
79
|
+
cpuPercent: 0,
|
|
80
|
+
threads: 0,
|
|
81
|
+
exePath: p.exePath,
|
|
82
|
+
installTime: p.installTime,
|
|
83
|
+
instances: []
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const g = groups.get(key);
|
|
87
|
+
g.memoryMB += p.memoryMB;
|
|
88
|
+
g.memoryPrivateMB += p.memoryPrivateMB;
|
|
89
|
+
g.cpuPercent = Math.max(g.cpuPercent, p.cpuPercent || 0);
|
|
90
|
+
g.threads += p.threads;
|
|
91
|
+
g.pids.push(p.pid);
|
|
92
|
+
g.instances.push(p);
|
|
93
|
+
// 主进程取内存最大的
|
|
94
|
+
if (p.memoryMB > g.memoryMB) {
|
|
95
|
+
g.pid = p.pid;
|
|
96
|
+
g.exePath = p.exePath;
|
|
97
|
+
g.installTime = p.installTime;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return Array.from(groups.values()).map((g) => ({
|
|
101
|
+
...g,
|
|
102
|
+
memoryMB: Math.round(g.memoryMB * 100) / 100,
|
|
103
|
+
memoryPrivateMB: Math.round(g.memoryPrivateMB * 100) / 100,
|
|
104
|
+
percent: Math.round((g.memoryMB / (totalmem() / 1048576)) * 1000) / 10
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---- koffi 后端 ----
|
|
109
|
+
function getProcessesKoffi(options = {}) {
|
|
110
|
+
const { top = 0, sortBy = 'memory', includeCpu = false, groupByName: group = false, includeInstallTime = false } = options;
|
|
111
|
+
|
|
112
|
+
let procs;
|
|
113
|
+
if (includeCpu) {
|
|
114
|
+
ensureProcessSampler();
|
|
115
|
+
procs = processCache.map((p) => ({ ...p }));
|
|
116
|
+
} else {
|
|
117
|
+
const raw = sampleProcesses();
|
|
118
|
+
procs = raw.map((p) => ({
|
|
119
|
+
pid: p.pid,
|
|
120
|
+
name: p.name.replace(/\.exe$/i, ''),
|
|
121
|
+
threads: p.threads,
|
|
122
|
+
parentPid: p.parentPid,
|
|
123
|
+
memoryMB: Math.round((p.workingSet / 1048576) * 100) / 100,
|
|
124
|
+
memoryPrivateMB: Math.round((p.privateUsage / 1048576) * 100) / 100,
|
|
125
|
+
cpuPercent: 0,
|
|
126
|
+
exePath: p.exePath,
|
|
127
|
+
installTime: ''
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 安装时间(按需,fs.statSync 毫秒级但全量遍历有开销)
|
|
132
|
+
if (includeInstallTime) {
|
|
133
|
+
for (const p of procs) {
|
|
134
|
+
if (p.exePath) p.installTime = getInstallTime(p.exePath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 按名称聚合
|
|
139
|
+
if (group) {
|
|
140
|
+
procs = groupByName(procs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 排序
|
|
144
|
+
if (sortBy === 'cpu') {
|
|
145
|
+
procs.sort((a, b) => (b.cpuPercent || 0) - (a.cpuPercent || 0));
|
|
146
|
+
} else if (sortBy === 'name') {
|
|
147
|
+
procs.sort((a, b) => a.name.localeCompare(b.name));
|
|
148
|
+
} else {
|
|
149
|
+
procs.sort((a, b) => b.memoryMB - a.memoryMB);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const total = procs.length;
|
|
153
|
+
if (top > 0) procs = procs.slice(0, top);
|
|
154
|
+
|
|
155
|
+
return { total, returned: procs.length, processes: procs, backend: 'koffi' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- WMI 后端(兼容,命令行更全)----
|
|
159
|
+
async function getProcessesWmi(options = {}) {
|
|
160
|
+
const top = options.top || 0;
|
|
161
|
+
const sortBy = options.sortBy || 'memory';
|
|
162
|
+
const includeCommandLine = options.includeCommandLine || false;
|
|
163
|
+
|
|
164
|
+
let sortProperty = 'WorkingSet64';
|
|
165
|
+
if (sortBy === 'cpu') sortProperty = 'CPU';
|
|
166
|
+
else if (sortBy === 'name') sortProperty = 'ProcessName';
|
|
167
|
+
|
|
168
|
+
const topClause = top > 0 ? ` | Select-Object -First ${top}` : '';
|
|
169
|
+
|
|
170
|
+
let cmdLineInit = '';
|
|
171
|
+
let cmdLineField = '$null';
|
|
172
|
+
if (includeCommandLine) {
|
|
173
|
+
cmdLineInit = `
|
|
174
|
+
$wmiProcs = Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine
|
|
175
|
+
$cmdLineMap = @{}
|
|
176
|
+
foreach ($w in $wmiProcs) { $cmdLineMap[$w.ProcessId] = $w.CommandLine }
|
|
177
|
+
`;
|
|
178
|
+
cmdLineField = 'if ($cmdLineMap.ContainsKey($p.Id)) { $cmdLineMap[$p.Id] } else { $null }';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const cmd = `
|
|
182
|
+
$procs = Get-Process | Sort-Object -Property ${sortProperty} -Descending${topClause}
|
|
183
|
+
${cmdLineInit}
|
|
184
|
+
$list = @()
|
|
185
|
+
foreach ($p in $procs) {
|
|
186
|
+
$list += [PSCustomObject]@{
|
|
187
|
+
pid = $p.Id
|
|
188
|
+
name = $p.ProcessName
|
|
189
|
+
cpuSeconds = if ($p.CPU) { [math]::Round($p.CPU, 2) } else { 0 }
|
|
190
|
+
memoryMB = [math]::Round($p.WorkingSet64 / 1MB, 2)
|
|
191
|
+
memoryPrivateMB = [math]::Round($p.PrivateMemorySize64 / 1MB, 2)
|
|
192
|
+
memoryVirtualMB = [math]::Round($p.VirtualMemorySize64 / 1MB, 2)
|
|
193
|
+
threads = $p.Threads.Count
|
|
194
|
+
handles = $p.HandleCount
|
|
195
|
+
startTime = if ($p.StartTime) { $p.StartTime.ToString('o') } else { $null }
|
|
196
|
+
path = $p.Path
|
|
197
|
+
company = $p.Company
|
|
198
|
+
fileVersion = $p.FileVersion
|
|
199
|
+
commandLine = ${cmdLineField}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
[PSCustomObject]@{
|
|
203
|
+
total = (Get-Process).Count
|
|
204
|
+
returned = $list.Count
|
|
205
|
+
processes = $list
|
|
206
|
+
backend = 'wmi'
|
|
207
|
+
} | ConvertTo-Json -Depth 4 -Compress
|
|
208
|
+
`;
|
|
209
|
+
|
|
210
|
+
return runPsJson(cmd, { timeout: 20000 });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 获取进程列表
|
|
215
|
+
* @param {Object} options
|
|
216
|
+
* @param {number} options.top - 返回前 N 个,默认 0 表示全部
|
|
217
|
+
* @param {string} options.sortBy - 排序: 'memory' | 'cpu' | 'name',默认 'memory'
|
|
218
|
+
* @param {string} options.backend - 'koffi'(默认,快)| 'wmi'(命令行全)
|
|
219
|
+
* @param {boolean} options.includeCpu - koffi 后端:是否计算实时 CPU%(启动常驻采样器)
|
|
220
|
+
* @param {boolean} options.groupByName - koffi 后端:是否按进程名聚合
|
|
221
|
+
* @param {boolean} options.includeInstallTime - koffi 后端:是否包含 exe 安装时间
|
|
222
|
+
* @param {boolean} options.includeCommandLine - wmi 后端:是否包含命令行
|
|
223
|
+
*/
|
|
224
|
+
async function getProcesses(options = {}) {
|
|
225
|
+
const backend = options.backend || 'koffi';
|
|
226
|
+
if (backend === 'wmi') {
|
|
227
|
+
return getProcessesWmi(options);
|
|
228
|
+
}
|
|
229
|
+
return getProcessesKoffi(options);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { getProcesses, stopProcessSampler };
|
package/lib/ps.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
|
|
3
|
+
// UTF-8 编码前缀,解决中文 Windows PowerShell 默认 GBK 输出导致的乱码
|
|
4
|
+
const UTF8_PREFIX = '[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;$OutputEncoding=[System.Text.Encoding]::UTF8;chcp 65001>$null;';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 执行 PowerShell 命令,返回解析后的 JSON
|
|
8
|
+
* @param {string} command - PowerShell 命令(输出必须是 JSON)
|
|
9
|
+
* @param {Object} options
|
|
10
|
+
* @param {number} options.timeout - 超时毫秒,默认 15000
|
|
11
|
+
* @returns {Promise<any>}
|
|
12
|
+
*/
|
|
13
|
+
function runPsJson(command, options = {}) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const ps = spawn('powershell', [
|
|
16
|
+
'-NoProfile',
|
|
17
|
+
'-ExecutionPolicy', 'Bypass',
|
|
18
|
+
'-Command', UTF8_PREFIX + command
|
|
19
|
+
], { timeout: options.timeout || 15000 });
|
|
20
|
+
|
|
21
|
+
let out = '';
|
|
22
|
+
let err = '';
|
|
23
|
+
ps.stdout.on('data', d => (out += d));
|
|
24
|
+
ps.stderr.on('data', d => (err += d));
|
|
25
|
+
ps.on('close', code => {
|
|
26
|
+
if (code !== 0 && !out.trim()) {
|
|
27
|
+
return reject(new Error(`PowerShell exited ${code}: ${err || out}`));
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const trimmed = out.trim();
|
|
31
|
+
if (!trimmed) return resolve(null);
|
|
32
|
+
resolve(JSON.parse(trimmed));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
reject(new Error(`Failed to parse PowerShell JSON: ${e.message}\nRaw output: ${out.slice(0, 500)}`));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
ps.on('error', reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 执行 PowerShell 脚本文件,返回解析后的 JSON
|
|
43
|
+
* @param {string} scriptPath - 脚本路径
|
|
44
|
+
* @param {string[]} args - 参数
|
|
45
|
+
* @returns {Promise<any>}
|
|
46
|
+
*/
|
|
47
|
+
function runPsScript(scriptPath, args = []) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
// 用 -Command 方式执行,先设置 UTF-8 编码再 dot-source 脚本
|
|
50
|
+
const argStr = args.map(a => `"${a}"`).join(' ');
|
|
51
|
+
const command = `${UTF8_PREFIX}& "${scriptPath}" ${argStr}`;
|
|
52
|
+
|
|
53
|
+
const ps = spawn('powershell', [
|
|
54
|
+
'-NoProfile',
|
|
55
|
+
'-ExecutionPolicy', 'Bypass',
|
|
56
|
+
'-Command', command
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
let out = '';
|
|
60
|
+
let err = '';
|
|
61
|
+
ps.stdout.on('data', d => (out += d));
|
|
62
|
+
ps.stderr.on('data', d => (err += d));
|
|
63
|
+
ps.on('close', code => {
|
|
64
|
+
if (code !== 0 && !out.trim()) {
|
|
65
|
+
return reject(new Error(`Script exited ${code}: ${err || out}`));
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const trimmed = out.trim();
|
|
69
|
+
if (!trimmed) return resolve(null);
|
|
70
|
+
resolve(JSON.parse(trimmed));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
reject(new Error(`Failed to parse script JSON: ${e.message}\nRaw: ${out.slice(0, 500)}`));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
ps.on('error', reject);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 把 CIM 查询结果转成 JSON 输出的 PowerShell 命令片段
|
|
81
|
+
* @param {string} cimClass - WMI/CIM 类名
|
|
82
|
+
* @param {string} namespace - 命名空间,默认 root/cimv2
|
|
83
|
+
* @param {string} filter - 过滤条件
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function cimToJson(cimClass, namespace = 'root/cimv2', filter = '') {
|
|
87
|
+
let cmd = `Get-CimInstance -ClassName ${cimClass} -Namespace "${namespace}"`;
|
|
88
|
+
if (filter) cmd += ` -Filter "${filter}"`;
|
|
89
|
+
cmd += ` | ConvertTo-Json -Depth 4 -Compress`;
|
|
90
|
+
return cmd;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { runPsJson, runPsScript, cimToJson };
|
package/lib/public-ip.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 公网 IP 获取模块
|
|
3
|
+
* 主方式:DNS 查询(OpenDNS / Google DNS),比 HTTP 快 3-5 倍
|
|
4
|
+
* 兜底:HTTPS 多源(ifconfig.me / ipify / icanhazip)
|
|
5
|
+
* 零依赖 + 60 秒缓存
|
|
6
|
+
*/
|
|
7
|
+
const dns = require('dns');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
|
|
10
|
+
// ========== DNS 查询方式 ==========
|
|
11
|
+
|
|
12
|
+
const openDns = new dns.promises.Resolver({ timeout: 3000, tries: 1 });
|
|
13
|
+
openDns.setServers(['208.67.222.222', '208.67.220.220']);
|
|
14
|
+
|
|
15
|
+
const googleDns = new dns.promises.Resolver({ timeout: 3000, tries: 1 });
|
|
16
|
+
googleDns.setServers(['8.8.8.8', '8.8.4.4']);
|
|
17
|
+
|
|
18
|
+
// OpenDNS: 查询 myip.opendns.com 的 A 记录,返回值就是公网 IP
|
|
19
|
+
async function queryOpenDns() {
|
|
20
|
+
const result = await openDns.resolve4('myip.opendns.com');
|
|
21
|
+
const ip = result[0];
|
|
22
|
+
if (!ip || !/^[\d.]+$/.test(ip)) throw new Error('OpenDNS returned invalid IP');
|
|
23
|
+
return ip;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Google DNS: 查询 o-o.myaddr.l.google.com 的 TXT 记录,内容是公网 IP
|
|
27
|
+
async function queryGoogleDns() {
|
|
28
|
+
const result = await googleDns.resolveTxt('o-o.myaddr.l.google.com');
|
|
29
|
+
// result 格式: [['ip'], ...]
|
|
30
|
+
const ip = result[0]?.[0];
|
|
31
|
+
if (!ip || !/^[\d.]+$/.test(ip)) throw new Error('Google DNS returned invalid IP');
|
|
32
|
+
return ip;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ========== HTTPS 兜底方式 ==========
|
|
36
|
+
|
|
37
|
+
const HTTPS_APIS = [
|
|
38
|
+
{ url: 'https://ifconfig.me/ip', parse: d => d.trim() },
|
|
39
|
+
{ url: 'https://api.ipify.org?format=json', parse: d => JSON.parse(d).ip },
|
|
40
|
+
{ url: 'https://icanhazip.com', parse: d => d.trim() },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function queryHttps(api) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const req = https.get(api.url, { timeout: 5000 }, (res) => {
|
|
46
|
+
let data = '';
|
|
47
|
+
res.on('data', c => data += c);
|
|
48
|
+
res.on('end', () => {
|
|
49
|
+
try {
|
|
50
|
+
const ip = api.parse(data);
|
|
51
|
+
if (ip && /^[\d.]+$/.test(ip)) resolve(ip);
|
|
52
|
+
else reject(new Error('invalid IP'));
|
|
53
|
+
} catch (e) { reject(e); }
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
req.on('error', reject);
|
|
57
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function queryHttpsFallback() {
|
|
62
|
+
for (const api of HTTPS_APIS) {
|
|
63
|
+
try { return await queryHttps(api); }
|
|
64
|
+
catch (e) { /* 继续下一个 */ }
|
|
65
|
+
}
|
|
66
|
+
throw new Error('All HTTPS IP APIs failed');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ========== 主逻辑:依次尝试 DNS → DNS → HTTPS ==========
|
|
70
|
+
|
|
71
|
+
const STRATEGIES = [
|
|
72
|
+
{ name: 'opendns', fn: queryOpenDns },
|
|
73
|
+
{ name: 'google-dns', fn: queryGoogleDns },
|
|
74
|
+
{ name: 'https-fallback', fn: queryHttpsFallback },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
async function fetchPublicIp() {
|
|
78
|
+
const errors = [];
|
|
79
|
+
for (const s of STRATEGIES) {
|
|
80
|
+
try {
|
|
81
|
+
const ip = await s.fn();
|
|
82
|
+
return { ip, source: s.name };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
errors.push(`${s.name}: ${e.message}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`All strategies failed: ${errors.join('; ')}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ========== 缓存 ==========
|
|
91
|
+
|
|
92
|
+
let cache = { ip: null, source: null, time: 0 };
|
|
93
|
+
const CACHE_TTL = 60000; // 60 秒
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 获取公网 IP
|
|
97
|
+
* @param {Object} [options]
|
|
98
|
+
* @param {boolean} [options.force] - 强制刷新,跳过缓存
|
|
99
|
+
* @param {number} [options.cacheTtl] - 缓存时间(毫秒),默认 60000
|
|
100
|
+
* @param {boolean} [options.includeSource] - 返回值包含数据来源
|
|
101
|
+
* @returns {Promise<string|{ip:string,source:string}>} 公网 IP 地址
|
|
102
|
+
*/
|
|
103
|
+
async function getPublicIp(options = {}) {
|
|
104
|
+
const ttl = options.cacheTtl || CACHE_TTL;
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
|
|
107
|
+
if (!options.force && cache.ip && (now - cache.time) < ttl) {
|
|
108
|
+
return options.includeSource ? { ip: cache.ip, source: cache.source } : cache.ip;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const { ip, source } = await fetchPublicIp();
|
|
113
|
+
cache = { ip, source, time: now };
|
|
114
|
+
return options.includeSource ? { ip, source } : ip;
|
|
115
|
+
} catch (e) {
|
|
116
|
+
// 失败时返回旧缓存
|
|
117
|
+
if (cache.ip) {
|
|
118
|
+
return options.includeSource ? { ip: cache.ip, source: cache.source + ' (cached)' } : cache.ip;
|
|
119
|
+
}
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { getPublicIp };
|
package/lib/services.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { runPsJson } = require('./ps');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 获取 Windows 服务列表
|
|
5
|
+
* @param {Object} options
|
|
6
|
+
* @param {boolean} options.runningOnly - 只返回运行中的服务,默认 false
|
|
7
|
+
*/
|
|
8
|
+
async function getServicesInfo(options = {}) {
|
|
9
|
+
const runningOnly = options.runningOnly || false;
|
|
10
|
+
const filter = runningOnly ? '-Filter "State=\'Running\'"' : '';
|
|
11
|
+
|
|
12
|
+
const cmd = `
|
|
13
|
+
$services = Get-CimInstance Win32_Service ${filter} | Sort-Object Name
|
|
14
|
+
$list = @()
|
|
15
|
+
foreach ($s in $services) {
|
|
16
|
+
$list += [PSCustomObject]@{
|
|
17
|
+
name = $s.Name
|
|
18
|
+
displayName = $s.DisplayName
|
|
19
|
+
state = $s.State
|
|
20
|
+
status = $s.Status
|
|
21
|
+
startMode = $s.StartMode
|
|
22
|
+
startName = $s.StartName
|
|
23
|
+
pathName = $s.PathName
|
|
24
|
+
description = $s.Description
|
|
25
|
+
processId = $s.ProcessId
|
|
26
|
+
acceptStop = $s.AcceptStop
|
|
27
|
+
acceptPause = $s.AcceptPause
|
|
28
|
+
desktopInteract = $s.DesktopInteract
|
|
29
|
+
exitCode = $s.ExitCode
|
|
30
|
+
serviceType = $s.ServiceType
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
[PSCustomObject]@{
|
|
34
|
+
total = $list.Count
|
|
35
|
+
running = ($list | Where-Object { $_.state -eq 'Running' }).Count
|
|
36
|
+
stopped = ($list | Where-Object { $_.state -eq 'Stopped' }).Count
|
|
37
|
+
services = $list
|
|
38
|
+
} | ConvertTo-Json -Depth 4 -Compress
|
|
39
|
+
`;
|
|
40
|
+
return runPsJson(cmd, { timeout: 20000 });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { getServicesInfo };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
const SCRIPT = path.join(__dirname, '..', 'scripts', 'sysinfo-daemon.ps1');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 统一硬件信息常驻进程
|
|
9
|
+
* 一个 PowerShell 进程服务所有模块,每次查询只走 stdin/stdout,延迟 ~50ms
|
|
10
|
+
* 支持高频调用,内存占用 ~50MB
|
|
11
|
+
*/
|
|
12
|
+
class SysInfoDaemon {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.process = null;
|
|
16
|
+
this.ready = false;
|
|
17
|
+
this.readyPromise = null;
|
|
18
|
+
this.queue = [];
|
|
19
|
+
this.currentRequest = null;
|
|
20
|
+
this.closed = false;
|
|
21
|
+
this.restartCount = 0;
|
|
22
|
+
this.maxRestarts = options.maxRestarts || 3;
|
|
23
|
+
// 缓存 TTL(毫秒),默认 system 30s,network 60s
|
|
24
|
+
this.systemCacheTtl = options.systemCacheTtl || 30000;
|
|
25
|
+
this.networkCacheTtl = options.networkCacheTtl || 30000;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.process) {
|
|
30
|
+
if (this.ready) return;
|
|
31
|
+
return this.readyPromise;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
35
|
+
this.process = spawn('powershell', [
|
|
36
|
+
'-NoProfile',
|
|
37
|
+
'-ExecutionPolicy', 'Bypass',
|
|
38
|
+
'-File', SCRIPT,
|
|
39
|
+
'-SystemCacheTtl', (this.systemCacheTtl / 1000).toString(),
|
|
40
|
+
'-NetworkCacheTtl', (this.networkCacheTtl / 1000).toString()
|
|
41
|
+
], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
42
|
+
|
|
43
|
+
const rl = readline.createInterface({ input: this.process.stdout, terminal: false });
|
|
44
|
+
let readyReceived = false;
|
|
45
|
+
|
|
46
|
+
rl.on('line', (line) => {
|
|
47
|
+
line = line.trim();
|
|
48
|
+
if (!line) return;
|
|
49
|
+
if (!readyReceived) {
|
|
50
|
+
if (line === 'READY') {
|
|
51
|
+
readyReceived = true;
|
|
52
|
+
this.ready = true;
|
|
53
|
+
resolve();
|
|
54
|
+
} else {
|
|
55
|
+
reject(new Error(`Daemon failed to start: ${line}`));
|
|
56
|
+
this._cleanup();
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this._handleResponse(line);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
this.process.stderr.on('data', (data) => {
|
|
64
|
+
if (this.options.verbose) console.error('[sysinfo-daemon stderr]', data.toString().trim());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.process.on('exit', (code) => {
|
|
68
|
+
this.ready = false;
|
|
69
|
+
this.process = null;
|
|
70
|
+
rl.close();
|
|
71
|
+
const err = new Error(`SysInfoDaemon exited with code ${code}`);
|
|
72
|
+
this._rejectAllPending(err);
|
|
73
|
+
if (!this.closed && this.restartCount < this.maxRestarts) {
|
|
74
|
+
this.restartCount++;
|
|
75
|
+
if (this.options.verbose) console.error(`[sysinfo-daemon] exited, restarting (${this.restartCount}/${this.maxRestarts})`);
|
|
76
|
+
this.start().catch(() => {});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.process.on('error', (err) => {
|
|
81
|
+
if (!readyReceived) reject(err);
|
|
82
|
+
this._cleanup();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return this.readyPromise;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 通用请求方法
|
|
90
|
+
async _request(command) {
|
|
91
|
+
if (this.closed) throw new Error('Daemon is closed');
|
|
92
|
+
if (!this.process || !this.ready) await this.start();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
this.queue.push({ command, resolve, reject });
|
|
95
|
+
this._processQueue();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 各模块查询方法
|
|
100
|
+
async getSystem() { return this._request('system'); }
|
|
101
|
+
async getMemory() { return this._request('memory'); }
|
|
102
|
+
async getDisk() { return this._request('disk'); }
|
|
103
|
+
async getNetwork(options = {}) {
|
|
104
|
+
const includeWifi = options.includeWifi || false;
|
|
105
|
+
return this._request(`network:${includeWifi}`);
|
|
106
|
+
}
|
|
107
|
+
async getBattery() { return this._request('battery'); }
|
|
108
|
+
async getGpu() { return this._request('gpu'); }
|
|
109
|
+
async getUsb() { return this._request('usb'); }
|
|
110
|
+
|
|
111
|
+
async getProcesses(options = {}) {
|
|
112
|
+
const top = options.top || 0;
|
|
113
|
+
const sortBy = options.sortBy || 'memory';
|
|
114
|
+
const includeCmdLine = options.includeCommandLine || false;
|
|
115
|
+
return this._request(`processes:${top}:${sortBy}:${includeCmdLine}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getServices(options = {}) {
|
|
119
|
+
const runningOnly = options.runningOnly || false;
|
|
120
|
+
return this._request(`services:${runningOnly}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getAll(options = {}) {
|
|
124
|
+
const includeProcesses = options.includeProcesses || false;
|
|
125
|
+
const includeServices = options.includeServices || false;
|
|
126
|
+
return this._request(`all:${includeProcesses}:${includeServices}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async close() {
|
|
130
|
+
this.closed = true;
|
|
131
|
+
if (this.process) {
|
|
132
|
+
try { this.process.stdin.write('exit\n'); } catch (e) {}
|
|
133
|
+
await new Promise((resolve) => {
|
|
134
|
+
const timer = setTimeout(() => { if (this.process) this.process.kill(); resolve(); }, 2000);
|
|
135
|
+
this.process.on('exit', () => { clearTimeout(timer); resolve(); });
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
this._cleanup();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_processQueue() {
|
|
142
|
+
if (this.currentRequest || this.queue.length === 0) return;
|
|
143
|
+
if (!this.process || !this.ready) return;
|
|
144
|
+
this.currentRequest = this.queue.shift();
|
|
145
|
+
try {
|
|
146
|
+
this.process.stdin.write(this.currentRequest.command + '\n');
|
|
147
|
+
} catch (e) {
|
|
148
|
+
this.currentRequest.reject(e);
|
|
149
|
+
this.currentRequest = null;
|
|
150
|
+
this._processQueue();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_handleResponse(line) {
|
|
155
|
+
if (!this.currentRequest) return;
|
|
156
|
+
try {
|
|
157
|
+
const data = JSON.parse(line);
|
|
158
|
+
if (data.error) {
|
|
159
|
+
this.currentRequest.reject(new Error(data.error));
|
|
160
|
+
} else {
|
|
161
|
+
this.currentRequest.resolve(data);
|
|
162
|
+
}
|
|
163
|
+
} catch (e) {
|
|
164
|
+
this.currentRequest.reject(new Error(`Failed to parse daemon response: ${e.message}`));
|
|
165
|
+
}
|
|
166
|
+
this.currentRequest = null;
|
|
167
|
+
this._processQueue();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_rejectAllPending(err) {
|
|
171
|
+
if (this.currentRequest) { this.currentRequest.reject(err); this.currentRequest = null; }
|
|
172
|
+
while (this.queue.length > 0) { this.queue.shift().reject(err); }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_cleanup() {
|
|
176
|
+
this.ready = false;
|
|
177
|
+
this.process = null;
|
|
178
|
+
this.currentRequest = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function createSysInfoDaemon(options = {}) {
|
|
183
|
+
const daemon = new SysInfoDaemon(options);
|
|
184
|
+
await daemon.start();
|
|
185
|
+
return daemon;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { SysInfoDaemon, createSysInfoDaemon };
|