panrouter 5.0.3 → 5.1.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 +126 -107
package/package.json
CHANGED
package/pool-worker.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Pan Router Pool Worker
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Pan Router Pool Worker — 事件驱动版
|
|
5
|
+
*
|
|
6
|
+
* 以 Sidecar 模式运行,守护 cloudflared 隧道进程。
|
|
7
|
+
* 状态改变时才通知主控(事件驱动),崩溃自动复活,关机优雅告别。
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -23,16 +24,66 @@ const NODE_ID = os.hostname() + "-worker-" + Math.floor(Math.random() * 1000);
|
|
|
23
24
|
const SERVER_PORT = 50816;
|
|
24
25
|
const PID_FILE = path.join(os.tmpdir(), "panrouter-pool-worker.pid");
|
|
25
26
|
|
|
26
|
-
let
|
|
27
|
-
let
|
|
28
|
-
let
|
|
27
|
+
let cfProcess = null;
|
|
28
|
+
let isShuttingDown = false;
|
|
29
|
+
let currentUrl = "";
|
|
29
30
|
|
|
30
31
|
function log(msg, type = "INFO") {
|
|
31
|
-
const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓" };
|
|
32
|
+
const icons = { INFO: "▪", OK: "✅", ERR: "❌", HART: "💓", WARN: "⚠️", OFF: "🔻", ON: "🟢" };
|
|
32
33
|
console.log(`${icons[type] || "▪"} [节点] ${msg}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// ───
|
|
36
|
+
// ─── 通知主控 ────────────────────────────────────────────────────────────────
|
|
37
|
+
function notifyHub(status, url = "") {
|
|
38
|
+
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
39
|
+
const req = https.request(MAIN_HUB_URL, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
44
|
+
"x-secret-token": AUTH_SECRET,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
req.on("error", () => {});
|
|
48
|
+
req.write(payload);
|
|
49
|
+
req.end();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 可等待的 notify — 用于首次上线确认
|
|
53
|
+
function notifyHubWait(status, url = "") {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const payload = JSON.stringify({ nodeId: NODE_ID, status, url });
|
|
56
|
+
const req = https.request(MAIN_HUB_URL, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
61
|
+
"x-secret-token": AUTH_SECRET,
|
|
62
|
+
},
|
|
63
|
+
}, (res) => resolve(res.statusCode === 200));
|
|
64
|
+
req.on("error", () => resolve(false));
|
|
65
|
+
req.setTimeout(5000, () => { req.destroy(); resolve(false); });
|
|
66
|
+
req.write(payload);
|
|
67
|
+
req.end();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 首次上线确认 — 失败则重试,最多 5 次
|
|
72
|
+
async function confirmOnline(url, retries = 5) {
|
|
73
|
+
const ok = await notifyHubWait("online", url);
|
|
74
|
+
if (ok) {
|
|
75
|
+
log("主控已确认节点注册", "OK");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (retries > 0) {
|
|
79
|
+
log(`注册尚未确认,3 秒后重试 (剩余 ${retries} 次)`, "INFO");
|
|
80
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
81
|
+
return confirmOnline(url, retries - 1);
|
|
82
|
+
}
|
|
83
|
+
log("注册确认已达最大重试次数", "WARN");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── 检查端口 ────────────────────────────────────────────────────────────────
|
|
36
87
|
function isPortOpen(port) {
|
|
37
88
|
return new Promise((resolve) => {
|
|
38
89
|
const req = http.get(`http://127.0.0.1:${port}/health`, () => {});
|
|
@@ -42,16 +93,15 @@ function isPortOpen(port) {
|
|
|
42
93
|
});
|
|
43
94
|
}
|
|
44
95
|
|
|
45
|
-
// ───
|
|
96
|
+
// ─── 查找 cloudflared ────────────────────────────────────────────────────────
|
|
46
97
|
function findCloudflared() {
|
|
47
98
|
const candidates = ["cloudflared", "cloudflared.exe"];
|
|
48
99
|
for (const name of candidates) {
|
|
49
100
|
try {
|
|
50
101
|
const r = spawnSync(name, ["--version"], { stdio: "pipe", timeout: 3000 });
|
|
51
102
|
if (r.status === 0) return name;
|
|
52
|
-
} catch { /*
|
|
103
|
+
} catch { /* next */ }
|
|
53
104
|
}
|
|
54
|
-
// 常见安装路径兜底
|
|
55
105
|
const fallbackPaths = [
|
|
56
106
|
path.join(process.env.USERPROFILE || "", "AppData", "Local", "cloudflared", "cloudflared.exe"),
|
|
57
107
|
path.join(process.env.LOCALAPPDATA || "", "cloudflared", "cloudflared.exe"),
|
|
@@ -63,11 +113,10 @@ function findCloudflared() {
|
|
|
63
113
|
return null;
|
|
64
114
|
}
|
|
65
115
|
|
|
66
|
-
// ───
|
|
116
|
+
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
67
117
|
async function ensureServer() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
|
|
118
|
+
if (await isPortOpen(SERVER_PORT)) {
|
|
119
|
+
log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
|
|
71
120
|
return true;
|
|
72
121
|
}
|
|
73
122
|
|
|
@@ -86,7 +135,6 @@ async function ensureServer() {
|
|
|
86
135
|
});
|
|
87
136
|
child.unref();
|
|
88
137
|
|
|
89
|
-
// 等待端口就绪
|
|
90
138
|
for (let i = 0; i < 15; i++) {
|
|
91
139
|
if (await isPortOpen(SERVER_PORT)) {
|
|
92
140
|
log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
|
|
@@ -99,141 +147,112 @@ async function ensureServer() {
|
|
|
99
147
|
return false;
|
|
100
148
|
}
|
|
101
149
|
|
|
102
|
-
// ───
|
|
150
|
+
// ─── 隧道守护 ────────────────────────────────────────────────────────────────
|
|
103
151
|
function startTunnel() {
|
|
152
|
+
if (isShuttingDown) return;
|
|
153
|
+
|
|
104
154
|
const cfPath = findCloudflared();
|
|
105
155
|
if (!cfPath) {
|
|
106
|
-
log("未找到 cloudflared
|
|
107
|
-
return
|
|
156
|
+
log("未找到 cloudflared,请先安装", "ERR");
|
|
157
|
+
return;
|
|
108
158
|
}
|
|
109
159
|
|
|
110
160
|
log("正在请求匿名公网入口...");
|
|
111
161
|
|
|
112
|
-
|
|
113
|
-
if (cloudflared) {
|
|
114
|
-
try { cloudflared.kill(); } catch {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
cloudflared = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
162
|
+
cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
118
163
|
stdio: ["ignore", "pipe", "pipe"],
|
|
119
164
|
windowsHide: true,
|
|
120
165
|
});
|
|
121
166
|
|
|
122
|
-
|
|
167
|
+
cfProcess.stderr.on("data", (data) => {
|
|
123
168
|
const text = data.toString();
|
|
169
|
+
|
|
170
|
+
// 抓取隧道 URL
|
|
124
171
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
125
|
-
if (match && match[0] !==
|
|
126
|
-
|
|
127
|
-
log(`成功获取公网入口: ${
|
|
128
|
-
|
|
172
|
+
if (match && match[0] !== currentUrl) {
|
|
173
|
+
currentUrl = match[0];
|
|
174
|
+
log(`成功获取公网入口: ${currentUrl}`, "OK");
|
|
175
|
+
confirmOnline(currentUrl); // 确保主控注册成功(异步重试)
|
|
129
176
|
}
|
|
130
|
-
});
|
|
131
177
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
178
|
+
// 网络波动提示
|
|
179
|
+
if (text.includes("connection lost") || text.includes("Retrying")) {
|
|
180
|
+
log("隧道网络不稳定,正在尝试重连...", "WARN");
|
|
181
|
+
}
|
|
135
182
|
});
|
|
136
183
|
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ─── 心跳上报 ─────────────────────────────────────────────────────────────────
|
|
141
|
-
function pushToHub() {
|
|
142
|
-
if (!currentTunnelUrl) return;
|
|
143
|
-
|
|
144
|
-
const payload = JSON.stringify({
|
|
145
|
-
nodeId: NODE_ID,
|
|
146
|
-
url: currentTunnelUrl,
|
|
147
|
-
});
|
|
184
|
+
cfProcess.on("close", (code) => {
|
|
185
|
+
log(`隧道进程断开退出 (code: ${code})`, "ERR");
|
|
186
|
+
cfProcess = null;
|
|
148
187
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
headers: {
|
|
154
|
-
"Content-Type": "application/json",
|
|
155
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
156
|
-
"x-secret-token": AUTH_SECRET,
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
(res) => {
|
|
160
|
-
let body = "";
|
|
161
|
-
res.on("data", (c) => (body += c));
|
|
162
|
-
res.on("end", () => {
|
|
163
|
-
log(`状态已同步至主控中心 (${res.statusCode})`, "HART");
|
|
164
|
-
});
|
|
188
|
+
// 通知主控剔除
|
|
189
|
+
if (currentUrl) {
|
|
190
|
+
notifyHub("offline");
|
|
191
|
+
currentUrl = "";
|
|
165
192
|
}
|
|
166
|
-
);
|
|
167
193
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
// 自动复活
|
|
195
|
+
if (!isShuttingDown) {
|
|
196
|
+
log("准备在 5 秒后自动重启隧道...", "WARN");
|
|
197
|
+
setTimeout(startTunnel, 5000);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
172
200
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
177
|
-
} catch {}
|
|
201
|
+
cfProcess.on("error", (err) => {
|
|
202
|
+
log(`隧道进程异常: ${err.message}`, "ERR");
|
|
203
|
+
});
|
|
178
204
|
}
|
|
179
205
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
206
|
+
// ─── 优雅退出 ────────────────────────────────────────────────────────────────
|
|
207
|
+
function gracefulShutdown() {
|
|
208
|
+
if (isShuttingDown) return;
|
|
209
|
+
isShuttingDown = true;
|
|
187
210
|
|
|
188
|
-
|
|
189
|
-
function stop() {
|
|
190
|
-
log("正在停止节点服务...");
|
|
211
|
+
log("收到关机指令,正在安全退出...", "OFF");
|
|
191
212
|
|
|
192
|
-
|
|
213
|
+
// 遗言:通知主控已离线
|
|
214
|
+
notifyHub("offline");
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
216
|
+
// 杀掉隧道进程
|
|
217
|
+
if (cfProcess) {
|
|
218
|
+
cfProcess.kill("SIGINT");
|
|
219
|
+
cfProcess = null;
|
|
197
220
|
}
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
} catch {}
|
|
222
|
+
// 清理 PID 文件
|
|
223
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
202
224
|
|
|
203
|
-
|
|
225
|
+
// 不等确认,200ms 后直接走
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
log("节点已安全下线,再见!", "OFF");
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}, 200);
|
|
204
230
|
}
|
|
205
231
|
|
|
206
|
-
// ───
|
|
232
|
+
// ─── PID 文件 ────────────────────────────────────────────────────────────────
|
|
233
|
+
function writePid() {
|
|
234
|
+
try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─── 入口 ────────────────────────────────────────────────────────────────────
|
|
207
238
|
export async function start() {
|
|
208
239
|
log(`节点 ID: ${NODE_ID}`);
|
|
209
240
|
|
|
210
|
-
// 1. 确保 server.mjs 在运行
|
|
211
241
|
const serverOk = await ensureServer();
|
|
212
242
|
if (!serverOk) {
|
|
213
243
|
log("无法启动代理服务,终止接入", "ERR");
|
|
214
244
|
process.exit(1);
|
|
215
245
|
}
|
|
216
246
|
|
|
217
|
-
// 2. 启动 cloudflared 隧道
|
|
218
|
-
const tunnelOk = startTunnel();
|
|
219
|
-
if (!tunnelOk) {
|
|
220
|
-
log("无法启动隧道,终止接入", "ERR");
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 3. 写 PID
|
|
225
247
|
writePid();
|
|
248
|
+
startTunnel();
|
|
226
249
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// 5. 优雅退出
|
|
231
|
-
process.on("SIGINT", () => { stop(); process.exit(0); });
|
|
232
|
-
process.on("SIGTERM", () => { stop(); process.exit(0); });
|
|
250
|
+
process.on("SIGINT", gracefulShutdown);
|
|
251
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
233
252
|
|
|
234
|
-
log("
|
|
253
|
+
log("节点看门狗已启动,等待公网入口...", "ON");
|
|
235
254
|
}
|
|
236
255
|
|
|
237
256
|
export function stopWorker() {
|
|
238
|
-
|
|
257
|
+
gracefulShutdown();
|
|
239
258
|
}
|