panrouter 5.0.2 → 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 +97 -111
package/package.json
CHANGED
package/pool-worker.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
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
|
-
import { spawn } from "node:child_process";
|
|
10
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
10
11
|
import https from "node:https";
|
|
11
12
|
import http from "node:http";
|
|
12
13
|
import os from "node:os";
|
|
@@ -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,31 +59,30 @@ 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
|
-
const r =
|
|
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
|
-
const winPaths = [
|
|
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"),
|
|
74
|
+
"/data/data/com.termux/files/usr/bin/cloudflared",
|
|
58
75
|
];
|
|
59
|
-
for (const p of
|
|
76
|
+
for (const p of fallbackPaths) {
|
|
60
77
|
if (fs.existsSync(p)) return p;
|
|
61
78
|
}
|
|
62
79
|
return null;
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
// ───
|
|
82
|
+
// ─── 确保 server.mjs 在运行 ──────────────────────────────────────────────
|
|
66
83
|
async function ensureServer() {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
log(`端口 ${SERVER_PORT} 已有服务在运行,跳过启动`, "OK");
|
|
84
|
+
if (await isPortOpen(SERVER_PORT)) {
|
|
85
|
+
log(`端口 ${SERVER_PORT} 已有服务在运行`, "OK");
|
|
70
86
|
return true;
|
|
71
87
|
}
|
|
72
88
|
|
|
@@ -85,7 +101,6 @@ async function ensureServer() {
|
|
|
85
101
|
});
|
|
86
102
|
child.unref();
|
|
87
103
|
|
|
88
|
-
// 等待端口就绪
|
|
89
104
|
for (let i = 0; i < 15; i++) {
|
|
90
105
|
if (await isPortOpen(SERVER_PORT)) {
|
|
91
106
|
log(`代理服务已就绪 (端口 ${SERVER_PORT})`, "OK");
|
|
@@ -98,141 +113,112 @@ async function ensureServer() {
|
|
|
98
113
|
return false;
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
// ───
|
|
116
|
+
// ─── 隧道守护 ────────────────────────────────────────────────────────────────
|
|
102
117
|
function startTunnel() {
|
|
118
|
+
if (isShuttingDown) return;
|
|
119
|
+
|
|
103
120
|
const cfPath = findCloudflared();
|
|
104
121
|
if (!cfPath) {
|
|
105
|
-
log("未找到 cloudflared
|
|
106
|
-
return
|
|
122
|
+
log("未找到 cloudflared,请先安装", "ERR");
|
|
123
|
+
return;
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
log("正在请求匿名公网入口...");
|
|
110
127
|
|
|
111
|
-
|
|
112
|
-
if (cloudflared) {
|
|
113
|
-
try { cloudflared.kill(); } catch {}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
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}`], {
|
|
117
129
|
stdio: ["ignore", "pipe", "pipe"],
|
|
118
130
|
windowsHide: true,
|
|
119
131
|
});
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
cfProcess.stderr.on("data", (data) => {
|
|
122
134
|
const text = data.toString();
|
|
135
|
+
|
|
136
|
+
// 抓取隧道 URL
|
|
123
137
|
const match = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
|
|
124
|
-
if (match && match[0] !==
|
|
125
|
-
|
|
126
|
-
log(`成功获取公网入口: ${
|
|
127
|
-
|
|
138
|
+
if (match && match[0] !== currentUrl) {
|
|
139
|
+
currentUrl = match[0];
|
|
140
|
+
log(`成功获取公网入口: ${currentUrl}`, "OK");
|
|
141
|
+
notifyHub("online", currentUrl);
|
|
128
142
|
}
|
|
129
|
-
});
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
// 网络波动提示
|
|
145
|
+
if (text.includes("connection lost") || text.includes("Retrying")) {
|
|
146
|
+
log("隧道网络不稳定,正在尝试重连...", "WARN");
|
|
147
|
+
}
|
|
134
148
|
});
|
|
135
149
|
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ─── 心跳上报 ─────────────────────────────────────────────────────────────────
|
|
140
|
-
function pushToHub() {
|
|
141
|
-
if (!currentTunnelUrl) return;
|
|
142
|
-
|
|
143
|
-
const payload = JSON.stringify({
|
|
144
|
-
nodeId: NODE_ID,
|
|
145
|
-
url: currentTunnelUrl,
|
|
146
|
-
});
|
|
150
|
+
cfProcess.on("close", (code) => {
|
|
151
|
+
log(`隧道进程断开退出 (code: ${code})`, "ERR");
|
|
152
|
+
cfProcess = null;
|
|
147
153
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
headers: {
|
|
153
|
-
"Content-Type": "application/json",
|
|
154
|
-
"Content-Length": Buffer.byteLength(payload),
|
|
155
|
-
"x-secret-token": AUTH_SECRET,
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
(res) => {
|
|
159
|
-
let body = "";
|
|
160
|
-
res.on("data", (c) => (body += c));
|
|
161
|
-
res.on("end", () => {
|
|
162
|
-
log(`状态已同步至主控中心 (${res.statusCode})`, "HART");
|
|
163
|
-
});
|
|
154
|
+
// 通知主控剔除
|
|
155
|
+
if (currentUrl) {
|
|
156
|
+
notifyHub("offline");
|
|
157
|
+
currentUrl = "";
|
|
164
158
|
}
|
|
165
|
-
);
|
|
166
159
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
160
|
+
// 自动复活
|
|
161
|
+
if (!isShuttingDown) {
|
|
162
|
+
log("准备在 5 秒后自动重启隧道...", "WARN");
|
|
163
|
+
setTimeout(startTunnel, 5000);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
171
166
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
fs.writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
176
|
-
} catch {}
|
|
167
|
+
cfProcess.on("error", (err) => {
|
|
168
|
+
log(`隧道进程异常: ${err.message}`, "ERR");
|
|
169
|
+
});
|
|
177
170
|
}
|
|
178
171
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
172
|
+
// ─── 优雅退出 ────────────────────────────────────────────────────────────────
|
|
173
|
+
function gracefulShutdown() {
|
|
174
|
+
if (isShuttingDown) return;
|
|
175
|
+
isShuttingDown = true;
|
|
186
176
|
|
|
187
|
-
|
|
188
|
-
function stop() {
|
|
189
|
-
log("正在停止节点服务...");
|
|
177
|
+
log("收到关机指令,正在安全退出...", "OFF");
|
|
190
178
|
|
|
191
|
-
|
|
179
|
+
// 遗言:通知主控已离线
|
|
180
|
+
notifyHub("offline");
|
|
192
181
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
182
|
+
// 杀掉隧道进程
|
|
183
|
+
if (cfProcess) {
|
|
184
|
+
cfProcess.kill("SIGINT");
|
|
185
|
+
cfProcess = null;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
188
|
+
// 清理 PID 文件
|
|
189
|
+
try { fs.unlinkSync(PID_FILE); } catch {}
|
|
190
|
+
|
|
191
|
+
// 不等确认,200ms 后直接走
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
log("节点已安全下线,再见!", "OFF");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}, 200);
|
|
196
|
+
}
|
|
201
197
|
|
|
202
|
-
|
|
198
|
+
// ─── PID 文件 ────────────────────────────────────────────────────────────────
|
|
199
|
+
function writePid() {
|
|
200
|
+
try { fs.writeFileSync(PID_FILE, String(process.pid), "utf-8"); } catch {}
|
|
203
201
|
}
|
|
204
202
|
|
|
205
|
-
// ───
|
|
203
|
+
// ─── 入口 ────────────────────────────────────────────────────────────────────
|
|
206
204
|
export async function start() {
|
|
207
205
|
log(`节点 ID: ${NODE_ID}`);
|
|
208
206
|
|
|
209
|
-
// 1. 确保 server.mjs 在运行
|
|
210
207
|
const serverOk = await ensureServer();
|
|
211
208
|
if (!serverOk) {
|
|
212
209
|
log("无法启动代理服务,终止接入", "ERR");
|
|
213
210
|
process.exit(1);
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
// 2. 启动 cloudflared 隧道
|
|
217
|
-
const tunnelOk = startTunnel();
|
|
218
|
-
if (!tunnelOk) {
|
|
219
|
-
log("无法启动隧道,终止接入", "ERR");
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// 3. 写 PID
|
|
224
213
|
writePid();
|
|
214
|
+
startTunnel();
|
|
225
215
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// 5. 优雅退出
|
|
230
|
-
process.on("SIGINT", () => { stop(); process.exit(0); });
|
|
231
|
-
process.on("SIGTERM", () => { stop(); process.exit(0); });
|
|
216
|
+
process.on("SIGINT", gracefulShutdown);
|
|
217
|
+
process.on("SIGTERM", gracefulShutdown);
|
|
232
218
|
|
|
233
|
-
log("
|
|
219
|
+
log("节点看门狗已启动,等待公网入口...", "ON");
|
|
234
220
|
}
|
|
235
221
|
|
|
236
222
|
export function stopWorker() {
|
|
237
|
-
|
|
223
|
+
gracefulShutdown();
|
|
238
224
|
}
|