@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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
const SCRIPT = path.join(__dirname, '..', 'scripts', 'ohm-daemon.ps1');
|
|
6
|
+
|
|
7
|
+
class CpuTempDaemon {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.process = null;
|
|
11
|
+
this.ready = false;
|
|
12
|
+
this.readyPromise = null;
|
|
13
|
+
this.queue = [];
|
|
14
|
+
this.currentRequest = null;
|
|
15
|
+
this.closed = false;
|
|
16
|
+
this.restartCount = 0;
|
|
17
|
+
this.maxRestarts = options.maxRestarts || 3;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 启动常驻进程,等待 READY 信号
|
|
22
|
+
*/
|
|
23
|
+
async start() {
|
|
24
|
+
if (this.process) {
|
|
25
|
+
if (this.ready) return;
|
|
26
|
+
return this.readyPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
30
|
+
this.process = spawn('powershell', [
|
|
31
|
+
'-NoProfile',
|
|
32
|
+
'-ExecutionPolicy', 'Bypass',
|
|
33
|
+
'-File', SCRIPT
|
|
34
|
+
], {
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const rl = readline.createInterface({
|
|
39
|
+
input: this.process.stdout,
|
|
40
|
+
terminal: false
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let readyReceived = false;
|
|
44
|
+
|
|
45
|
+
rl.on('line', (line) => {
|
|
46
|
+
line = line.trim();
|
|
47
|
+
if (!line) return;
|
|
48
|
+
|
|
49
|
+
if (!readyReceived) {
|
|
50
|
+
if (line === 'READY') {
|
|
51
|
+
readyReceived = true;
|
|
52
|
+
this.ready = true;
|
|
53
|
+
resolve();
|
|
54
|
+
} else {
|
|
55
|
+
// 第一行不是 READY,可能是错误
|
|
56
|
+
reject(new Error(`Daemon failed to start: ${line}`));
|
|
57
|
+
this._cleanup();
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 正常响应
|
|
63
|
+
this._handleResponse(line);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.process.stderr.on('data', (data) => {
|
|
67
|
+
const msg = data.toString().trim();
|
|
68
|
+
if (msg) {
|
|
69
|
+
// stderr 输出,记录但不中断
|
|
70
|
+
if (this.options.verbose) console.error('[daemon stderr]', msg);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
this.process.on('exit', (code) => {
|
|
75
|
+
this.ready = false;
|
|
76
|
+
this.process = null;
|
|
77
|
+
rl.close();
|
|
78
|
+
|
|
79
|
+
// 拒绝所有 pending 请求
|
|
80
|
+
const err = new Error(`Daemon exited with code ${code}`);
|
|
81
|
+
this._rejectAllPending(err);
|
|
82
|
+
|
|
83
|
+
if (!this.closed && this.restartCount < this.maxRestarts) {
|
|
84
|
+
this.restartCount++;
|
|
85
|
+
if (this.options.verbose) console.error(`[daemon] exited, restarting (${this.restartCount}/${this.maxRestarts})`);
|
|
86
|
+
this.start().catch(() => {});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.process.on('error', (err) => {
|
|
91
|
+
if (!readyReceived) {
|
|
92
|
+
reject(err);
|
|
93
|
+
}
|
|
94
|
+
this._cleanup();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return this.readyPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取一次 CPU 温度数据
|
|
103
|
+
*/
|
|
104
|
+
async getCpuTemp() {
|
|
105
|
+
if (this.closed) {
|
|
106
|
+
throw new Error('Daemon is closed');
|
|
107
|
+
}
|
|
108
|
+
if (!this.process || !this.ready) {
|
|
109
|
+
await this.start();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
this.queue.push({ resolve, reject, timestamp: Date.now() });
|
|
114
|
+
this._processQueue();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 关闭常驻进程
|
|
120
|
+
*/
|
|
121
|
+
async close() {
|
|
122
|
+
this.closed = true;
|
|
123
|
+
if (this.process) {
|
|
124
|
+
try {
|
|
125
|
+
this.process.stdin.write('exit\n');
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
// 等 2 秒,不退出就强杀
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
const timer = setTimeout(() => {
|
|
132
|
+
if (this.process) {
|
|
133
|
+
this.process.kill();
|
|
134
|
+
}
|
|
135
|
+
resolve();
|
|
136
|
+
}, 2000);
|
|
137
|
+
this.process.on('exit', () => {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
this._cleanup();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
_processQueue() {
|
|
147
|
+
if (this.currentRequest || this.queue.length === 0) return;
|
|
148
|
+
if (!this.process || !this.ready) return;
|
|
149
|
+
|
|
150
|
+
this.currentRequest = this.queue.shift();
|
|
151
|
+
try {
|
|
152
|
+
this.process.stdin.write('get\n');
|
|
153
|
+
} catch (e) {
|
|
154
|
+
this.currentRequest.reject(e);
|
|
155
|
+
this.currentRequest = null;
|
|
156
|
+
this._processQueue();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_handleResponse(line) {
|
|
161
|
+
if (!this.currentRequest) {
|
|
162
|
+
// 没有等待中的请求,忽略
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const data = JSON.parse(line);
|
|
168
|
+
if (data.error) {
|
|
169
|
+
this.currentRequest.reject(new Error(data.error));
|
|
170
|
+
} else {
|
|
171
|
+
this.currentRequest.resolve(data);
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
this.currentRequest.reject(new Error(`Failed to parse daemon response: ${e.message}`));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.currentRequest = null;
|
|
178
|
+
this._processQueue();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
_rejectAllPending(err) {
|
|
182
|
+
if (this.currentRequest) {
|
|
183
|
+
this.currentRequest.reject(err);
|
|
184
|
+
this.currentRequest = null;
|
|
185
|
+
}
|
|
186
|
+
while (this.queue.length > 0) {
|
|
187
|
+
const req = this.queue.shift();
|
|
188
|
+
req.reject(err);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_cleanup() {
|
|
193
|
+
this.ready = false;
|
|
194
|
+
this.process = null;
|
|
195
|
+
this.currentRequest = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 创建一个常驻 CPU 温度监控实例
|
|
201
|
+
*/
|
|
202
|
+
async function createCpuTempDaemon(options = {}) {
|
|
203
|
+
const daemon = new CpuTempDaemon(options);
|
|
204
|
+
await daemon.start();
|
|
205
|
+
return daemon;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = { CpuTempDaemon, createCpuTempDaemon };
|
package/lib/ohm.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const SCRIPT = path.join(__dirname, '..', 'scripts', 'get-lhm-temp.ps1');
|
|
5
|
+
|
|
6
|
+
function readOhmTemp() {
|
|
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(`OHM exited ${code}: ${err || out}`));
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const data = JSON.parse(out.trim());
|
|
24
|
+
if (data.error) {
|
|
25
|
+
return reject(new Error(`OHM: ${data.error}`));
|
|
26
|
+
}
|
|
27
|
+
const cores = data.temperature?.cores;
|
|
28
|
+
const hasCores = Array.isArray(cores) && cores.length > 0;
|
|
29
|
+
const hasPackage = data.temperature?.package != null;
|
|
30
|
+
if (!data.temperature || (!hasCores && !hasPackage)) {
|
|
31
|
+
return reject(new Error('OHM: no temperature sensors (likely no admin privileges)'));
|
|
32
|
+
}
|
|
33
|
+
resolve(data);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
reject(new Error(`OHM parse error: ${e.message}`));
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { readOhmTemp };
|
package/lib/perfctr.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
|
|
3
|
+
const PS_CMD = [
|
|
4
|
+
'-NoProfile',
|
|
5
|
+
'-Command',
|
|
6
|
+
'Get-CimInstance Win32_PerfFormattedData_Counters_ThermalZoneInformation -Namespace "root\\cimv2" | Select-Object Name, HighPrecisionTemperature, Temperature | ConvertTo-Json'
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function readPerfCounterTemp() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const ps = spawn('powershell', PS_CMD);
|
|
12
|
+
let out = '';
|
|
13
|
+
let err = '';
|
|
14
|
+
ps.stdout.on('data', d => (out += d));
|
|
15
|
+
ps.stderr.on('data', d => (err += d));
|
|
16
|
+
ps.on('close', code => {
|
|
17
|
+
if (code !== 0) {
|
|
18
|
+
return reject(new Error(`PerfCounter exited ${code}: ${err || out}`));
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const trimmed = out.trim();
|
|
22
|
+
if (!trimmed) return reject(new Error('PerfCounter: empty output'));
|
|
23
|
+
const data = JSON.parse(trimmed);
|
|
24
|
+
const list = Array.isArray(data) ? data : [data];
|
|
25
|
+
const zones = list.map(item => ({
|
|
26
|
+
zone: item.Name,
|
|
27
|
+
celsius: Math.round(((item.HighPrecisionTemperature / 10) - 273.15) * 100) / 100
|
|
28
|
+
}));
|
|
29
|
+
if (zones.length === 0) return reject(new Error('PerfCounter: no thermal zones'));
|
|
30
|
+
resolve({
|
|
31
|
+
source: 'performance_counter',
|
|
32
|
+
isCoreTemp: false,
|
|
33
|
+
zones,
|
|
34
|
+
maxZone: Math.round(Math.max(...zones.map(z => z.celsius)) * 100) / 100
|
|
35
|
+
});
|
|
36
|
+
} catch (e) {
|
|
37
|
+
reject(new Error(`PerfCounter parse error: ${e.message}`));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { readPerfCounterTemp };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-icon.js — 进程图标提取
|
|
3
|
+
*
|
|
4
|
+
* 用 PowerShell System.Drawing 提取 exe 关联图标,输出 base64 PNG。
|
|
5
|
+
* 按需调用,不进高频路径(图标是静态属性,建议调用方缓存)。
|
|
6
|
+
*
|
|
7
|
+
* 支持批量提取:路径列表走 stdin,无命令行长度限制。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
const { existsSync } = require('fs');
|
|
12
|
+
|
|
13
|
+
const PS_SCRIPT = [
|
|
14
|
+
'Add-Type -AssemblyName System.Drawing;',
|
|
15
|
+
'$paths = [Console]::In.ReadToEnd() | ConvertFrom-Json;',
|
|
16
|
+
'$out = @{};',
|
|
17
|
+
'foreach ($p in $paths) {',
|
|
18
|
+
' $b64 = \'\';',
|
|
19
|
+
' try {',
|
|
20
|
+
' $ico = [System.Drawing.Icon]::ExtractAssociatedIcon($p);',
|
|
21
|
+
' $bmp = $ico.ToBitmap();',
|
|
22
|
+
' $ms = New-Object System.IO.MemoryStream;',
|
|
23
|
+
' $bmp.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png);',
|
|
24
|
+
' $b64 = [Convert]::ToBase64String($ms.ToArray());',
|
|
25
|
+
' $ms.Dispose(); $bmp.Dispose(); $ico.Dispose();',
|
|
26
|
+
' } catch { };',
|
|
27
|
+
' $out[$p] = $b64;',
|
|
28
|
+
'};',
|
|
29
|
+
'Write-Output (ConvertTo-Json -InputObject $out -Compress);'
|
|
30
|
+
].join(' ');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 批量提取进程图标
|
|
34
|
+
* @param {string[]} paths - exe 路径数组
|
|
35
|
+
* @returns {Promise<Record<string, string>>} 路径 → base64 PNG(提取失败为空串)
|
|
36
|
+
*/
|
|
37
|
+
function getProcessIcons(paths) {
|
|
38
|
+
const valid = paths.filter((p) => typeof p === 'string' && p && existsSync(p));
|
|
39
|
+
if (!valid.length) return Promise.resolve({});
|
|
40
|
+
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const ps = spawn('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', PS_SCRIPT], {
|
|
43
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
let out = '';
|
|
47
|
+
ps.stdout.setEncoding('utf8');
|
|
48
|
+
ps.stdout.on('data', (c) => { out += c; });
|
|
49
|
+
ps.on('error', reject);
|
|
50
|
+
ps.on('close', (code) => {
|
|
51
|
+
if (code !== 0) {
|
|
52
|
+
reject(new Error(`PowerShell exited ${code}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const map = JSON.parse(out.trim());
|
|
57
|
+
resolve(map);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
reject(new Error('图标数据解析失败: ' + e.message));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
ps.stdin.end(JSON.stringify(valid));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 提取单个进程图标
|
|
68
|
+
* @param {string} exePath - exe 路径
|
|
69
|
+
* @returns {Promise<string>} base64 PNG(提取失败为空串)
|
|
70
|
+
*/
|
|
71
|
+
async function getProcessIcon(exePath) {
|
|
72
|
+
const map = await getProcessIcons([exePath]);
|
|
73
|
+
return map[exePath] || '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { getProcessIcon, getProcessIcons };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* process-ops.js — 进程操作
|
|
3
|
+
*
|
|
4
|
+
* - killProcess: fkill(跨平台成熟包,force+tree,Windows 内部调 taskkill)
|
|
5
|
+
* - showItemInFolder: explorer /select,<path>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { execFile, exec } = require('child_process');
|
|
9
|
+
const { existsSync } = require('fs');
|
|
10
|
+
const { sampleProcesses } = require('./win32-procs');
|
|
11
|
+
|
|
12
|
+
// fkill v10 是 ESM only,动态 import 并缓存
|
|
13
|
+
let fkillPromise = null;
|
|
14
|
+
function getFkill() {
|
|
15
|
+
if (!fkillPromise) {
|
|
16
|
+
fkillPromise = import('fkill').then((m) => m.default);
|
|
17
|
+
}
|
|
18
|
+
return fkillPromise;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 查找占用指定端口的进程 PID(Windows netstat,零依赖)
|
|
23
|
+
* @param {number} port - 端口号
|
|
24
|
+
* @param {'tcp'|'udp'|'all'} [protocol='all'] - 协议类型
|
|
25
|
+
* @returns {Promise<number[]>} PID 数组(去重)
|
|
26
|
+
*/
|
|
27
|
+
function findPidsByPort(port, protocol = 'all') {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const p = Number(port);
|
|
30
|
+
if (!p || p < 1 || p > 65535) {
|
|
31
|
+
resolve([]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// netstat -ano 同时包含 TCP 和 UDP,输出最后一列是 PID
|
|
35
|
+
exec('netstat -ano', { windowsHide: true }, (err, stdout) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
reject(err);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const pids = new Set();
|
|
41
|
+
const lines = stdout.split('\n');
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const trimmed = line.trim();
|
|
44
|
+
if (!trimmed) continue;
|
|
45
|
+
// 跳过表头
|
|
46
|
+
if (trimmed.startsWith('Proto') || trimmed.startsWith('活动连接')) continue;
|
|
47
|
+
const parts = trimmed.split(/\s+/);
|
|
48
|
+
if (parts.length < 5) continue;
|
|
49
|
+
const proto = parts[0].toUpperCase(); // TCP / UDP
|
|
50
|
+
const localAddr = parts[1]; // 0.0.0.0:3000 或 [::]:3000
|
|
51
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
52
|
+
if (!pid || pid <= 0) continue;
|
|
53
|
+
// 协议过滤
|
|
54
|
+
if (protocol === 'tcp' && proto !== 'TCP') continue;
|
|
55
|
+
if (protocol === 'udp' && proto !== 'UDP') continue;
|
|
56
|
+
// 本地端口匹配(支持 IPv4 和 IPv6)
|
|
57
|
+
const portMatch = localAddr.match(/:(\d+)$/);
|
|
58
|
+
if (portMatch && parseInt(portMatch[1], 10) === p) {
|
|
59
|
+
pids.add(pid);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
resolve([...pids]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 按进程名查找 PID(koffi 进程枚举,不区分大小写,支持 .exe 后缀)
|
|
69
|
+
* @param {string|string[]} name - 进程名或进程名数组
|
|
70
|
+
* @returns {number[]} PID 数组(去重)
|
|
71
|
+
*/
|
|
72
|
+
function findPidsByName(name) {
|
|
73
|
+
const names = (Array.isArray(name) ? name : [name])
|
|
74
|
+
.map((n) => String(n).toLowerCase().replace(/\.exe$/i, ''));
|
|
75
|
+
if (!names.length) return [];
|
|
76
|
+
const all = sampleProcesses();
|
|
77
|
+
const pids = new Set();
|
|
78
|
+
for (const p of all) {
|
|
79
|
+
const procName = p.name.toLowerCase().replace(/\.exe$/i, '');
|
|
80
|
+
if (names.includes(procName)) {
|
|
81
|
+
pids.add(p.pid);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...pids];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 结束进程(含子进程树),支持按 PID、端口号或进程名
|
|
89
|
+
* @param {number|number[]|string|string[]|Object} input - 杀进程目标
|
|
90
|
+
* - number: 按 PID 杀
|
|
91
|
+
* - number[]: 批量按 PID 杀
|
|
92
|
+
* - string: 按进程名杀(如 'chrome'、'notepad')
|
|
93
|
+
* - string[]: 批量按进程名杀
|
|
94
|
+
* - { pid: number|number[] }: 按 PID 杀
|
|
95
|
+
* - { port: number|number[], protocol?: 'tcp'|'udp'|'all' }: 按端口杀
|
|
96
|
+
* - { name: string|string[] }: 按进程名杀
|
|
97
|
+
* @returns {Promise<{ok: boolean, killed: number, failed: number, pids?: number[], message?: string}>}
|
|
98
|
+
*/
|
|
99
|
+
async function killProcess(input) {
|
|
100
|
+
let pids = [];
|
|
101
|
+
let ports = [];
|
|
102
|
+
let names = [];
|
|
103
|
+
let protocol = 'all';
|
|
104
|
+
|
|
105
|
+
// 解析输入
|
|
106
|
+
if (typeof input === 'number') {
|
|
107
|
+
pids = [input];
|
|
108
|
+
} else if (typeof input === 'string') {
|
|
109
|
+
names = [input];
|
|
110
|
+
} else if (Array.isArray(input)) {
|
|
111
|
+
// 数组元素可能是 PID(数字)或进程名(字符串)
|
|
112
|
+
for (const item of input) {
|
|
113
|
+
if (typeof item === 'number') pids.push(item);
|
|
114
|
+
else if (typeof item === 'string') names.push(item);
|
|
115
|
+
}
|
|
116
|
+
} else if (input && typeof input === 'object') {
|
|
117
|
+
if (input.pid !== undefined) {
|
|
118
|
+
pids = Array.isArray(input.pid) ? input.pid : [input.pid];
|
|
119
|
+
}
|
|
120
|
+
if (input.name !== undefined) {
|
|
121
|
+
names = Array.isArray(input.name) ? input.name : [input.name];
|
|
122
|
+
}
|
|
123
|
+
if (input.port !== undefined) {
|
|
124
|
+
ports = Array.isArray(input.port) ? input.port : [input.port];
|
|
125
|
+
if (input.protocol) protocol = input.protocol;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
return { ok: false, killed: 0, failed: 0, message: '无效的参数(需要 PID、端口号或进程名)' };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 按进程名查找 PID
|
|
132
|
+
if (names.length) {
|
|
133
|
+
const found = findPidsByName(names);
|
|
134
|
+
pids.push(...found);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 按端口查找 PID
|
|
138
|
+
if (ports.length) {
|
|
139
|
+
for (const port of ports) {
|
|
140
|
+
const found = await findPidsByPort(port, protocol);
|
|
141
|
+
pids.push(...found);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 去重 + 过滤无效 PID
|
|
146
|
+
const list = [...new Set(pids.map(Number).filter((n) => n > 0))];
|
|
147
|
+
if (!list.length) {
|
|
148
|
+
const reason = ports.length ? '未找到占用指定端口的进程' : names.length ? '未找到匹配进程名的进程' : '无效的 PID';
|
|
149
|
+
return { ok: false, killed: 0, failed: 0, message: reason };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 拒绝自杀(Node 主进程)和父进程
|
|
153
|
+
if (list.includes(process.pid)) {
|
|
154
|
+
return { ok: false, killed: 0, failed: list.length, message: '不能结束当前进程自身' };
|
|
155
|
+
}
|
|
156
|
+
if (process.ppid && list.includes(process.ppid)) {
|
|
157
|
+
return { ok: false, killed: 0, failed: list.length, message: '不能结束当前进程的父进程' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fkill = await getFkill();
|
|
161
|
+
let killed = 0;
|
|
162
|
+
let failed = 0;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// fkill 支持一次传 PID 数组,内部并行处理
|
|
166
|
+
await fkill(list, { force: true, tree: true });
|
|
167
|
+
killed = list.length;
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// fkill 失败时抛 AggregateError,errors 数组包含每个 PID 的具体错误
|
|
170
|
+
if (e.name === 'AggregateError' && Array.isArray(e.errors)) {
|
|
171
|
+
for (const err of e.errors) {
|
|
172
|
+
const msg = String(err).toLowerCase();
|
|
173
|
+
// 进程已不存在,视为达成目标
|
|
174
|
+
if (msg.includes("doesn't exist") || msg.includes('not found') || msg.includes('不存在')) {
|
|
175
|
+
killed++;
|
|
176
|
+
} else {
|
|
177
|
+
failed++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
failed = list.length;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (killed > 0) {
|
|
186
|
+
return { ok: true, killed, failed, pids: list };
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
killed: 0,
|
|
191
|
+
failed,
|
|
192
|
+
pids: list,
|
|
193
|
+
message: '结束失败:权限不足或进程受系统保护,请以管理员身份运行'
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 在资源管理器中打开并定位到文件
|
|
199
|
+
* @param {string} filePath - 文件完整路径
|
|
200
|
+
* @returns {{ok: boolean, message?: string}}
|
|
201
|
+
*/
|
|
202
|
+
function showItemInFolder(filePath) {
|
|
203
|
+
if (typeof filePath !== 'string' || !filePath) {
|
|
204
|
+
return { ok: false, message: '路径无效' };
|
|
205
|
+
}
|
|
206
|
+
if (!existsSync(filePath)) {
|
|
207
|
+
return { ok: false, message: '文件不存在(进程可能已退出)' };
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
// explorer /select, 路径 — 打开资源管理器并选中该文件
|
|
211
|
+
execFile('explorer', ['/select,', filePath], () => {});
|
|
212
|
+
return { ok: true };
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return { ok: false, message: e.message };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = { killProcess, showItemInFolder, findPidsByPort, findPidsByName };
|