panrouter 5.0.3 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/pool-worker.mjs +92 -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,32 @@ 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
|
+
// ─── 检查端口 ────────────────────────────────────────────────────────────────
|
|
36
53
|
function isPortOpen(port) {
|
|
37
54
|
return new Promise((resolve) => {
|
|
38
55
|
const req = http.get(`http://127.0.0.1:${port}/health`, () => {});
|
|
@@ -42,16 +59,15 @@ function isPortOpen(port) {
|
|
|
42
59
|
});
|
|
43
60
|
}
|
|
44
61
|
|
|
45
|
-
// ───
|
|
62
|
+
// ─── 查找 cloudflared ────────────────────────────────────────────────────────
|
|
46
63
|
function findCloudflared() {
|
|
47
64
|
const candidates = ["cloudflared", "cloudflared.exe"];
|
|
48
65
|
for (const name of candidates) {
|
|
49
66
|
try {
|
|
50
67
|
const r = spawnSync(name, ["--version"], { stdio: "pipe", timeout: 3000 });
|
|
51
68
|
if (r.status === 0) return name;
|
|
52
|
-
} catch { /*
|
|
69
|
+
} catch { /* next */ }
|
|
53
70
|
}
|
|
54
|
-
// 常见安装路径兜底
|
|
55
71
|
const fallbackPaths = [
|
|
56
72
|
path.join(process.env.USERPROFILE || "", "AppData", "Local", "cloudflared", "cloudflared.exe"),
|
|
57
73
|
path.join(process.env.LOCALAPPDATA || "", "cloudflared", "cloudflared.exe"),
|
|
@@ -63,11 +79,10 @@ function findCloudflared() {
|
|
|
63
79
|
return null;
|
|
64
80
|
}
|
|
65
81
|
|
|
66
|
-
// ───
|
|
82
|
+
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
67
83
|
async function ensureServer() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
|
|
84
|
+
if (await isPortOpen(SERVER_PORT)) {
|
|
85
|
+
log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
|
|
71
86
|
return true;
|
|
72
87
|
}
|
|
73
88
|
|
|
@@ -86,7 +101,6 @@ async function ensureServer() {
|
|
|
86
101
|
});
|
|
87
102
|
child.unref();
|
|
88
103
|
|
|
89
|
-
// 等待端口就绪
|
|
90
104
|
for (let i = 0; i < 15; i++) {
|
|
91
105
|
if (await isPortOpen(SERVER_PORT)) {
|
|
92
106
|
log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
|
|
@@ -99,141 +113,112 @@ async function ensureServer() {
|
|
|
99
113
|
return false;
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
// ───
|
|
116
|
+
// ─── 隧道守护 ────────────────────────────────────────────────────────────────
|
|
103
117
|
function startTunnel() {
|
|
118
|
+
if (isShuttingDown) return;
|
|
119
|
+
|
|
104
120
|
const cfPath = findCloudflared();
|
|
105
121
|
if (!cfPath) {
|
|
106
|
-
log("未找到 cloudflared
|
|
107
|
-
return
|
|
122
|
+
log("未找到 cloudflared,请先安装", "ERR");
|
|
123
|
+
return;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
log("正在请求匿名公网入口...");
|
|
111
127
|
|
|
112
|
-
|
|
113
|
-
if (cloudflared) {
|
|
114
|
-
try { cloudflared.kill(); } catch {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
cloudflared = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
128
|
+
cfProcess = spawn(cfPath, ["tunnel", "--url", `http://127.0.0.1:${SERVER_PORT}`], {
|
|
118
129
|
stdio: ["ignore", "pipe", "pipe"],
|
|
119
130
|
windowsHide: true,
|
|
120
131
|
});
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
cfProcess.stderr.on("data", (data) => {
|
|
123
134
|
const text = data.toString();
|
|
135
|
+
|
|
136
|
+
// 抓取隧道 URL
|
|
124
137
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
125
|
-
if (match && match[0] !==
|
|
126
|
-
|
|
127
|
-
log(`成功获取公网入口: ${
|
|
128
|
-
|
|
138
|
+
if (match && match[0] !== currentUrl) {
|
|
139
|
+
currentUrl = match[0];
|
|
140
|
+
log(`成功获取公网入口: ${currentUrl}`, "OK");
|
|
141
|
+
notifyHub("online", currentUrl);
|
|
129
142
|
}
|
|
130
|
-
});
|
|
131
143
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
// 网络波动提示
|
|
145
|
+
if (text.includes("connection lost") || text.includes("Retrying")) {
|
|
146
|
+
log("隧道网络不稳定,正在尝试重连...", "WARN");
|
|
147
|
+
}
|
|
135
148
|
});
|
|
136
149
|
|
|
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
|
-
});
|
|
150
|
+
cfProcess.on("close", (code) => {
|
|
151
|
+
log(`隧道进程断开退出 (code: ${code})`, "ERR");
|
|
152
|
+
cfProcess = null;
|
|
148
153
|
|
|
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
|
-
});
|
|
154
|
+
// 通知主控剔除
|
|
155
|
+
if (currentUrl) {
|
|
156
|
+
notifyHub("offline");
|
|
157
|
+
currentUrl = "";
|
|
165
158
|
}
|
|
166
|
-
);
|
|
167
159
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
// 自动复活
|
|
161
|
+
if (!isShuttingDown) {
|
|
162
|
+
log("准备在 5 秒后自动重启隧道...", "WARN");
|
|
163
|
+
setTimeout(startTunnel, 5000);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
172
166
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
177
|
-
} catch {}
|
|
167
|
+
cfProcess.on("error", (err) => {
|
|
168
|
+
log(`隧道进程异常: ${err.message}`, "ERR");
|
|
169
|
+
});
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
172
|
+
// ─── 优雅退出 ────────────────────────────────────────────────────────────────
|
|
173
|
+
function gracefulShutdown() {
|
|
174
|
+
if (isShuttingDown) return;
|
|
175
|
+
isShuttingDown = true;
|
|
187
176
|
|
|
188
|
-
|
|
189
|
-
function stop() {
|
|
190
|
-
log("正在停止节点服务...");
|
|
177
|
+
log("收到关机指令,正在安全退出...", "OFF");
|
|
191
178
|
|
|
192
|
-
|
|
179
|
+
// 遗言:通知主控已离线
|
|
180
|
+
notifyHub("offline");
|
|
193
181
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
182
|
+
// 杀掉隧道进程
|
|
183
|
+
if (cfProcess) {
|
|
184
|
+
cfProcess.kill("SIGINT");
|
|
185
|
+
cfProcess = null;
|
|
197
186
|
}
|
|
198
187
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
188
|
+
// 清理 PID 文件
|
|
189
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
190
|
+
|
|
191
|
+
// 不等确认,200ms 后直接走
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
log("节点已安全下线,再见!", "OFF");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}, 200);
|
|
196
|
+
}
|
|
202
197
|
|
|
203
|
-
|
|
198
|
+
// ─── PID 文件 ────────────────────────────────────────────────────────────────
|
|
199
|
+
function writePid() {
|
|
200
|
+
try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
|
|
204
201
|
}
|
|
205
202
|
|
|
206
|
-
// ───
|
|
203
|
+
// ─── 入口 ────────────────────────────────────────────────────────────────────
|
|
207
204
|
export async function start() {
|
|
208
205
|
log(`节点 ID: ${NODE_ID}`);
|
|
209
206
|
|
|
210
|
-
// 1. 确保 server.mjs 在运行
|
|
211
207
|
const serverOk = await ensureServer();
|
|
212
208
|
if (!serverOk) {
|
|
213
209
|
log("无法启动代理服务,终止接入", "ERR");
|
|
214
210
|
process.exit(1);
|
|
215
211
|
}
|
|
216
212
|
|
|
217
|
-
// 2. 启动 cloudflared 隧道
|
|
218
|
-
const tunnelOk = startTunnel();
|
|
219
|
-
if (!tunnelOk) {
|
|
220
|
-
log("无法启动隧道,终止接入", "ERR");
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 3. 写 PID
|
|
225
213
|
writePid();
|
|
214
|
+
startTunnel();
|
|
226
215
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// 5. 优雅退出
|
|
231
|
-
process.on("SIGINT", () => { stop(); process.exit(0); });
|
|
232
|
-
process.on("SIGTERM", () => { stop(); process.exit(0); });
|
|
216
|
+
process.on("SIGINT", gracefulShutdown);
|
|
217
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
233
218
|
|
|
234
|
-
log("
|
|
219
|
+
log("节点看门狗已启动,等待公网入口...", "ON");
|
|
235
220
|
}
|
|
236
221
|
|
|
237
222
|
export function stopWorker() {
|
|
238
|
-
|
|
223
|
+
gracefulShutdown();
|
|
239
224
|
}
|