@lancewu602/mihomo-dash 0.0.0-stage → 0.6.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/LICENSE +21 -0
- package/README.md +99 -2
- package/bin/mihomo-dash.js +4 -0
- package/package.json +63 -4
- package/server/CHANGELOG.md +51 -0
- package/server/package.json +13 -0
- package/server/src/cli/index.js +75 -0
- package/server/src/cli/service.js +167 -0
- package/server/src/cli/status.js +245 -0
- package/server/src/http/config-files.js +148 -0
- package/server/src/http/config-order.js +186 -0
- package/server/src/http/config-sections.js +278 -0
- package/server/src/http/default-config.js +69 -0
- package/server/src/http/groups.js +386 -0
- package/server/src/http/index.js +260 -0
- package/server/src/http/kernel-admin.js +522 -0
- package/server/src/http/kernel-proxy.js +43 -0
- package/server/src/http/providers.js +251 -0
- package/server/src/http/rules.js +509 -0
- package/server/src/http/settings.js +409 -0
- package/server/src/http/system-proxy.js +86 -0
- package/server/src/index.js +26 -0
- package/server/src/kernel/archive.js +181 -0
- package/server/src/kernel/binary.js +33 -0
- package/server/src/kernel/client.js +138 -0
- package/server/src/kernel/install.js +150 -0
- package/server/src/kernel/lifecycle.js +339 -0
- package/server/src/kernel/run.js +11 -0
- package/server/src/shared/args.js +57 -0
- package/server/src/shared/atomic-file.js +41 -0
- package/server/src/shared/exec.js +44 -0
- package/server/src/shared/kernel-config.js +27 -0
- package/server/src/shared/log-file.js +92 -0
- package/server/src/shared/panel.js +58 -0
- package/server/src/shared/paths.js +72 -0
- package/server/src/shared/service.js +552 -0
- package/server/src/shared/system-proxy.js +375 -0
- package/server/src/shared/text.js +11 -0
- package/server/tests/cli-service.test.js +153 -0
- package/server/tests/config-dir.test.js +133 -0
- package/server/tests/config-order.test.js +148 -0
- package/server/tests/groups.test.js +558 -0
- package/server/tests/kernel-admin.test.js +647 -0
- package/server/tests/kernel.test.js +250 -0
- package/server/tests/log-file.test.js +110 -0
- package/server/tests/providers.test.js +259 -0
- package/server/tests/rules.test.js +463 -0
- package/server/tests/service.test.js +470 -0
- package/server/tests/settings.test.js +456 -0
- package/server/tests/status.test.js +269 -0
- package/server/tests/system-proxy.test.js +497 -0
- package/server/tsconfig.json +17 -0
- package/web/dist/assets/index-BpDzYhuI.css +1 -0
- package/web/dist/assets/index-WmujD2EM.js +515 -0
- package/web/dist/favicon.svg +5 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mihomo-dash status` 的总览与渲染(server/src/cli/status.js)测试。
|
|
3
|
+
*
|
|
4
|
+
* 探测函数注入桩(proxy / service),内核与面板用两个桩 HTTP 服务替代 —— 不碰真实 mihomo、
|
|
5
|
+
* 本机系统设置与 launchd / systemd。断言的是「哪几行、写了什么」以及内核不通时的退出语义。
|
|
6
|
+
* 运行:npm test
|
|
7
|
+
*/
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { after, before, beforeEach, test } from 'node:test';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import http from 'node:http';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/** @type {string} */ let tmpDir;
|
|
16
|
+
/** @type {string} */ let cliDir;
|
|
17
|
+
/** @type {string} */ let cfgDir;
|
|
18
|
+
/** @type {Record<string, any>} */ let mods = {};
|
|
19
|
+
/** @type {http.Server} */ let kernelServer;
|
|
20
|
+
/** @type {http.Server} */ let panelServer;
|
|
21
|
+
let kernelPort = 0;
|
|
22
|
+
let panelPort = 0;
|
|
23
|
+
|
|
24
|
+
const CONFIG = `external-controller: 127.0.0.1:KERNEL_PORT
|
|
25
|
+
mode: rule
|
|
26
|
+
proxy-providers:
|
|
27
|
+
airport:
|
|
28
|
+
type: http
|
|
29
|
+
url: "https://example.com/link"
|
|
30
|
+
proxy-groups:
|
|
31
|
+
- name: 节点选择
|
|
32
|
+
type: select
|
|
33
|
+
proxies: [DIRECT]
|
|
34
|
+
rules:
|
|
35
|
+
- MATCH,节点选择
|
|
36
|
+
- DOMAIN-SUFFIX,a.com,DIRECT
|
|
37
|
+
- DOMAIN-SUFFIX,b.com,DIRECT
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
/** 桩系统代理:开着的,指向 127.0.0.1:7890,绕过 3 条 */
|
|
41
|
+
const proxyStub = (over = {}) => ({
|
|
42
|
+
read: async () => ({
|
|
43
|
+
supported: true,
|
|
44
|
+
enabled: true,
|
|
45
|
+
service: 'Wi-Fi',
|
|
46
|
+
protocols: {
|
|
47
|
+
http: { enabled: true, host: '127.0.0.1', port: 7890 },
|
|
48
|
+
https: { enabled: true, host: '127.0.0.1', port: 7890 },
|
|
49
|
+
socks: { enabled: true, host: '127.0.0.1', port: 7890 },
|
|
50
|
+
},
|
|
51
|
+
bypass: ['localhost', '::1', '*.local'],
|
|
52
|
+
...over,
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** 桩开机自启:两个 job 都装了,macOS 域 */
|
|
57
|
+
const serviceStub = (over = {}) => ({
|
|
58
|
+
status: async () => ({
|
|
59
|
+
supported: true,
|
|
60
|
+
platform: 'darwin',
|
|
61
|
+
dir: '/tmp/LaunchAgents',
|
|
62
|
+
linger: null,
|
|
63
|
+
jobs: [
|
|
64
|
+
{ job: 'panel', installed: true, loaded: true, running: true },
|
|
65
|
+
{ job: 'kernel', installed: true, loaded: true, running: false },
|
|
66
|
+
],
|
|
67
|
+
...over,
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const writeConfig = (kernelPortNo = kernelPort) =>
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(cfgDir, 'config.yaml'),
|
|
74
|
+
CONFIG.replace('KERNEL_PORT', String(kernelPortNo)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
before(async () => {
|
|
78
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mihomo-dash-status-'));
|
|
79
|
+
cliDir = path.join(tmpDir, 'cli');
|
|
80
|
+
cfgDir = path.join(tmpDir, 'mihomo');
|
|
81
|
+
fs.mkdirSync(cliDir, { recursive: true });
|
|
82
|
+
fs.mkdirSync(cfgDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
kernelServer = http.createServer((req, res) => {
|
|
85
|
+
res.setHeader('content-type', 'application/json');
|
|
86
|
+
if (req.url === '/version') return res.end(JSON.stringify({ version: 'v1.19.32', meta: true }));
|
|
87
|
+
if (req.url === '/configs') return res.end(JSON.stringify({ mode: 'rule' }));
|
|
88
|
+
res.statusCode = 404;
|
|
89
|
+
res.end('{}');
|
|
90
|
+
});
|
|
91
|
+
await new Promise((r) => kernelServer.listen(0, '127.0.0.1', r));
|
|
92
|
+
kernelPort = kernelServer.address().port;
|
|
93
|
+
|
|
94
|
+
panelServer = http.createServer((req, res) => {
|
|
95
|
+
res.setHeader('content-type', 'application/json');
|
|
96
|
+
if (req.url === '/api/health') {
|
|
97
|
+
return res.end(
|
|
98
|
+
JSON.stringify({
|
|
99
|
+
ok: true,
|
|
100
|
+
name: 'mihomo-dash',
|
|
101
|
+
version: '0.4.0',
|
|
102
|
+
pid: process.pid,
|
|
103
|
+
uptime: 3600,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
res.statusCode = 404;
|
|
108
|
+
res.end('{}');
|
|
109
|
+
});
|
|
110
|
+
await new Promise((r) => panelServer.listen(0, '127.0.0.1', r));
|
|
111
|
+
panelPort = panelServer.address().port;
|
|
112
|
+
|
|
113
|
+
// paths.js / kernel.js 在加载时读环境变量,必须先设好再 import
|
|
114
|
+
process.env.MIHOMO_DASH_DIR = cliDir;
|
|
115
|
+
process.env.MIHOMO_DIR = cfgDir;
|
|
116
|
+
mods.status = await import('../src/cli/status.js');
|
|
117
|
+
mods.kernel = await import('../src/kernel/client.js');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
writeConfig();
|
|
122
|
+
mods.kernel.resetControllerCache();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
after(() => {
|
|
126
|
+
kernelServer.close();
|
|
127
|
+
panelServer.close();
|
|
128
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** 面板探活用真 HTTP(假面板在 beforeEach 之后的 before 里起好了) */
|
|
132
|
+
const gather = (opts = {}) =>
|
|
133
|
+
mods.status.gatherStatus({
|
|
134
|
+
proxy: proxyStub(),
|
|
135
|
+
service: serviceStub(),
|
|
136
|
+
panel: { port: panelPort },
|
|
137
|
+
...opts,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('内核可达 + 面板在跑 + 代理开 + 自启已装:五块都写出来', async () => {
|
|
141
|
+
const d = await gather();
|
|
142
|
+
|
|
143
|
+
assert.equal(d.kernel.up, true);
|
|
144
|
+
assert.equal(d.kernel.version, '1.19.32 (Meta)');
|
|
145
|
+
assert.equal(d.kernel.mode, 'rule');
|
|
146
|
+
assert.equal(d.panel.running, true);
|
|
147
|
+
assert.equal(d.panel.version, '0.4.0');
|
|
148
|
+
assert.equal(d.panel.uptime, 3600);
|
|
149
|
+
assert.deepEqual(
|
|
150
|
+
{ providers: d.config.providers, groups: d.config.groups, rules: d.config.rules },
|
|
151
|
+
{ providers: 1, groups: 1, rules: 3 },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const out = mods.status.formatStatus(d);
|
|
155
|
+
assert.match(out, /^内核\s+运行中 · v1\.19\.32 \(Meta\) · rule$/m);
|
|
156
|
+
assert.match(out, /^ {2}控制接口 http:\/\/127\.0\.0\.1:\d+ · 密钥未配置/m);
|
|
157
|
+
assert.match(out, /^配置文件\s+.*config\.yaml(订阅 1 \/ 策略组 1 \/ 规则 3)$/m);
|
|
158
|
+
assert.match(
|
|
159
|
+
out,
|
|
160
|
+
/^面板\s+运行中 · http:\/\/127\.0\.0\.1:\d+ · pid \d+ · v0\.4\.0 · 已运行 1 小时 0 分钟$/m,
|
|
161
|
+
);
|
|
162
|
+
assert.match(
|
|
163
|
+
out,
|
|
164
|
+
/^系统代理\s+开 · HTTP\/HTTPS\/SOCKS → 127\.0\.0\.1:7890 · 绕过 3 条 · 服务 Wi-Fi$/m,
|
|
165
|
+
);
|
|
166
|
+
assert.match(out, /^开机自启\s+已安装 2\/2 · macOS LaunchAgent · 登录后启动$/m);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('内核不通:如实说原因 + 给启动去处(退出码由 cmdStatus 判成 1)', async () => {
|
|
170
|
+
// 把控制接口指到一个没人听的端口
|
|
171
|
+
const dead = await new Promise((resolve) => {
|
|
172
|
+
const srv = http.createServer();
|
|
173
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
174
|
+
const port = srv.address().port;
|
|
175
|
+
srv.close(() => resolve(port));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
writeConfig(dead);
|
|
179
|
+
mods.kernel.resetControllerCache();
|
|
180
|
+
|
|
181
|
+
const d = await gather();
|
|
182
|
+
assert.equal(d.kernel.up, false);
|
|
183
|
+
assert.ok(d.kernel.error, '要带上不通的原因');
|
|
184
|
+
|
|
185
|
+
const out = mods.status.formatStatus(d);
|
|
186
|
+
assert.match(
|
|
187
|
+
out,
|
|
188
|
+
new RegExp(`^内核\\s+未运行(控制接口 http://127\\.0\\.0\\.1:${dead} 不通:.+)$`, 'm'),
|
|
189
|
+
);
|
|
190
|
+
assert.match(out, /用侧栏底部的「启动内核」/);
|
|
191
|
+
// 其它块照样在(status 是总览,不该因为内核挂了就什么都不显示)
|
|
192
|
+
assert.match(out, /^配置文件\s+/m);
|
|
193
|
+
assert.match(out, /^系统代理\s+/m);
|
|
194
|
+
assert.match(out, /^开机自启\s+/m);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('面板没起:如实说,并指向 service install', async () => {
|
|
198
|
+
const closed = await new Promise((resolve) => {
|
|
199
|
+
const srv = http.createServer();
|
|
200
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
201
|
+
const port = srv.address().port;
|
|
202
|
+
srv.close(() => resolve(port));
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
const d = await gather({ panel: { port: closed } });
|
|
206
|
+
assert.equal(d.panel.running, false);
|
|
207
|
+
const out = mods.status.formatStatus(d);
|
|
208
|
+
assert.match(
|
|
209
|
+
out,
|
|
210
|
+
new RegExp(
|
|
211
|
+
'^面板\\s+未运行(端口 ' + closed + ';用 `mihomo-dash service install` 装成系统服务)$',
|
|
212
|
+
'm',
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('还没有 config.yaml:有一句「写好保存就会创建」,不当成错', async () => {
|
|
218
|
+
fs.rmSync(path.join(cfgDir, 'config.yaml'), { force: true });
|
|
219
|
+
mods.kernel.resetControllerCache();
|
|
220
|
+
const d = await gather();
|
|
221
|
+
assert.equal(d.config.exists, false);
|
|
222
|
+
const out = mods.status.formatStatus(d);
|
|
223
|
+
assert.match(
|
|
224
|
+
out,
|
|
225
|
+
/^配置文件\s+还没有 config\.yaml(.+):在面板「配置文件」页写好保存就会创建$/m,
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('平台不支持 / 没装:系统代理与开机自启各写一行,不影响其它块', async () => {
|
|
230
|
+
const d = await gather({
|
|
231
|
+
proxy: proxyStub({ supported: false, hint: '本平台不支持改系统代理', enabled: false }),
|
|
232
|
+
service: serviceStub({
|
|
233
|
+
supported: false,
|
|
234
|
+
hint: '暂不支持在 win32 上装开机自启',
|
|
235
|
+
jobs: [],
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
const out = mods.status.formatStatus(d);
|
|
239
|
+
assert.match(out, /^系统代理\s+不支持(本平台不支持改系统代理)$/m);
|
|
240
|
+
assert.match(out, /^开机自启\s+不支持(暂不支持在 win32 上装开机自启)$/m);
|
|
241
|
+
|
|
242
|
+
const off = await gather({
|
|
243
|
+
proxy: proxyStub({ enabled: false }),
|
|
244
|
+
service: serviceStub({
|
|
245
|
+
jobs: [
|
|
246
|
+
{ job: 'panel', installed: false },
|
|
247
|
+
{ job: 'kernel', installed: false },
|
|
248
|
+
],
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
const out2 = mods.status.formatStatus(off);
|
|
252
|
+
assert.match(out2, /^系统代理\s+关(系统直连,内核照跑)$/m);
|
|
253
|
+
assert.match(out2, /^开机自启\s+未安装(`mihomo-dash service install`)$/m);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('某个块抛错:只记下原因,不连累别的块', async () => {
|
|
257
|
+
const d = await gather({
|
|
258
|
+
proxy: {
|
|
259
|
+
read: async () => {
|
|
260
|
+
throw new Error('scutil 炸了');
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
assert.equal(d.proxy.error, 'scutil 炸了');
|
|
265
|
+
const out = mods.status.formatStatus(d);
|
|
266
|
+
// 探测失败时 supported 默认 false → 走「不支持」分支;关键是别的块还在
|
|
267
|
+
assert.match(out, /^系统代理\s+/m);
|
|
268
|
+
assert.match(out, /^内核\s+运行中/m);
|
|
269
|
+
});
|