mslxdff 0.1.179 → 0.1.180
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/src/warp-pool/index.js +56 -19
- package/src/warp-pool/ports.js +12 -0
- package/src/warp-pool/rotate.js +1 -1
package/package.json
CHANGED
package/src/warp-pool/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { ensureDir, loadNodes, removeNode, saveNode } from "./nodes.js";
|
|
|
8
8
|
import { registerWarpDevice } from "./register.js";
|
|
9
9
|
import { ensureBinary, probeTunnel, resolveAsset, startTunnel, writeTunnelConf } from "./wireproxy.js";
|
|
10
10
|
import { createReviver, labelExits, tailReason } from "./revive.js";
|
|
11
|
-
import { portInUse, reclaimPort } from "./ports.js";
|
|
11
|
+
import { portInUse, reclaimPort, waitPortFree } from "./ports.js";
|
|
12
12
|
import { wireRotation } from "./identity.js";
|
|
13
13
|
|
|
14
14
|
/** 重拨节流:连续失败指数退避。网络不通的机器绝不能让重注册把 CF 刷到 429。 */
|
|
@@ -38,6 +38,9 @@ export function createWarpPool({
|
|
|
38
38
|
ensureBin = ensureBinary,
|
|
39
39
|
start = startTunnel,
|
|
40
40
|
probe = probeTunnel,
|
|
41
|
+
reclaim = reclaimPort,
|
|
42
|
+
inUse = portInUse,
|
|
43
|
+
waitFree = waitPortFree,
|
|
41
44
|
now = Date.now,
|
|
42
45
|
rnd = Math.random,
|
|
43
46
|
wait = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
@@ -75,7 +78,12 @@ export function createWarpPool({
|
|
|
75
78
|
let redialGapMs = REDIAL_MIN_GAP_MS;
|
|
76
79
|
|
|
77
80
|
// 兜底清理:Node 退出不会回收 spawn 的子进程,daemon 突然退出时同步杀掉隧道。
|
|
81
|
+
// 同时置 closed:否则「杀子进程 → onExit → 立刻复活」会在退出过程中再 spawn 出一批孤儿(真机:每次重启漏一批)。
|
|
78
82
|
process.once("exit", () => {
|
|
83
|
+
closed = true;
|
|
84
|
+
try {
|
|
85
|
+
reviver.stop();
|
|
86
|
+
} catch {}
|
|
79
87
|
for (const t of tunnels.values()) {
|
|
80
88
|
try {
|
|
81
89
|
t.stop();
|
|
@@ -83,13 +91,14 @@ export function createWarpPool({
|
|
|
83
91
|
}
|
|
84
92
|
});
|
|
85
93
|
|
|
86
|
-
/**
|
|
94
|
+
/** 起一条隧道的完整动作:确认不是重复起 → 清残留 → 写 conf → 等端口真空出来 → spawn → 记表。 */
|
|
87
95
|
async function bringUp(node) {
|
|
88
|
-
|
|
96
|
+
if (tunnels.has(node.port)) return; // 已归我们管:绝不再清残留(reclaim 会把自己这条隧道当残留杀掉)
|
|
97
|
+
await reclaim({ dir: cfg.dir, port: node.port, log }); // 清上一代孤儿(daemon 被强杀时子进程不会自己退)
|
|
89
98
|
writeTunnelConf(cfg.dir, node);
|
|
90
|
-
|
|
91
|
-
if (await
|
|
92
|
-
throw new Error(`端口 :${node.port}
|
|
99
|
+
// SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着——先等它真空出来再判
|
|
100
|
+
if (!(await waitFree(node.port, { inUse }))) {
|
|
101
|
+
throw new Error(`端口 :${node.port} 被非本池进程占着,拒绝再起(硬起只会 bind 失败、无限重启刷屏);查明:ss -ltnp | grep :${node.port}`);
|
|
93
102
|
}
|
|
94
103
|
const t = start({
|
|
95
104
|
binPath,
|
|
@@ -100,7 +109,7 @@ export function createWarpPool({
|
|
|
100
109
|
tunnels.delete(port);
|
|
101
110
|
const why = tailReason(tail) || "进程没留下任何输出";
|
|
102
111
|
log(`[warp-pool] :${port} 隧道意外退出(退出码 ${code}${signal ? `,信号 ${signal}` : ""}):${why}`);
|
|
103
|
-
void reviver.onExit(port);
|
|
112
|
+
if (!closed) void reviver.onExit(port); // 收摊中就别复活了,否则退出过程里又 spawn 出一批孤儿
|
|
104
113
|
},
|
|
105
114
|
});
|
|
106
115
|
tunnels.set(node.port, t);
|
|
@@ -117,6 +126,35 @@ export function createWarpPool({
|
|
|
117
126
|
log,
|
|
118
127
|
});
|
|
119
128
|
|
|
129
|
+
/** 探活闸门参数:刚起的隧道要等 WireGuard 握手完成才通(真机:一起来就探 ⇒ 4 条全没通 ⇒ 池为空、WARP 白开)。 */
|
|
130
|
+
const PROBE_SETTLE_MS = 2_000;
|
|
131
|
+
const PROBE_ATTEMPTS = 2;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 入池前的实测闸门:给握手留时间 → 逐个探活 → 探不通的绝不进池。
|
|
135
|
+
* 为什么必须有:wireproxy 进程活着但隧道转不动流量时,请求会挂到超时(比直连更糟)。
|
|
136
|
+
* @returns {Promise<Array<object>>} 通过探活的节点
|
|
137
|
+
*/
|
|
138
|
+
async function probeGate(list) {
|
|
139
|
+
const live = [];
|
|
140
|
+
await wait(PROBE_SETTLE_MS);
|
|
141
|
+
for (const n of list) {
|
|
142
|
+
if (!tunnels.has(n.port)) continue;
|
|
143
|
+
let r = { ok: false };
|
|
144
|
+
for (let i = 1; i <= PROBE_ATTEMPTS; i++) {
|
|
145
|
+
r = await probe({ port: n.port, baseUrl, path: cfg.probePath });
|
|
146
|
+
if (r.ok) break;
|
|
147
|
+
if (i < PROBE_ATTEMPTS) await wait(PROBE_SETTLE_MS);
|
|
148
|
+
}
|
|
149
|
+
if (r.ok) {
|
|
150
|
+
live.push(n);
|
|
151
|
+
log(`[warp-pool] :${n.port} 探通(HTTP ${r.status}),进池`);
|
|
152
|
+
} else {
|
|
153
|
+
log(`[warp-pool] :${n.port} 探了 ${PROBE_ATTEMPTS} 次都不通(${r.status || r.error || "?"}),不进池:进池会把请求挂到超时,比直连更糟`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return live;
|
|
157
|
+
}
|
|
120
158
|
async function bootstrap() {
|
|
121
159
|
if (closed) return;
|
|
122
160
|
ensureDir(cfg.dir);
|
|
@@ -162,14 +200,8 @@ export function createWarpPool({
|
|
|
162
200
|
}
|
|
163
201
|
}
|
|
164
202
|
|
|
165
|
-
// 入池前实测探活:WARP
|
|
166
|
-
const live =
|
|
167
|
-
for (const n of nodes) {
|
|
168
|
-
if (!tunnels.has(n.port)) continue;
|
|
169
|
-
const r = await probe({ port: n.port, baseUrl, path: cfg.probePath });
|
|
170
|
-
if (r.ok) live.push(n);
|
|
171
|
-
else log(`[warp-pool] :${n.port} 隧道没探通(${r.status || r.error || "?"}),不进池:进池会把请求挂到超时,比直连更糟`);
|
|
172
|
-
}
|
|
203
|
+
// 入池前实测探活:WARP 握手没完成时 wireproxy 在跑却转不动流量(请求会挂到超时),那种节点进池会黑洞全部请求
|
|
204
|
+
const live = await probeGate(nodes);
|
|
173
205
|
if (live.length) {
|
|
174
206
|
await dispatcher.setNodes(live.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
175
207
|
log(`[warp-pool] 出口池就绪:${live.length} 条隧道可用(选路策略 ${cfg.strategy}${cfg.rotate ? ",撞 429 会自动换身份换出口" : ""})`);
|
|
@@ -215,10 +247,15 @@ export function createWarpPool({
|
|
|
215
247
|
}
|
|
216
248
|
await wait(gapMs(cfg, rnd));
|
|
217
249
|
}
|
|
218
|
-
if (!closed &&
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
250
|
+
if (!closed && tunnels.size) {
|
|
251
|
+
// 重拨起来的隧道同样要过闸门:没过探活的隧道进池 = 把请求挂到超时(原先这里直接 setNodes,等于绕过闸门)
|
|
252
|
+
const alive = await probeGate([...tunnels.keys()].map((port) => ({ port })));
|
|
253
|
+
if (alive.length) {
|
|
254
|
+
redialGapMs = REDIAL_MIN_GAP_MS; // 确实有隧道能通 ⇒ 退避复位
|
|
255
|
+
await dispatcher.setNodes(alive.map((n) => ({ port: n.port, uri: `http://127.0.0.1:${n.port}` })));
|
|
256
|
+
} else {
|
|
257
|
+
log("[warp-pool] 重拨后仍没有一条隧道探通,保持直连(等下一轮健康检查)");
|
|
258
|
+
}
|
|
222
259
|
}
|
|
223
260
|
},
|
|
224
261
|
});
|
package/src/warp-pool/ports.js
CHANGED
|
@@ -108,3 +108,15 @@ export function portInUse(port, { host = "127.0.0.1", timeoutMs = 800, connectFn
|
|
|
108
108
|
sock.once("error", () => finish(false));
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 等端口真正空出来(SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着)。
|
|
114
|
+
* 返回 true = 空了;false = 等了 rounds 轮还是被占。
|
|
115
|
+
*/
|
|
116
|
+
export async function waitPortFree(port, { inUse = portInUse, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), rounds = 12, stepMs = 250 } = {}) {
|
|
117
|
+
for (let i = 0; i < rounds; i++) {
|
|
118
|
+
if (!(await inUse(port))) return true;
|
|
119
|
+
await sleep(stepMs);
|
|
120
|
+
}
|
|
121
|
+
return !(await inUse(port));
|
|
122
|
+
}
|
package/src/warp-pool/rotate.js
CHANGED
|
@@ -78,7 +78,7 @@ export function createRotatingDispatcher({
|
|
|
78
78
|
if (!e) return;
|
|
79
79
|
e.fails += 1;
|
|
80
80
|
e.cooldownUntil = now() + cooldownMs;
|
|
81
|
-
log(`[warp-pool] 节点 :${e.port} 连续失败 ${e.fails} 次,冷却 ${Math.round(cooldownMs / 1000)}s
|
|
81
|
+
if (e.fails === 1 || e.fails % 10 === 0) log(`[warp-pool] 节点 :${e.port} 连续失败 ${e.fails} 次,冷却 ${Math.round(cooldownMs / 1000)}s(中间次数静默:健康探活每分钟一轮,一个不通的节点曾把日志刷成 12 行/分钟)`);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
function markOk(index) {
|