mihomo-cli 3.7.0 → 3.8.1
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 +66 -0
- package/README.md +61 -3
- package/dist/index.js +1021 -110
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3182,7 +3182,11 @@ var DIRS = {
|
|
|
3182
3182
|
subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
|
|
3183
3183
|
logs: path.join(USER_DATA_DIR, "logs"),
|
|
3184
3184
|
data: path.join(USER_DATA_DIR, "data"),
|
|
3185
|
-
runtime: path.join(USER_DATA_DIR, "runtime")
|
|
3185
|
+
runtime: path.join(USER_DATA_DIR, "runtime"),
|
|
3186
|
+
// 隧道运行态。刻意独立于 runtime/:clearRuntime() 会在 stop() 成功路径 rmrf 整个
|
|
3187
|
+
// runtime 目录,隧道状态放那里会被 `mihomo stop` 连同 config.yaml 一起抹掉,
|
|
3188
|
+
// 于是「谁起的」标记丢失、手动起的隧道再也无法被识别
|
|
3189
|
+
tunnel: path.join(USER_DATA_DIR, "tunnel")
|
|
3186
3190
|
};
|
|
3187
3191
|
var PATHS = {
|
|
3188
3192
|
mihomoBinary: path.join(DIRS.kernel, "mihomo"),
|
|
@@ -3229,6 +3233,55 @@ function atomicWriteFileSync(filePath, content, options) {
|
|
|
3229
3233
|
function rmrf(dir) {
|
|
3230
3234
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
3231
3235
|
}
|
|
3236
|
+
var LOCK_STALE_MS = 1e4;
|
|
3237
|
+
var LOCK_RETRY_MS = 20;
|
|
3238
|
+
function withFileLock(filePath, fn) {
|
|
3239
|
+
const lockPath = `${filePath}.lock`;
|
|
3240
|
+
const deadline = Date.now() + LOCK_STALE_MS;
|
|
3241
|
+
let fd = null;
|
|
3242
|
+
while (fd === null) {
|
|
3243
|
+
try {
|
|
3244
|
+
fd = fs.openSync(lockPath, "wx");
|
|
3245
|
+
} catch (e) {
|
|
3246
|
+
if (e.code !== "EEXIST") throw e;
|
|
3247
|
+
let stale = false;
|
|
3248
|
+
try {
|
|
3249
|
+
stale = Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS;
|
|
3250
|
+
} catch {
|
|
3251
|
+
}
|
|
3252
|
+
if (stale) {
|
|
3253
|
+
try {
|
|
3254
|
+
fs.rmSync(lockPath, { force: true });
|
|
3255
|
+
} catch {
|
|
3256
|
+
}
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
if (Date.now() > deadline) {
|
|
3260
|
+
try {
|
|
3261
|
+
fs.rmSync(lockPath, { force: true });
|
|
3262
|
+
} catch {
|
|
3263
|
+
}
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
sleepSyncMs(LOCK_RETRY_MS);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
try {
|
|
3270
|
+
return fn();
|
|
3271
|
+
} finally {
|
|
3272
|
+
try {
|
|
3273
|
+
fs.closeSync(fd);
|
|
3274
|
+
} catch {
|
|
3275
|
+
}
|
|
3276
|
+
try {
|
|
3277
|
+
fs.rmSync(lockPath, { force: true });
|
|
3278
|
+
} catch {
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
function sleepSyncMs(ms) {
|
|
3283
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
3284
|
+
}
|
|
3232
3285
|
|
|
3233
3286
|
// src/settings.ts
|
|
3234
3287
|
import fs2 from "fs";
|
|
@@ -3265,6 +3318,10 @@ function recoverCorruptedSettings() {
|
|
|
3265
3318
|
}
|
|
3266
3319
|
function writeSettings(settings) {
|
|
3267
3320
|
ensureDirs();
|
|
3321
|
+
return withFileLock(PATHS.settingsFile, () => writeSettingsUnlocked(settings));
|
|
3322
|
+
}
|
|
3323
|
+
function writeSettingsUnlocked(settings) {
|
|
3324
|
+
settingsCache = null;
|
|
3268
3325
|
const existing = readSettings();
|
|
3269
3326
|
const merged = { ...existing, ...settings };
|
|
3270
3327
|
for (const key of Object.keys(settings)) {
|
|
@@ -3274,6 +3331,14 @@ function writeSettings(settings) {
|
|
|
3274
3331
|
settingsCache = merged;
|
|
3275
3332
|
return settingsCache;
|
|
3276
3333
|
}
|
|
3334
|
+
function updateSettings(mutate) {
|
|
3335
|
+
ensureDirs();
|
|
3336
|
+
return withFileLock(PATHS.settingsFile, () => {
|
|
3337
|
+
settingsCache = null;
|
|
3338
|
+
const current = readSettings();
|
|
3339
|
+
return writeSettingsUnlocked(mutate(current));
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3277
3342
|
function invalidateSettingsCache() {
|
|
3278
3343
|
settingsCache = null;
|
|
3279
3344
|
}
|
|
@@ -3319,10 +3384,13 @@ function maskUrl(url) {
|
|
|
3319
3384
|
}
|
|
3320
3385
|
function readSubscriptionCache() {
|
|
3321
3386
|
ensureDirs();
|
|
3387
|
+
const empty = () => /* @__PURE__ */ Object.create(null);
|
|
3322
3388
|
if (fs2.existsSync(PATHS.subscriptionsCacheFile)) {
|
|
3323
3389
|
try {
|
|
3324
3390
|
const content = fs2.readFileSync(PATHS.subscriptionsCacheFile, "utf8");
|
|
3325
|
-
|
|
3391
|
+
const parsed = JSON.parse(content);
|
|
3392
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return empty();
|
|
3393
|
+
return Object.assign(empty(), parsed);
|
|
3326
3394
|
} catch {
|
|
3327
3395
|
try {
|
|
3328
3396
|
fs2.copyFileSync(PATHS.subscriptionsCacheFile, `${PATHS.subscriptionsCacheFile}.bak`);
|
|
@@ -3330,10 +3398,10 @@ function readSubscriptionCache() {
|
|
|
3330
3398
|
} catch {
|
|
3331
3399
|
console.warn("\u8B66\u544A: \u8BA2\u9605\u7F13\u5B58\u683C\u5F0F\u635F\u574F\uFF0C\u5DF2\u5FFD\u7565");
|
|
3332
3400
|
}
|
|
3333
|
-
return
|
|
3401
|
+
return empty();
|
|
3334
3402
|
}
|
|
3335
3403
|
}
|
|
3336
|
-
return
|
|
3404
|
+
return empty();
|
|
3337
3405
|
}
|
|
3338
3406
|
function writeSubscriptionCache(cache) {
|
|
3339
3407
|
ensureDirs();
|
|
@@ -3371,31 +3439,41 @@ function validateSubscriptionName(name) {
|
|
|
3371
3439
|
}
|
|
3372
3440
|
function addSubscription(url, name = "default") {
|
|
3373
3441
|
validateSubscriptionName(name);
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3442
|
+
let duplicate = false;
|
|
3443
|
+
updateSettings((settings) => {
|
|
3444
|
+
const subs = [...getSubscriptions()];
|
|
3445
|
+
if (subs.some((s) => s.name === name)) {
|
|
3446
|
+
duplicate = true;
|
|
3447
|
+
return {};
|
|
3448
|
+
}
|
|
3449
|
+
subs.push({ name, url });
|
|
3450
|
+
const updates = { subscriptions: subs };
|
|
3451
|
+
if (!settings.active_subscription && subs.length === 1) {
|
|
3452
|
+
updates.active_subscription = name;
|
|
3453
|
+
}
|
|
3454
|
+
return updates;
|
|
3455
|
+
});
|
|
3456
|
+
if (duplicate) {
|
|
3377
3457
|
throw new CliError(`\u8BA2\u9605 "${name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF08mihomo sub add <url> <\u540D\u79F0>\uFF09\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo sub remove ${name}\uFF09`);
|
|
3378
3458
|
}
|
|
3379
|
-
subs.push({ name, url });
|
|
3380
|
-
const updates = { subscriptions: subs };
|
|
3381
|
-
if (!settings.active_subscription && subs.length === 1) {
|
|
3382
|
-
updates.active_subscription = name;
|
|
3383
|
-
}
|
|
3384
|
-
writeSettings(updates);
|
|
3385
3459
|
}
|
|
3386
3460
|
function removeSubscription(name) {
|
|
3387
|
-
const settings = readSettings();
|
|
3388
|
-
const subs = [...getSubscriptions()];
|
|
3389
|
-
const idx = subs.findIndex((s) => s.name === name);
|
|
3390
|
-
if (idx < 0) return null;
|
|
3391
|
-
subs.splice(idx, 1);
|
|
3392
|
-
const updates = { subscriptions: subs };
|
|
3393
3461
|
let switchedTo = null;
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3462
|
+
let found = false;
|
|
3463
|
+
updateSettings((settings) => {
|
|
3464
|
+
const subs = [...getSubscriptions()];
|
|
3465
|
+
const idx = subs.findIndex((s) => s.name === name);
|
|
3466
|
+
if (idx < 0) return {};
|
|
3467
|
+
found = true;
|
|
3468
|
+
subs.splice(idx, 1);
|
|
3469
|
+
const updates = { subscriptions: subs };
|
|
3470
|
+
if (settings.active_subscription === name) {
|
|
3471
|
+
switchedTo = subs.length > 0 ? subs[0].name : null;
|
|
3472
|
+
updates.active_subscription = switchedTo ?? void 0;
|
|
3473
|
+
}
|
|
3474
|
+
return updates;
|
|
3475
|
+
});
|
|
3476
|
+
if (!found) return null;
|
|
3399
3477
|
const cache = readSubscriptionCache();
|
|
3400
3478
|
if (cache[name]) {
|
|
3401
3479
|
delete cache[name];
|
|
@@ -3764,10 +3842,40 @@ function parseIntArg(args, short, long, defaultValue) {
|
|
|
3764
3842
|
}
|
|
3765
3843
|
return defaultValue;
|
|
3766
3844
|
}
|
|
3767
|
-
|
|
3845
|
+
function parseStringArg(args, long, short) {
|
|
3846
|
+
if (!args) return null;
|
|
3847
|
+
for (let i = 0; i < args.length; i++) {
|
|
3848
|
+
if (args[i] === long || short !== void 0 && args[i] === short) {
|
|
3849
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
3850
|
+
return args[i + 1];
|
|
3851
|
+
}
|
|
3852
|
+
throw new CliError(`\u9009\u9879 ${args[i]} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long} <\u503C>`] });
|
|
3853
|
+
}
|
|
3854
|
+
if (args[i].startsWith(`${long}=`)) {
|
|
3855
|
+
const value = args[i].slice(long.length + 1);
|
|
3856
|
+
if (!value) throw new CliError(`\u9009\u9879 ${long} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long}=<\u503C>`] });
|
|
3857
|
+
return value;
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
return null;
|
|
3861
|
+
}
|
|
3862
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
3863
|
+
"-t",
|
|
3864
|
+
"--timeout",
|
|
3865
|
+
"-j",
|
|
3866
|
+
"--concurrency",
|
|
3867
|
+
"-r",
|
|
3868
|
+
"--rounds",
|
|
3869
|
+
"-n",
|
|
3870
|
+
"--lines",
|
|
3871
|
+
"-u",
|
|
3872
|
+
"--update-timeout",
|
|
3873
|
+
"--host",
|
|
3874
|
+
"--port"
|
|
3875
|
+
]);
|
|
3768
3876
|
function extractStartOptions(args) {
|
|
3769
3877
|
if (!args) return [];
|
|
3770
|
-
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean"]);
|
|
3878
|
+
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean", "--no-tunnel"]);
|
|
3771
3879
|
const out = [];
|
|
3772
3880
|
for (let i = 0; i < args.length; i++) {
|
|
3773
3881
|
const a = args[i];
|
|
@@ -3825,14 +3933,24 @@ function suggestSimilar(input, candidates) {
|
|
|
3825
3933
|
function normalizeMirrorUrl(val) {
|
|
3826
3934
|
if (!val) return null;
|
|
3827
3935
|
if (val === "direct" || val === "no" || val === "none") return null;
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3936
|
+
const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(val) ? val : `https://${val}`;
|
|
3937
|
+
let parsed;
|
|
3938
|
+
try {
|
|
3939
|
+
parsed = new URL(withScheme);
|
|
3940
|
+
} catch {
|
|
3941
|
+
throw new CliError(`\u955C\u50CF\u5730\u5740\u65E0\u6548: "${val}"`, {
|
|
3942
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
3943
|
+
hint: ["\u683C\u5F0F\u5982: --mirror https://gh-proxy.org/ \u6216 --mirror gh-proxy.org", "\u4E0D\u4F7F\u7528\u955C\u50CF: --no-mirror"]
|
|
3944
|
+
});
|
|
3831
3945
|
}
|
|
3832
|
-
if (
|
|
3833
|
-
|
|
3946
|
+
if (parsed.protocol !== "https:") {
|
|
3947
|
+
throw new CliError(`\u955C\u50CF\u5730\u5740\u5FC5\u987B\u4F7F\u7528 https: "${val}"`, {
|
|
3948
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
3949
|
+
hint: ["\u955C\u50CF\u4F1A\u4E2D\u8F6C\u5185\u6838\u4E8C\u8FDB\u5236\uFF0C\u8BE5\u4EA7\u7269\u968F\u540E\u4EE5 root \u8FD0\u884C\uFF0C\u4E0D\u5141\u8BB8\u660E\u6587\u4F20\u8F93\u3002"]
|
|
3950
|
+
});
|
|
3834
3951
|
}
|
|
3835
|
-
|
|
3952
|
+
const url = parsed.toString();
|
|
3953
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
3836
3954
|
}
|
|
3837
3955
|
function parseMirrorArg(args) {
|
|
3838
3956
|
if (!args || args.length < 2) {
|
|
@@ -4011,6 +4129,14 @@ function validateConfig(config) {
|
|
|
4011
4129
|
warnings.push(`\u79FB\u9664\u4E86 ${groupDedup.duplicates.length} \u4E2A\u91CD\u540D\u5206\u7EC4: ${groupDedup.duplicates.map((n) => `"${n}"`).join(", ")}`);
|
|
4012
4130
|
}
|
|
4013
4131
|
const validNames = /* @__PURE__ */ new Set([...BUILTIN_PROXY_NAMES, ...proxyDedup.names, ...groupDedup.names]);
|
|
4132
|
+
for (const name of groupDedup.names) {
|
|
4133
|
+
if (proxyDedup.names.has(name)) {
|
|
4134
|
+
warnings.push(`\u540D\u79F0\u51B2\u7A81: "${name}" \u540C\u65F6\u662F\u8282\u70B9\u548C\u5206\u7EC4\u540D\uFF0Cmihomo \u4F1A\u62D2\u7EDD\u52A0\u8F7D\uFF08\u8BF7\u91CD\u547D\u540D\u5176\u4E00\uFF09`);
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
const providerNames = new Set(
|
|
4138
|
+
config["proxy-providers"] && typeof config["proxy-providers"] === "object" && !Array.isArray(config["proxy-providers"]) ? Object.keys(config["proxy-providers"]) : []
|
|
4139
|
+
);
|
|
4014
4140
|
const activeGroups = groupDedup.result;
|
|
4015
4141
|
const removedGroups = /* @__PURE__ */ new Set();
|
|
4016
4142
|
let changed = true;
|
|
@@ -4018,12 +4144,32 @@ function validateConfig(config) {
|
|
|
4018
4144
|
changed = false;
|
|
4019
4145
|
for (const group of activeGroups) {
|
|
4020
4146
|
if (removedGroups.has(group.name)) continue;
|
|
4147
|
+
if (group.proxies !== void 0 && !Array.isArray(group.proxies)) {
|
|
4148
|
+
if (typeof group.proxies === "string") {
|
|
4149
|
+
warnings.push(`proxy-group "${group.name}": proxies \u5E94\u4E3A\u5217\u8868\uFF0C\u5DF2\u6309\u5355\u5143\u7D20\u5904\u7406\uFF08"${group.proxies}"\uFF09`);
|
|
4150
|
+
group.proxies = [group.proxies];
|
|
4151
|
+
} else {
|
|
4152
|
+
warnings.push(`proxy-group "${group.name}": proxies \u4E0D\u662F\u5217\u8868\uFF0C\u5DF2\u5FFD\u7565\u8BE5\u5B57\u6BB5`);
|
|
4153
|
+
group.proxies = [];
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4021
4156
|
if (!Array.isArray(group.proxies)) continue;
|
|
4157
|
+
if (Array.isArray(group.use)) {
|
|
4158
|
+
const ghosts = group.use.filter((u) => typeof u === "string" && !providerNames.has(u));
|
|
4159
|
+
if (ghosts.length > 0) {
|
|
4160
|
+
group.use = group.use.filter((u) => providerNames.has(u));
|
|
4161
|
+
warnings.push(`proxy-group "${group.name}": \u79FB\u9664\u4E86\u4E0D\u5B58\u5728\u7684 provider \u5F15\u7528 ${ghosts.map((n) => `"${n}"`).join(", ")}`);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4022
4164
|
const invalid = group.proxies.filter((name) => !validNames.has(name));
|
|
4023
|
-
if (invalid.length
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4165
|
+
if (invalid.length > 0) {
|
|
4166
|
+
group.proxies = group.proxies.filter((name) => validNames.has(name));
|
|
4167
|
+
warnings.push(`proxy-group "${group.name}": \u79FB\u9664\u4E86\u4E0D\u5B58\u5728\u7684\u5F15\u7528 ${invalid.map((n) => `"${n}"`).join(", ")}`);
|
|
4168
|
+
}
|
|
4169
|
+
const hasUse = Array.isArray(group.use) ? group.use.length > 0 : Boolean(group.use);
|
|
4170
|
+
const includesAll = Boolean(group["include-all"] || group["include-all-proxies"]);
|
|
4171
|
+
const includeAllUsable = includesAll && proxyDedup.result.length > 0;
|
|
4172
|
+
const hasOtherSource = hasUse || includeAllUsable;
|
|
4027
4173
|
if (group.proxies.length === 0 && !hasOtherSource) {
|
|
4028
4174
|
removedGroups.add(group.name);
|
|
4029
4175
|
validNames.delete(group.name);
|
|
@@ -4232,12 +4378,13 @@ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}`, "", "\u547D\u4EE4\u522B\
|
|
|
4232
4378
|
" mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605",
|
|
4233
4379
|
" mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)",
|
|
4234
4380
|
" mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09",
|
|
4381
|
+
" mihomo tunnel add work --host m4 --port 1080 # \u52A0 ssh \u96A7\u9053\u51FA\u53E3",
|
|
4235
4382
|
" mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)",
|
|
4236
4383
|
" mihomo ui # \u6253\u5F00 Web UI",
|
|
4237
4384
|
"",
|
|
4238
4385
|
`${colors.cyan("\u5FEB\u6377\u547D\u4EE4:")}`,
|
|
4239
4386
|
" tun = start tun use = sub use on/off = ow on/off open = dir open",
|
|
4240
|
-
" up = start down = stop upd/upgrade = update",
|
|
4387
|
+
" up = start down = stop upd/upgrade = update ssh = tunnel",
|
|
4241
4388
|
"",
|
|
4242
4389
|
`${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}`,
|
|
4243
4390
|
" mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)",
|
|
@@ -4516,6 +4663,7 @@ exit 2
|
|
|
4516
4663
|
`;
|
|
4517
4664
|
const scriptPath = path4.join(DIRS.runtime, "launch-tun.sh");
|
|
4518
4665
|
fs5.writeFileSync(scriptPath, scriptContent, { mode: 448 });
|
|
4666
|
+
fs5.chmodSync(scriptPath, 448);
|
|
4519
4667
|
return scriptPath;
|
|
4520
4668
|
}
|
|
4521
4669
|
function getProcessInfo(pid) {
|
|
@@ -4885,6 +5033,7 @@ function runSudoScript(scriptBody, opts) {
|
|
|
4885
5033
|
ensureDirs();
|
|
4886
5034
|
const scriptPath = path5.join(DIRS.runtime, opts.file);
|
|
4887
5035
|
fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
|
|
5036
|
+
fs6.chmodSync(scriptPath, 448);
|
|
4888
5037
|
try {
|
|
4889
5038
|
const result = spawnSync3("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
|
|
4890
5039
|
if (result.error) throw result.error;
|
|
@@ -4933,10 +5082,15 @@ function enableDaemon() {
|
|
|
4933
5082
|
const plistDest = shellQuote(PATHS.launchDaemonPlist);
|
|
4934
5083
|
const stage = shellQuote(stagePath);
|
|
4935
5084
|
const pattern = shellQuote(MAIN_INSTANCE_PATTERN);
|
|
5085
|
+
const pidFile = shellQuote(PATHS.pidFile);
|
|
4936
5086
|
const script = [
|
|
4937
5087
|
"#!/bin/bash",
|
|
4938
5088
|
`launchctl bootout ${target} 2>/dev/null || true`,
|
|
4939
5089
|
`pkill -9 -f ${pattern} 2>/dev/null || true`,
|
|
5090
|
+
// 一并清掉 pid 文件:`start tun` 留下的是 **root 属主**的 pid 文件,普通用户删不掉。
|
|
5091
|
+
// 不删会让后续 `daemon off` → `start` 撞上 hasRootResidue() 的拒绝启动,
|
|
5092
|
+
// 而这个死胡同完全由 CLI 自身的 on/off 循环造成
|
|
5093
|
+
`rm -f ${pidFile}`,
|
|
4940
5094
|
"sleep 0.2",
|
|
4941
5095
|
`install -m 644 -o root -g wheel ${stage} ${plistDest} || exit 2`,
|
|
4942
5096
|
`launchctl bootstrap system ${plistDest} || { launchctl bootout ${target} 2>/dev/null; rm -f ${plistDest}; exit 3; }`,
|
|
@@ -4961,7 +5115,13 @@ function enableDaemon() {
|
|
|
4961
5115
|
}
|
|
4962
5116
|
function disableDaemon() {
|
|
4963
5117
|
assertDaemonLabelSafe();
|
|
4964
|
-
|
|
5118
|
+
const plistExists = isDaemonEnabled();
|
|
5119
|
+
const rootKernelRunning = getMihomoPids().some(isProcessRoot);
|
|
5120
|
+
if (!plistExists && !rootKernelRunning) return;
|
|
5121
|
+
if (!plistExists) {
|
|
5122
|
+
console.log("\u672A\u627E\u5230 plist\uFF0C\u4F46\u68C0\u6D4B\u5230 root \u5185\u6838\u5728\u8FD0\u884C\uFF08\u53EF\u80FD plist \u88AB\u624B\u52A8\u5220\u9664\u800C\u4EFB\u52A1\u4ECD\u88C5\u8F7D\uFF09");
|
|
5123
|
+
console.log("\u5C06\u6267\u884C launchctl bootout \u5378\u8F7D\u6B8B\u7559\u4EFB\u52A1");
|
|
5124
|
+
}
|
|
4965
5125
|
const target = shellQuote(SERVICE_TARGET);
|
|
4966
5126
|
const plistDest = shellQuote(PATHS.launchDaemonPlist);
|
|
4967
5127
|
const logFile = shellQuote(PATHS.logFile);
|
|
@@ -4984,12 +5144,17 @@ function disableDaemon() {
|
|
|
4984
5144
|
}
|
|
4985
5145
|
}
|
|
4986
5146
|
async function tryHotReload() {
|
|
5147
|
+
if (!isDaemonRunning(getDaemonStatus())) return false;
|
|
4987
5148
|
const controller = new AbortController();
|
|
4988
5149
|
const timer = setTimeout(() => controller.abort(), HOT_RELOAD_TIMEOUT_MS);
|
|
4989
5150
|
const secret = readSettings().controller_secret;
|
|
4990
5151
|
const headers = { "Content-Type": "application/json" };
|
|
4991
5152
|
if (secret) headers.Authorization = `Bearer ${secret}`;
|
|
4992
5153
|
try {
|
|
5154
|
+
const probe = await fetch(`${CONTROLLER_BASE_URL}/version`, { headers, signal: controller.signal });
|
|
5155
|
+
if (!probe.ok) return false;
|
|
5156
|
+
const info = await probe.json();
|
|
5157
|
+
if (typeof info?.version !== "string") return false;
|
|
4993
5158
|
const res = await fetch(`${CONTROLLER_BASE_URL}/configs?force=true`, {
|
|
4994
5159
|
method: "PUT",
|
|
4995
5160
|
headers,
|
|
@@ -5146,15 +5311,18 @@ function saveSubscriptionConfig(subName, parsed) {
|
|
|
5146
5311
|
function parseUserInfo(header) {
|
|
5147
5312
|
if (!header) return null;
|
|
5148
5313
|
const info = {};
|
|
5149
|
-
|
|
5150
|
-
for (const part of
|
|
5151
|
-
const [
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5314
|
+
let hasAny = false;
|
|
5315
|
+
for (const part of header.split(";")) {
|
|
5316
|
+
const [rawKey, rawVal] = part.split("=");
|
|
5317
|
+
const key = rawKey?.trim();
|
|
5318
|
+
const val = rawVal?.trim();
|
|
5319
|
+
if (!key || val === void 0 || val === "") continue;
|
|
5320
|
+
const numVal = Number(val);
|
|
5321
|
+
if (!Number.isFinite(numVal) || numVal < 0) continue;
|
|
5322
|
+
info[key] = numVal;
|
|
5323
|
+
hasAny = true;
|
|
5324
|
+
}
|
|
5325
|
+
return hasAny ? info : null;
|
|
5158
5326
|
}
|
|
5159
5327
|
function parsePositiveInterval(header) {
|
|
5160
5328
|
if (!header) return null;
|
|
@@ -5180,10 +5348,11 @@ function extractSubscriptionMeta(headers) {
|
|
|
5180
5348
|
function saveSubscriptionMeta(subName, meta) {
|
|
5181
5349
|
const cacheData = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
5182
5350
|
if (meta.userInfo) {
|
|
5183
|
-
|
|
5184
|
-
cacheData.
|
|
5185
|
-
cacheData.
|
|
5186
|
-
cacheData.
|
|
5351
|
+
const { upload, download, total, expire } = meta.userInfo;
|
|
5352
|
+
if (upload !== void 0) cacheData.upload = upload;
|
|
5353
|
+
if (download !== void 0) cacheData.download = download;
|
|
5354
|
+
if (total !== void 0) cacheData.total = total;
|
|
5355
|
+
if (expire !== void 0) cacheData.expire = expire;
|
|
5187
5356
|
}
|
|
5188
5357
|
if (meta.updateInterval) cacheData.update_interval = meta.updateInterval;
|
|
5189
5358
|
if (meta.webPageUrl) cacheData.web_page_url = meta.webPageUrl;
|
|
@@ -5387,6 +5556,7 @@ function needsAutoUpdate(sub) {
|
|
|
5387
5556
|
if (!sub.updated_at) return true;
|
|
5388
5557
|
const lastUpdate = new Date(sub.updated_at).getTime();
|
|
5389
5558
|
if (Number.isNaN(lastUpdate)) return true;
|
|
5559
|
+
if (lastUpdate > Date.now()) return true;
|
|
5390
5560
|
const intervalHours = resolveUpdateInterval(sub.url, sub.update_interval);
|
|
5391
5561
|
const intervalMs = intervalHours * 60 * 60 * 1e3;
|
|
5392
5562
|
return Date.now() - lastUpdate > intervalMs;
|
|
@@ -5734,8 +5904,397 @@ function formatTestSummary(summary) {
|
|
|
5734
5904
|
return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
|
|
5735
5905
|
}
|
|
5736
5906
|
|
|
5907
|
+
// src/tunnel.ts
|
|
5908
|
+
import { spawn as spawn2 } from "child_process";
|
|
5909
|
+
import fs7 from "fs";
|
|
5910
|
+
import net from "net";
|
|
5911
|
+
import path6 from "path";
|
|
5912
|
+
var PORT_PROBE_TIMEOUT_MS = 300;
|
|
5913
|
+
var STOP_WAIT_ATTEMPTS = 20;
|
|
5914
|
+
var STOP_WAIT_INTERVAL = 100;
|
|
5915
|
+
var START_WAIT_ATTEMPTS = 40;
|
|
5916
|
+
var START_WAIT_INTERVAL = 500;
|
|
5917
|
+
function getReservedPorts() {
|
|
5918
|
+
const reserved = /* @__PURE__ */ new Map();
|
|
5919
|
+
const mixedPort = Number(BASE_CONFIG["mixed-port"]);
|
|
5920
|
+
if (Number.isInteger(mixedPort)) reserved.set(mixedPort, "mihomo \u6DF7\u5408\u4EE3\u7406\u7AEF\u53E3");
|
|
5921
|
+
reserved.set(CONTROLLER_PORT, "mihomo \u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5922
|
+
const testPort = Number(TEST_CONFIG["mixed-port"]);
|
|
5923
|
+
if (Number.isInteger(testPort)) reserved.set(testPort, "\u6D4B\u901F\u5B9E\u4F8B\u4EE3\u7406\u7AEF\u53E3");
|
|
5924
|
+
const testController = Number(TEST_CONTROLLER_ADDR.split(":")[1]);
|
|
5925
|
+
if (Number.isInteger(testController)) reserved.set(testController, "\u6D4B\u901F\u5B9E\u4F8B\u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5926
|
+
return reserved;
|
|
5927
|
+
}
|
|
5928
|
+
function validateTunnelName(name) {
|
|
5929
|
+
if (!name || !SAFE_NAME_RE.test(name)) {
|
|
5930
|
+
throw new CliError(`\u96A7\u9053\u540D\u79F0\u65E0\u6548: "${name}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C\u4E2D\u6587\uFF08\u6700\u957F 64 \u5B57\u7B26\uFF09`);
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
var SAFE_HOST_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
|
|
5934
|
+
function validateTunnelHost(host) {
|
|
5935
|
+
if (!host) {
|
|
5936
|
+
throw new CliError("\u7F3A\u5C11 --host", { hint: ["\u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"] });
|
|
5937
|
+
}
|
|
5938
|
+
if (host.startsWith("-")) {
|
|
5939
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"`, {
|
|
5940
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5941
|
+
hint: ['\u4E3B\u673A\u540D\u4E0D\u80FD\u4EE5 "-" \u5F00\u5934\u2014\u2014\u5B83\u4F1A\u88AB ssh \u5F53\u4F5C\u9009\u9879\u89E3\u6790\uFF08\u5982 -oProxyCommand=... \u53EF\u6267\u884C\u4EFB\u610F\u547D\u4EE4\uFF09\u3002']
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
if (!SAFE_HOST_RE.test(host)) {
|
|
5945
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C @`, {
|
|
5946
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5947
|
+
hint: ["\u53EF\u7528 ssh \u522B\u540D\uFF08~/.ssh/config \u91CC\u7684 Host\uFF09\u6216 user@hostname\u3002"]
|
|
5948
|
+
});
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
function validateTunnelPort(port, exclude) {
|
|
5952
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5953
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: ${port}\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
5954
|
+
}
|
|
5955
|
+
const reservedLabel = getReservedPorts().get(port);
|
|
5956
|
+
if (reservedLabel) {
|
|
5957
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB ${reservedLabel} \u5360\u7528`, {
|
|
5958
|
+
label: "\u7AEF\u53E3\u51B2\u7A81",
|
|
5959
|
+
hint: ["\u8BF7\u6362\u4E00\u4E2A\u7AEF\u53E3\uFF0C\u4F8B\u5982 1080\u3002"]
|
|
5960
|
+
});
|
|
5961
|
+
}
|
|
5962
|
+
const conflict = getTunnels().find((t) => t.port === port && t.name !== exclude);
|
|
5963
|
+
if (conflict) {
|
|
5964
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u96A7\u9053 "${conflict.name}" \u4F7F\u7528`, { label: "\u7AEF\u53E3\u51B2\u7A81" });
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
function getTunnels() {
|
|
5968
|
+
const settings = readSettings();
|
|
5969
|
+
const tunnels = settings.tunnels;
|
|
5970
|
+
if (!Array.isArray(tunnels)) {
|
|
5971
|
+
if (tunnels !== void 0) {
|
|
5972
|
+
console.warn("\u8B66\u544A: settings.json \u7684 tunnels \u4E0D\u662F\u5217\u8868\uFF0C\u5DF2\u5FFD\u7565\uFF08\u53EF\u7528 mihomo tunnel add \u91CD\u65B0\u6DFB\u52A0\uFF09");
|
|
5973
|
+
}
|
|
5974
|
+
return [];
|
|
5975
|
+
}
|
|
5976
|
+
return tunnels.filter((t) => t != null && typeof t === "object" && typeof t.name === "string" && typeof t.host === "string" && Number.isInteger(t.port));
|
|
5977
|
+
}
|
|
5978
|
+
function findTunnel(name) {
|
|
5979
|
+
return getTunnels().find((t) => t.name === name);
|
|
5980
|
+
}
|
|
5981
|
+
function addTunnel(config) {
|
|
5982
|
+
validateTunnelName(config.name);
|
|
5983
|
+
validateTunnelHost(config.host);
|
|
5984
|
+
validateTunnelPort(config.port);
|
|
5985
|
+
let duplicate = false;
|
|
5986
|
+
updateSettings(() => {
|
|
5987
|
+
const tunnels = [...getTunnels()];
|
|
5988
|
+
if (tunnels.some((t) => t.name === config.name)) {
|
|
5989
|
+
duplicate = true;
|
|
5990
|
+
return {};
|
|
5991
|
+
}
|
|
5992
|
+
tunnels.push(config);
|
|
5993
|
+
return { tunnels };
|
|
5994
|
+
});
|
|
5995
|
+
if (duplicate) {
|
|
5996
|
+
throw new CliError(`\u96A7\u9053 "${config.name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo tunnel rm ${config.name}\uFF09`);
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
function removeTunnel(name) {
|
|
6000
|
+
let found = false;
|
|
6001
|
+
updateSettings(() => {
|
|
6002
|
+
const tunnels = [...getTunnels()];
|
|
6003
|
+
const idx = tunnels.findIndex((t) => t.name === name);
|
|
6004
|
+
if (idx < 0) return {};
|
|
6005
|
+
found = true;
|
|
6006
|
+
tunnels.splice(idx, 1);
|
|
6007
|
+
return { tunnels };
|
|
6008
|
+
});
|
|
6009
|
+
if (!found) return false;
|
|
6010
|
+
clearTunnelRuntime(name);
|
|
6011
|
+
return true;
|
|
6012
|
+
}
|
|
6013
|
+
function getTunnelRuntimePath(name) {
|
|
6014
|
+
validateTunnelName(name);
|
|
6015
|
+
return path6.join(DIRS.tunnel, `${name}.json`);
|
|
6016
|
+
}
|
|
6017
|
+
function readTunnelRuntime(name) {
|
|
6018
|
+
try {
|
|
6019
|
+
const raw = fs7.readFileSync(getTunnelRuntimePath(name), "utf8");
|
|
6020
|
+
const parsed = JSON.parse(raw);
|
|
6021
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
6022
|
+
const r = parsed;
|
|
6023
|
+
if (!Number.isInteger(r.pid) || r.pid <= 0) return null;
|
|
6024
|
+
return {
|
|
6025
|
+
pid: r.pid,
|
|
6026
|
+
started_by: r.started_by === "manual" ? "manual" : "auto",
|
|
6027
|
+
started_at: typeof r.started_at === "string" ? r.started_at : "",
|
|
6028
|
+
port: Number.isInteger(r.port) ? r.port : 0
|
|
6029
|
+
};
|
|
6030
|
+
} catch {
|
|
6031
|
+
return null;
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
function writeTunnelRuntime(name, runtime) {
|
|
6035
|
+
ensureDirs();
|
|
6036
|
+
atomicWriteFileSync(getTunnelRuntimePath(name), JSON.stringify(runtime, null, 2), { mode: 384 });
|
|
6037
|
+
}
|
|
6038
|
+
function clearTunnelRuntime(name) {
|
|
6039
|
+
try {
|
|
6040
|
+
fs7.rmSync(getTunnelRuntimePath(name), { force: true });
|
|
6041
|
+
} catch {
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
function buildSshArgs(tunnel) {
|
|
6045
|
+
return [
|
|
6046
|
+
"-D",
|
|
6047
|
+
`127.0.0.1:${tunnel.port}`,
|
|
6048
|
+
"-N",
|
|
6049
|
+
"-o",
|
|
6050
|
+
"ExitOnForwardFailure=yes",
|
|
6051
|
+
"-o",
|
|
6052
|
+
"BatchMode=yes",
|
|
6053
|
+
"-o",
|
|
6054
|
+
"ServerAliveInterval=30",
|
|
6055
|
+
"-o",
|
|
6056
|
+
"ServerAliveCountMax=3",
|
|
6057
|
+
"-o",
|
|
6058
|
+
"ConnectTimeout=15",
|
|
6059
|
+
tunnel.host
|
|
6060
|
+
];
|
|
6061
|
+
}
|
|
6062
|
+
function commandNeedle(port) {
|
|
6063
|
+
return `-D 127.0.0.1:${port}`;
|
|
6064
|
+
}
|
|
6065
|
+
function getTunnelLogPath(name) {
|
|
6066
|
+
validateTunnelName(name);
|
|
6067
|
+
return path6.join(DIRS.logs, `tunnel-${name}.log`);
|
|
6068
|
+
}
|
|
6069
|
+
function isTunnelProcessAlive(pid, port) {
|
|
6070
|
+
return isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port));
|
|
6071
|
+
}
|
|
6072
|
+
function isPortListening(port, timeoutMs = PORT_PROBE_TIMEOUT_MS) {
|
|
6073
|
+
return new Promise((resolve) => {
|
|
6074
|
+
const socket = new net.Socket();
|
|
6075
|
+
let settled = false;
|
|
6076
|
+
const finish = (result) => {
|
|
6077
|
+
if (settled) return;
|
|
6078
|
+
settled = true;
|
|
6079
|
+
socket.destroy();
|
|
6080
|
+
resolve(result);
|
|
6081
|
+
};
|
|
6082
|
+
socket.setTimeout(timeoutMs);
|
|
6083
|
+
socket.once("connect", () => finish(true));
|
|
6084
|
+
socket.once("timeout", () => finish(false));
|
|
6085
|
+
socket.once("error", () => finish(false));
|
|
6086
|
+
socket.connect(port, "127.0.0.1");
|
|
6087
|
+
});
|
|
6088
|
+
}
|
|
6089
|
+
async function getTunnelStatus(config) {
|
|
6090
|
+
const runtime = readTunnelRuntime(config.name);
|
|
6091
|
+
const alive = runtime !== null && isTunnelProcessAlive(runtime.pid, runtime.port || config.port);
|
|
6092
|
+
if (!alive) {
|
|
6093
|
+
return { config, state: "stopped", pid: null, started_by: null, started_at: null };
|
|
6094
|
+
}
|
|
6095
|
+
const listening = await isPortListening(config.port);
|
|
6096
|
+
return {
|
|
6097
|
+
config,
|
|
6098
|
+
state: listening ? "running" : "dead-port",
|
|
6099
|
+
pid: runtime.pid,
|
|
6100
|
+
started_by: runtime.started_by,
|
|
6101
|
+
started_at: runtime.started_at || null
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
async function getAllTunnelStatus() {
|
|
6105
|
+
return Promise.all(getTunnels().map(getTunnelStatus));
|
|
6106
|
+
}
|
|
6107
|
+
async function startTunnel(name, options) {
|
|
6108
|
+
const config = findTunnel(name);
|
|
6109
|
+
if (!config) {
|
|
6110
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, { hint: ["\u67E5\u770B\u5168\u90E8\u96A7\u9053: mihomo tunnel"] });
|
|
6111
|
+
}
|
|
6112
|
+
const existing = readTunnelRuntime(name);
|
|
6113
|
+
if (existing && isTunnelProcessAlive(existing.pid, existing.port || config.port)) {
|
|
6114
|
+
if (options.startedBy === "manual" && existing.started_by === "auto") {
|
|
6115
|
+
writeTunnelRuntime(name, { ...existing, started_by: "manual" });
|
|
6116
|
+
}
|
|
6117
|
+
return { alreadyRunning: true, pid: existing.pid };
|
|
6118
|
+
}
|
|
6119
|
+
if (existing) clearTunnelRuntime(name);
|
|
6120
|
+
if (await isPortListening(config.port)) {
|
|
6121
|
+
throw new CliError(`\u7AEF\u53E3 ${config.port} \u5DF2\u88AB\u5360\u7528`, {
|
|
6122
|
+
label: "\u65E0\u6CD5\u542F\u52A8\u96A7\u9053",
|
|
6123
|
+
hint: [`\u53EF\u80FD\u5DF2\u6709\u96A7\u9053\u6216\u5176\u4ED6\u7A0B\u5E8F\u5728\u76D1\u542C 127.0.0.1:${config.port}\u3002`, `\u67E5\u770B\u5360\u7528: lsof -nP -iTCP:${config.port} -sTCP:LISTEN`]
|
|
6124
|
+
});
|
|
6125
|
+
}
|
|
6126
|
+
ensureDirs();
|
|
6127
|
+
const logPath = getTunnelLogPath(name);
|
|
6128
|
+
const logFd = fs7.openSync(logPath, "w");
|
|
6129
|
+
const child = spawn2("ssh", buildSshArgs(config), {
|
|
6130
|
+
detached: true,
|
|
6131
|
+
stdio: ["ignore", logFd, logFd]
|
|
6132
|
+
});
|
|
6133
|
+
child.on("error", () => {
|
|
6134
|
+
});
|
|
6135
|
+
fs7.closeSync(logFd);
|
|
6136
|
+
child.unref();
|
|
6137
|
+
const pid = child.pid;
|
|
6138
|
+
if (!pid) {
|
|
6139
|
+
throw new CliError("\u65E0\u6CD5\u521B\u5EFA ssh \u8FDB\u7A0B", { label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25", hint: ["\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 ssh \u5BA2\u6237\u7AEF\u3002"] });
|
|
6140
|
+
}
|
|
6141
|
+
writeTunnelRuntime(name, {
|
|
6142
|
+
pid,
|
|
6143
|
+
started_by: options.startedBy,
|
|
6144
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6145
|
+
port: config.port
|
|
6146
|
+
});
|
|
6147
|
+
const unregisterCleanup = registerCleanup(() => {
|
|
6148
|
+
try {
|
|
6149
|
+
process.kill(pid, "SIGKILL");
|
|
6150
|
+
} catch {
|
|
6151
|
+
}
|
|
6152
|
+
clearTunnelRuntime(name);
|
|
6153
|
+
});
|
|
6154
|
+
try {
|
|
6155
|
+
for (let i = 0; i < START_WAIT_ATTEMPTS; i++) {
|
|
6156
|
+
if (!isProcessRunning(pid)) break;
|
|
6157
|
+
if (await isPortListening(config.port)) {
|
|
6158
|
+
unregisterCleanup();
|
|
6159
|
+
return { alreadyRunning: false, pid };
|
|
6160
|
+
}
|
|
6161
|
+
await new Promise((resolve) => setTimeout(resolve, START_WAIT_INTERVAL));
|
|
6162
|
+
}
|
|
6163
|
+
} finally {
|
|
6164
|
+
unregisterCleanup();
|
|
6165
|
+
}
|
|
6166
|
+
clearTunnelRuntime(name);
|
|
6167
|
+
if (isProcessRunning(pid)) {
|
|
6168
|
+
try {
|
|
6169
|
+
process.kill(pid, "SIGKILL");
|
|
6170
|
+
} catch {
|
|
6171
|
+
}
|
|
6172
|
+
}
|
|
6173
|
+
throw new CliError("ssh \u96A7\u9053\u672A\u80FD\u5EFA\u7ACB", {
|
|
6174
|
+
label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25",
|
|
6175
|
+
hint: [...readLogTail(logPath), `\u5B8C\u6574\u65E5\u5FD7: ${logPath}`]
|
|
6176
|
+
});
|
|
6177
|
+
}
|
|
6178
|
+
function readLogTail(logPath, maxLines = 5) {
|
|
6179
|
+
try {
|
|
6180
|
+
const content = fs7.readFileSync(logPath, "utf8").trim();
|
|
6181
|
+
if (!content) return [];
|
|
6182
|
+
return content.split("\n").slice(-maxLines).map((line) => ` ${line.trim()}`);
|
|
6183
|
+
} catch {
|
|
6184
|
+
return [];
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
function stopTunnel(name) {
|
|
6188
|
+
const config = findTunnel(name);
|
|
6189
|
+
const runtime = readTunnelRuntime(name);
|
|
6190
|
+
if (!runtime) {
|
|
6191
|
+
clearTunnelRuntime(name);
|
|
6192
|
+
return { notRunning: true, pid: null };
|
|
6193
|
+
}
|
|
6194
|
+
const port = runtime.port || config?.port || 0;
|
|
6195
|
+
const { pid } = runtime;
|
|
6196
|
+
if (!isTunnelProcessAlive(pid, port)) {
|
|
6197
|
+
clearTunnelRuntime(name);
|
|
6198
|
+
return { notRunning: true, pid: null };
|
|
6199
|
+
}
|
|
6200
|
+
try {
|
|
6201
|
+
process.kill(pid, "SIGTERM");
|
|
6202
|
+
} catch {
|
|
6203
|
+
}
|
|
6204
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6205
|
+
if (!isProcessRunning(pid)) break;
|
|
6206
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6207
|
+
}
|
|
6208
|
+
if (isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port))) {
|
|
6209
|
+
try {
|
|
6210
|
+
process.kill(pid, "SIGKILL");
|
|
6211
|
+
} catch {
|
|
6212
|
+
}
|
|
6213
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6214
|
+
if (!isProcessRunning(pid)) break;
|
|
6215
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6216
|
+
}
|
|
6217
|
+
}
|
|
6218
|
+
clearTunnelRuntime(name);
|
|
6219
|
+
return { notRunning: false, pid };
|
|
6220
|
+
}
|
|
6221
|
+
async function startAutoTunnels() {
|
|
6222
|
+
const outcomes = [];
|
|
6223
|
+
for (const tunnel of getTunnels()) {
|
|
6224
|
+
if (!tunnel.auto) continue;
|
|
6225
|
+
try {
|
|
6226
|
+
const result = await startTunnel(tunnel.name, { startedBy: "auto" });
|
|
6227
|
+
outcomes.push({ name: tunnel.name, ok: true, alreadyRunning: result.alreadyRunning });
|
|
6228
|
+
} catch (e) {
|
|
6229
|
+
outcomes.push({
|
|
6230
|
+
name: tunnel.name,
|
|
6231
|
+
ok: false,
|
|
6232
|
+
error: e instanceof CliError ? e : new CliError(e.message)
|
|
6233
|
+
});
|
|
6234
|
+
}
|
|
6235
|
+
}
|
|
6236
|
+
return outcomes;
|
|
6237
|
+
}
|
|
6238
|
+
function stopAutoTunnels() {
|
|
6239
|
+
const stopped = [];
|
|
6240
|
+
for (const tunnel of getTunnels()) {
|
|
6241
|
+
const runtime = readTunnelRuntime(tunnel.name);
|
|
6242
|
+
if (runtime?.started_by !== "auto") continue;
|
|
6243
|
+
const result = stopTunnel(tunnel.name);
|
|
6244
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6245
|
+
}
|
|
6246
|
+
return stopped;
|
|
6247
|
+
}
|
|
6248
|
+
function stopAllTunnels() {
|
|
6249
|
+
const stopped = [];
|
|
6250
|
+
for (const tunnel of getTunnels()) {
|
|
6251
|
+
const result = stopTunnel(tunnel.name);
|
|
6252
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6253
|
+
}
|
|
6254
|
+
return stopped;
|
|
6255
|
+
}
|
|
6256
|
+
function getTunnelOverwritePath(name) {
|
|
6257
|
+
validateTunnelName(name);
|
|
6258
|
+
return path6.join(USER_DATA_DIR, `overwrite.tunnel-${name}.yaml`);
|
|
6259
|
+
}
|
|
6260
|
+
function renderTunnelOverwrite(tunnel) {
|
|
6261
|
+
const proxyName = `Tunnel-${tunnel.name}-Host`;
|
|
6262
|
+
const groupName = `Tunnel-${tunnel.name}`;
|
|
6263
|
+
return `# mihomo-cli \u96A7\u9053\u8986\u5199\uFF08tunnel: ${tunnel.name}\uFF09
|
|
6264
|
+
# \u672C\u6587\u4EF6\u7531 mihomo-cli \u9996\u6B21\u521B\u5EFA\uFF0C\u4E4B\u540E\u5B8C\u5168\u7531\u4F60\u7EF4\u62A4\u2014\u2014CLI \u4E0D\u4F1A\u518D\u6539\u5199\u6216\u5220\u9664\u5B83\u3002
|
|
6265
|
+
#
|
|
6266
|
+
# ~ \u662F\u300C\u6309 name \u5C31\u5730\u5408\u5E76\u300D\u8BED\u4E49\uFF1A\u4E0E\u5176\u4ED6\u8986\u5199\u6587\u4EF6\u4E2D\u7684\u540C\u540D\u8282\u70B9/\u5206\u7EC4\u5408\u5E76\uFF0C
|
|
6267
|
+
# \u4E0D\u4F9D\u8D56\u6587\u4EF6\u52A0\u8F7D\u987A\u5E8F\uFF08+proxies \u5219\u4F9D\u8D56\u5B57\u6BCD\u5E8F\uFF0C\u4F1A\u88AB\u540E\u6765\u7684\u6587\u4EF6\u538B\u8FC7\u53BB\uFF09\u3002
|
|
6268
|
+
|
|
6269
|
+
~proxies:
|
|
6270
|
+
- name: ${proxyName}
|
|
6271
|
+
type: socks5
|
|
6272
|
+
server: 127.0.0.1
|
|
6273
|
+
port: ${tunnel.port}
|
|
6274
|
+
|
|
6275
|
+
~proxy-groups:
|
|
6276
|
+
- name: ${groupName}
|
|
6277
|
+
type: select
|
|
6278
|
+
proxies:
|
|
6279
|
+
- ${proxyName}
|
|
6280
|
+
- DIRECT
|
|
6281
|
+
|
|
6282
|
+
# \u53D6\u6D88\u6CE8\u91CA\u5E76\u586B\u5165\u9700\u8981\u8D70\u96A7\u9053\u7684\u5185\u7F51\u57DF\u540D/\u7F51\u6BB5\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u5730\u5740\uFF09\uFF1A
|
|
6283
|
+
# +rules:
|
|
6284
|
+
# - DOMAIN-SUFFIX,example.internal,${groupName}
|
|
6285
|
+
# - IP-CIDR,10.0.0.0/8,${groupName}
|
|
6286
|
+
`;
|
|
6287
|
+
}
|
|
6288
|
+
function ensureTunnelOverwriteFile(tunnel) {
|
|
6289
|
+
const filePath = getTunnelOverwritePath(tunnel.name);
|
|
6290
|
+
if (fs7.existsSync(filePath)) return false;
|
|
6291
|
+
ensureDirs();
|
|
6292
|
+
atomicWriteFileSync(filePath, renderTunnelOverwrite(tunnel), { mode: 384 });
|
|
6293
|
+
return true;
|
|
6294
|
+
}
|
|
6295
|
+
|
|
5737
6296
|
// src/commands/status.ts
|
|
5738
|
-
function printStatus() {
|
|
6297
|
+
async function printStatus() {
|
|
5739
6298
|
const status = getStatus();
|
|
5740
6299
|
const state = getRunningState();
|
|
5741
6300
|
const info = getConfigInfo();
|
|
@@ -5802,6 +6361,17 @@ function printStatus() {
|
|
|
5802
6361
|
if (isDaemonEnabled()) {
|
|
5803
6362
|
console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
|
|
5804
6363
|
}
|
|
6364
|
+
const tunnels = getTunnels();
|
|
6365
|
+
if (tunnels.length > 0) {
|
|
6366
|
+
const statuses = await getAllTunnelStatus();
|
|
6367
|
+
const parts = statuses.map((s) => {
|
|
6368
|
+
const label = `${s.config.name}:${s.config.port}`;
|
|
6369
|
+
if (s.state === "running") return colors.green(label);
|
|
6370
|
+
if (s.state === "dead-port") return colors.yellow(`${label} \u5047\u6D3B`);
|
|
6371
|
+
return colors.gray(label);
|
|
6372
|
+
});
|
|
6373
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${parts.join(", ")}`);
|
|
6374
|
+
}
|
|
5805
6375
|
console.log("");
|
|
5806
6376
|
}
|
|
5807
6377
|
|
|
@@ -5811,23 +6381,51 @@ function handleStopResult(result) {
|
|
|
5811
6381
|
throw new CliError(result.remaining.join(", "), { label: "\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62", hint: "\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo" });
|
|
5812
6382
|
}
|
|
5813
6383
|
}
|
|
5814
|
-
|
|
6384
|
+
function stopAutoTunnelsWithLog() {
|
|
6385
|
+
const stopped = stopAutoTunnels();
|
|
6386
|
+
if (stopped.length > 0) {
|
|
6387
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")}: ${stopped.join(", ")}`);
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6390
|
+
async function cmdStop(args) {
|
|
5815
6391
|
if (isDaemonEnabled()) {
|
|
5816
6392
|
console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
|
|
5817
6393
|
console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
|
|
5818
6394
|
return;
|
|
5819
6395
|
}
|
|
6396
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5820
6397
|
const pids = getMihomoPids();
|
|
5821
6398
|
if (pids.length === 0) {
|
|
5822
6399
|
console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
|
|
6400
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5823
6401
|
return;
|
|
5824
6402
|
}
|
|
5825
6403
|
console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
|
|
5826
6404
|
handleStopResult(stop());
|
|
5827
6405
|
console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
|
|
6406
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5828
6407
|
}
|
|
5829
6408
|
|
|
5830
6409
|
// src/commands/start.ts
|
|
6410
|
+
async function startAutoTunnelsWithWarning() {
|
|
6411
|
+
const outcomes = await startAutoTunnels();
|
|
6412
|
+
if (outcomes.length === 0) return;
|
|
6413
|
+
const started = outcomes.filter((o) => o.ok && !o.alreadyRunning);
|
|
6414
|
+
if (started.length > 0) {
|
|
6415
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")}: ${started.map((o) => o.name).join(", ")}`);
|
|
6416
|
+
}
|
|
6417
|
+
for (const failed of outcomes.filter((o) => !o.ok)) {
|
|
6418
|
+
console.log("");
|
|
6419
|
+
console.log(colors.yellow(`\u8B66\u544A: \u96A7\u9053 "${failed.name}" \u542F\u52A8\u5931\u8D25`));
|
|
6420
|
+
console.log(colors.gray(` ${failed.error?.message ?? "\u672A\u77E5\u9519\u8BEF"}`));
|
|
6421
|
+
for (const line of failed.error?.hint ?? []) {
|
|
6422
|
+
if (line.trim()) console.log(colors.gray(line.startsWith(" ") ? line : ` ${line}`));
|
|
6423
|
+
}
|
|
6424
|
+
console.log(colors.gray(" \u5185\u7F51\u5206\u6D41\u89C4\u5219\u5C06\u4E0D\u53EF\u7528\uFF0C\u5176\u4F59\u6D41\u91CF\u6B63\u5E38"));
|
|
6425
|
+
console.log(colors.gray(` \u6392\u67E5: mihomo tunnel status ${failed.name}`));
|
|
6426
|
+
console.log("");
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
5831
6429
|
async function cmdStart(args) {
|
|
5832
6430
|
const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
|
|
5833
6431
|
if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
|
|
@@ -5846,6 +6444,7 @@ async function cmdStart(args) {
|
|
|
5846
6444
|
const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
|
|
5847
6445
|
const skipUpdate = hasFlag(args, "-s", "--no-update");
|
|
5848
6446
|
const skipClean = hasFlag(args, "--no-clean");
|
|
6447
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5849
6448
|
const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
|
|
5850
6449
|
const sub = requireActiveSubscription("\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
|
|
5851
6450
|
if (!skipUpdate) {
|
|
@@ -5887,6 +6486,9 @@ async function cmdStart(args) {
|
|
|
5887
6486
|
const lines = e.message.split("\n");
|
|
5888
6487
|
throw new CliError(lines[0], { label: "\u542F\u52A8\u5931\u8D25", hint: lines.slice(1) });
|
|
5889
6488
|
}
|
|
6489
|
+
if (!skipTunnel) {
|
|
6490
|
+
await startAutoTunnelsWithWarning();
|
|
6491
|
+
}
|
|
5890
6492
|
const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
|
|
5891
6493
|
if (!skipClean && configInfo.proxies > cleanThreshold) {
|
|
5892
6494
|
const cache = readSubscriptionCache();
|
|
@@ -5926,7 +6528,7 @@ async function cmdStart(args) {
|
|
|
5926
6528
|
saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5927
6529
|
}
|
|
5928
6530
|
}
|
|
5929
|
-
printStatus();
|
|
6531
|
+
await printStatus();
|
|
5930
6532
|
}
|
|
5931
6533
|
|
|
5932
6534
|
// src/commands/shared.ts
|
|
@@ -6010,7 +6612,8 @@ async function daemonOn() {
|
|
|
6010
6612
|
printDaemonStatus();
|
|
6011
6613
|
}
|
|
6012
6614
|
function daemonOff() {
|
|
6013
|
-
|
|
6615
|
+
const residualRootKernel = getMihomoPids().some(isProcessRoot);
|
|
6616
|
+
if (!isDaemonEnabled() && !residualRootKernel) {
|
|
6014
6617
|
console.log("\u4FDD\u6D3B\u5DF2\u662F\u5173\u95ED\u72B6\u6001");
|
|
6015
6618
|
console.log("");
|
|
6016
6619
|
printDaemonStatus();
|
|
@@ -6133,8 +6736,8 @@ async function cmdDirectory(args) {
|
|
|
6133
6736
|
|
|
6134
6737
|
// src/kernel.ts
|
|
6135
6738
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
6136
|
-
import
|
|
6137
|
-
import
|
|
6739
|
+
import fs8 from "fs";
|
|
6740
|
+
import path7 from "path";
|
|
6138
6741
|
|
|
6139
6742
|
// node_modules/compare-versions/lib/esm/utils.js
|
|
6140
6743
|
var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
|
|
@@ -6202,6 +6805,23 @@ function withMirror(url, mirror) {
|
|
|
6202
6805
|
}
|
|
6203
6806
|
return url;
|
|
6204
6807
|
}
|
|
6808
|
+
var ALLOWED_ASSET_HOSTS = /* @__PURE__ */ new Set(["github.com", "api.github.com", "objects.githubusercontent.com", "release-assets.githubusercontent.com"]);
|
|
6809
|
+
function assertTrustedAssetUrl(rawUrl) {
|
|
6810
|
+
let parsed;
|
|
6811
|
+
try {
|
|
6812
|
+
parsed = new URL(rawUrl);
|
|
6813
|
+
} catch {
|
|
6814
|
+
throw new Error(`\u5185\u6838\u4E0B\u8F7D\u5730\u5740\u65E0\u6CD5\u89E3\u6790: ${rawUrl}`);
|
|
6815
|
+
}
|
|
6816
|
+
if (parsed.protocol !== "https:") {
|
|
6817
|
+
throw new Error(`\u5185\u6838\u4E0B\u8F7D\u5730\u5740\u5FC5\u987B\u662F https: ${rawUrl}`);
|
|
6818
|
+
}
|
|
6819
|
+
const host = parsed.hostname.toLowerCase();
|
|
6820
|
+
if (!ALLOWED_ASSET_HOSTS.has(host)) {
|
|
6821
|
+
throw new Error(`\u5185\u6838\u4E0B\u8F7D\u5730\u5740\u7684\u4E3B\u673A\u4E0D\u5728\u767D\u540D\u5355\u5185: ${host}
|
|
6822
|
+
\u4EC5\u5141\u8BB8: ${[...ALLOWED_ASSET_HOSTS].join(", ")}`);
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6205
6825
|
function getArch() {
|
|
6206
6826
|
const arch = process.arch;
|
|
6207
6827
|
if (arch === "arm64") return "arm64";
|
|
@@ -6260,15 +6880,16 @@ async function checkUpdate(mirror) {
|
|
|
6260
6880
|
}
|
|
6261
6881
|
function findBinaryInDir(dir, maxDepth = 4) {
|
|
6262
6882
|
if (maxDepth <= 0) return null;
|
|
6263
|
-
const files =
|
|
6883
|
+
const files = fs8.readdirSync(dir);
|
|
6264
6884
|
for (const f of files) {
|
|
6265
|
-
const fullPath =
|
|
6266
|
-
const stat =
|
|
6885
|
+
const fullPath = path7.join(dir, f);
|
|
6886
|
+
const stat = fs8.lstatSync(fullPath);
|
|
6267
6887
|
if (stat.isDirectory()) {
|
|
6268
6888
|
const found = findBinaryInDir(fullPath, maxDepth - 1);
|
|
6269
6889
|
if (found) return found;
|
|
6270
6890
|
continue;
|
|
6271
6891
|
}
|
|
6892
|
+
if (!stat.isFile()) continue;
|
|
6272
6893
|
if (f === "mihomo") return fullPath;
|
|
6273
6894
|
if (f.includes("mihomo") && !f.endsWith(".gz")) return fullPath;
|
|
6274
6895
|
}
|
|
@@ -6288,8 +6909,9 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6288
6909
|
throw new Error(`\u672A\u627E\u5230\u5339\u914D\u7684\u5185\u6838\u6587\u4EF6
|
|
6289
6910
|
\u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
|
|
6290
6911
|
}
|
|
6912
|
+
assertTrustedAssetUrl(asset.browser_download_url);
|
|
6291
6913
|
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
6292
|
-
const tempPath =
|
|
6914
|
+
const tempPath = path7.join(DIRS.kernel, path7.basename(asset.name));
|
|
6293
6915
|
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
6294
6916
|
if (mirror && progressCallback) {
|
|
6295
6917
|
progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
|
|
@@ -6297,9 +6919,26 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6297
6919
|
if (progressCallback) {
|
|
6298
6920
|
progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
|
|
6299
6921
|
}
|
|
6922
|
+
const maxBytes = Number.isFinite(asset.size) && asset.size > 0 ? Math.floor(asset.size * 2 + 1024 * 1024) : 512 * 1024 * 1024;
|
|
6300
6923
|
const curlResult = spawnSync4(
|
|
6301
6924
|
"curl",
|
|
6302
|
-
[
|
|
6925
|
+
[
|
|
6926
|
+
"-L",
|
|
6927
|
+
"--proto",
|
|
6928
|
+
"=https",
|
|
6929
|
+
"--proto-redir",
|
|
6930
|
+
"=https",
|
|
6931
|
+
"--max-filesize",
|
|
6932
|
+
String(maxBytes),
|
|
6933
|
+
"--progress-bar",
|
|
6934
|
+
"--connect-timeout",
|
|
6935
|
+
"30",
|
|
6936
|
+
"--max-time",
|
|
6937
|
+
String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)),
|
|
6938
|
+
"-o",
|
|
6939
|
+
tempPath,
|
|
6940
|
+
downloadUrl
|
|
6941
|
+
],
|
|
6303
6942
|
{ stdio: "inherit" }
|
|
6304
6943
|
);
|
|
6305
6944
|
if (curlResult.error) {
|
|
@@ -6310,14 +6949,27 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6310
6949
|
}
|
|
6311
6950
|
if (curlResult.status !== 0) {
|
|
6312
6951
|
try {
|
|
6313
|
-
|
|
6952
|
+
fs8.unlinkSync(tempPath);
|
|
6314
6953
|
} catch {
|
|
6315
6954
|
}
|
|
6316
6955
|
throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
|
|
6317
6956
|
}
|
|
6318
|
-
if (!
|
|
6957
|
+
if (!fs8.existsSync(tempPath)) {
|
|
6319
6958
|
throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
|
|
6320
6959
|
}
|
|
6960
|
+
if (Number.isFinite(asset.size) && asset.size > 0) {
|
|
6961
|
+
const actual = fs8.statSync(tempPath).size;
|
|
6962
|
+
if (actual !== asset.size) {
|
|
6963
|
+
try {
|
|
6964
|
+
fs8.unlinkSync(tempPath);
|
|
6965
|
+
} catch {
|
|
6966
|
+
}
|
|
6967
|
+
throw new Error(
|
|
6968
|
+
`\u4E0B\u8F7D\u7684\u6587\u4EF6\u5927\u5C0F\u4E0E release \u5143\u6570\u636E\u4E0D\u7B26\uFF08\u671F\u671B ${asset.size} \u5B57\u8282\uFF0C\u5B9E\u9645 ${actual} \u5B57\u8282\uFF09
|
|
6969
|
+
\u53EF\u80FD\u662F\u4E0B\u8F7D\u88AB\u622A\u65AD\u6216\u5185\u5BB9\u88AB\u66FF\u6362\uFF0C\u8BF7\u91CD\u8BD5\u6216\u6539\u7528 --no-mirror \u76F4\u8FDE`
|
|
6970
|
+
);
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6321
6973
|
if (progressCallback) {
|
|
6322
6974
|
progressCallback("\u89E3\u538B\u5185\u6838...");
|
|
6323
6975
|
}
|
|
@@ -6328,27 +6980,38 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6328
6980
|
const listResult = spawnSync4("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
|
|
6329
6981
|
if (listResult.error) throw listResult.error;
|
|
6330
6982
|
if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
|
|
6331
|
-
const
|
|
6332
|
-
for (const entry of entries) {
|
|
6983
|
+
for (const entry of (listResult.stdout || "").split("\n").filter(Boolean)) {
|
|
6333
6984
|
if (entry.startsWith("/") || entry.split("/").includes("..")) {
|
|
6334
6985
|
throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
|
|
6335
6986
|
}
|
|
6336
6987
|
}
|
|
6337
|
-
const
|
|
6988
|
+
const typeResult = spawnSync4("tar", ["-tvzf", tempPath], { encoding: "utf8", timeout: 6e4 });
|
|
6989
|
+
if (typeResult.error) throw typeResult.error;
|
|
6990
|
+
if (typeResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${typeResult.status}`);
|
|
6991
|
+
for (const line of (typeResult.stdout || "").split("\n").filter(Boolean)) {
|
|
6992
|
+
const typeChar = line[0];
|
|
6993
|
+
if (typeChar !== "-" && typeChar !== "d") {
|
|
6994
|
+
throw new Error(`\u5F52\u6863\u542B\u975E\u666E\u901A\u6587\u4EF6\u6761\u76EE\uFF08\u7C7B\u578B "${typeChar}"\uFF09: ${line}`);
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
const tarResult = spawnSync4("tar", ["--no-same-owner", "-xzf", tempPath, "-C", extractPath], {
|
|
6998
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
6999
|
+
timeout: 6e4
|
|
7000
|
+
});
|
|
6338
7001
|
if (tarResult.error) throw tarResult.error;
|
|
6339
7002
|
if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
|
|
6340
7003
|
} else if (tempPath.endsWith(".gz")) {
|
|
6341
|
-
const baseName =
|
|
6342
|
-
const outputPath =
|
|
7004
|
+
const baseName = path7.basename(tempPath, ".gz");
|
|
7005
|
+
const outputPath = path7.join(extractPath, baseName);
|
|
6343
7006
|
const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
|
|
6344
7007
|
if (gzipResult.error) throw gzipResult.error;
|
|
6345
7008
|
if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
|
|
6346
|
-
|
|
7009
|
+
fs8.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
|
|
6347
7010
|
extractedBinary = outputPath;
|
|
6348
7011
|
}
|
|
6349
7012
|
} catch (e) {
|
|
6350
7013
|
try {
|
|
6351
|
-
|
|
7014
|
+
fs8.unlinkSync(tempPath);
|
|
6352
7015
|
} catch {
|
|
6353
7016
|
}
|
|
6354
7017
|
throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
|
|
@@ -6356,23 +7019,23 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6356
7019
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
6357
7020
|
if (!foundBinary) {
|
|
6358
7021
|
try {
|
|
6359
|
-
|
|
7022
|
+
fs8.unlinkSync(tempPath);
|
|
6360
7023
|
} catch {
|
|
6361
7024
|
}
|
|
6362
7025
|
throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
|
|
6363
7026
|
}
|
|
6364
7027
|
const targetPath = PATHS.mihomoBinary;
|
|
6365
7028
|
if (foundBinary !== targetPath) {
|
|
6366
|
-
if (
|
|
6367
|
-
|
|
7029
|
+
if (fs8.existsSync(targetPath)) {
|
|
7030
|
+
fs8.chmodSync(targetPath, 493);
|
|
6368
7031
|
try {
|
|
6369
|
-
|
|
7032
|
+
fs8.unlinkSync(targetPath);
|
|
6370
7033
|
} catch {
|
|
6371
7034
|
}
|
|
6372
7035
|
}
|
|
6373
|
-
|
|
7036
|
+
fs8.renameSync(foundBinary, targetPath);
|
|
6374
7037
|
}
|
|
6375
|
-
|
|
7038
|
+
fs8.chmodSync(targetPath, 493);
|
|
6376
7039
|
if (progressCallback) {
|
|
6377
7040
|
progressCallback("\u6821\u9A8C\u5185\u6838...");
|
|
6378
7041
|
}
|
|
@@ -6380,11 +7043,11 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6380
7043
|
const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
|
|
6381
7044
|
if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
|
|
6382
7045
|
try {
|
|
6383
|
-
|
|
7046
|
+
fs8.unlinkSync(targetPath);
|
|
6384
7047
|
} catch {
|
|
6385
7048
|
}
|
|
6386
7049
|
try {
|
|
6387
|
-
|
|
7050
|
+
fs8.unlinkSync(tempPath);
|
|
6388
7051
|
} catch {
|
|
6389
7052
|
}
|
|
6390
7053
|
throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
|
|
@@ -6392,7 +7055,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6392
7055
|
\u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
|
|
6393
7056
|
}
|
|
6394
7057
|
try {
|
|
6395
|
-
|
|
7058
|
+
fs8.unlinkSync(tempPath);
|
|
6396
7059
|
} catch {
|
|
6397
7060
|
}
|
|
6398
7061
|
clearKernelVersionCache();
|
|
@@ -6527,7 +7190,7 @@ function cmdLogs(args) {
|
|
|
6527
7190
|
}
|
|
6528
7191
|
|
|
6529
7192
|
// src/commands/overwrite.ts
|
|
6530
|
-
import
|
|
7193
|
+
import path8 from "path";
|
|
6531
7194
|
function printOverwriteList() {
|
|
6532
7195
|
const info = listOverwriteFile();
|
|
6533
7196
|
const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
|
|
@@ -6537,8 +7200,8 @@ function printOverwriteList() {
|
|
|
6537
7200
|
if (info.files.length === 0) {
|
|
6538
7201
|
console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
|
|
6539
7202
|
console.log("");
|
|
6540
|
-
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${
|
|
6541
|
-
console.log(` \u6216 ${
|
|
7203
|
+
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path8.join(info.dir, "overwrite.yaml")}`);
|
|
7204
|
+
console.log(` \u6216 ${path8.join(info.dir, "overwrite.dns.yaml")}`);
|
|
6542
7205
|
console.log("");
|
|
6543
7206
|
} else {
|
|
6544
7207
|
console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
|
|
@@ -6602,7 +7265,7 @@ async function cmdOverwrite(args) {
|
|
|
6602
7265
|
}
|
|
6603
7266
|
|
|
6604
7267
|
// src/commands/reset.ts
|
|
6605
|
-
import
|
|
7268
|
+
import fs9 from "fs";
|
|
6606
7269
|
var RESET_TARGETS = [
|
|
6607
7270
|
{
|
|
6608
7271
|
id: "subs",
|
|
@@ -6635,6 +7298,21 @@ var RESET_TARGETS = [
|
|
|
6635
7298
|
paths: () => [DIRS.runtime],
|
|
6636
7299
|
needsStop: true
|
|
6637
7300
|
},
|
|
7301
|
+
{
|
|
7302
|
+
// 隧道要在删 pid 文件之前先停进程(onBefore):文件一删就再也找不到那些 ssh 进程,
|
|
7303
|
+
// 它们会继续占着端口跑下去,且 CLI 无任何路径能停掉。
|
|
7304
|
+
// **必须排在 settings 之前**:onAfter 会 writeSettings 重建 settings.json,
|
|
7305
|
+
// 排在 settings 之后会让 `reset --full` 留下一个 {}(同 subs 的处理,见执行顺序注释)
|
|
7306
|
+
id: "tunnel",
|
|
7307
|
+
aliases: ["tunnel", "tunnels", "ssh"],
|
|
7308
|
+
label: "\u96A7\u9053",
|
|
7309
|
+
paths: () => [DIRS.tunnel],
|
|
7310
|
+
needsStop: false,
|
|
7311
|
+
onBefore: () => stopAllTunnels(),
|
|
7312
|
+
// 同步清空 settings 里的隧道列表:只删运行态会留下「列表在但状态没了」的半重置状态。
|
|
7313
|
+
// 覆写文件不动——那是用户维护的资产,由 overwrites target 负责
|
|
7314
|
+
onAfter: () => writeSettings({ tunnels: void 0 })
|
|
7315
|
+
},
|
|
6638
7316
|
{
|
|
6639
7317
|
id: "settings",
|
|
6640
7318
|
aliases: ["setting", "settings", "config"],
|
|
@@ -6662,8 +7340,8 @@ var RESET_TARGETS = [
|
|
|
6662
7340
|
label: "\u8986\u5199",
|
|
6663
7341
|
paths: () => {
|
|
6664
7342
|
const dir = USER_DATA_DIR;
|
|
6665
|
-
if (!
|
|
6666
|
-
return
|
|
7343
|
+
if (!fs9.existsSync(dir)) return [];
|
|
7344
|
+
return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
|
|
6667
7345
|
},
|
|
6668
7346
|
needsStop: false
|
|
6669
7347
|
},
|
|
@@ -6726,7 +7404,7 @@ async function cmdReset(args) {
|
|
|
6726
7404
|
}
|
|
6727
7405
|
targets = matched;
|
|
6728
7406
|
} else {
|
|
6729
|
-
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
|
|
7407
|
+
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon", "tunnel"].includes(t.id));
|
|
6730
7408
|
}
|
|
6731
7409
|
for (const t of targets) {
|
|
6732
7410
|
if (t.checkEmpty?.()) {
|
|
@@ -6786,8 +7464,9 @@ async function cmdReset(args) {
|
|
|
6786
7464
|
}
|
|
6787
7465
|
}
|
|
6788
7466
|
for (const t of targets) {
|
|
7467
|
+
t.onBefore?.();
|
|
6789
7468
|
for (const p of t.paths()) {
|
|
6790
|
-
if (
|
|
7469
|
+
if (fs9.existsSync(p)) {
|
|
6791
7470
|
try {
|
|
6792
7471
|
rmrf(p);
|
|
6793
7472
|
} catch (e) {
|
|
@@ -6805,9 +7484,9 @@ async function cmdReset(args) {
|
|
|
6805
7484
|
}
|
|
6806
7485
|
|
|
6807
7486
|
// src/test-instance.ts
|
|
6808
|
-
import { spawn as
|
|
6809
|
-
import
|
|
6810
|
-
import
|
|
7487
|
+
import { spawn as spawn3 } from "child_process";
|
|
7488
|
+
import fs10 from "fs";
|
|
7489
|
+
import path9 from "path";
|
|
6811
7490
|
function isProxyValid(proxy) {
|
|
6812
7491
|
if (proxy === null || typeof proxy !== "object") return false;
|
|
6813
7492
|
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
@@ -6818,20 +7497,20 @@ function isProxyValid(proxy) {
|
|
|
6818
7497
|
}
|
|
6819
7498
|
return true;
|
|
6820
7499
|
}
|
|
6821
|
-
var TEST_DIR =
|
|
7500
|
+
var TEST_DIR = path9.join(USER_DATA_DIR, "test");
|
|
6822
7501
|
var TEST_DIRS = {
|
|
6823
|
-
data:
|
|
6824
|
-
runtime:
|
|
7502
|
+
data: path9.join(TEST_DIR, "data"),
|
|
7503
|
+
runtime: path9.join(TEST_DIR, "runtime")
|
|
6825
7504
|
};
|
|
6826
7505
|
var TEST_PATHS = {
|
|
6827
|
-
configFile:
|
|
6828
|
-
pidFile:
|
|
6829
|
-
logFile:
|
|
7506
|
+
configFile: path9.join(TEST_DIRS.runtime, "config.yaml"),
|
|
7507
|
+
pidFile: path9.join(TEST_DIRS.runtime, "pid"),
|
|
7508
|
+
logFile: path9.join(TEST_DIR, "test.log")
|
|
6830
7509
|
};
|
|
6831
7510
|
var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
|
|
6832
7511
|
function ensureTestDirs() {
|
|
6833
7512
|
for (const dir of Object.values(TEST_DIRS)) {
|
|
6834
|
-
|
|
7513
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6835
7514
|
}
|
|
6836
7515
|
}
|
|
6837
7516
|
function cleanupTestDir() {
|
|
@@ -6869,25 +7548,25 @@ function buildTestConfig(subName) {
|
|
|
6869
7548
|
rules: ["MATCH,PROXY"]
|
|
6870
7549
|
};
|
|
6871
7550
|
const content = dumpYaml(config);
|
|
6872
|
-
|
|
7551
|
+
fs10.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
|
|
6873
7552
|
}
|
|
6874
7553
|
async function startTestInstance() {
|
|
6875
7554
|
const binary = PATHS.mihomoBinary;
|
|
6876
|
-
if (!
|
|
7555
|
+
if (!fs10.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
|
|
6877
7556
|
stopTestInstance();
|
|
6878
|
-
const logFd =
|
|
6879
|
-
const child =
|
|
7557
|
+
const logFd = fs10.openSync(TEST_PATHS.logFile, "a");
|
|
7558
|
+
const child = spawn3(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
|
|
6880
7559
|
detached: true,
|
|
6881
7560
|
stdio: ["ignore", logFd, logFd]
|
|
6882
7561
|
});
|
|
6883
7562
|
child.on("error", () => {
|
|
6884
7563
|
});
|
|
6885
|
-
|
|
7564
|
+
fs10.closeSync(logFd);
|
|
6886
7565
|
child.unref();
|
|
6887
7566
|
const pid = child.pid;
|
|
6888
7567
|
if (!pid) throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
|
|
6889
7568
|
spawnedTestPid = pid;
|
|
6890
|
-
|
|
7569
|
+
fs10.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
|
|
6891
7570
|
const client = createHttpClient({ timeout: 2e3 });
|
|
6892
7571
|
let ready = false;
|
|
6893
7572
|
for (let i = 0; i < 60; i++) {
|
|
@@ -6903,7 +7582,7 @@ async function startTestInstance() {
|
|
|
6903
7582
|
if (!isProcessRunning(pid)) {
|
|
6904
7583
|
let errorDetail = "";
|
|
6905
7584
|
try {
|
|
6906
|
-
errorDetail =
|
|
7585
|
+
errorDetail = fs10.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
|
|
6907
7586
|
} catch {
|
|
6908
7587
|
}
|
|
6909
7588
|
throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
|
|
@@ -6917,7 +7596,7 @@ var spawnedTestPid = null;
|
|
|
6917
7596
|
function stopTestInstance() {
|
|
6918
7597
|
let pid = null;
|
|
6919
7598
|
try {
|
|
6920
|
-
const fromFile = parseInt(
|
|
7599
|
+
const fromFile = parseInt(fs10.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
|
|
6921
7600
|
if (fromFile > 0) pid = fromFile;
|
|
6922
7601
|
} catch {
|
|
6923
7602
|
}
|
|
@@ -6932,7 +7611,7 @@ function stopTestInstance() {
|
|
|
6932
7611
|
}
|
|
6933
7612
|
spawnedTestPid = null;
|
|
6934
7613
|
try {
|
|
6935
|
-
|
|
7614
|
+
fs10.unlinkSync(TEST_PATHS.pidFile);
|
|
6936
7615
|
} catch {
|
|
6937
7616
|
}
|
|
6938
7617
|
}
|
|
@@ -7342,6 +8021,224 @@ async function cmdClean(args) {
|
|
|
7342
8021
|
}
|
|
7343
8022
|
}
|
|
7344
8023
|
|
|
8024
|
+
// src/commands/tunnel.ts
|
|
8025
|
+
function formatState(status) {
|
|
8026
|
+
switch (status.state) {
|
|
8027
|
+
case "running":
|
|
8028
|
+
return colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`);
|
|
8029
|
+
case "dead-port":
|
|
8030
|
+
return colors.yellow(`\u5047\u6D3B (PID ${status.pid}\uFF0C\u7AEF\u53E3 ${status.config.port} \u4E0D\u901A)`);
|
|
8031
|
+
default:
|
|
8032
|
+
return colors.yellow("\u672A\u8FD0\u884C");
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
function getTunnelNameArg(args) {
|
|
8036
|
+
return getNonFlagArg(args, 2);
|
|
8037
|
+
}
|
|
8038
|
+
function requireTunnelNameArg(args, usage) {
|
|
8039
|
+
const name = getTunnelNameArg(args);
|
|
8040
|
+
if (!name) {
|
|
8041
|
+
const tunnels = getTunnels();
|
|
8042
|
+
throw new CliError("\u8BF7\u6307\u5B9A\u96A7\u9053\u540D\u79F0", {
|
|
8043
|
+
hint: [usage, ...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : []]
|
|
8044
|
+
});
|
|
8045
|
+
}
|
|
8046
|
+
return name;
|
|
8047
|
+
}
|
|
8048
|
+
async function printTunnelList() {
|
|
8049
|
+
const tunnels = getTunnels();
|
|
8050
|
+
console.log("");
|
|
8051
|
+
if (tunnels.length === 0) {
|
|
8052
|
+
console.log("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
8053
|
+
console.log("");
|
|
8054
|
+
console.log("\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>");
|
|
8055
|
+
console.log(colors.gray(" \u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"));
|
|
8056
|
+
console.log(colors.gray(" \u96A7\u9053\u628A\u5185\u7F51\u51FA\u53E3\u66B4\u9732\u4E3A\u672C\u5730 SOCKS5\uFF0C\u914D\u5408\u8986\u5199\u6587\u4EF6\u5206\u6D41\u5185\u7F51\u57DF\u540D"));
|
|
8057
|
+
console.log("");
|
|
8058
|
+
return;
|
|
8059
|
+
}
|
|
8060
|
+
const statuses = await getAllTunnelStatus();
|
|
8061
|
+
console.log(colors.cyan("\u96A7\u9053\u5217\u8868:"));
|
|
8062
|
+
console.log("");
|
|
8063
|
+
for (const status of statuses) {
|
|
8064
|
+
const { config } = status;
|
|
8065
|
+
const autoLabel = config.auto ? colors.gray(" [auto]") : "";
|
|
8066
|
+
console.log(` ${colors.bold(config.name)}${autoLabel}`);
|
|
8067
|
+
console.log(` ${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
8068
|
+
console.log(` ${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
8069
|
+
if (status.started_by) {
|
|
8070
|
+
console.log(` ${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
console.log("");
|
|
8074
|
+
console.log("\u542F\u52A8: mihomo tunnel up [\u540D\u5B57] \u505C\u6B62: mihomo tunnel down [\u540D\u5B57]");
|
|
8075
|
+
console.log("\u72B6\u6001: mihomo tunnel status \u5220\u9664: mihomo tunnel rm <\u540D\u5B57>");
|
|
8076
|
+
console.log("");
|
|
8077
|
+
}
|
|
8078
|
+
async function tunnelAdd(args) {
|
|
8079
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]");
|
|
8080
|
+
const host = parseStringArg(args, "--host");
|
|
8081
|
+
if (!host) {
|
|
8082
|
+
throw new CliError("\u7F3A\u5C11 --host", {
|
|
8083
|
+
hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>", "", "\u4E3B\u673A\u53EF\u7528 ~/.ssh/config \u91CC\u7684\u522B\u540D\uFF0C\u4F8B\u5982 --host m4"]
|
|
8084
|
+
});
|
|
8085
|
+
}
|
|
8086
|
+
const portRaw = parseStringArg(args, "--port");
|
|
8087
|
+
if (!portRaw) {
|
|
8088
|
+
throw new CliError("\u7F3A\u5C11 --port", { hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
8089
|
+
}
|
|
8090
|
+
if (!/^\d+$/.test(portRaw.trim())) {
|
|
8091
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: "${portRaw}"\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
8092
|
+
}
|
|
8093
|
+
const port = Number(portRaw.trim());
|
|
8094
|
+
const auto = !hasFlag(args, "--no-auto");
|
|
8095
|
+
const config = { name, host, port, auto };
|
|
8096
|
+
addTunnel(config);
|
|
8097
|
+
const created = ensureTunnelOverwriteFile(config);
|
|
8098
|
+
const overwritePath = getTunnelOverwritePath(name);
|
|
8099
|
+
console.log(`${colors.green("\u5DF2\u6DFB\u52A0\u96A7\u9053")} ${name} \xB7 ${host} \u2192 127.0.0.1:${port}${auto ? " \xB7 auto" : ""}`);
|
|
8100
|
+
console.log("");
|
|
8101
|
+
if (created) {
|
|
8102
|
+
console.log(`\u5DF2\u751F\u6210\u8986\u5199\u6A21\u677F: ${overwritePath}`);
|
|
8103
|
+
console.log(colors.gray(" \u6A21\u677F\u53EA\u5EFA\u597D socks5 \u8282\u70B9\u4E0E\u5206\u7EC4\uFF0C\u5206\u6D41\u89C4\u5219\u9700\u4F60\u586B\u5199\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u57DF\u540D\uFF09"));
|
|
8104
|
+
console.log(colors.gray(" \u7F16\u8F91\u540E\u6267\u884C mihomo start \u751F\u6548"));
|
|
8105
|
+
} else {
|
|
8106
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u672A\u6539\u52A8: ${overwritePath}`);
|
|
8107
|
+
}
|
|
8108
|
+
console.log("");
|
|
8109
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${name}`);
|
|
8110
|
+
if (created) {
|
|
8111
|
+
await restartToApply(args);
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
async function tunnelUp(args) {
|
|
8115
|
+
const name = getTunnelNameArg(args);
|
|
8116
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
8117
|
+
if (targets.length === 0) {
|
|
8118
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053", { hint: ["\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
8119
|
+
}
|
|
8120
|
+
for (const config of targets) {
|
|
8121
|
+
if (ensureTunnelOverwriteFile(config)) {
|
|
8122
|
+
console.log(colors.gray(`\u5DF2\u8865\u5EFA\u8986\u5199\u6A21\u677F: ${getTunnelOverwritePath(config.name)}`));
|
|
8123
|
+
}
|
|
8124
|
+
const result = await startTunnel(config.name, { startedBy: "manual" });
|
|
8125
|
+
if (result.alreadyRunning) {
|
|
8126
|
+
console.log(`${colors.gray("\u5DF2\u5728\u8FD0\u884C")} ${config.name} (PID ${result.pid})`);
|
|
8127
|
+
} else {
|
|
8128
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")} ${config.name} \xB7 ${config.host} \u2192 127.0.0.1:${config.port} (PID ${result.pid})`);
|
|
8129
|
+
}
|
|
8130
|
+
}
|
|
8131
|
+
}
|
|
8132
|
+
function tunnelDown(args) {
|
|
8133
|
+
const name = getTunnelNameArg(args);
|
|
8134
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
8135
|
+
if (targets.length === 0) {
|
|
8136
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
8137
|
+
}
|
|
8138
|
+
let stopped = 0;
|
|
8139
|
+
for (const config of targets) {
|
|
8140
|
+
const result = stopTunnel(config.name);
|
|
8141
|
+
if (result.notRunning) {
|
|
8142
|
+
console.log(`${colors.yellow("\u4E0D\u5728\u8FD0\u884C")} ${config.name}`);
|
|
8143
|
+
} else {
|
|
8144
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")} ${config.name} (PID ${result.pid})`);
|
|
8145
|
+
stopped++;
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
if (stopped > 0) {
|
|
8149
|
+
console.log("");
|
|
8150
|
+
console.log(colors.gray("\u6CE8\u610F: \u8986\u5199\u6587\u4EF6\u4ECD\u5728\uFF0C\u914D\u7F6E\u91CC\u7684\u96A7\u9053\u8282\u70B9\u73B0\u5728\u6307\u5411\u672A\u76D1\u542C\u7684\u7AEF\u53E3"));
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
async function tunnelStatus(args) {
|
|
8154
|
+
const name = getTunnelNameArg(args);
|
|
8155
|
+
if (!name) {
|
|
8156
|
+
await printTunnelList();
|
|
8157
|
+
return;
|
|
8158
|
+
}
|
|
8159
|
+
const config = resolveTunnel(name);
|
|
8160
|
+
const status = await getTunnelStatus(config);
|
|
8161
|
+
console.log("");
|
|
8162
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${config.name}${config.auto ? colors.gray(" [auto]") : ""}`);
|
|
8163
|
+
console.log(`${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
8164
|
+
console.log(`${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
8165
|
+
if (status.started_by) {
|
|
8166
|
+
console.log(`${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
8167
|
+
}
|
|
8168
|
+
console.log("");
|
|
8169
|
+
if (status.state === "dead-port") {
|
|
8170
|
+
console.log(colors.yellow("ssh \u8FDB\u7A0B\u8FD8\u5728\uFF0C\u4F46\u7AEF\u53E3\u6CA1\u6709\u76D1\u542C\u2014\u2014\u5185\u7F51\u5206\u6D41\u5F53\u524D\u4E0D\u53EF\u7528"));
|
|
8171
|
+
console.log(`\u91CD\u542F\u96A7\u9053: mihomo tunnel down ${config.name} && mihomo tunnel up ${config.name}`);
|
|
8172
|
+
console.log(colors.gray(` \u65E5\u5FD7: ${getTunnelLogPath(config.name)}`));
|
|
8173
|
+
console.log("");
|
|
8174
|
+
} else if (status.state === "stopped") {
|
|
8175
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${config.name}`);
|
|
8176
|
+
console.log("");
|
|
8177
|
+
}
|
|
8178
|
+
}
|
|
8179
|
+
async function tunnelRemove(args) {
|
|
8180
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel rm <\u540D\u5B57> [-y]");
|
|
8181
|
+
const config = resolveTunnel(name);
|
|
8182
|
+
const skipConfirm = hasFlag(args, "-y", "--yes");
|
|
8183
|
+
if (!skipConfirm) {
|
|
8184
|
+
if (!process.stdin.isTTY) {
|
|
8185
|
+
throw new CliError("\u5220\u9664\u96A7\u9053\u9700\u8981\u786E\u8BA4", {
|
|
8186
|
+
label: "\u5DF2\u53D6\u6D88",
|
|
8187
|
+
hint: [`\u8DF3\u8FC7\u786E\u8BA4: mihomo tunnel rm ${config.name} -y`]
|
|
8188
|
+
});
|
|
8189
|
+
}
|
|
8190
|
+
const confirmed = await confirmPrompt(`\u786E\u8BA4\u5220\u9664\u96A7\u9053 "${config.name}"?`);
|
|
8191
|
+
if (!confirmed) {
|
|
8192
|
+
console.log("\u5DF2\u53D6\u6D88");
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8196
|
+
stopTunnel(config.name);
|
|
8197
|
+
removeTunnel(config.name);
|
|
8198
|
+
console.log(`${colors.green("\u5DF2\u5220\u9664\u96A7\u9053")} ${config.name}`);
|
|
8199
|
+
console.log("");
|
|
8200
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u672A\u5220\u9664: ${getTunnelOverwritePath(config.name)}`);
|
|
8201
|
+
console.log(colors.gray(" \u5B83\u4ECD\u4F1A\u5411\u914D\u7F6E\u6CE8\u5165\u6307\u5411\u8BE5\u7AEF\u53E3\u7684\u8282\u70B9\uFF0C\u5982\u4E0D\u518D\u9700\u8981\u8BF7\u81EA\u884C\u5220\u9664"));
|
|
8202
|
+
}
|
|
8203
|
+
function resolveTunnel(name) {
|
|
8204
|
+
const config = findTunnel(name);
|
|
8205
|
+
if (!config) {
|
|
8206
|
+
const tunnels = getTunnels();
|
|
8207
|
+
const suggestion = suggestSimilar(
|
|
8208
|
+
name,
|
|
8209
|
+
tunnels.map((t) => t.name)
|
|
8210
|
+
);
|
|
8211
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, {
|
|
8212
|
+
hint: [
|
|
8213
|
+
...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [],
|
|
8214
|
+
...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : ["", "\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"]
|
|
8215
|
+
]
|
|
8216
|
+
});
|
|
8217
|
+
}
|
|
8218
|
+
return config;
|
|
8219
|
+
}
|
|
8220
|
+
var SUBCOMMANDS5 = [
|
|
8221
|
+
{ name: "list", handler: printTunnelList },
|
|
8222
|
+
{ name: "add", handler: tunnelAdd },
|
|
8223
|
+
{ name: "up", aliases: ["start"], handler: tunnelUp },
|
|
8224
|
+
{ name: "down", aliases: ["stop"], handler: tunnelDown },
|
|
8225
|
+
{ name: "status", handler: tunnelStatus },
|
|
8226
|
+
{ name: "remove", aliases: ["rm", "delete"], handler: tunnelRemove }
|
|
8227
|
+
];
|
|
8228
|
+
async function cmdTunnel(args) {
|
|
8229
|
+
await dispatchSubcommand(args, SUBCOMMANDS5, {
|
|
8230
|
+
// 无 action → 列表;未知 action → 报错(不静默回落,否则 `tunnel upp` 会看似成功)
|
|
8231
|
+
fallback: printTunnelList,
|
|
8232
|
+
onUnknown: (action) => {
|
|
8233
|
+
const names = SUBCOMMANDS5.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
8234
|
+
const suggestion = suggestSimilar(action, names);
|
|
8235
|
+
throw new CliError(`\u672A\u77E5\u7684 tunnel \u5B50\u547D\u4EE4: ${action}`, {
|
|
8236
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: list, add, up, down, status, rm"]
|
|
8237
|
+
});
|
|
8238
|
+
}
|
|
8239
|
+
});
|
|
8240
|
+
}
|
|
8241
|
+
|
|
7345
8242
|
// src/commands/ui.ts
|
|
7346
8243
|
function cmdUI(args) {
|
|
7347
8244
|
const uiName = args[1] || "zash";
|
|
@@ -7366,7 +8263,7 @@ function cmdUI(args) {
|
|
|
7366
8263
|
}
|
|
7367
8264
|
|
|
7368
8265
|
// src/commands/update.ts
|
|
7369
|
-
import { execFile, spawn as
|
|
8266
|
+
import { execFile, spawn as spawn4 } from "child_process";
|
|
7370
8267
|
import { promisify } from "util";
|
|
7371
8268
|
var execFileAsync = promisify(execFile);
|
|
7372
8269
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
@@ -7397,7 +8294,7 @@ async function cmdUpdate() {
|
|
|
7397
8294
|
console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
|
|
7398
8295
|
console.log("");
|
|
7399
8296
|
await new Promise((resolve, reject) => {
|
|
7400
|
-
const npm =
|
|
8297
|
+
const npm = spawn4("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
|
|
7401
8298
|
npm.on("close", (code) => {
|
|
7402
8299
|
if (code === 0) {
|
|
7403
8300
|
resolve();
|
|
@@ -7573,6 +8470,20 @@ var COMMANDS = [
|
|
|
7573
8470
|
group: "system",
|
|
7574
8471
|
usage: ["daemon on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF0C\u9700\u7BA1\u7406\u5458\u5BC6\u7801\uFF09", "daemon status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001"]
|
|
7575
8472
|
},
|
|
8473
|
+
{
|
|
8474
|
+
// 别名不能用 tun——已被 TUN 模式快捷命令占用(注册表重复 token 会在模块加载期直接抛错)
|
|
8475
|
+
name: "tunnel",
|
|
8476
|
+
aliases: ["ssh", "tunnels"],
|
|
8477
|
+
handler: cmdTunnel,
|
|
8478
|
+
group: "system",
|
|
8479
|
+
usage: [
|
|
8480
|
+
"tunnel \u5217\u51FA ssh \u96A7\u9053\uFF08\u522B\u540D ssh\uFF09",
|
|
8481
|
+
"tunnel add <\u540D\u5B57> --host <\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]",
|
|
8482
|
+
"tunnel up|down [\u540D\u5B57] \u542F\u52A8/\u505C\u6B62\u96A7\u9053\uFF08\u65E0\u53C2\u5373\u5168\u90E8\uFF09",
|
|
8483
|
+
"tunnel status [\u540D\u5B57] \u67E5\u770B\u96A7\u9053\u72B6\u6001\uFF08\u771F\u5B9E\u63A2\u6D4B\u7AEF\u53E3\uFF09",
|
|
8484
|
+
"tunnel rm <\u540D\u5B57> [-y] \u5220\u9664\u96A7\u9053\uFF08\u4E0D\u5220\u8986\u5199\u6587\u4EF6\uFF09"
|
|
8485
|
+
]
|
|
8486
|
+
},
|
|
7576
8487
|
{
|
|
7577
8488
|
name: "update",
|
|
7578
8489
|
aliases: ["upd", "upgrade"],
|
|
@@ -7678,7 +8589,7 @@ async function main() {
|
|
|
7678
8589
|
if (args.length === 0) {
|
|
7679
8590
|
assertSupportedPlatform("status");
|
|
7680
8591
|
ensureDirs();
|
|
7681
|
-
printStatus();
|
|
8592
|
+
await printStatus();
|
|
7682
8593
|
printShortHelp();
|
|
7683
8594
|
return;
|
|
7684
8595
|
}
|