mslxdff 0.1.203 → 0.1.208
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/README.md +3 -3
- package/bin/mslxdff.js +1 -1
- package/docs/cli_help_mini.md +147 -0
- package/package.json +38 -38
- package/plugins/README.md +14 -14
- package/plugins/prefer-model.mjs.example +14 -14
- package/src/anthropic/canonical-json.js +55 -0
- package/src/anthropic/count-tokens.js +61 -61
- package/src/anthropic/request-body.js +183 -126
- package/src/anthropic/request-shape.js +60 -0
- package/src/anthropic/session-id.js +58 -0
- package/src/anthropic/shared.js +18 -18
- package/src/anthropic/sse-aggregate.js +56 -56
- package/src/anthropic/tool-media.js +75 -0
- package/src/anthropic/translate.js +296 -287
- package/src/anthropic/web-search.js +361 -361
- package/src/api-kind.js +31 -31
- package/src/autostart.js +288 -288
- package/src/bench/probe.js +1 -1
- package/src/bench/runner.js +1 -1
- package/src/bench/via-probe.js +1 -1
- package/src/bench/via.js +144 -144
- package/src/bench/workbuddy-bench.js +1 -1
- package/src/chat/config.js +11 -11
- package/src/chat/cooling.js +81 -81
- package/src/chat/direct.js +58 -58
- package/src/chat/index.js +6 -6
- package/src/chat/spinner.js +38 -38
- package/src/chat/sse.js +69 -69
- package/src/chat/stats.js +247 -247
- package/src/chat/store.js +48 -48
- package/src/chat/tool-handlers.js +67 -67
- package/src/chat/tools.js +1 -1
- package/src/chat/upstream.js +59 -59
- package/src/chat-pipeline/auto-race.js +158 -158
- package/src/chat-pipeline/empty-turn.js +147 -147
- package/src/chat-pipeline/engine.js +17 -17
- package/src/chat-pipeline/index.js +8 -40
- package/src/chat-pipeline/serial-trial.js +279 -283
- package/src/chat-pipeline/terminal-verdict.js +132 -147
- package/src/chooser.js +54 -54
- package/src/claude-desktop/apply.js +101 -101
- package/src/claude-desktop/context1m.js +38 -38
- package/src/claude-desktop/doctor.js +146 -146
- package/src/claude-desktop/slots.js +152 -152
- package/src/cli/bootstrap.js +1 -1
- package/src/cli/commands/claude-desktop.js +284 -284
- package/src/cli/commands/daemon.js +231 -231
- package/src/cli/commands/model/list-live.js +22 -22
- package/src/cli/commands/model/list-providers.js +76 -76
- package/src/cli/commands/model/list-render.js +77 -77
- package/src/cli/commands/model/list-sort.js +52 -52
- package/src/cli/commands/model/list-tty.js +76 -76
- package/src/cli/commands/model/list.js +166 -166
- package/src/cli/commands/model/live-models.js +24 -24
- package/src/cli/commands/model/stats.js +43 -43
- package/src/cli/commands/model/status.js +47 -47
- package/src/cli/commands/model.js +51 -51
- package/src/cli/commands/provider/add.js +108 -108
- package/src/cli/commands/provider/allowlist.js +99 -99
- package/src/cli/commands/provider/cline-free.js +99 -99
- package/src/cli/commands/provider/cline-login.js +218 -218
- package/src/cli/commands/provider/cline-quota.js +65 -65
- package/src/cli/commands/provider/codearts-login.js +45 -45
- package/src/cli/commands/provider/config.js +119 -119
- package/src/cli/commands/provider/globalqwenwork-login.js +126 -126
- package/src/cli/commands/provider/keys-guard.js +50 -50
- package/src/cli/commands/provider/keys.js +161 -161
- package/src/cli/commands/provider/qoder-checkin.js +81 -81
- package/src/cli/commands/provider/qoder-login.js +102 -102
- package/src/cli/commands/provider/raccoon-login.js +180 -180
- package/src/cli/commands/provider/raccoon-password.js +162 -162
- package/src/cli/commands/provider/raccoon-quota.js +109 -109
- package/src/cli/commands/provider/traework-checkin.js +144 -144
- package/src/cli/commands/provider/traework-login.js +164 -164
- package/src/cli/commands/provider/workbuddy-login.js +153 -153
- package/src/cli/commands/provider/zcode-login.js +77 -77
- package/src/cli/commands/provider/zcode-quota.js +55 -55
- package/src/cli/commands/provider.js +1 -1
- package/src/cli/commands/stats.js +158 -149
- package/src/cli/commands/system.js +261 -261
- package/src/cli/commands/timezone.js +47 -47
- package/src/cli/commands/use-group.js +60 -60
- package/src/cli/commands/v6-pool.js +123 -0
- package/src/cli/commands/workbuddy.js +189 -189
- package/src/cli/format.js +141 -141
- package/src/cli/group-helpers.js +70 -70
- package/src/cli/help.js +80 -80
- package/src/cli/index.js +73 -70
- package/src/cli/interactive.js +121 -121
- package/src/cli/join-core.js +99 -99
- package/src/cli/join-wizard.js +175 -175
- package/src/cli/policy.js +136 -136
- package/src/cli/provider-row.js +126 -126
- package/src/cli/util.js +24 -24
- package/src/compat.js +53 -53
- package/src/events.js +38 -38
- package/src/free-lane.js +131 -131
- package/src/free-watcher.js +63 -63
- package/src/groups.js +262 -262
- package/src/logs.js +195 -195
- package/src/metrics.js +71 -62
- package/src/model-capabilities/context-lookup.js +65 -65
- package/src/model-capabilities/merge.js +142 -142
- package/src/model-trace.js +187 -187
- package/src/models.js +1 -1
- package/src/opencode-identity.js +72 -72
- package/src/peers.js +254 -254
- package/src/plugins.js +107 -107
- package/src/providers/AGENTS.md +49 -0
- package/src/providers/base.js +164 -164
- package/src/providers/classify.js +22 -22
- package/src/providers/cline/allowlist-sync.js +93 -93
- package/src/providers/cline/free-catalog.js +71 -71
- package/src/providers/cline/headers.js +37 -37
- package/src/providers/cline/index.js +130 -130
- package/src/providers/cline/models.js +150 -150
- package/src/providers/cline/usage.js +274 -274
- package/src/providers/codearts/auth-pool.js +148 -148
- package/src/providers/codearts/concurrency.js +34 -34
- package/src/providers/codearts/const.js +65 -65
- package/src/providers/codearts/dpop.js +95 -95
- package/src/providers/codearts/index.js +175 -175
- package/src/providers/codearts/login.js +214 -214
- package/src/providers/codearts/models.js +182 -182
- package/src/providers/codearts/sign.js +96 -96
- package/src/providers/codearts/sse.js +204 -204
- package/src/providers/codearts/stream.js +209 -209
- package/src/providers/codearts/sts.js +124 -124
- package/src/providers/codearts.js +1 -1
- package/src/providers/dispatcher.js +146 -146
- package/src/providers/generic.js +156 -156
- package/src/providers/globalqwenwork/account-store.js +141 -141
- package/src/providers/globalqwenwork/constants.js +73 -73
- package/src/providers/globalqwenwork/crypto.js +2 -2
- package/src/providers/globalqwenwork/http.js +2 -2
- package/src/providers/globalqwenwork/index.js +331 -331
- package/src/providers/globalqwenwork/payload.js +147 -147
- package/src/providers/globalqwenwork/rsa.js +2 -2
- package/src/providers/globalqwenwork/sse.js +2 -2
- package/src/providers/globalqwenwork/stream.js +2 -2
- package/src/providers/globalqwenwork.js +1 -1
- package/src/providers/globalworkbuddy/account-store.js +198 -198
- package/src/providers/globalworkbuddy/auth.js +97 -97
- package/src/providers/globalworkbuddy/catalog.js +138 -138
- package/src/providers/globalworkbuddy/constants.js +73 -73
- package/src/providers/globalworkbuddy/device-auth.js +120 -120
- package/src/providers/globalworkbuddy/errors.js +98 -98
- package/src/providers/globalworkbuddy/headers.js +89 -89
- package/src/providers/globalworkbuddy/models.js +96 -96
- package/src/providers/globalworkbuddy/payload.js +80 -80
- package/src/providers/index.js +5 -5
- package/src/providers/model-id.js +172 -172
- package/src/providers/opencode.js +19 -19
- package/src/providers/openrouter.js +31 -31
- package/src/providers/qoder/account-store.js +106 -106
- package/src/providers/qoder/aggregate.js +60 -60
- package/src/providers/qoder/baseprompt.json +591 -591
- package/src/providers/qoder/checkin.js +160 -160
- package/src/providers/qoder/constants.js +44 -44
- package/src/providers/qoder/encode.js +41 -41
- package/src/providers/qoder/fingerprint.js +38 -38
- package/src/providers/qoder/models.js +77 -77
- package/src/providers/qoder/oauth.js +84 -84
- package/src/providers/qoder/payload.js +162 -162
- package/src/providers/qoder/request.js +27 -27
- package/src/providers/qoder/session.js +134 -134
- package/src/providers/qoder/sticky.js +38 -38
- package/src/providers/qoder/stream.js +116 -116
- package/src/providers/qoder.js +1 -1
- package/src/providers/raccoon/account-store.js +216 -216
- package/src/providers/raccoon/auth.js +67 -67
- package/src/providers/raccoon/chat.js +228 -228
- package/src/providers/raccoon/const.js +180 -180
- package/src/providers/raccoon/credits.js +185 -185
- package/src/providers/raccoon/crypto.js +43 -43
- package/src/providers/raccoon/envelope.js +31 -31
- package/src/providers/raccoon/headers.js +45 -45
- package/src/providers/raccoon/index.js +216 -216
- package/src/providers/raccoon/login.js +279 -279
- package/src/providers/raccoon/models.js +160 -160
- package/src/providers/raccoon/password-login.js +102 -102
- package/src/providers/raccoon/qr-codewords.js +115 -115
- package/src/providers/raccoon/qr.js +236 -236
- package/src/providers/raccoon/sse.js +135 -135
- package/src/providers/responses-channel.js +120 -120
- package/src/providers/share-keys.js +53 -53
- package/src/providers/traework/account-store.js +234 -234
- package/src/providers/traework/chat.js +217 -217
- package/src/providers/traework/checkin.js +77 -77
- package/src/providers/traework/constants.js +41 -41
- package/src/providers/traework/errors.js +52 -52
- package/src/providers/traework/headers.js +79 -79
- package/src/providers/traework/index.js +132 -132
- package/src/providers/traework/market-identity.js +116 -116
- package/src/providers/traework/models.js +98 -98
- package/src/providers/traework/payload.js +86 -86
- package/src/providers/traework/sse.js +184 -184
- package/src/providers/traework/token.js +52 -52
- package/src/providers/traework.js +2 -2
- package/src/providers/workbuddy/account-store.js +186 -186
- package/src/providers/workbuddy/auth.js +158 -158
- package/src/providers/workbuddy/balance.js +86 -86
- package/src/providers/workbuddy/cat-travel.js +125 -125
- package/src/providers/workbuddy/checkin.js +75 -75
- package/src/providers/workbuddy/desktop-info.js +78 -78
- package/src/providers/workbuddy/growth-api.js +67 -67
- package/src/providers/workbuddy/growth-plans.js +63 -63
- package/src/providers/workbuddy/growth.js +181 -181
- package/src/providers/workbuddy/index.js +217 -217
- package/src/providers/workbuddy/models.js +126 -126
- package/src/providers/workbuddy/payload.js +141 -141
- package/src/providers/workbuddy/rotation-log.js +83 -83
- package/src/providers/workbuddy/sanitize-tools.js +61 -61
- package/src/providers/workbuddy/sdk-chat.js +26 -26
- package/src/providers/workbuddy-balance.js +78 -78
- package/src/providers/zcode/account-store.js +129 -129
- package/src/providers/zcode/auth.js +28 -28
- package/src/providers/zcode/chat.js +179 -179
- package/src/providers/zcode/const.js +54 -54
- package/src/providers/zcode/context-shape.js +162 -162
- package/src/providers/zcode/headers.js +93 -93
- package/src/providers/zcode/index.js +124 -124
- package/src/providers/zcode/models.js +65 -65
- package/src/providers/zcode/oauth.js +120 -120
- package/src/providers/zcode/quota.js +176 -176
- package/src/providers/zcode/sse.js +196 -196
- package/src/readline-compat.js +25 -25
- package/src/responses/translate.js +311 -311
- package/src/routes/AGENTS.md +37 -0
- package/src/routes/chat/broadband-handler.js +72 -72
- package/src/routes/chat/empty-turn-report.js +31 -31
- package/src/routes/chat/exhausted-handler.js +87 -87
- package/src/routes/chat/hedge-handler.js +130 -130
- package/src/routes/chat/index.js +1 -1
- package/src/routes/chat/local-handler.js +63 -63
- package/src/routes/chat/peer-handler.js +81 -81
- package/src/routes/chat.js +3 -3
- package/src/routes/fallback.js +88 -88
- package/src/routes/groups-relay.js +214 -214
- package/src/routes/groups.js +122 -122
- package/src/routes/helpers.js +93 -83
- package/src/routes/index.js +147 -147
- package/src/routes/messages-route.js +375 -365
- package/src/routes/peers.js +10 -2
- package/src/routes/relay-queue.js +181 -181
- package/src/routes/relay.js +60 -60
- package/src/routes/responses-route.js +213 -213
- package/src/routes/stream-body.js +84 -84
- package/src/routes/stream-hold.js +337 -337
- package/src/routes/stream-scan.js +366 -366
- package/src/routes.js +3 -3
- package/src/runtime/auto-update.js +113 -113
- package/src/runtime/broadband-stream.js +79 -79
- package/src/runtime/broadband.js +103 -103
- package/src/runtime/group-sync.js +81 -81
- package/src/runtime/keep-warm.js +120 -120
- package/src/runtime/lifecycle-forensics.js +116 -116
- package/src/runtime/lifecycle-log.js +87 -87
- package/src/runtime/log-stamp.js +73 -73
- package/src/runtime/proc-info.js +137 -137
- package/src/runtime/qoder-checkin.js +93 -93
- package/src/runtime/raccoon-checkin.js +126 -126
- package/src/runtime/server-lifecycle.js +189 -189
- package/src/runtime/traework-checkin.js +118 -118
- package/src/runtime/workbuddy-checkin.js +120 -120
- package/src/runtime/workbuddy-growth.js +133 -133
- package/src/state/facade.js +58 -58
- package/src/state/memory.js +162 -162
- package/src/state/merge.js +25 -25
- package/src/state/migrations/cline-unify.js +94 -94
- package/src/state/migrations.js +33 -33
- package/src/state/persist.js +42 -42
- package/src/state/provider-config.js +142 -142
- package/src/state/schemas/group.js +31 -31
- package/src/state/schemas/model.js +94 -94
- package/src/state/schemas/peer.js +31 -31
- package/src/state/schemas/port.js +10 -10
- package/src/state/schemas/provider.js +202 -202
- package/src/state/schemas/timezone.js +70 -70
- package/src/state/schemas/token.js +48 -48
- package/src/state/schemas/use-group.js +102 -102
- package/src/state/store.js +176 -176
- package/src/state.js +1 -1
- package/src/sync-claude-desktop.js +234 -234
- package/src/sync-claude.js +152 -152
- package/src/sync-codex.js +116 -116
- package/src/sync-workbuddy.js +158 -158
- package/src/talk-full-redact.js +129 -129
- package/src/talk-full.js +131 -131
- package/src/talk-log.js +257 -257
- package/src/text-tokens.js +52 -52
- package/src/thinking-effort.js +67 -67
- package/src/time.js +106 -106
- package/src/timeline.js +26 -26
- package/src/transport/index.js +1 -1
- package/src/upstream-engine/index.js +74 -74
- package/src/upstream-engine/mode.js +15 -15
- package/src/upstream-engine/sdk/attempt.js +221 -221
- package/src/upstream-engine/sdk/chat.js +28 -28
- package/src/upstream-engine/sdk/convert.js +192 -192
- package/src/upstream-engine/sdk/diagnose.js +62 -62
- package/src/upstream-engine/sdk/dispatch.js +55 -55
- package/src/upstream-engine/sdk/responses.js +176 -176
- package/src/upstream-engine/sdk/sse.js +166 -166
- package/src/upstream-responses.js +305 -305
- package/src/usage/cache-fields.js +58 -0
- package/src/usage/record.js +109 -106
- package/src/usage/report.js +209 -195
- package/src/v6pool/claim.js +157 -157
- package/src/v6pool/config.js +65 -65
- package/src/v6pool/detect.js +130 -130
- package/src/v6pool/index.js +152 -152
- package/src/v6pool/prefix.js +140 -140
- package/src/v6pool/probe.js +40 -40
- package/src/v6pool/report.js +104 -104
- package/src/v6pool/rotate.js +263 -263
- package/src/v6pool/selftest.js +154 -0
- package/src/warp-pool/config.js +74 -74
- package/src/warp-pool/health.js +106 -106
- package/src/warp-pool/identity.js +141 -141
- package/src/warp-pool/index.js +358 -358
- package/src/warp-pool/keys.js +29 -29
- package/src/warp-pool/nodes.js +95 -95
- package/src/warp-pool/ports.js +160 -160
- package/src/warp-pool/register.js +113 -113
- package/src/warp-pool/report.js +232 -232
- package/src/warp-pool/revive.js +168 -168
- package/src/warp-pool/rotate.js +214 -214
- package/src/warp-pool/strategy.js +43 -43
- package/src/warp-pool/verdict.js +69 -69
- package/src/warp-pool/wireproxy.js +162 -162
package/src/warp-pool/keys.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
// warp-pool · WireGuard 密钥(`wg genkey` / `wg pubkey` 的 Node 原生等价物)。
|
|
2
|
-
// WireGuard 用 Curve25519(x25519):密钥是 32 字节 raw,编码为 44 字符 base64。
|
|
3
|
-
// Node 原生 generateKeyPairSync('x25519') 导出的 DER 末 32 字节即 raw key(本机实测通过)。
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
|
|
6
|
-
const RAW_BYTES = 32;
|
|
7
|
-
|
|
8
|
-
/** DER(SPKI/PKCS8) 的末 32 字节就是 Curve25519 raw key。 */
|
|
9
|
-
function rawTail(der) {
|
|
10
|
-
const raw = der.subarray(-RAW_BYTES);
|
|
11
|
-
if (raw.length !== RAW_BYTES) throw new Error("x25519 密钥导出异常:DER 长度不足 32 字节");
|
|
12
|
-
return raw;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 生成一对 WireGuard 密钥(base64,44 字符)。
|
|
17
|
-
* 注册 WARP 设备时把 publicKey 填进请求体的 `key` 字段。
|
|
18
|
-
*/
|
|
19
|
-
export function generateWgKeyPair() {
|
|
20
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
|
|
21
|
-
const privateKeyB64 = rawTail(privateKey.export({ type: "pkcs8", format: "der" })).toString("base64");
|
|
22
|
-
const publicKeyB64 = rawTail(publicKey.export({ type: "spki", format: "der" })).toString("base64");
|
|
23
|
-
return { privateKey: privateKeyB64, publicKey: publicKeyB64 };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** WireGuard base64 密钥形状自检:恒 44 字符、以 '=' 收尾(Nothing-up-my-sleeve 检查用)。 */
|
|
27
|
-
export function isWgKey(v) {
|
|
28
|
-
return typeof v === "string" && /^[A-Za-z0-9+/]{43}=$/.test(v);
|
|
29
|
-
}
|
|
1
|
+
// warp-pool · WireGuard 密钥(`wg genkey` / `wg pubkey` 的 Node 原生等价物)。
|
|
2
|
+
// WireGuard 用 Curve25519(x25519):密钥是 32 字节 raw,编码为 44 字符 base64。
|
|
3
|
+
// Node 原生 generateKeyPairSync('x25519') 导出的 DER 末 32 字节即 raw key(本机实测通过)。
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
const RAW_BYTES = 32;
|
|
7
|
+
|
|
8
|
+
/** DER(SPKI/PKCS8) 的末 32 字节就是 Curve25519 raw key。 */
|
|
9
|
+
function rawTail(der) {
|
|
10
|
+
const raw = der.subarray(-RAW_BYTES);
|
|
11
|
+
if (raw.length !== RAW_BYTES) throw new Error("x25519 密钥导出异常:DER 长度不足 32 字节");
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 生成一对 WireGuard 密钥(base64,44 字符)。
|
|
17
|
+
* 注册 WARP 设备时把 publicKey 填进请求体的 `key` 字段。
|
|
18
|
+
*/
|
|
19
|
+
export function generateWgKeyPair() {
|
|
20
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
|
|
21
|
+
const privateKeyB64 = rawTail(privateKey.export({ type: "pkcs8", format: "der" })).toString("base64");
|
|
22
|
+
const publicKeyB64 = rawTail(publicKey.export({ type: "spki", format: "der" })).toString("base64");
|
|
23
|
+
return { privateKey: privateKeyB64, publicKey: publicKeyB64 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** WireGuard base64 密钥形状自检:恒 44 字符、以 '=' 收尾(Nothing-up-my-sleeve 检查用)。 */
|
|
27
|
+
export function isWgKey(v) {
|
|
28
|
+
return typeof v === "string" && /^[A-Za-z0-9+/]{43}=$/.test(v);
|
|
29
|
+
}
|
package/src/warp-pool/nodes.js
CHANGED
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
// warp-pool · 节点元数据持久化。
|
|
2
|
-
// 含 WireGuard 私钥文件与设备 token ⇒ 一律 0600,且只落在日志根(MSLXDFF_WARP_DIR),不进工作树。
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const SECRET_MODE = 0o600;
|
|
7
|
-
|
|
8
|
-
/** 节点两件套:WireGuard 配置(含私钥)与元数据(含设备 token)。 */
|
|
9
|
-
export function nodePaths(dir, port) {
|
|
10
|
-
return {
|
|
11
|
-
conf: join(dir, `node-${port}.conf`),
|
|
12
|
-
meta: join(dir, `node-${port}.json`),
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** 写敏感文件:新建用 0600,存量补 chmod(mode 只在新建时生效)。 */
|
|
17
|
-
export function writeSecret(file, content) {
|
|
18
|
-
const existed = existsSync(file);
|
|
19
|
-
writeFileSync(file, content, existed ? undefined : { mode: SECRET_MODE });
|
|
20
|
-
try { chmodSync(file, SECRET_MODE); } catch {}
|
|
21
|
-
return file;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function ensureDir(dir) {
|
|
25
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
-
return dir;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 读单个节点元数据;文件缺失/损坏返回 null,绝不抛。 */
|
|
30
|
-
export function loadNode(dir, port) {
|
|
31
|
-
const { meta } = nodePaths(dir, port);
|
|
32
|
-
try {
|
|
33
|
-
const j = JSON.parse(readFileSync(meta, "utf8"));
|
|
34
|
-
if (!j || typeof j !== "object") return null;
|
|
35
|
-
return {
|
|
36
|
-
port,
|
|
37
|
-
id: String(j.id || ""),
|
|
38
|
-
token: String(j.token || ""),
|
|
39
|
-
v4: String(j.v4 || ""),
|
|
40
|
-
v6: String(j.v6 || ""),
|
|
41
|
-
endpoint: String(j.endpoint || ""),
|
|
42
|
-
peerKey: String(j.peerKey || ""),
|
|
43
|
-
privateKey: String(j.privateKey || ""), // 必须读回:bringUp 靠它渲染 conf,漏了就是 PrivateKey = undefined(2026-10-16 真机事故:复用节点的隧道全起不来)
|
|
44
|
-
registeredAt: String(j.registeredAt || ""),
|
|
45
|
-
...(j.cooldownUntil ? { cooldownUntil: Number(j.cooldownUntil) } : {}),
|
|
46
|
-
};
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function saveNode(dir, node) {
|
|
53
|
-
const { meta } = nodePaths(dir, node.port);
|
|
54
|
-
return writeSecret(meta, `${JSON.stringify(node, null, 2)}\n`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function removeNode(dir, port) {
|
|
58
|
-
const { conf, meta } = nodePaths(dir, port);
|
|
59
|
-
for (const f of [conf, meta]) {
|
|
60
|
-
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 读回配置目录里已注册的节点(按端口升序)。 */
|
|
65
|
-
export function loadNodes(dir, ports) {
|
|
66
|
-
const out = [];
|
|
67
|
-
for (const p of ports) {
|
|
68
|
-
const n = loadNode(dir, p);
|
|
69
|
-
if (n && n.id && n.token && n.privateKey) out.push(n);
|
|
70
|
-
}
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** WireGuard + wireproxy INI([http] 段把隧道暴露成本地 HTTP 代理出口;undici v5 的 ProxyAgent 只吃 http/https)。 */
|
|
75
|
-
export function renderWireproxyConf({ privateKey, v4, v6, endpoint, peerKey, port }) {
|
|
76
|
-
const addresses = [
|
|
77
|
-
v4 ? `Address = ${v4}/32` : "",
|
|
78
|
-
v6 ? `Address = ${v6}/128` : "",
|
|
79
|
-
].filter(Boolean).join("\n");
|
|
80
|
-
return `[Interface]
|
|
81
|
-
PrivateKey = ${privateKey}
|
|
82
|
-
${addresses}
|
|
83
|
-
DNS = 1.1.1.1
|
|
84
|
-
MTU = 1280
|
|
85
|
-
|
|
86
|
-
[Peer]
|
|
87
|
-
PublicKey = ${peerKey}
|
|
88
|
-
AllowedIPs = 0.0.0.0/0, ::/0
|
|
89
|
-
Endpoint = ${endpoint}
|
|
90
|
-
PersistentKeepalive = 25
|
|
91
|
-
|
|
92
|
-
[http]
|
|
93
|
-
BindAddress = 127.0.0.1:${port}
|
|
94
|
-
`;
|
|
95
|
-
}
|
|
1
|
+
// warp-pool · 节点元数据持久化。
|
|
2
|
+
// 含 WireGuard 私钥文件与设备 token ⇒ 一律 0600,且只落在日志根(MSLXDFF_WARP_DIR),不进工作树。
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const SECRET_MODE = 0o600;
|
|
7
|
+
|
|
8
|
+
/** 节点两件套:WireGuard 配置(含私钥)与元数据(含设备 token)。 */
|
|
9
|
+
export function nodePaths(dir, port) {
|
|
10
|
+
return {
|
|
11
|
+
conf: join(dir, `node-${port}.conf`),
|
|
12
|
+
meta: join(dir, `node-${port}.json`),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 写敏感文件:新建用 0600,存量补 chmod(mode 只在新建时生效)。 */
|
|
17
|
+
export function writeSecret(file, content) {
|
|
18
|
+
const existed = existsSync(file);
|
|
19
|
+
writeFileSync(file, content, existed ? undefined : { mode: SECRET_MODE });
|
|
20
|
+
try { chmodSync(file, SECRET_MODE); } catch {}
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ensureDir(dir) {
|
|
25
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
return dir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 读单个节点元数据;文件缺失/损坏返回 null,绝不抛。 */
|
|
30
|
+
export function loadNode(dir, port) {
|
|
31
|
+
const { meta } = nodePaths(dir, port);
|
|
32
|
+
try {
|
|
33
|
+
const j = JSON.parse(readFileSync(meta, "utf8"));
|
|
34
|
+
if (!j || typeof j !== "object") return null;
|
|
35
|
+
return {
|
|
36
|
+
port,
|
|
37
|
+
id: String(j.id || ""),
|
|
38
|
+
token: String(j.token || ""),
|
|
39
|
+
v4: String(j.v4 || ""),
|
|
40
|
+
v6: String(j.v6 || ""),
|
|
41
|
+
endpoint: String(j.endpoint || ""),
|
|
42
|
+
peerKey: String(j.peerKey || ""),
|
|
43
|
+
privateKey: String(j.privateKey || ""), // 必须读回:bringUp 靠它渲染 conf,漏了就是 PrivateKey = undefined(2026-10-16 真机事故:复用节点的隧道全起不来)
|
|
44
|
+
registeredAt: String(j.registeredAt || ""),
|
|
45
|
+
...(j.cooldownUntil ? { cooldownUntil: Number(j.cooldownUntil) } : {}),
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveNode(dir, node) {
|
|
53
|
+
const { meta } = nodePaths(dir, node.port);
|
|
54
|
+
return writeSecret(meta, `${JSON.stringify(node, null, 2)}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function removeNode(dir, port) {
|
|
58
|
+
const { conf, meta } = nodePaths(dir, port);
|
|
59
|
+
for (const f of [conf, meta]) {
|
|
60
|
+
try { if (existsSync(f)) unlinkSync(f); } catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 读回配置目录里已注册的节点(按端口升序)。 */
|
|
65
|
+
export function loadNodes(dir, ports) {
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const p of ports) {
|
|
68
|
+
const n = loadNode(dir, p);
|
|
69
|
+
if (n && n.id && n.token && n.privateKey) out.push(n);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** WireGuard + wireproxy INI([http] 段把隧道暴露成本地 HTTP 代理出口;undici v5 的 ProxyAgent 只吃 http/https)。 */
|
|
75
|
+
export function renderWireproxyConf({ privateKey, v4, v6, endpoint, peerKey, port }) {
|
|
76
|
+
const addresses = [
|
|
77
|
+
v4 ? `Address = ${v4}/32` : "",
|
|
78
|
+
v6 ? `Address = ${v6}/128` : "",
|
|
79
|
+
].filter(Boolean).join("\n");
|
|
80
|
+
return `[Interface]
|
|
81
|
+
PrivateKey = ${privateKey}
|
|
82
|
+
${addresses}
|
|
83
|
+
DNS = 1.1.1.1
|
|
84
|
+
MTU = 1280
|
|
85
|
+
|
|
86
|
+
[Peer]
|
|
87
|
+
PublicKey = ${peerKey}
|
|
88
|
+
AllowedIPs = 0.0.0.0/0, ::/0
|
|
89
|
+
Endpoint = ${endpoint}
|
|
90
|
+
PersistentKeepalive = 25
|
|
91
|
+
|
|
92
|
+
[http]
|
|
93
|
+
BindAddress = 127.0.0.1:${port}
|
|
94
|
+
`;
|
|
95
|
+
}
|
package/src/warp-pool/ports.js
CHANGED
|
@@ -1,160 +1,160 @@
|
|
|
1
|
-
// warp-pool · 端口占用与「上一代进程残留」清理(2026-10-16 真机事故后新增;2026-10-17 七次修订)。
|
|
2
|
-
//
|
|
3
|
-
// 事故一(0.1.179):daemon 被强杀时 `process.once("exit")` 钩子不执行,wireproxy 子进程变孤儿继续占着端口。
|
|
4
|
-
// 事故二(七次修订,真机铁证):**两个 daemon 同时活着**(一个从昨天起活了一整天),两者都用同一份
|
|
5
|
-
// `node-<port>.conf` 判「本池残留」⇒ 互相把对方的**活隧道**当残留杀掉 ⇒ 无限互杀、池永远不稳。
|
|
6
|
-
// 两条教训写进判据:
|
|
7
|
-
// ① 父进程是**另一个活着的 daemon** ⇒ 那不是残留,是「别人的活隧道」,清了就是互杀 ⇒ 不许清,只报出来;
|
|
8
|
-
// ② 「SIGKILL 都清不掉」多半是**僵尸**(state=Z:kill 无效,但 kill(pid,0) 仍返回成功)⇒ 僵尸算已死。
|
|
9
|
-
import { connect } from "node:net";
|
|
10
|
-
import { defaultCmdline, defaultPids, defaultStat, isZombie, otherDaemonParent, parseCmdline, pidAlive } from "../runtime/proc-info.js";
|
|
11
|
-
import { nodePaths } from "./nodes.js";
|
|
12
|
-
import { fmtReclaim } from "./report.js";
|
|
13
|
-
|
|
14
|
-
// /proc 读取层已上移到 runtime/proc-info.js(daemon 侧也要用同一套判据);这里原样再导出,既有调用方路径不变。
|
|
15
|
-
export { parseCmdline, pidAlive };
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 该进程是不是「正在跑我们这条 conf」的 wireproxy。
|
|
19
|
-
* 判据刻意收紧(argv 里得有 wireproxy 这个可执行文件名,且 conf 路径逐字相等),
|
|
20
|
-
* 免得误杀别人的进程。缺一不可。
|
|
21
|
-
*/
|
|
22
|
-
export function isOurWireproxy(argv, conf) {
|
|
23
|
-
const list = Array.isArray(argv) ? argv : [];
|
|
24
|
-
const isBin = list.some((a) => /(^|[\\/])wireproxy(\.exe)?$/i.test(a));
|
|
25
|
-
return isBin && list.includes(conf);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 清掉占着该端口那条 conf 的**真残留**:自家丢引用的子进程、父进程已死的孤儿。
|
|
30
|
-
* 不动的三类:僵尸(kill 无效,也不是真占用者)、**父进程是另一个活 daemon 的活隧道**(清了就是互杀)、
|
|
31
|
-
* argv 不匹配的别人的进程。
|
|
32
|
-
* 非 Linux 直接返回空(读不到 /proc,别拿别的平台做危险猜测)。
|
|
33
|
-
* @returns {Promise<{scanned:number, killed:number[], zombies:number[],
|
|
34
|
-
* foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, waitedMs:number, gone:boolean}>}
|
|
35
|
-
*/
|
|
36
|
-
export async function reclaimPort({
|
|
37
|
-
dir,
|
|
38
|
-
port,
|
|
39
|
-
log = () => {},
|
|
40
|
-
platform = process.platform,
|
|
41
|
-
pids = defaultPids,
|
|
42
|
-
cmdline = defaultCmdline,
|
|
43
|
-
stat = defaultStat,
|
|
44
|
-
kill = process.kill,
|
|
45
|
-
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
46
|
-
waitRounds = 15,
|
|
47
|
-
} = {}) {
|
|
48
|
-
const startedAt = Date.now();
|
|
49
|
-
const shape = (scanned, killed, zombies, foreign) => ({
|
|
50
|
-
scanned,
|
|
51
|
-
killed,
|
|
52
|
-
zombies,
|
|
53
|
-
foreign,
|
|
54
|
-
waitedMs: Date.now() - startedAt,
|
|
55
|
-
gone: !killed.some((p) => pidAlive(p, kill)),
|
|
56
|
-
});
|
|
57
|
-
if (platform !== "linux") return shape(0, [], [], []);
|
|
58
|
-
|
|
59
|
-
const conf = nodePaths(dir, port).conf;
|
|
60
|
-
const all = pids();
|
|
61
|
-
const zombies = [];
|
|
62
|
-
const foreign = [];
|
|
63
|
-
const targets = [];
|
|
64
|
-
for (const pid of all) {
|
|
65
|
-
if (!isOurWireproxy(cmdline(pid), conf)) continue;
|
|
66
|
-
const st = stat(pid);
|
|
67
|
-
if (st && isZombie(st.state)) {
|
|
68
|
-
zombies.push(pid);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const other = otherDaemonParent(pid, { stat, cmdline, kill });
|
|
72
|
-
if (other) {
|
|
73
|
-
foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
targets.push(pid);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (targets.length) {
|
|
80
|
-
for (const pid of targets) {
|
|
81
|
-
try {
|
|
82
|
-
kill(pid, "SIGTERM");
|
|
83
|
-
} catch {}
|
|
84
|
-
}
|
|
85
|
-
for (let i = 0; i < waitRounds && targets.some((p) => pidAlive(p, kill)); i++) await sleep(100);
|
|
86
|
-
for (const pid of targets.filter((p) => pidAlive(p, kill))) {
|
|
87
|
-
try {
|
|
88
|
-
kill(pid, "SIGKILL");
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
const r = shape(all.length, targets, zombies, foreign);
|
|
93
|
-
log(fmtReclaim({ port, ...r }));
|
|
94
|
-
return r;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** 该端口是否已有人在监听(TCP 连得上 = 占用;连不上/被拒 = 空闲)。 */
|
|
98
|
-
export function portInUse(port, { host = "127.0.0.1", timeoutMs = 800, connectFn = connect } = {}) {
|
|
99
|
-
return new Promise((resolve) => {
|
|
100
|
-
let done = false;
|
|
101
|
-
const sock = connectFn({ host, port });
|
|
102
|
-
const finish = (v) => {
|
|
103
|
-
if (done) return;
|
|
104
|
-
done = true;
|
|
105
|
-
try {
|
|
106
|
-
sock.destroy();
|
|
107
|
-
} catch {}
|
|
108
|
-
resolve(v);
|
|
109
|
-
};
|
|
110
|
-
sock.setTimeout(timeoutMs, () => finish(false));
|
|
111
|
-
sock.once("connect", () => finish(true));
|
|
112
|
-
sock.once("error", () => finish(false));
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 等端口真正空出来(SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着)。
|
|
118
|
-
* 返回 true = 空了;false = 等了 rounds 轮还是被占。
|
|
119
|
-
*/
|
|
120
|
-
export async function waitPortFree(port, { inUse = portInUse, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), rounds = 12, stepMs = 250 } = {}) {
|
|
121
|
-
for (let i = 0; i < rounds; i++) {
|
|
122
|
-
if (!(await inUse(port))) return true;
|
|
123
|
-
await sleep(stepMs);
|
|
124
|
-
}
|
|
125
|
-
return !(await inUse(port));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 端口占用者取证:谁在监听——本池残留?另一个 daemon 的活隧道?僵尸?别人的进程?还是空闲。
|
|
130
|
-
* 「孤儿占端口」「两个 daemon 互杀」这两类事故唯一能一眼看出结论的读数:只报「占用 / 拒绝再起」是查不出问题的。
|
|
131
|
-
* @returns {Promise<{busy:boolean, ours:number[], zombies:number[],
|
|
132
|
-
* foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, others:Array<{pid:number,name:string}>}>}
|
|
133
|
-
*/
|
|
134
|
-
export async function portOwner({ dir, port, platform = process.platform, pids = defaultPids, cmdline = defaultCmdline, stat = defaultStat, kill = process.kill, inUse = portInUse } = {}) {
|
|
135
|
-
const busy = await inUse(port);
|
|
136
|
-
const out = { busy, ours: [], zombies: [], foreign: [], others: [] };
|
|
137
|
-
if (!busy || platform !== "linux") return out;
|
|
138
|
-
const conf = nodePaths(dir, port).conf;
|
|
139
|
-
for (const pid of pids()) {
|
|
140
|
-
const argv = cmdline(pid);
|
|
141
|
-
if (!argv.length) continue;
|
|
142
|
-
if (isOurWireproxy(argv, conf)) {
|
|
143
|
-
const st = stat(pid);
|
|
144
|
-
if (st && isZombie(st.state)) {
|
|
145
|
-
out.zombies.push(pid);
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const other = otherDaemonParent(pid, { stat, cmdline, kill });
|
|
149
|
-
if (other) {
|
|
150
|
-
out.foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
out.ours.push(pid);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
// 非 root 看不到别人的 listen 表,退一步:命令行里带着这个端口号的进程也值得报出来
|
|
157
|
-
if (argv.some((a) => String(a).includes(`:${port}`))) out.others.push({ pid, name: String(argv[0]).split("/").pop() });
|
|
158
|
-
}
|
|
159
|
-
return out;
|
|
160
|
-
}
|
|
1
|
+
// warp-pool · 端口占用与「上一代进程残留」清理(2026-10-16 真机事故后新增;2026-10-17 七次修订)。
|
|
2
|
+
//
|
|
3
|
+
// 事故一(0.1.179):daemon 被强杀时 `process.once("exit")` 钩子不执行,wireproxy 子进程变孤儿继续占着端口。
|
|
4
|
+
// 事故二(七次修订,真机铁证):**两个 daemon 同时活着**(一个从昨天起活了一整天),两者都用同一份
|
|
5
|
+
// `node-<port>.conf` 判「本池残留」⇒ 互相把对方的**活隧道**当残留杀掉 ⇒ 无限互杀、池永远不稳。
|
|
6
|
+
// 两条教训写进判据:
|
|
7
|
+
// ① 父进程是**另一个活着的 daemon** ⇒ 那不是残留,是「别人的活隧道」,清了就是互杀 ⇒ 不许清,只报出来;
|
|
8
|
+
// ② 「SIGKILL 都清不掉」多半是**僵尸**(state=Z:kill 无效,但 kill(pid,0) 仍返回成功)⇒ 僵尸算已死。
|
|
9
|
+
import { connect } from "node:net";
|
|
10
|
+
import { defaultCmdline, defaultPids, defaultStat, isZombie, otherDaemonParent, parseCmdline, pidAlive } from "../runtime/proc-info.js";
|
|
11
|
+
import { nodePaths } from "./nodes.js";
|
|
12
|
+
import { fmtReclaim } from "./report.js";
|
|
13
|
+
|
|
14
|
+
// /proc 读取层已上移到 runtime/proc-info.js(daemon 侧也要用同一套判据);这里原样再导出,既有调用方路径不变。
|
|
15
|
+
export { parseCmdline, pidAlive };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 该进程是不是「正在跑我们这条 conf」的 wireproxy。
|
|
19
|
+
* 判据刻意收紧(argv 里得有 wireproxy 这个可执行文件名,且 conf 路径逐字相等),
|
|
20
|
+
* 免得误杀别人的进程。缺一不可。
|
|
21
|
+
*/
|
|
22
|
+
export function isOurWireproxy(argv, conf) {
|
|
23
|
+
const list = Array.isArray(argv) ? argv : [];
|
|
24
|
+
const isBin = list.some((a) => /(^|[\\/])wireproxy(\.exe)?$/i.test(a));
|
|
25
|
+
return isBin && list.includes(conf);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 清掉占着该端口那条 conf 的**真残留**:自家丢引用的子进程、父进程已死的孤儿。
|
|
30
|
+
* 不动的三类:僵尸(kill 无效,也不是真占用者)、**父进程是另一个活 daemon 的活隧道**(清了就是互杀)、
|
|
31
|
+
* argv 不匹配的别人的进程。
|
|
32
|
+
* 非 Linux 直接返回空(读不到 /proc,别拿别的平台做危险猜测)。
|
|
33
|
+
* @returns {Promise<{scanned:number, killed:number[], zombies:number[],
|
|
34
|
+
* foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, waitedMs:number, gone:boolean}>}
|
|
35
|
+
*/
|
|
36
|
+
export async function reclaimPort({
|
|
37
|
+
dir,
|
|
38
|
+
port,
|
|
39
|
+
log = () => {},
|
|
40
|
+
platform = process.platform,
|
|
41
|
+
pids = defaultPids,
|
|
42
|
+
cmdline = defaultCmdline,
|
|
43
|
+
stat = defaultStat,
|
|
44
|
+
kill = process.kill,
|
|
45
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
46
|
+
waitRounds = 15,
|
|
47
|
+
} = {}) {
|
|
48
|
+
const startedAt = Date.now();
|
|
49
|
+
const shape = (scanned, killed, zombies, foreign) => ({
|
|
50
|
+
scanned,
|
|
51
|
+
killed,
|
|
52
|
+
zombies,
|
|
53
|
+
foreign,
|
|
54
|
+
waitedMs: Date.now() - startedAt,
|
|
55
|
+
gone: !killed.some((p) => pidAlive(p, kill)),
|
|
56
|
+
});
|
|
57
|
+
if (platform !== "linux") return shape(0, [], [], []);
|
|
58
|
+
|
|
59
|
+
const conf = nodePaths(dir, port).conf;
|
|
60
|
+
const all = pids();
|
|
61
|
+
const zombies = [];
|
|
62
|
+
const foreign = [];
|
|
63
|
+
const targets = [];
|
|
64
|
+
for (const pid of all) {
|
|
65
|
+
if (!isOurWireproxy(cmdline(pid), conf)) continue;
|
|
66
|
+
const st = stat(pid);
|
|
67
|
+
if (st && isZombie(st.state)) {
|
|
68
|
+
zombies.push(pid);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const other = otherDaemonParent(pid, { stat, cmdline, kill });
|
|
72
|
+
if (other) {
|
|
73
|
+
foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
targets.push(pid);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (targets.length) {
|
|
80
|
+
for (const pid of targets) {
|
|
81
|
+
try {
|
|
82
|
+
kill(pid, "SIGTERM");
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
for (let i = 0; i < waitRounds && targets.some((p) => pidAlive(p, kill)); i++) await sleep(100);
|
|
86
|
+
for (const pid of targets.filter((p) => pidAlive(p, kill))) {
|
|
87
|
+
try {
|
|
88
|
+
kill(pid, "SIGKILL");
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const r = shape(all.length, targets, zombies, foreign);
|
|
93
|
+
log(fmtReclaim({ port, ...r }));
|
|
94
|
+
return r;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 该端口是否已有人在监听(TCP 连得上 = 占用;连不上/被拒 = 空闲)。 */
|
|
98
|
+
export function portInUse(port, { host = "127.0.0.1", timeoutMs = 800, connectFn = connect } = {}) {
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
let done = false;
|
|
101
|
+
const sock = connectFn({ host, port });
|
|
102
|
+
const finish = (v) => {
|
|
103
|
+
if (done) return;
|
|
104
|
+
done = true;
|
|
105
|
+
try {
|
|
106
|
+
sock.destroy();
|
|
107
|
+
} catch {}
|
|
108
|
+
resolve(v);
|
|
109
|
+
};
|
|
110
|
+
sock.setTimeout(timeoutMs, () => finish(false));
|
|
111
|
+
sock.once("connect", () => finish(true));
|
|
112
|
+
sock.once("error", () => finish(false));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 等端口真正空出来(SIGTERM 之后套接字要几百毫秒才释放,这期间连得上 ≠ 别人的进程占着)。
|
|
118
|
+
* 返回 true = 空了;false = 等了 rounds 轮还是被占。
|
|
119
|
+
*/
|
|
120
|
+
export async function waitPortFree(port, { inUse = portInUse, sleep = (ms) => new Promise((r) => setTimeout(r, ms)), rounds = 12, stepMs = 250 } = {}) {
|
|
121
|
+
for (let i = 0; i < rounds; i++) {
|
|
122
|
+
if (!(await inUse(port))) return true;
|
|
123
|
+
await sleep(stepMs);
|
|
124
|
+
}
|
|
125
|
+
return !(await inUse(port));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 端口占用者取证:谁在监听——本池残留?另一个 daemon 的活隧道?僵尸?别人的进程?还是空闲。
|
|
130
|
+
* 「孤儿占端口」「两个 daemon 互杀」这两类事故唯一能一眼看出结论的读数:只报「占用 / 拒绝再起」是查不出问题的。
|
|
131
|
+
* @returns {Promise<{busy:boolean, ours:number[], zombies:number[],
|
|
132
|
+
* foreign:Array<{pid:number,ppid:number,aliveSec:number|null}>, others:Array<{pid:number,name:string}>}>}
|
|
133
|
+
*/
|
|
134
|
+
export async function portOwner({ dir, port, platform = process.platform, pids = defaultPids, cmdline = defaultCmdline, stat = defaultStat, kill = process.kill, inUse = portInUse } = {}) {
|
|
135
|
+
const busy = await inUse(port);
|
|
136
|
+
const out = { busy, ours: [], zombies: [], foreign: [], others: [] };
|
|
137
|
+
if (!busy || platform !== "linux") return out;
|
|
138
|
+
const conf = nodePaths(dir, port).conf;
|
|
139
|
+
for (const pid of pids()) {
|
|
140
|
+
const argv = cmdline(pid);
|
|
141
|
+
if (!argv.length) continue;
|
|
142
|
+
if (isOurWireproxy(argv, conf)) {
|
|
143
|
+
const st = stat(pid);
|
|
144
|
+
if (st && isZombie(st.state)) {
|
|
145
|
+
out.zombies.push(pid);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const other = otherDaemonParent(pid, { stat, cmdline, kill });
|
|
149
|
+
if (other) {
|
|
150
|
+
out.foreign.push({ pid, ppid: other.pid, aliveSec: other.aliveSec });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
out.ours.push(pid);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
// 非 root 看不到别人的 listen 表,退一步:命令行里带着这个端口号的进程也值得报出来
|
|
157
|
+
if (argv.some((a) => String(a).includes(`:${port}`))) out.others.push({ pid, name: String(argv[0]).split("/").pop() });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|