mihomo-cli 1.2.4 → 1.3.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 +56 -0
- package/README.md +1 -0
- package/index.js +247 -287
- package/package.json +18 -11
- package/src/config.js +59 -40
- package/src/kernel.js +23 -11
- package/src/overwrite.js +7 -1
- package/src/process.js +87 -24
- package/src/subscription.js +72 -15
- package/src/utils.js +99 -4
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mihomo-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "A terminal-based mihomo (Clash.Meta) client for macOS",
|
|
5
|
-
"main": "index.js",
|
|
6
5
|
"bin": {
|
|
7
6
|
"mihomo-cli": "index.js",
|
|
8
7
|
"mihomo": "index.js",
|
|
@@ -15,7 +14,10 @@
|
|
|
15
14
|
"CHANGELOG.md"
|
|
16
15
|
],
|
|
17
16
|
"scripts": {
|
|
18
|
-
"
|
|
17
|
+
"format": "prettier --write \"**/*.{js,json,md}\"",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"lint:fix": "eslint . --fix",
|
|
20
|
+
"prepare": "husky"
|
|
19
21
|
},
|
|
20
22
|
"keywords": [
|
|
21
23
|
"mihomo",
|
|
@@ -26,21 +28,26 @@
|
|
|
26
28
|
"cli",
|
|
27
29
|
"macos"
|
|
28
30
|
],
|
|
29
|
-
"author": "",
|
|
31
|
+
"author": "Aex",
|
|
30
32
|
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/adaex/mihomo-cli.git"
|
|
36
|
+
},
|
|
31
37
|
"engines": {
|
|
32
38
|
"node": ">=18.0.0"
|
|
33
39
|
},
|
|
34
|
-
"os": [
|
|
35
|
-
"darwin"
|
|
36
|
-
],
|
|
37
|
-
"cpu": [
|
|
38
|
-
"x64",
|
|
39
|
-
"arm64"
|
|
40
|
-
],
|
|
41
40
|
"dependencies": {
|
|
42
41
|
"axios": "^1.6.0",
|
|
43
42
|
"compare-versions": "^6.1.0",
|
|
44
43
|
"js-yaml": "^4.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@eslint/js": "^10.0.1",
|
|
47
|
+
"eslint": "^10.2.0",
|
|
48
|
+
"globals": "^17.4.0",
|
|
49
|
+
"husky": "^9.1.7",
|
|
50
|
+
"lint-staged": "^16.4.0",
|
|
51
|
+
"prettier": "^3.0.0"
|
|
45
52
|
}
|
|
46
53
|
}
|
package/src/config.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
// 内置模块
|
|
1
2
|
const path = require('path');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
|
-
const yaml = require('js-yaml');
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
6
|
|
|
7
|
+
// 第三方模块
|
|
8
|
+
const yaml = require('js-yaml');
|
|
9
|
+
|
|
10
|
+
// 本地模块
|
|
11
|
+
// (无额外本地模块,overwrite.js 在 buildConfig 中延迟加载以避免循环依赖)
|
|
12
|
+
|
|
7
13
|
const IS_PKG = typeof process.pkg !== 'undefined';
|
|
8
14
|
|
|
9
15
|
let PROJECT_ROOT;
|
|
@@ -75,23 +81,23 @@ function maskUrl(url) {
|
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
let
|
|
84
|
+
let settingsCache = null;
|
|
79
85
|
|
|
80
86
|
function readSettings() {
|
|
81
|
-
if (
|
|
87
|
+
if (settingsCache !== null) return settingsCache;
|
|
82
88
|
ensureDirs();
|
|
83
89
|
if (fs.existsSync(PATHS.settingsFile)) {
|
|
84
90
|
try {
|
|
85
91
|
const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
} catch (
|
|
89
|
-
|
|
90
|
-
return
|
|
92
|
+
settingsCache = JSON.parse(content);
|
|
93
|
+
return settingsCache;
|
|
94
|
+
} catch (_e) {
|
|
95
|
+
settingsCache = {};
|
|
96
|
+
return settingsCache;
|
|
91
97
|
}
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
return
|
|
99
|
+
settingsCache = {};
|
|
100
|
+
return settingsCache;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
function writeSettings(settings) {
|
|
@@ -103,7 +109,7 @@ function writeSettings(settings) {
|
|
|
103
109
|
if (settings[key] === undefined) delete merged[key];
|
|
104
110
|
}
|
|
105
111
|
fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
106
|
-
|
|
112
|
+
settingsCache = merged;
|
|
107
113
|
return merged;
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -150,28 +156,28 @@ function setGitHubMirror(mirror) {
|
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// 订阅缓存读写(动态数据:流量、用户名、更新时间等)
|
|
153
|
-
function
|
|
159
|
+
function readSubscriptionCache() {
|
|
154
160
|
ensureDirs();
|
|
155
161
|
if (fs.existsSync(PATHS.subscriptionsCacheFile)) {
|
|
156
162
|
try {
|
|
157
163
|
const content = fs.readFileSync(PATHS.subscriptionsCacheFile, 'utf8');
|
|
158
164
|
return JSON.parse(content);
|
|
159
|
-
} catch (
|
|
165
|
+
} catch (_e) {
|
|
160
166
|
return {};
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
return {};
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
function
|
|
172
|
+
function writeSubscriptionCache(cache) {
|
|
167
173
|
ensureDirs();
|
|
168
174
|
fs.writeFileSync(PATHS.subscriptionsCacheFile, JSON.stringify(cache, null, 2), { mode: 0o600 });
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
function saveSubscriptionCache(subName, data) {
|
|
172
|
-
const cache =
|
|
178
|
+
const cache = readSubscriptionCache();
|
|
173
179
|
cache[subName] = { ...cache[subName], ...data };
|
|
174
|
-
|
|
180
|
+
writeSubscriptionCache(cache);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
function getSubscriptions() {
|
|
@@ -182,7 +188,7 @@ function getSubscriptions() {
|
|
|
182
188
|
// 获取合并了缓存数据的订阅列表
|
|
183
189
|
function getSubscriptionsWithCache() {
|
|
184
190
|
const subs = getSubscriptions();
|
|
185
|
-
const cache =
|
|
191
|
+
const cache = readSubscriptionCache();
|
|
186
192
|
return subs.map(s => ({
|
|
187
193
|
...s,
|
|
188
194
|
...(cache[s.name] || {}),
|
|
@@ -218,18 +224,18 @@ function setDefaultSubscription(name) {
|
|
|
218
224
|
return true;
|
|
219
225
|
}
|
|
220
226
|
|
|
221
|
-
function
|
|
227
|
+
function getSubscriptionRawConfigPath(subName) {
|
|
222
228
|
return path.join(DIRS.subscriptions, subName + '.yaml');
|
|
223
229
|
}
|
|
224
230
|
|
|
225
|
-
function
|
|
231
|
+
function saveSubscriptionRawConfig(subName, content) {
|
|
226
232
|
ensureDirs();
|
|
227
|
-
const filePath =
|
|
233
|
+
const filePath = getSubscriptionRawConfigPath(subName);
|
|
228
234
|
fs.writeFileSync(filePath, content, { mode: 0o600 });
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
function
|
|
232
|
-
const filePath =
|
|
237
|
+
function readSubscriptionRawConfig(subName) {
|
|
238
|
+
const filePath = getSubscriptionRawConfigPath(subName);
|
|
233
239
|
if (!fs.existsSync(filePath)) {
|
|
234
240
|
return null;
|
|
235
241
|
}
|
|
@@ -240,28 +246,28 @@ function hasKernel() {
|
|
|
240
246
|
return fs.existsSync(PATHS.mihomoBinary);
|
|
241
247
|
}
|
|
242
248
|
|
|
243
|
-
let
|
|
249
|
+
let kernelVersionCache = undefined;
|
|
244
250
|
|
|
245
251
|
function getKernelVersion() {
|
|
246
252
|
if (!hasKernel()) {
|
|
247
|
-
|
|
253
|
+
kernelVersionCache = undefined;
|
|
248
254
|
return null;
|
|
249
255
|
}
|
|
250
|
-
if (
|
|
256
|
+
if (kernelVersionCache !== undefined) return kernelVersionCache;
|
|
251
257
|
try {
|
|
252
258
|
const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
|
|
253
259
|
encoding: 'utf8',
|
|
254
260
|
}).trim();
|
|
255
261
|
if (output) {
|
|
256
262
|
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
257
|
-
|
|
258
|
-
return
|
|
263
|
+
kernelVersionCache = match ? match[0] : output;
|
|
264
|
+
return kernelVersionCache;
|
|
259
265
|
}
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
} catch (
|
|
263
|
-
|
|
264
|
-
return
|
|
266
|
+
kernelVersionCache = 'unknown';
|
|
267
|
+
return kernelVersionCache;
|
|
268
|
+
} catch (_e) {
|
|
269
|
+
kernelVersionCache = 'unknown';
|
|
270
|
+
return kernelVersionCache;
|
|
265
271
|
}
|
|
266
272
|
}
|
|
267
273
|
|
|
@@ -296,10 +302,10 @@ function parseYamlOrJson(content, errorMsg) {
|
|
|
296
302
|
try {
|
|
297
303
|
const result = yaml.load(content);
|
|
298
304
|
if (result !== undefined) return result;
|
|
299
|
-
} catch (
|
|
305
|
+
} catch (_e) {}
|
|
300
306
|
try {
|
|
301
307
|
return JSON.parse(content);
|
|
302
|
-
} catch (
|
|
308
|
+
} catch (_e2) {
|
|
303
309
|
throw new Error((errorMsg || '内容') + '格式错误,无法解析为 YAML 或 JSON');
|
|
304
310
|
}
|
|
305
311
|
}
|
|
@@ -369,7 +375,7 @@ function getConfigInfo() {
|
|
|
369
375
|
socksPort: cfg['socks-port'] || null,
|
|
370
376
|
tun: cfg.tun ? cfg.tun.enable : false,
|
|
371
377
|
};
|
|
372
|
-
} catch (
|
|
378
|
+
} catch (_e) {
|
|
373
379
|
return null;
|
|
374
380
|
}
|
|
375
381
|
}
|
|
@@ -401,30 +407,43 @@ function resetUserData(options) {
|
|
|
401
407
|
}
|
|
402
408
|
|
|
403
409
|
ensureDirs();
|
|
404
|
-
|
|
410
|
+
settingsCache = null;
|
|
405
411
|
return removedCount;
|
|
406
412
|
}
|
|
407
413
|
|
|
414
|
+
// 目录目标映射(从 index.js 移入,精确匹配)
|
|
415
|
+
const DIRECTORY_TARGETS = {
|
|
416
|
+
root: { path: null, label: '根目录' },
|
|
417
|
+
subs: { path: DIRS.subscriptions, label: '订阅目录' },
|
|
418
|
+
logs: { path: DIRS.logs, label: '日志目录' },
|
|
419
|
+
data: { path: DIRS.data, label: 'mihomo 数据目录' },
|
|
420
|
+
runtime: { path: DIRS.runtime, label: '运行时目录' },
|
|
421
|
+
overwrites: { path: DIRS.overwrites, label: '覆写目录' },
|
|
422
|
+
settings: { path: PATHS.settingsFile, label: '设置文件' },
|
|
423
|
+
kernel: { path: DIRS.core, label: '内核目录' },
|
|
424
|
+
};
|
|
425
|
+
|
|
408
426
|
module.exports = {
|
|
409
427
|
PATHS,
|
|
410
428
|
DIRS,
|
|
411
429
|
USER_DATA_DIR,
|
|
430
|
+
DIRECTORY_TARGETS,
|
|
412
431
|
ensureDirs,
|
|
413
432
|
readSettings,
|
|
414
433
|
writeSettings,
|
|
415
|
-
|
|
434
|
+
readSubscriptionCache,
|
|
416
435
|
saveSubscriptionCache,
|
|
417
436
|
maskUrl,
|
|
418
437
|
getSubscriptions,
|
|
419
438
|
getSubscriptionsWithCache,
|
|
420
439
|
addSubscription,
|
|
421
440
|
setDefaultSubscription,
|
|
422
|
-
|
|
423
|
-
|
|
441
|
+
saveSubscriptionRawConfig,
|
|
442
|
+
readSubscriptionRawConfig,
|
|
424
443
|
hasKernel,
|
|
425
444
|
getKernelVersion,
|
|
426
445
|
clearKernelVersionCache: () => {
|
|
427
|
-
|
|
446
|
+
kernelVersionCache = undefined;
|
|
428
447
|
},
|
|
429
448
|
getGitHubMirror,
|
|
430
449
|
setGitHubMirror,
|
package/src/kernel.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
// 内置模块
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// 第三方模块
|
|
5
7
|
const { compareVersions } = require('compare-versions');
|
|
8
|
+
|
|
9
|
+
// 本地模块
|
|
6
10
|
const config = require('./config');
|
|
11
|
+
const utils = require('./utils');
|
|
7
12
|
|
|
13
|
+
// 常量定义
|
|
8
14
|
const GITHUB_REPO = 'MetaCubeX/mihomo';
|
|
15
|
+
const KERNEL_HTTP_TIMEOUT = 120000;
|
|
16
|
+
const KERNEL_MAX_CONTENT_LENGTH = 200 * 1024 * 1024;
|
|
17
|
+
const KERNEL_DOWNLOAD_TIMEOUT = 180000;
|
|
18
|
+
|
|
19
|
+
// 内核专用 HTTP 客户端(超时和容量较大,适合下载大文件)
|
|
20
|
+
const HTTP_CLIENT = utils.createHttpClient({
|
|
21
|
+
timeout: KERNEL_HTTP_TIMEOUT,
|
|
22
|
+
maxContentLength: KERNEL_MAX_CONTENT_LENGTH,
|
|
23
|
+
});
|
|
9
24
|
|
|
10
25
|
function withMirror(url, overrideMirror) {
|
|
11
26
|
const mirror = overrideMirror !== undefined ? overrideMirror : config.getGitHubMirror();
|
|
@@ -15,12 +30,6 @@ function withMirror(url, overrideMirror) {
|
|
|
15
30
|
return url;
|
|
16
31
|
}
|
|
17
32
|
|
|
18
|
-
const HTTP_CLIENT = axios.create({
|
|
19
|
-
timeout: 120000,
|
|
20
|
-
headers: { 'User-Agent': 'mihomo-cli' },
|
|
21
|
-
maxContentLength: 200 * 1024 * 1024,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
33
|
function getArch() {
|
|
25
34
|
const arch = process.arch;
|
|
26
35
|
if (arch === 'arm64') return 'arm64';
|
|
@@ -94,7 +103,7 @@ async function checkUpdate() {
|
|
|
94
103
|
} else {
|
|
95
104
|
try {
|
|
96
105
|
needsUpdate = compareVersions(latestVersion.replace(/^v/, ''), currentVersion.replace(/^v/, '')) > 0;
|
|
97
|
-
} catch (
|
|
106
|
+
} catch (_e) {
|
|
98
107
|
needsUpdate = latestVersion !== currentVersion;
|
|
99
108
|
}
|
|
100
109
|
}
|
|
@@ -162,7 +171,7 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
162
171
|
method: 'get',
|
|
163
172
|
url: downloadUrl,
|
|
164
173
|
responseType: 'stream',
|
|
165
|
-
timeout:
|
|
174
|
+
timeout: KERNEL_DOWNLOAD_TIMEOUT,
|
|
166
175
|
});
|
|
167
176
|
|
|
168
177
|
const writer = fs.createWriteStream(tempPath);
|
|
@@ -225,9 +234,8 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
225
234
|
|
|
226
235
|
try {
|
|
227
236
|
fs.unlinkSync(tempPath);
|
|
228
|
-
} catch (
|
|
237
|
+
} catch (_e) {}
|
|
229
238
|
|
|
230
|
-
// 内核已更新,清除版本缓存
|
|
231
239
|
config.clearKernelVersionCache();
|
|
232
240
|
|
|
233
241
|
return {
|
|
@@ -237,6 +245,10 @@ async function downloadKernel(progressCallback, mirror) {
|
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
module.exports = {
|
|
248
|
+
GITHUB_REPO,
|
|
249
|
+
KERNEL_HTTP_TIMEOUT,
|
|
250
|
+
KERNEL_MAX_CONTENT_LENGTH,
|
|
251
|
+
KERNEL_DOWNLOAD_TIMEOUT,
|
|
240
252
|
checkUpdate,
|
|
241
253
|
downloadKernel,
|
|
242
254
|
};
|
package/src/overwrite.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// 内置模块
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// 第三方模块
|
|
3
6
|
const yaml = require('js-yaml');
|
|
7
|
+
|
|
8
|
+
// 本地模块
|
|
4
9
|
const config = require('./config');
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -178,7 +183,8 @@ function loadOverwriteFile() {
|
|
|
178
183
|
return [];
|
|
179
184
|
}
|
|
180
185
|
|
|
181
|
-
const files = fs
|
|
186
|
+
const files = fs
|
|
187
|
+
.readdirSync(dir)
|
|
182
188
|
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
|
|
183
189
|
.sort();
|
|
184
190
|
|
package/src/process.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
// 内置模块
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { spawn, execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
// 第三方模块
|
|
7
|
+
// (无第三方模块依赖)
|
|
8
|
+
|
|
9
|
+
// 本地模块
|
|
4
10
|
const config = require('./config');
|
|
5
11
|
const utils = require('./utils');
|
|
6
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
|
+
|
|
20
|
+
// 日志清理常量
|
|
21
|
+
const DEFAULT_LOG_RETENTION_DAYS = 7;
|
|
22
|
+
|
|
7
23
|
function clearRuntime() {
|
|
8
24
|
if (fs.existsSync(config.DIRS.runtime)) {
|
|
9
25
|
config.rmrf(config.DIRS.runtime);
|
|
@@ -18,7 +34,7 @@ function getPid() {
|
|
|
18
34
|
try {
|
|
19
35
|
const pid = parseInt(fs.readFileSync(config.PATHS.pidFile, 'utf8').trim());
|
|
20
36
|
return pid > 0 ? pid : null;
|
|
21
|
-
} catch (
|
|
37
|
+
} catch (_e) {
|
|
22
38
|
return null;
|
|
23
39
|
}
|
|
24
40
|
}
|
|
@@ -52,7 +68,7 @@ function isPidFileOwnedByRoot() {
|
|
|
52
68
|
try {
|
|
53
69
|
const stat = fs.statSync(config.PATHS.pidFile);
|
|
54
70
|
return stat.uid === 0;
|
|
55
|
-
} catch (
|
|
71
|
+
} catch (_e) {
|
|
56
72
|
return false;
|
|
57
73
|
}
|
|
58
74
|
}
|
|
@@ -86,13 +102,13 @@ function clearPid() {
|
|
|
86
102
|
stdio: 'inherit',
|
|
87
103
|
timeout: 10000,
|
|
88
104
|
});
|
|
89
|
-
} catch (
|
|
105
|
+
} catch (_e) {
|
|
90
106
|
// 忽略失败,后续操作可能会检测到问题
|
|
91
107
|
}
|
|
92
108
|
} else {
|
|
93
109
|
try {
|
|
94
110
|
fs.unlinkSync(config.PATHS.pidFile);
|
|
95
|
-
} catch (
|
|
111
|
+
} catch (_e) {
|
|
96
112
|
// ignore
|
|
97
113
|
}
|
|
98
114
|
}
|
|
@@ -108,11 +124,11 @@ function killProcess(pid, needsSudo) {
|
|
|
108
124
|
timeout: 10000,
|
|
109
125
|
});
|
|
110
126
|
return true;
|
|
111
|
-
} catch (
|
|
127
|
+
} catch (_e) {
|
|
112
128
|
try {
|
|
113
129
|
process.kill(pid, 'SIGKILL');
|
|
114
130
|
return true;
|
|
115
|
-
} catch (
|
|
131
|
+
} catch (_e2) {
|
|
116
132
|
return false;
|
|
117
133
|
}
|
|
118
134
|
}
|
|
@@ -120,7 +136,7 @@ function killProcess(pid, needsSudo) {
|
|
|
120
136
|
process.kill(pid, 'SIGKILL');
|
|
121
137
|
return true;
|
|
122
138
|
}
|
|
123
|
-
} catch (
|
|
139
|
+
} catch (_e) {
|
|
124
140
|
return false;
|
|
125
141
|
}
|
|
126
142
|
}
|
|
@@ -158,9 +174,7 @@ function cleanupAll(forceSudo) {
|
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
|
|
161
|
-
const hasRootPidFile = isPidFileOwnedByRoot();
|
|
162
177
|
const needsSudo = hasRootProcess;
|
|
163
|
-
const allowSudo = forceSudo || hasRootProcess || hasRootPidFile;
|
|
164
178
|
|
|
165
179
|
let killedCount = 0;
|
|
166
180
|
let failedPids = [];
|
|
@@ -187,9 +201,10 @@ function cleanupAll(forceSudo) {
|
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
203
|
|
|
190
|
-
|
|
204
|
+
// 等待进程终止
|
|
205
|
+
for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
|
|
191
206
|
if (getAllMihomoPids().length === 0) break;
|
|
192
|
-
utils.sleepSync(
|
|
207
|
+
utils.sleepSync(PROCESS_WAIT_INTERVAL);
|
|
193
208
|
}
|
|
194
209
|
|
|
195
210
|
clearPid();
|
|
@@ -281,7 +296,7 @@ function getProcessInfo(pid) {
|
|
|
281
296
|
cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
|
|
282
297
|
isRoot: utils.isProcessRoot(pid),
|
|
283
298
|
};
|
|
284
|
-
} catch (
|
|
299
|
+
} catch (_e) {
|
|
285
300
|
return { pid, memory: '未知', cpu: '未知', isRoot: false };
|
|
286
301
|
}
|
|
287
302
|
}
|
|
@@ -367,7 +382,7 @@ async function startMixedMode(staleState) {
|
|
|
367
382
|
const pid = child.pid;
|
|
368
383
|
savePid(pid);
|
|
369
384
|
|
|
370
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
385
|
+
await new Promise(resolve => setTimeout(resolve, STARTUP_WAIT_MS));
|
|
371
386
|
|
|
372
387
|
if (!isRunning()) {
|
|
373
388
|
clearPid();
|
|
@@ -416,12 +431,12 @@ async function startTunMode(staleState) {
|
|
|
416
431
|
try {
|
|
417
432
|
execSync('sudo "' + launchScript + '"', {
|
|
418
433
|
stdio: 'inherit',
|
|
419
|
-
timeout:
|
|
434
|
+
timeout: SUDO_TIMEOUT_MS,
|
|
420
435
|
});
|
|
421
436
|
} catch (e) {
|
|
422
437
|
try {
|
|
423
438
|
fs.unlinkSync(launchScript);
|
|
424
|
-
} catch
|
|
439
|
+
} catch {}
|
|
425
440
|
if (e.status === 1) {
|
|
426
441
|
throw new Error('密码错误或取消');
|
|
427
442
|
}
|
|
@@ -430,9 +445,9 @@ async function startTunMode(staleState) {
|
|
|
430
445
|
|
|
431
446
|
try {
|
|
432
447
|
fs.unlinkSync(launchScript);
|
|
433
|
-
} catch (
|
|
448
|
+
} catch (_e) {}
|
|
434
449
|
|
|
435
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
450
|
+
await new Promise(resolve => setTimeout(resolve, TUN_MODE_POST_WAIT_MS));
|
|
436
451
|
|
|
437
452
|
const finalPid = getPid();
|
|
438
453
|
if (!finalPid) {
|
|
@@ -468,7 +483,7 @@ function stop(forceSudo) {
|
|
|
468
483
|
|
|
469
484
|
function rotateAndCleanupLogs() {
|
|
470
485
|
rotateLog();
|
|
471
|
-
cleanupOldLogs(
|
|
486
|
+
cleanupOldLogs(DEFAULT_LOG_RETENTION_DAYS);
|
|
472
487
|
}
|
|
473
488
|
|
|
474
489
|
function getLogPath() {
|
|
@@ -496,7 +511,7 @@ function rotateLog() {
|
|
|
496
511
|
}
|
|
497
512
|
|
|
498
513
|
function cleanupOldLogs(maxAgeDays) {
|
|
499
|
-
if (maxAgeDays === undefined) maxAgeDays =
|
|
514
|
+
if (maxAgeDays === undefined) maxAgeDays = DEFAULT_LOG_RETENTION_DAYS;
|
|
500
515
|
const logsDir = config.DIRS.logs;
|
|
501
516
|
|
|
502
517
|
if (!fs.existsSync(logsDir)) {
|
|
@@ -524,7 +539,7 @@ function cleanupOldLogs(maxAgeDays) {
|
|
|
524
539
|
fs.unlinkSync(filePath);
|
|
525
540
|
deleted++;
|
|
526
541
|
}
|
|
527
|
-
} catch (
|
|
542
|
+
} catch (_e) {
|
|
528
543
|
errors++;
|
|
529
544
|
}
|
|
530
545
|
}
|
|
@@ -570,7 +585,7 @@ function listLogs() {
|
|
|
570
585
|
mtime: stat.mtime,
|
|
571
586
|
isCurrent: false,
|
|
572
587
|
});
|
|
573
|
-
} catch (
|
|
588
|
+
} catch (_e) {
|
|
574
589
|
// ignore
|
|
575
590
|
}
|
|
576
591
|
}
|
|
@@ -588,7 +603,6 @@ function isPathUnderDir(filePath, baseDir) {
|
|
|
588
603
|
function getLogPathByName(name) {
|
|
589
604
|
const logsDir = config.DIRS.logs;
|
|
590
605
|
|
|
591
|
-
// 处理部分匹配(用户只输入时间戳部分)
|
|
592
606
|
let targetName = name;
|
|
593
607
|
if (!name.endsWith('.log')) {
|
|
594
608
|
targetName = 'mihomo.' + name + '.log';
|
|
@@ -602,7 +616,6 @@ function getLogPathByName(name) {
|
|
|
602
616
|
return filePath;
|
|
603
617
|
}
|
|
604
618
|
|
|
605
|
-
// 尝试模糊匹配(readdirSync 返回的文件名已是安全的,但为了一致性仍校验)
|
|
606
619
|
if (fs.existsSync(logsDir)) {
|
|
607
620
|
const files = fs.readdirSync(logsDir);
|
|
608
621
|
for (const file of files) {
|
|
@@ -622,12 +635,60 @@ function openUrl(url) {
|
|
|
622
635
|
try {
|
|
623
636
|
spawn('open', [url], { stdio: 'ignore', detached: true });
|
|
624
637
|
return true;
|
|
625
|
-
} catch (
|
|
638
|
+
} catch (_e) {
|
|
626
639
|
return false;
|
|
627
640
|
}
|
|
628
641
|
}
|
|
629
642
|
|
|
643
|
+
/**
|
|
644
|
+
* 打开日志文件(从 index.js 移入)
|
|
645
|
+
*/
|
|
646
|
+
function openLogFile(logPath, label) {
|
|
647
|
+
const displayLabel = label || logPath;
|
|
648
|
+
console.log('用系统默认程序打开: ' + displayLabel);
|
|
649
|
+
const success = openUrl(logPath);
|
|
650
|
+
if (!success) {
|
|
651
|
+
console.log('请手动打开: ' + logPath);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 用 tail 查看日志(从 index.js 移入)
|
|
657
|
+
*/
|
|
658
|
+
function viewLogWithTail(logPath, options) {
|
|
659
|
+
const follow = options && options.follow;
|
|
660
|
+
const lines = (options && options.lines) || 100;
|
|
661
|
+
|
|
662
|
+
console.log('日志: ' + logPath);
|
|
663
|
+
if (follow) {
|
|
664
|
+
console.log('按 Ctrl+C 退出\n');
|
|
665
|
+
} else {
|
|
666
|
+
console.log('显示最后 ' + lines + ' 行\n');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const tailArgs = [];
|
|
670
|
+
if (follow) tailArgs.push('-f');
|
|
671
|
+
tailArgs.push('-n', lines.toString());
|
|
672
|
+
tailArgs.push(logPath);
|
|
673
|
+
|
|
674
|
+
const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
|
|
675
|
+
|
|
676
|
+
tail.on('close', () => process.exit(0));
|
|
677
|
+
tail.on('error', e => {
|
|
678
|
+
console.error('无法读取日志: ' + e.message);
|
|
679
|
+
process.exit(1);
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
630
683
|
module.exports = {
|
|
684
|
+
// 常量
|
|
685
|
+
PROCESS_WAIT_ATTEMPTS,
|
|
686
|
+
PROCESS_WAIT_INTERVAL,
|
|
687
|
+
STARTUP_WAIT_MS,
|
|
688
|
+
SUDO_TIMEOUT_MS,
|
|
689
|
+
TUN_MODE_POST_WAIT_MS,
|
|
690
|
+
DEFAULT_LOG_RETENTION_DAYS,
|
|
691
|
+
// 函数
|
|
631
692
|
getAllMihomoPids,
|
|
632
693
|
cleanupAll,
|
|
633
694
|
getStatus,
|
|
@@ -637,4 +698,6 @@ module.exports = {
|
|
|
637
698
|
listLogs,
|
|
638
699
|
getLogPathByName,
|
|
639
700
|
openUrl,
|
|
701
|
+
openLogFile,
|
|
702
|
+
viewLogWithTail,
|
|
640
703
|
};
|