mihomo-cli 1.5.1 → 2.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/CHANGELOG.md +20 -0
- package/README.md +3 -2
- package/dist/index.js +5141 -0
- package/package.json +23 -16
- package/index.js +0 -1176
- package/src/config.js +0 -516
- package/src/kernel.js +0 -250
- package/src/overwrite.js +0 -258
- package/src/process.js +0 -691
- package/src/subscription.js +0 -257
- package/src/utils.js +0 -202
package/src/process.js
DELETED
|
@@ -1,691 +0,0 @@
|
|
|
1
|
-
// 内置模块
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { spawn, execSync } = require('child_process');
|
|
5
|
-
|
|
6
|
-
// 第三方模块
|
|
7
|
-
// (无第三方模块依赖)
|
|
8
|
-
|
|
9
|
-
// 本地模块
|
|
10
|
-
const config = require('./config');
|
|
11
|
-
const utils = require('./utils');
|
|
12
|
-
|
|
13
|
-
// 进程等待常量
|
|
14
|
-
const PROCESS_WAIT_ATTEMPTS = 50;
|
|
15
|
-
const PROCESS_WAIT_INTERVAL = 100; // ms
|
|
16
|
-
const STARTUP_WAIT_MS = 800; // ms
|
|
17
|
-
const SUDO_TIMEOUT_MS = 60000; // ms
|
|
18
|
-
const TUN_MODE_POST_WAIT_MS = 500; // ms
|
|
19
|
-
const BATCH_KILL_THRESHOLD = 3;
|
|
20
|
-
|
|
21
|
-
// 日志清理常量
|
|
22
|
-
const DEFAULT_LOG_RETENTION_DAYS = 7;
|
|
23
|
-
|
|
24
|
-
function clearRuntime() {
|
|
25
|
-
if (fs.existsSync(config.DIRS.runtime)) {
|
|
26
|
-
config.rmrf(config.DIRS.runtime);
|
|
27
|
-
}
|
|
28
|
-
config.ensureDirs();
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getPid() {
|
|
32
|
-
if (!fs.existsSync(config.PATHS.pidFile)) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const pid = parseInt(fs.readFileSync(config.PATHS.pidFile, 'utf8').trim());
|
|
37
|
-
return pid > 0 ? pid : null;
|
|
38
|
-
} catch (_e) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isRunning() {
|
|
44
|
-
const pid = getPid();
|
|
45
|
-
return pid ? utils.isProcessRunning(pid) : false;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function getAllMihomoPids() {
|
|
49
|
-
const binaryPath = config.PATHS.mihomoBinary;
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const output = execSync('pgrep -f "' + binaryPath + '" 2>/dev/null || true', {
|
|
53
|
-
encoding: 'utf8',
|
|
54
|
-
}).trim();
|
|
55
|
-
if (!output) return [];
|
|
56
|
-
return output
|
|
57
|
-
.split('\n')
|
|
58
|
-
.filter(Boolean)
|
|
59
|
-
.map(p => parseInt(p));
|
|
60
|
-
} catch {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isPidFileOwnedByRoot() {
|
|
66
|
-
if (!fs.existsSync(config.PATHS.pidFile)) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
const stat = fs.statSync(config.PATHS.pidFile);
|
|
71
|
-
return stat.uid === 0;
|
|
72
|
-
} catch (_e) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function checkStaleState() {
|
|
78
|
-
const allPids = getAllMihomoPids();
|
|
79
|
-
const hasRootProcess = allPids.some(p => utils.isProcessRoot(p));
|
|
80
|
-
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
needsCleanup: allPids.length > 0 || hasRootPidFile,
|
|
84
|
-
allPids,
|
|
85
|
-
hasRootProcess,
|
|
86
|
-
hasRootPidFile,
|
|
87
|
-
needsSudo: hasRootProcess || hasRootPidFile,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function savePid(pid) {
|
|
92
|
-
config.ensureDirs();
|
|
93
|
-
fs.writeFileSync(config.PATHS.pidFile, pid.toString(), { mode: 0o600 });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function clearPid() {
|
|
97
|
-
if (!fs.existsSync(config.PATHS.pidFile)) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
if (isPidFileOwnedByRoot()) {
|
|
101
|
-
try {
|
|
102
|
-
execSync('sudo rm -f "' + config.PATHS.pidFile + '" 2>/dev/null', {
|
|
103
|
-
stdio: 'inherit',
|
|
104
|
-
timeout: 10000,
|
|
105
|
-
});
|
|
106
|
-
} catch (_e) {
|
|
107
|
-
// 忽略失败,后续操作可能会检测到问题
|
|
108
|
-
}
|
|
109
|
-
} else {
|
|
110
|
-
try {
|
|
111
|
-
fs.unlinkSync(config.PATHS.pidFile);
|
|
112
|
-
} catch (_e) {
|
|
113
|
-
// ignore
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function killProcess(pid, needsSudo) {
|
|
119
|
-
if (needsSudo === undefined) needsSudo = false;
|
|
120
|
-
try {
|
|
121
|
-
if (needsSudo) {
|
|
122
|
-
try {
|
|
123
|
-
execSync('sudo kill -9 ' + pid + ' 2>/dev/null', {
|
|
124
|
-
stdio: 'inherit',
|
|
125
|
-
timeout: 10000,
|
|
126
|
-
});
|
|
127
|
-
return true;
|
|
128
|
-
} catch (_e) {
|
|
129
|
-
try {
|
|
130
|
-
process.kill(pid, 'SIGKILL');
|
|
131
|
-
return true;
|
|
132
|
-
} catch (_e2) {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} else {
|
|
137
|
-
process.kill(pid, 'SIGKILL');
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
} catch (_e) {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function killAllMihomo(forceSudo) {
|
|
146
|
-
if (forceSudo === undefined) forceSudo = false;
|
|
147
|
-
const binaryPath = config.PATHS.mihomoBinary;
|
|
148
|
-
|
|
149
|
-
if (forceSudo) {
|
|
150
|
-
try {
|
|
151
|
-
execSync('sudo pkill -9 -f "' + binaryPath + '" 2>/dev/null || true', {
|
|
152
|
-
stdio: 'inherit',
|
|
153
|
-
timeout: 15000,
|
|
154
|
-
});
|
|
155
|
-
return true;
|
|
156
|
-
} catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
try {
|
|
161
|
-
execSync('pkill -9 -f "' + binaryPath + '" 2>/dev/null || true');
|
|
162
|
-
return true;
|
|
163
|
-
} catch {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function cleanupAll(forceSudo) {
|
|
170
|
-
if (forceSudo === undefined) forceSudo = false;
|
|
171
|
-
const pids = getAllMihomoPids();
|
|
172
|
-
if (pids.length === 0) {
|
|
173
|
-
clearPid();
|
|
174
|
-
return { killed: 0, failed: 0, remaining: [] };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
|
|
178
|
-
const needsSudo = hasRootProcess;
|
|
179
|
-
|
|
180
|
-
let killedCount = 0;
|
|
181
|
-
let failedPids = [];
|
|
182
|
-
|
|
183
|
-
if (needsSudo) {
|
|
184
|
-
const success = killAllMihomo(true);
|
|
185
|
-
if (success) {
|
|
186
|
-
killedCount = pids.length;
|
|
187
|
-
} else {
|
|
188
|
-
failedPids = pids;
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
if (pids.length > BATCH_KILL_THRESHOLD) {
|
|
192
|
-
killAllMihomo(false);
|
|
193
|
-
killedCount = pids.length;
|
|
194
|
-
} else {
|
|
195
|
-
pids.forEach(pid => {
|
|
196
|
-
if (killProcess(pid, false)) {
|
|
197
|
-
killedCount++;
|
|
198
|
-
} else {
|
|
199
|
-
failedPids.push(pid);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 等待进程终止
|
|
206
|
-
for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
|
|
207
|
-
if (getAllMihomoPids().length === 0) break;
|
|
208
|
-
utils.sleepSync(PROCESS_WAIT_INTERVAL);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
clearPid();
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
killed: killedCount,
|
|
215
|
-
failed: failedPids.length,
|
|
216
|
-
remaining: getAllMihomoPids(),
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function createTunLaunchScript() {
|
|
221
|
-
const binary = config.PATHS.mihomoBinary;
|
|
222
|
-
const configFile = config.PATHS.configFile;
|
|
223
|
-
const logFile = config.PATHS.logFile;
|
|
224
|
-
const pidFile = config.PATHS.pidFile;
|
|
225
|
-
const dataDir = config.DIRS.data;
|
|
226
|
-
|
|
227
|
-
const scriptContent =
|
|
228
|
-
'#!/bin/bash\n' +
|
|
229
|
-
'BINARY="' +
|
|
230
|
-
binary +
|
|
231
|
-
'"\n' +
|
|
232
|
-
'CONFIG_FILE="' +
|
|
233
|
-
configFile +
|
|
234
|
-
'"\n' +
|
|
235
|
-
'LOG_FILE="' +
|
|
236
|
-
logFile +
|
|
237
|
-
'"\n' +
|
|
238
|
-
'PID_FILE="' +
|
|
239
|
-
pidFile +
|
|
240
|
-
'"\n' +
|
|
241
|
-
'DATA_DIR="' +
|
|
242
|
-
dataDir +
|
|
243
|
-
'"\n' +
|
|
244
|
-
'\n' +
|
|
245
|
-
'# 终止旧进程\n' +
|
|
246
|
-
'pkill -9 -f "${BINARY}" 2>/dev/null || true\n' +
|
|
247
|
-
'sleep 0.2\n' +
|
|
248
|
-
'rm -f "${PID_FILE}" 2>/dev/null || true\n' +
|
|
249
|
-
'\n' +
|
|
250
|
-
'# 写入启动标记\n' +
|
|
251
|
-
'echo "=== TUN 启动: $(date) ===" >> "${LOG_FILE}"\n' +
|
|
252
|
-
'\n' +
|
|
253
|
-
'# 启动\n' +
|
|
254
|
-
'cd /tmp\n' +
|
|
255
|
-
'"${BINARY}" -d "${DATA_DIR}" -f "${CONFIG_FILE}" >> "${LOG_FILE}" 2>&1 &\n' +
|
|
256
|
-
'NEW_PID=$!\n' +
|
|
257
|
-
'echo ${NEW_PID} > "${PID_FILE}"\n' +
|
|
258
|
-
'\n' +
|
|
259
|
-
'# 验证\n' +
|
|
260
|
-
'for i in 1 2 3 4 5; do\n' +
|
|
261
|
-
' sleep 0.4\n' +
|
|
262
|
-
' if kill -0 ${NEW_PID} 2>/dev/null; then\n' +
|
|
263
|
-
' exit 0\n' +
|
|
264
|
-
' fi\n' +
|
|
265
|
-
'done\n' +
|
|
266
|
-
'\n' +
|
|
267
|
-
'# 失败,显示日志\n' +
|
|
268
|
-
'echo "TUN 启动失败"\n' +
|
|
269
|
-
'echo ""\n' +
|
|
270
|
-
'echo "--- 日志 ---"\n' +
|
|
271
|
-
'tail -25 "${LOG_FILE}" 2>/dev/null\n' +
|
|
272
|
-
'exit 1\n';
|
|
273
|
-
|
|
274
|
-
const scriptPath = path.join(config.DIRS.runtime, 'launch-tun.sh');
|
|
275
|
-
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o700 });
|
|
276
|
-
|
|
277
|
-
return scriptPath;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function getProcessInfo(pid) {
|
|
281
|
-
try {
|
|
282
|
-
const psOutput = execSync('ps -p ' + pid + ' -o rss=,pcpu=,comm= 2>/dev/null || true', {
|
|
283
|
-
encoding: 'utf8',
|
|
284
|
-
}).trim();
|
|
285
|
-
|
|
286
|
-
if (!psOutput) return null;
|
|
287
|
-
|
|
288
|
-
const parts = psOutput.split(/\s+/).filter(p => p);
|
|
289
|
-
if (parts.length < 2) return null;
|
|
290
|
-
|
|
291
|
-
const rss = parseInt(parts[0]);
|
|
292
|
-
const pcpu = parseFloat(parts[1]);
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
pid,
|
|
296
|
-
memory: rss ? (rss / 1024).toFixed(1) + ' MB' : '未知',
|
|
297
|
-
cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
|
|
298
|
-
isRoot: utils.isProcessRoot(pid),
|
|
299
|
-
};
|
|
300
|
-
} catch (_e) {
|
|
301
|
-
return { pid, memory: '未知', cpu: '未知', isRoot: false };
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function getStatus() {
|
|
306
|
-
const running = isRunning();
|
|
307
|
-
const pid = getPid();
|
|
308
|
-
const allPids = getAllMihomoPids();
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
running,
|
|
312
|
-
pid: running ? pid : null,
|
|
313
|
-
processInfo: running && pid ? getProcessInfo(pid) : null,
|
|
314
|
-
hasConfig: config.hasConfig(),
|
|
315
|
-
hasKernel: config.hasKernel(),
|
|
316
|
-
kernelVersion: config.getKernelVersion(),
|
|
317
|
-
allProcesses: allPids,
|
|
318
|
-
hasStaleProcesses: allPids.length > (running ? 1 : 0),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function start(mode) {
|
|
323
|
-
if (mode === undefined) mode = 'mixed';
|
|
324
|
-
const isTunMode = mode === 'tun';
|
|
325
|
-
|
|
326
|
-
config.ensureDirs();
|
|
327
|
-
rotateAndCleanupLogs();
|
|
328
|
-
|
|
329
|
-
const binary = config.PATHS.mihomoBinary;
|
|
330
|
-
if (!fs.existsSync(binary)) {
|
|
331
|
-
throw new Error('未找到 mihomo 内核,请先下载内核');
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const configFile = config.PATHS.configFile;
|
|
335
|
-
if (!fs.existsSync(configFile)) {
|
|
336
|
-
throw new Error('未找到配置文件,请先添加订阅并启动');
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const staleState = checkStaleState();
|
|
340
|
-
|
|
341
|
-
if (isTunMode) {
|
|
342
|
-
return startTunMode(staleState);
|
|
343
|
-
} else {
|
|
344
|
-
return startMixedMode(staleState);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async function startMixedMode(staleState) {
|
|
349
|
-
if (staleState.needsCleanup) {
|
|
350
|
-
if (staleState.needsSudo) {
|
|
351
|
-
console.log('\n发现需要 root 权限清理的残留进程/文件');
|
|
352
|
-
console.log('请先手动清理: sudo pkill -9 mihomo');
|
|
353
|
-
console.log('或者切换到 TUN 模式,启动时会自动清理');
|
|
354
|
-
throw new Error('存在需要 root 权限清理的残留');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const cleanupResult = cleanupAll();
|
|
358
|
-
if (cleanupResult.killed > 0) {
|
|
359
|
-
console.log('清理了 ' + cleanupResult.killed + ' 个残留进程');
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (isRunning()) {
|
|
364
|
-
const pid = getPid();
|
|
365
|
-
return { success: true, pid, alreadyRunning: true };
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const configFile = config.PATHS.configFile;
|
|
369
|
-
const logFile = config.PATHS.logFile;
|
|
370
|
-
|
|
371
|
-
const args = ['-d', config.DIRS.data, '-f', configFile];
|
|
372
|
-
|
|
373
|
-
const out = fs.openSync(logFile, 'a');
|
|
374
|
-
const err = fs.openSync(logFile, 'a');
|
|
375
|
-
|
|
376
|
-
const child = spawn(config.PATHS.mihomoBinary, args, {
|
|
377
|
-
detached: true,
|
|
378
|
-
stdio: ['ignore', out, err],
|
|
379
|
-
cwd: config.PATHS.root,
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
child.unref();
|
|
383
|
-
|
|
384
|
-
const pid = child.pid;
|
|
385
|
-
savePid(pid);
|
|
386
|
-
|
|
387
|
-
await new Promise(resolve => setTimeout(resolve, STARTUP_WAIT_MS));
|
|
388
|
-
|
|
389
|
-
if (!isRunning()) {
|
|
390
|
-
clearPid();
|
|
391
|
-
let errorMsg = '启动失败';
|
|
392
|
-
if (fs.existsSync(logFile)) {
|
|
393
|
-
try {
|
|
394
|
-
const logs = fs.readFileSync(logFile, 'utf8').slice(-3000);
|
|
395
|
-
if (logs.trim()) {
|
|
396
|
-
errorMsg +=
|
|
397
|
-
'\n最近的日志:\n' +
|
|
398
|
-
logs
|
|
399
|
-
.split('\n')
|
|
400
|
-
.map(l => ' ' + l)
|
|
401
|
-
.join('\n');
|
|
402
|
-
}
|
|
403
|
-
} catch {}
|
|
404
|
-
}
|
|
405
|
-
throw new Error(errorMsg);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
return { success: true, pid, mode: 'mixed' };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
async function startTunMode(staleState) {
|
|
412
|
-
const launchScript = createTunLaunchScript();
|
|
413
|
-
|
|
414
|
-
if (staleState.needsCleanup) {
|
|
415
|
-
console.log('清理 ' + staleState.allPids.length + ' 个残留进程...');
|
|
416
|
-
}
|
|
417
|
-
console.log('TUN 模式需要 sudo 权限...');
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
execSync('sudo "' + launchScript + '"', {
|
|
421
|
-
stdio: 'inherit',
|
|
422
|
-
timeout: SUDO_TIMEOUT_MS,
|
|
423
|
-
});
|
|
424
|
-
} catch (e) {
|
|
425
|
-
try {
|
|
426
|
-
fs.unlinkSync(launchScript);
|
|
427
|
-
} catch {}
|
|
428
|
-
if (e.status === 1) {
|
|
429
|
-
throw new Error('密码错误或取消');
|
|
430
|
-
}
|
|
431
|
-
throw new Error(e.message);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
fs.unlinkSync(launchScript);
|
|
436
|
-
} catch (_e) {}
|
|
437
|
-
|
|
438
|
-
await new Promise(resolve => setTimeout(resolve, TUN_MODE_POST_WAIT_MS));
|
|
439
|
-
|
|
440
|
-
const finalPid = getPid();
|
|
441
|
-
if (!finalPid) {
|
|
442
|
-
throw new Error('TUN 启动失败');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
return { success: true, pid: finalPid, mode: 'tun' };
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function stop(forceSudo) {
|
|
449
|
-
const allPids = getAllMihomoPids();
|
|
450
|
-
if (allPids.length === 0) {
|
|
451
|
-
clearPid();
|
|
452
|
-
clearRuntime();
|
|
453
|
-
return { success: true, notRunning: true };
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const result = cleanupAll(forceSudo);
|
|
457
|
-
|
|
458
|
-
const remaining = getAllMihomoPids();
|
|
459
|
-
if (remaining.length > 0) {
|
|
460
|
-
console.log('');
|
|
461
|
-
console.log('仍有进程残留,需要手动清理:');
|
|
462
|
-
console.log('进程 PID: ' + remaining.join(', '));
|
|
463
|
-
console.log('手动命令: sudo pkill -9 mihomo');
|
|
464
|
-
console.log('');
|
|
465
|
-
return { success: true, warning: '部分进程未终止', remaining };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
clearRuntime();
|
|
469
|
-
return { success: true, killed: result.killed };
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function rotateAndCleanupLogs() {
|
|
473
|
-
rotateLog();
|
|
474
|
-
cleanupOldLogs(DEFAULT_LOG_RETENTION_DAYS);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function getLogPath() {
|
|
478
|
-
return config.PATHS.logFile;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function rotateLog() {
|
|
482
|
-
const logFile = config.PATHS.logFile;
|
|
483
|
-
if (!fs.existsSync(logFile)) {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const stat = fs.statSync(logFile);
|
|
488
|
-
if (stat.size === 0) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const timestamp = new Date().toISOString().replace(/T/, '_').replace(/:/g, '-').replace(/\..+/, '');
|
|
493
|
-
|
|
494
|
-
const rotatedName = `mihomo.${timestamp}.log`;
|
|
495
|
-
const rotatedPath = path.join(config.DIRS.logs, rotatedName);
|
|
496
|
-
|
|
497
|
-
fs.renameSync(logFile, rotatedPath);
|
|
498
|
-
return rotatedPath;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function cleanupOldLogs(maxAgeDays) {
|
|
502
|
-
if (maxAgeDays === undefined) maxAgeDays = DEFAULT_LOG_RETENTION_DAYS;
|
|
503
|
-
const logsDir = config.DIRS.logs;
|
|
504
|
-
|
|
505
|
-
if (!fs.existsSync(logsDir)) {
|
|
506
|
-
return { deleted: 0, errors: 0 };
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const files = fs.readdirSync(logsDir);
|
|
510
|
-
const now = Date.now();
|
|
511
|
-
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
512
|
-
|
|
513
|
-
let deleted = 0;
|
|
514
|
-
let errors = 0;
|
|
515
|
-
|
|
516
|
-
for (const file of files) {
|
|
517
|
-
if (!file.match(/^mihomo\.\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/)) {
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
const filePath = path.join(logsDir, file);
|
|
523
|
-
const stat = fs.statSync(filePath);
|
|
524
|
-
const ageMs = now - stat.mtimeMs;
|
|
525
|
-
|
|
526
|
-
if (ageMs > maxAgeMs) {
|
|
527
|
-
fs.unlinkSync(filePath);
|
|
528
|
-
deleted++;
|
|
529
|
-
}
|
|
530
|
-
} catch (_e) {
|
|
531
|
-
errors++;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
return { deleted, errors };
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
function listLogs() {
|
|
539
|
-
const logsDir = config.DIRS.logs;
|
|
540
|
-
const result = {
|
|
541
|
-
current: null,
|
|
542
|
-
archives: [],
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
if (fs.existsSync(config.PATHS.logFile)) {
|
|
546
|
-
const stat = fs.statSync(config.PATHS.logFile);
|
|
547
|
-
result.current = {
|
|
548
|
-
name: 'mihomo.log (当前)',
|
|
549
|
-
path: config.PATHS.logFile,
|
|
550
|
-
size: stat.size,
|
|
551
|
-
mtime: stat.mtime,
|
|
552
|
-
isCurrent: true,
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (!fs.existsSync(logsDir)) {
|
|
557
|
-
return result;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const files = fs.readdirSync(logsDir);
|
|
561
|
-
for (const file of files) {
|
|
562
|
-
const match = file.match(/^mihomo\.(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.log$/);
|
|
563
|
-
if (!match) continue;
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
const filePath = path.join(logsDir, file);
|
|
567
|
-
const stat = fs.statSync(filePath);
|
|
568
|
-
result.archives.push({
|
|
569
|
-
name: file,
|
|
570
|
-
timestamp: match[1],
|
|
571
|
-
path: filePath,
|
|
572
|
-
size: stat.size,
|
|
573
|
-
mtime: stat.mtime,
|
|
574
|
-
isCurrent: false,
|
|
575
|
-
});
|
|
576
|
-
} catch (_e) {
|
|
577
|
-
// ignore
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
result.archives.sort((a, b) => b.mtime - a.mtime);
|
|
582
|
-
return result;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
function isPathUnderDir(filePath, baseDir) {
|
|
586
|
-
const resolvedPath = path.resolve(filePath);
|
|
587
|
-
const resolvedBase = path.resolve(baseDir);
|
|
588
|
-
return resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + path.sep);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function getLogPathByName(name) {
|
|
592
|
-
const logsDir = config.DIRS.logs;
|
|
593
|
-
|
|
594
|
-
let targetName = name;
|
|
595
|
-
if (!name.endsWith('.log')) {
|
|
596
|
-
targetName = 'mihomo.' + name + '.log';
|
|
597
|
-
}
|
|
598
|
-
if (!targetName.startsWith('mihomo.')) {
|
|
599
|
-
targetName = 'mihomo.' + targetName;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const filePath = path.join(logsDir, targetName);
|
|
603
|
-
if (fs.existsSync(filePath) && isPathUnderDir(filePath, logsDir)) {
|
|
604
|
-
return filePath;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
if (fs.existsSync(logsDir)) {
|
|
608
|
-
const files = fs.readdirSync(logsDir);
|
|
609
|
-
for (const file of files) {
|
|
610
|
-
if (file.includes(name)) {
|
|
611
|
-
const candidatePath = path.join(logsDir, file);
|
|
612
|
-
if (isPathUnderDir(candidatePath, logsDir)) {
|
|
613
|
-
return candidatePath;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function openUrl(url) {
|
|
623
|
-
try {
|
|
624
|
-
spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
625
|
-
return true;
|
|
626
|
-
} catch (_e) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* 打开日志文件(从 index.js 移入)
|
|
633
|
-
*/
|
|
634
|
-
function openLogFile(logPath, label) {
|
|
635
|
-
const displayLabel = label || logPath;
|
|
636
|
-
console.log('用系统默认程序打开: ' + displayLabel);
|
|
637
|
-
const success = openUrl(logPath);
|
|
638
|
-
if (!success) {
|
|
639
|
-
console.log('请手动打开: ' + logPath);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* 用 tail 查看日志(从 index.js 移入)
|
|
645
|
-
*/
|
|
646
|
-
function viewLogWithTail(logPath, options) {
|
|
647
|
-
const follow = options && options.follow;
|
|
648
|
-
const lines = (options && options.lines) || 100;
|
|
649
|
-
|
|
650
|
-
console.log('日志: ' + logPath);
|
|
651
|
-
if (follow) {
|
|
652
|
-
console.log('按 Ctrl+C 退出\n');
|
|
653
|
-
} else {
|
|
654
|
-
console.log('显示最后 ' + lines + ' 行\n');
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const tailArgs = [];
|
|
658
|
-
if (follow) tailArgs.push('-f');
|
|
659
|
-
tailArgs.push('-n', lines.toString());
|
|
660
|
-
tailArgs.push(logPath);
|
|
661
|
-
|
|
662
|
-
const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
|
|
663
|
-
|
|
664
|
-
tail.on('close', () => process.exit(0));
|
|
665
|
-
tail.on('error', e => {
|
|
666
|
-
console.error('无法读取日志: ' + e.message);
|
|
667
|
-
process.exit(1);
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
module.exports = {
|
|
672
|
-
// 常量
|
|
673
|
-
PROCESS_WAIT_ATTEMPTS,
|
|
674
|
-
PROCESS_WAIT_INTERVAL,
|
|
675
|
-
STARTUP_WAIT_MS,
|
|
676
|
-
SUDO_TIMEOUT_MS,
|
|
677
|
-
TUN_MODE_POST_WAIT_MS,
|
|
678
|
-
DEFAULT_LOG_RETENTION_DAYS,
|
|
679
|
-
// 函数
|
|
680
|
-
getAllMihomoPids,
|
|
681
|
-
cleanupAll,
|
|
682
|
-
getStatus,
|
|
683
|
-
start,
|
|
684
|
-
stop,
|
|
685
|
-
getLogPath,
|
|
686
|
-
listLogs,
|
|
687
|
-
getLogPathByName,
|
|
688
|
-
openUrl,
|
|
689
|
-
openLogFile,
|
|
690
|
-
viewLogWithTail,
|
|
691
|
-
};
|