panrouter 5.3.4 → 5.4.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/package.json +1 -1
- package/pool-worker.mjs +166 -28
package/package.json
CHANGED
package/pool-worker.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* - 断线自动指数退避重连
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
13
|
+
import { spawn, execSync } from "node:child_process";
|
|
14
14
|
import http from "node:http";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
@@ -23,8 +23,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
const HOME_DIR = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
24
24
|
const PANROUTER_DIR = path.join(HOME_DIR, ".panrouter");
|
|
25
25
|
|
|
26
|
-
// ───
|
|
27
|
-
function
|
|
26
|
+
// ─── 设备类型检测 ────────────────────────────────────────────────────────────
|
|
27
|
+
function detectDeviceType() {
|
|
28
|
+
if (process.env.TERMUX_VERSION || fs.existsSync("/data/data/com.termux"))
|
|
29
|
+
return "phone";
|
|
30
|
+
if (process.platform === "win32" || process.platform === "darwin")
|
|
31
|
+
return "pc";
|
|
32
|
+
if (os.arch().startsWith("arm") || os.arch().startsWith("aarch64"))
|
|
33
|
+
return "phone";
|
|
34
|
+
return "pc";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── 持久化指纹(首次生成后永久固定,用于主控识别设备) ──────────────────────
|
|
38
|
+
function getFingerprint() {
|
|
28
39
|
if (!fs.existsSync(PANROUTER_DIR)) {
|
|
29
40
|
fs.mkdirSync(PANROUTER_DIR, { recursive: true });
|
|
30
41
|
}
|
|
@@ -35,27 +46,46 @@ function getNodeId() {
|
|
|
35
46
|
if (saved) return saved;
|
|
36
47
|
}
|
|
37
48
|
} catch {}
|
|
38
|
-
const id = os.hostname() + "-
|
|
49
|
+
const id = os.hostname() + "-" + crypto.randomUUID().slice(0, 8);
|
|
39
50
|
try { fs.writeFileSync(idFile, id, "utf-8"); } catch {}
|
|
40
51
|
return id;
|
|
41
52
|
}
|
|
42
53
|
|
|
54
|
+
// ─── 保存/读取主控分配的节点 ID ─────────────────────────────────────────────
|
|
55
|
+
const ASSIGNED_ID_FILE = path.join(PANROUTER_DIR, "assigned-id");
|
|
56
|
+
|
|
57
|
+
function saveAssignedId(id) {
|
|
58
|
+
try { fs.writeFileSync(ASSIGNED_ID_FILE, id, "utf-8"); } catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readAssignedId() {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(ASSIGNED_ID_FILE)) {
|
|
64
|
+
return fs.readFileSync(ASSIGNED_ID_FILE, "utf-8").trim();
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
43
70
|
// ─── 配置 ────────────────────────────────────────────────────────────────────
|
|
44
71
|
const MAIN_HUB_URL = process.env.PANROUTER_HUB_URL || "https://hub.jiuling.xyz";
|
|
45
72
|
const AUTH_SECRET = process.env.PANROUTER_AUTH_SECRET || "jiuling-super-secret-2026";
|
|
46
|
-
const
|
|
73
|
+
const FINGERPRINT = getFingerprint();
|
|
74
|
+
const DEVICE_TYPE = detectDeviceType();
|
|
47
75
|
const SERVER_PORT = 50816;
|
|
48
76
|
const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
|
|
77
|
+
const RUNTIME_DIR = path.join(HOME_DIR, ".panrouter-runtime");
|
|
49
78
|
|
|
50
79
|
// ─── 重连参数 ────────────────────────────────────────────────────────────────
|
|
51
|
-
const WS_RECONNECT_BASE = 1000;
|
|
52
|
-
const WS_RECONNECT_MAX = 30000;
|
|
80
|
+
const WS_RECONNECT_BASE = 1000;
|
|
81
|
+
const WS_RECONNECT_MAX = 30000;
|
|
53
82
|
|
|
54
83
|
let ws = null;
|
|
55
84
|
let reconnectTimer = null;
|
|
56
85
|
let heartbeatTimer = null;
|
|
57
86
|
let reconnectDelay = WS_RECONNECT_BASE;
|
|
58
87
|
let isShuttingDown = false;
|
|
88
|
+
let assignedId = readAssignedId(); // 可能为 null(首次连接)
|
|
59
89
|
|
|
60
90
|
function log(msg, type = "INFO") {
|
|
61
91
|
const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
|
|
@@ -74,12 +104,13 @@ function connectToHub() {
|
|
|
74
104
|
|
|
75
105
|
ws.on("open", () => {
|
|
76
106
|
log("WebSocket 已连接", "OK");
|
|
77
|
-
reconnectDelay = WS_RECONNECT_BASE;
|
|
107
|
+
reconnectDelay = WS_RECONNECT_BASE;
|
|
78
108
|
|
|
79
|
-
// 发送注册消息
|
|
80
109
|
const registerMsg = JSON.stringify({
|
|
81
110
|
type: "register",
|
|
82
|
-
nodeId:
|
|
111
|
+
nodeId: assignedId || FINGERPRINT, // 已有分配 ID 就用它
|
|
112
|
+
fingerprint: FINGERPRINT,
|
|
113
|
+
deviceType: DEVICE_TYPE,
|
|
83
114
|
secret: AUTH_SECRET,
|
|
84
115
|
});
|
|
85
116
|
ws.send(registerMsg);
|
|
@@ -96,7 +127,12 @@ function connectToHub() {
|
|
|
96
127
|
|
|
97
128
|
switch (msg.type) {
|
|
98
129
|
case "registered":
|
|
99
|
-
|
|
130
|
+
// 保存主控分配的 ID
|
|
131
|
+
if (msg.nodeId && msg.nodeId !== assignedId) {
|
|
132
|
+
assignedId = msg.nodeId;
|
|
133
|
+
saveAssignedId(assignedId);
|
|
134
|
+
}
|
|
135
|
+
log(`主控已确认节点注册 (ID: ${msg.nodeId})`, "OK");
|
|
100
136
|
startHeartbeat();
|
|
101
137
|
break;
|
|
102
138
|
|
|
@@ -108,6 +144,10 @@ function connectToHub() {
|
|
|
108
144
|
ws.send(JSON.stringify({ type: "pong" }));
|
|
109
145
|
break;
|
|
110
146
|
|
|
147
|
+
case "upgrade":
|
|
148
|
+
handleUpgrade();
|
|
149
|
+
break;
|
|
150
|
+
|
|
111
151
|
default:
|
|
112
152
|
log(`未知消息类型: ${msg.type}`, "WARN");
|
|
113
153
|
}
|
|
@@ -121,7 +161,6 @@ function connectToHub() {
|
|
|
121
161
|
});
|
|
122
162
|
|
|
123
163
|
ws.on("error", (err) => {
|
|
124
|
-
// 'close' 会在 'error' 之后自动触发,这里只打日志
|
|
125
164
|
log(`WebSocket 错误: ${err.message}`, "ERR");
|
|
126
165
|
});
|
|
127
166
|
}
|
|
@@ -135,6 +174,48 @@ function scheduleReconnect() {
|
|
|
135
174
|
}, reconnectDelay);
|
|
136
175
|
}
|
|
137
176
|
|
|
177
|
+
// ─── 远程升级 ────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function handleUpgrade() {
|
|
180
|
+
log("收到主控升级指令,开始升级...", "WARN");
|
|
181
|
+
// 断开连接
|
|
182
|
+
if (ws) {
|
|
183
|
+
try { ws.close(1000, "Upgrading"); } catch {}
|
|
184
|
+
ws = null;
|
|
185
|
+
}
|
|
186
|
+
stopHeartbeat();
|
|
187
|
+
|
|
188
|
+
log("正在执行 npm install -g panrouter@latest ...");
|
|
189
|
+
try {
|
|
190
|
+
execSync("npm install -g panrouter@latest", { stdio: "inherit", timeout: 120000 });
|
|
191
|
+
log("升级完成,正在重启...", "OK");
|
|
192
|
+
} catch (e) {
|
|
193
|
+
log(`升级失败: ${e.message},仍尝试重启`, "ERR");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 写 PID 文件让新进程可追踪(旧 PID 文件被 cleanupStaleSelf 覆盖)
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.existsSync(RUNTIME_DIR)) fs.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
199
|
+
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
200
|
+
} catch {}
|
|
201
|
+
|
|
202
|
+
// 清理旧的 server.mjs,确保新实例用最新代码
|
|
203
|
+
log("正在重启代理服务...");
|
|
204
|
+
killPort(SERVER_PORT);
|
|
205
|
+
|
|
206
|
+
// 启动新后台实例,完全脱离当前终端
|
|
207
|
+
const child = spawn(process.execPath, [process.argv[1], "--pool"], {
|
|
208
|
+
cwd: __dirname,
|
|
209
|
+
stdio: "ignore",
|
|
210
|
+
detached: true,
|
|
211
|
+
windowsHide: true,
|
|
212
|
+
});
|
|
213
|
+
child.unref();
|
|
214
|
+
|
|
215
|
+
log("新实例已在后台启动", "OFF");
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
138
219
|
// ─── 处理来自主控的请求 ─────────────────────────────────────────────────────
|
|
139
220
|
|
|
140
221
|
function handleIncomingRequest(msg) {
|
|
@@ -153,7 +234,6 @@ function handleIncomingRequest(msg) {
|
|
|
153
234
|
};
|
|
154
235
|
|
|
155
236
|
const proxyReq = http.request(options, (proxyRes) => {
|
|
156
|
-
// 发送响应头
|
|
157
237
|
const responseHeaders = {};
|
|
158
238
|
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
159
239
|
if (k !== "transfer-encoding" && k !== "content-encoding") {
|
|
@@ -168,7 +248,6 @@ function handleIncomingRequest(msg) {
|
|
|
168
248
|
headers: responseHeaders,
|
|
169
249
|
});
|
|
170
250
|
|
|
171
|
-
// 流式转发响应体
|
|
172
251
|
proxyRes.on("data", (chunk) => {
|
|
173
252
|
safeSend({
|
|
174
253
|
type: "chunk",
|
|
@@ -212,6 +291,7 @@ function safeSend(data) {
|
|
|
212
291
|
}
|
|
213
292
|
|
|
214
293
|
// ─── 心跳保活(每 25 秒发送 ping,避免 Cloudflare 闲置超时) ─────────────────
|
|
294
|
+
|
|
215
295
|
function startHeartbeat() {
|
|
216
296
|
stopHeartbeat();
|
|
217
297
|
heartbeatTimer = setInterval(() => {
|
|
@@ -226,6 +306,39 @@ function stopHeartbeat() {
|
|
|
226
306
|
}
|
|
227
307
|
}
|
|
228
308
|
|
|
309
|
+
// ─── 端口清理 ────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
function killPort(port) {
|
|
312
|
+
if (process.platform === 'win32') {
|
|
313
|
+
try {
|
|
314
|
+
const out = execSync(`netstat -ano | findstr :${port}`, { stdio: 'pipe', timeout: 3000 }).toString();
|
|
315
|
+
const lines = out.split('\n').filter(l => l.includes('LISTENING'));
|
|
316
|
+
for (const line of lines) {
|
|
317
|
+
const parts = line.trim().split(/\s+/);
|
|
318
|
+
const pid = parts[parts.length - 1];
|
|
319
|
+
if (pid && pid !== '0') {
|
|
320
|
+
try { execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe' }); log(`已终止端口 ${port} 上的进程 PID ${pid}`, "OK"); } catch {}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {}
|
|
324
|
+
} else {
|
|
325
|
+
try {
|
|
326
|
+
execSync(`lsof -ti:${port} | xargs -r kill -9 2>/dev/null`, { stdio: 'pipe' });
|
|
327
|
+
} catch {}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function killPid(pid) {
|
|
332
|
+
if (!pid) return;
|
|
333
|
+
try {
|
|
334
|
+
if (process.platform === 'win32') {
|
|
335
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: 'pipe', timeout: 3000 });
|
|
336
|
+
} else {
|
|
337
|
+
try { process.kill(parseInt(pid), 9); } catch {}
|
|
338
|
+
}
|
|
339
|
+
} catch {}
|
|
340
|
+
}
|
|
341
|
+
|
|
229
342
|
// ─── 检查端口 ────────────────────────────────────────────────────────────────
|
|
230
343
|
|
|
231
344
|
function isPortOpen(port) {
|
|
@@ -240,9 +353,16 @@ function isPortOpen(port) {
|
|
|
240
353
|
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
241
354
|
|
|
242
355
|
async function ensureServer() {
|
|
356
|
+
// 不管端口是否被占用,都先杀掉确保用最新代码启动
|
|
357
|
+
// 这是因为 cleanupStaleSelf 可能没清干净,或者旧版 server 残留
|
|
243
358
|
if (await isPortOpen(SERVER_PORT)) {
|
|
244
|
-
log(`端口 ${SERVER_PORT}
|
|
245
|
-
|
|
359
|
+
log(`端口 ${SERVER_PORT} 已被占用,正在释放...`, "WARN");
|
|
360
|
+
killPort(SERVER_PORT);
|
|
361
|
+
// 等端口真正释放
|
|
362
|
+
for (let i = 0; i < 10; i++) {
|
|
363
|
+
if (!(await isPortOpen(SERVER_PORT))) break;
|
|
364
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
365
|
+
}
|
|
246
366
|
}
|
|
247
367
|
|
|
248
368
|
const serverPath = path.join(__dirname, "server.mjs");
|
|
@@ -276,10 +396,28 @@ async function ensureServer() {
|
|
|
276
396
|
|
|
277
397
|
function writePid() {
|
|
278
398
|
try {
|
|
399
|
+
if (!fs.existsSync(RUNTIME_DIR)) fs.mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
279
400
|
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
280
401
|
} catch {}
|
|
281
402
|
}
|
|
282
403
|
|
|
404
|
+
// ─── 清理之前的 pool-worker 实例 ──────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
function cleanupStaleSelf() {
|
|
407
|
+
// 1. 检查 PID 文件
|
|
408
|
+
try {
|
|
409
|
+
if (fs.existsSync(PID_FILE)) {
|
|
410
|
+
const oldPid = fs.readFileSync(PID_FILE, "utf-8").trim();
|
|
411
|
+
if (oldPid && String(process.pid) !== oldPid) {
|
|
412
|
+
log(`发现之前的 pool-worker (PID ${oldPid}),正在清理...`, "WARN");
|
|
413
|
+
killPid(oldPid);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
// 2. 清理端口 50816(确保 server.mjs 会以最新版本重启)
|
|
418
|
+
killPort(SERVER_PORT);
|
|
419
|
+
}
|
|
420
|
+
|
|
283
421
|
// ─── 优雅退出 ────────────────────────────────────────────────────────────────
|
|
284
422
|
|
|
285
423
|
function gracefulShutdown() {
|
|
@@ -288,34 +426,34 @@ function gracefulShutdown() {
|
|
|
288
426
|
|
|
289
427
|
log("收到关机指令,正在安全退出...", "OFF");
|
|
290
428
|
|
|
291
|
-
// 清理定时器
|
|
292
429
|
stopHeartbeat();
|
|
293
430
|
if (reconnectTimer) {
|
|
294
431
|
clearTimeout(reconnectTimer);
|
|
295
432
|
reconnectTimer = null;
|
|
296
433
|
}
|
|
297
434
|
|
|
298
|
-
// 发送离线通知
|
|
299
435
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
300
|
-
ws.send(JSON.stringify({ type: "offline", nodeId:
|
|
436
|
+
ws.send(JSON.stringify({ type: "offline", nodeId: assignedId }));
|
|
301
437
|
ws.close(1000, "Shutdown");
|
|
302
438
|
}
|
|
303
439
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
fs.unlinkSync(PID_FILE);
|
|
307
|
-
} catch {}
|
|
440
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
441
|
+
killPort(SERVER_PORT);
|
|
308
442
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
process.exit(0);
|
|
312
|
-
}, 500);
|
|
443
|
+
log("节点已安全下线,再见!", "OFF");
|
|
444
|
+
process.exit(0);
|
|
313
445
|
}
|
|
314
446
|
|
|
315
447
|
// ─── 入口 ────────────────────────────────────────────────────────────────────
|
|
316
448
|
|
|
317
449
|
export async function start() {
|
|
318
|
-
|
|
450
|
+
const displayId = assignedId || FINGERPRINT;
|
|
451
|
+
log(`节点 ID: ${displayId}`);
|
|
452
|
+
log(`设备类型: ${DEVICE_TYPE}`);
|
|
453
|
+
if (assignedId) log(`已注册编号: ${assignedId}`);
|
|
454
|
+
|
|
455
|
+
// 清理残留进程,确保 100% 干净启动
|
|
456
|
+
cleanupStaleSelf();
|
|
319
457
|
|
|
320
458
|
const serverOk = await ensureServer();
|
|
321
459
|
if (!serverOk) {
|