claude-cac 1.4.0-beta.4 → 1.4.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/README.md +2 -2
- package/cac +23 -1
- package/package.json +1 -1
- package/relay.js +135 -71
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ cac docker port 6287 # 端口转发
|
|
|
202
202
|
- **首次登录**:启动 `claude` 后,输入 `/login` 完成 OAuth 授权
|
|
203
203
|
- **安全验证**:随时运行 `cac env check` 确认隐私保护状态,也可以 `which claude` 确认使用的是 cac 托管的 claude
|
|
204
204
|
- **自动安全检查**:每次启动 Claude Code 会话时,cac 会快速检查环境。如有异常会终止会话,不会发送任何数据
|
|
205
|
-
-
|
|
205
|
+
- **网络稳定性**:流量严格走代理——代理断开时流量完全停止,不会回退直连。内置心跳检测和自动恢复,断线后无需手动重启
|
|
206
206
|
- **IPv6**:建议系统级关闭,防止真实地址泄露
|
|
207
207
|
|
|
208
208
|
---
|
|
@@ -377,7 +377,7 @@ Proxy formats: `ip:port:user:pass` (SOCKS5), `ss://...`, `vmess://...`, `vless:/
|
|
|
377
377
|
- **First login**: Run `claude`, then type `/login`. Health check is automatically bypassed.
|
|
378
378
|
- **Verify your setup**: Run `cac env check` anytime. Use `which claude` to confirm you're using the cac-managed wrapper.
|
|
379
379
|
- **Automatic safety checks**: Every new Claude Code session runs a quick cac check. If anything is wrong, the session is terminated before any data is sent.
|
|
380
|
-
- **Network resilience**: Traffic is strictly routed through your proxy. If the proxy drops, traffic stops entirely — no fallback to direct connection.
|
|
380
|
+
- **Network resilience**: Traffic is strictly routed through your proxy. If the proxy drops, traffic stops entirely — no fallback to direct connection. Built-in heartbeat detection and auto-recovery — no manual restart needed after disconnections.
|
|
381
381
|
- **IPv6**: Recommend disabling system-wide to prevent real address exposure.
|
|
382
382
|
|
|
383
383
|
---
|
package/cac
CHANGED
|
@@ -11,7 +11,7 @@ VERSIONS_DIR="$CAC_DIR/versions"
|
|
|
11
11
|
# ── utils: colors, read/write, UUID, proxy parsing ───────────────────────
|
|
12
12
|
|
|
13
13
|
# shellcheck disable=SC2034 # used in build-concatenated cac script
|
|
14
|
-
CAC_VERSION="1.4.0
|
|
14
|
+
CAC_VERSION="1.4.0"
|
|
15
15
|
|
|
16
16
|
_read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
|
|
17
17
|
_die() { printf '%b\n' "$(_red "error:") $*" >&2; exit 1; }
|
|
@@ -1321,6 +1321,8 @@ fi
|
|
|
1321
1321
|
|
|
1322
1322
|
# cleanup function
|
|
1323
1323
|
_cleanup_all() {
|
|
1324
|
+
# stop watchdog
|
|
1325
|
+
[[ -n "${_watchdog_pid:-}" ]] && kill "$_watchdog_pid" 2>/dev/null || true
|
|
1324
1326
|
# cleanup relay
|
|
1325
1327
|
if [[ "$_relay_active" == "true" ]] && [[ -f "$CAC_DIR/relay.pid" ]]; then
|
|
1326
1328
|
local _p; _p=$(cat "$CAC_DIR/relay.pid" 2>/dev/null) || true
|
|
@@ -1331,6 +1333,26 @@ _cleanup_all() {
|
|
|
1331
1333
|
|
|
1332
1334
|
trap _cleanup_all EXIT INT TERM
|
|
1333
1335
|
|
|
1336
|
+
# ── Relay watchdog: auto-restart relay if it crashes ──
|
|
1337
|
+
if [[ "$_relay_active" == "true" ]]; then
|
|
1338
|
+
(while true; do
|
|
1339
|
+
sleep 10
|
|
1340
|
+
[[ -f "$_relay_pid_file" ]] || break # pid file gone = intentional shutdown
|
|
1341
|
+
_wpid=$(tr -d '[:space:]' < "$_relay_pid_file" 2>/dev/null) || break
|
|
1342
|
+
if ! kill -0 "$_wpid" 2>/dev/null; then
|
|
1343
|
+
echo "[cac] relay crashed, restarting..." >&2
|
|
1344
|
+
node "$_relay_js" "$_rport" "$PROXY" "$_relay_pid_file" </dev/null >>"$CAC_DIR/relay.log" 2>&1 &
|
|
1345
|
+
disown
|
|
1346
|
+
for _wi in {1..30}; do
|
|
1347
|
+
(echo >/dev/tcp/127.0.0.1/$_rport) 2>/dev/null && break
|
|
1348
|
+
sleep 0.1
|
|
1349
|
+
done
|
|
1350
|
+
fi
|
|
1351
|
+
done) &
|
|
1352
|
+
_watchdog_pid=$!
|
|
1353
|
+
disown
|
|
1354
|
+
fi
|
|
1355
|
+
|
|
1334
1356
|
# ── Concurrent session check ──
|
|
1335
1357
|
_max_sessions=10
|
|
1336
1358
|
[[ -f "$CAC_DIR/max_sessions" ]] && _ms=$(tr -d '[:space:]' < "$CAC_DIR/max_sessions") && [[ -n "$_ms" ]] && _max_sessions="$_ms"
|
package/package.json
CHANGED
package/relay.js
CHANGED
|
@@ -5,42 +5,81 @@
|
|
|
5
5
|
// Listens on 127.0.0.1:<port> as an HTTP proxy, forwards upstream via:
|
|
6
6
|
// - HTTP CONNECT (for http:// upstream)
|
|
7
7
|
// - SOCKS5 (for socks5:// upstream)
|
|
8
|
+
//
|
|
9
|
+
// Safety: fail-closed design — if relay dies, HTTPS_PROXY points to dead port,
|
|
10
|
+
// connections refuse (no IP leak). Watchdog in wrapper auto-restarts relay.
|
|
8
11
|
'use strict';
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
var net = require('net');
|
|
14
|
+
var fs = require('fs');
|
|
12
15
|
|
|
13
16
|
// ── Parse CLI args ──────────────────────────────────────────────
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
var listenPort = parseInt(process.argv[2], 10);
|
|
19
|
+
var upstreamUrl = process.argv[3];
|
|
20
|
+
var pidFile = process.argv[4];
|
|
18
21
|
|
|
19
22
|
if (!listenPort || !upstreamUrl) {
|
|
20
23
|
process.stderr.write('Usage: node relay.js <port> <upstream_proxy_url> [pid_file]\n');
|
|
21
24
|
process.exit(1);
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
var upstream = new URL(upstreamUrl);
|
|
28
|
+
var upstreamHost = upstream.hostname;
|
|
29
|
+
var upstreamPort = parseInt(upstream.port, 10);
|
|
30
|
+
var upstreamUser = decodeURIComponent(upstream.username || '');
|
|
31
|
+
var upstreamPass = decodeURIComponent(upstream.password || '');
|
|
32
|
+
var isSocks5 = upstream.protocol === 'socks5:';
|
|
30
33
|
|
|
31
34
|
function log(msg) { process.stderr.write('[cac-relay] ' + msg + '\n'); }
|
|
32
35
|
|
|
36
|
+
// ── Global error handlers (never crash from unhandled errors) ───
|
|
37
|
+
|
|
38
|
+
process.on('uncaughtException', function(err) {
|
|
39
|
+
log('uncaught exception: ' + (err && err.message || err));
|
|
40
|
+
});
|
|
41
|
+
process.on('unhandledRejection', function(reason) {
|
|
42
|
+
log('unhandled rejection: ' + (reason && reason.message || reason));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── Upstream heartbeat ──────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
var _upstreamHealthy = true;
|
|
48
|
+
var HEARTBEAT_INTERVAL = 30000; // 30s
|
|
49
|
+
var HEARTBEAT_TIMEOUT = 5000; // 5s connect timeout
|
|
50
|
+
|
|
51
|
+
function heartbeat() {
|
|
52
|
+
var sock = net.connect({ port: upstreamPort, host: upstreamHost, timeout: HEARTBEAT_TIMEOUT });
|
|
53
|
+
sock.on('connect', function() {
|
|
54
|
+
if (!_upstreamHealthy) log('upstream recovered: ' + upstreamHost + ':' + upstreamPort);
|
|
55
|
+
_upstreamHealthy = true;
|
|
56
|
+
sock.destroy();
|
|
57
|
+
});
|
|
58
|
+
sock.on('error', function() {
|
|
59
|
+
if (_upstreamHealthy) log('upstream unreachable: ' + upstreamHost + ':' + upstreamPort);
|
|
60
|
+
_upstreamHealthy = false;
|
|
61
|
+
sock.destroy();
|
|
62
|
+
});
|
|
63
|
+
sock.on('timeout', function() {
|
|
64
|
+
if (_upstreamHealthy) log('upstream timeout: ' + upstreamHost + ':' + upstreamPort);
|
|
65
|
+
_upstreamHealthy = false;
|
|
66
|
+
sock.destroy();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var _heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL);
|
|
71
|
+
|
|
33
72
|
// ── SOCKS5 handshake ────────────────────────────────────────────
|
|
34
73
|
|
|
35
74
|
function socks5Connect(targetHost, targetPort, cb) {
|
|
36
|
-
|
|
37
|
-
|
|
75
|
+
var sock = net.connect(upstreamPort, upstreamHost, function() {
|
|
76
|
+
var hasAuth = upstreamUser && upstreamPass;
|
|
38
77
|
|
|
39
78
|
// Greeting: version=5, nmethods=1, method=(0x02 if auth, 0x00 if none)
|
|
40
79
|
sock.write(Buffer.from([0x05, 0x01, hasAuth ? 0x02 : 0x00]));
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
81
|
+
var state = 'greeting';
|
|
82
|
+
var buf = Buffer.alloc(0);
|
|
44
83
|
|
|
45
84
|
sock.on('data', onData);
|
|
46
85
|
|
|
@@ -48,14 +87,14 @@ function socks5Connect(targetHost, targetPort, cb) {
|
|
|
48
87
|
buf = Buffer.concat([buf, chunk]);
|
|
49
88
|
if (state === 'greeting') {
|
|
50
89
|
if (buf.length < 2) return;
|
|
51
|
-
|
|
90
|
+
var method = buf[1];
|
|
52
91
|
buf = buf.slice(2);
|
|
53
92
|
|
|
54
93
|
if (method === 0x02 && hasAuth) {
|
|
55
94
|
// Sub-negotiation: version=1, ulen, username, plen, password
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
var uBuf = Buffer.from(upstreamUser);
|
|
96
|
+
var pBuf = Buffer.from(upstreamPass);
|
|
97
|
+
var authReq = Buffer.alloc(3 + uBuf.length + pBuf.length);
|
|
59
98
|
authReq[0] = 0x01;
|
|
60
99
|
authReq[1] = uBuf.length;
|
|
61
100
|
uBuf.copy(authReq, 2);
|
|
@@ -86,16 +125,16 @@ function socks5Connect(targetHost, targetPort, cb) {
|
|
|
86
125
|
return;
|
|
87
126
|
}
|
|
88
127
|
// Parse variable-length address to consume the full reply
|
|
89
|
-
|
|
90
|
-
|
|
128
|
+
var atyp = buf[3];
|
|
129
|
+
var addrLen;
|
|
91
130
|
if (atyp === 0x01) addrLen = 4; // IPv4
|
|
92
131
|
else if (atyp === 0x04) addrLen = 16; // IPv6
|
|
93
132
|
else if (atyp === 0x03) addrLen = 1 + (buf[4] || 0); // Domain
|
|
94
133
|
else addrLen = 0;
|
|
95
|
-
|
|
134
|
+
var totalLen = 4 + addrLen + 2; // header + addr + port
|
|
96
135
|
if (buf.length < totalLen) return;
|
|
97
136
|
|
|
98
|
-
|
|
137
|
+
var remaining = buf.slice(totalLen);
|
|
99
138
|
sock.removeListener('data', onData);
|
|
100
139
|
cb(null, sock, remaining);
|
|
101
140
|
}
|
|
@@ -103,8 +142,8 @@ function socks5Connect(targetHost, targetPort, cb) {
|
|
|
103
142
|
|
|
104
143
|
function sendConnectRequest() {
|
|
105
144
|
// CONNECT request: ver=5, cmd=1(connect), rsv=0, atyp=3(domain)
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
var hostBuf = Buffer.from(targetHost);
|
|
146
|
+
var req = Buffer.alloc(5 + hostBuf.length + 2);
|
|
108
147
|
req[0] = 0x05; // version
|
|
109
148
|
req[1] = 0x01; // connect
|
|
110
149
|
req[2] = 0x00; // reserved
|
|
@@ -117,31 +156,31 @@ function socks5Connect(targetHost, targetPort, cb) {
|
|
|
117
156
|
}
|
|
118
157
|
});
|
|
119
158
|
|
|
120
|
-
sock.on('error', (err)
|
|
159
|
+
sock.on('error', function(err) { cb(err); });
|
|
121
160
|
}
|
|
122
161
|
|
|
123
162
|
// ── HTTP CONNECT upstream ───────────────────────────────────────
|
|
124
163
|
|
|
125
164
|
function httpConnect(targetHost, targetPort, cb) {
|
|
126
|
-
|
|
127
|
-
|
|
165
|
+
var sock = net.connect(upstreamPort, upstreamHost, function() {
|
|
166
|
+
var connectReq = 'CONNECT ' + targetHost + ':' + targetPort + ' HTTP/1.1\r\n' +
|
|
128
167
|
'Host: ' + targetHost + ':' + targetPort + '\r\n';
|
|
129
168
|
if (upstreamUser) {
|
|
130
|
-
|
|
169
|
+
var cred = Buffer.from(upstreamUser + ':' + upstreamPass).toString('base64');
|
|
131
170
|
connectReq += 'Proxy-Authorization: Basic ' + cred + '\r\n';
|
|
132
171
|
}
|
|
133
172
|
connectReq += '\r\n';
|
|
134
173
|
sock.write(connectReq);
|
|
135
174
|
|
|
136
|
-
|
|
175
|
+
var buf = Buffer.alloc(0);
|
|
137
176
|
sock.on('data', function onData(chunk) {
|
|
138
177
|
buf = Buffer.concat([buf, chunk]);
|
|
139
|
-
|
|
178
|
+
var idx = buf.indexOf('\r\n\r\n');
|
|
140
179
|
if (idx === -1) return;
|
|
141
180
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
181
|
+
var statusLine = buf.slice(0, buf.indexOf('\r\n')).toString();
|
|
182
|
+
var statusCode = parseInt(statusLine.split(' ')[1], 10);
|
|
183
|
+
var remaining = buf.slice(idx + 4);
|
|
145
184
|
|
|
146
185
|
sock.removeListener('data', onData);
|
|
147
186
|
|
|
@@ -154,7 +193,7 @@ function httpConnect(targetHost, targetPort, cb) {
|
|
|
154
193
|
});
|
|
155
194
|
});
|
|
156
195
|
|
|
157
|
-
sock.on('error', (err)
|
|
196
|
+
sock.on('error', function(err) { cb(err); });
|
|
158
197
|
}
|
|
159
198
|
|
|
160
199
|
// ── Connect to upstream (protocol dispatch) ─────────────────────
|
|
@@ -169,33 +208,36 @@ function connectUpstream(targetHost, targetPort, cb) {
|
|
|
169
208
|
|
|
170
209
|
// ── Local HTTP proxy server ─────────────────────────────────────
|
|
171
210
|
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
var MAX_CONNECTIONS = 128;
|
|
212
|
+
var IDLE_TIMEOUT = 1800000; // 30 min — streaming responses can be very long
|
|
213
|
+
var activeConnections = 0;
|
|
174
214
|
|
|
175
|
-
|
|
215
|
+
var server = net.createServer({ pauseOnConnect: true }, function(clientSock) {
|
|
176
216
|
if (activeConnections >= MAX_CONNECTIONS) {
|
|
177
217
|
clientSock.destroy();
|
|
178
218
|
return;
|
|
179
219
|
}
|
|
180
220
|
activeConnections++;
|
|
181
|
-
clientSock.on('close', ()
|
|
221
|
+
clientSock.on('close', function() { activeConnections--; });
|
|
182
222
|
|
|
183
|
-
|
|
223
|
+
// Idle timeout: only kill truly idle sockets, not active streaming ones
|
|
224
|
+
clientSock.setTimeout(IDLE_TIMEOUT, function() { clientSock.destroy(); });
|
|
225
|
+
clientSock.on('error', function() {}); // per-connection error: don't crash
|
|
184
226
|
clientSock.resume();
|
|
185
227
|
|
|
186
|
-
|
|
228
|
+
var headerBuf = '';
|
|
187
229
|
clientSock.on('data', function onHeader(chunk) {
|
|
188
230
|
headerBuf += chunk.toString();
|
|
189
|
-
|
|
231
|
+
var idx = headerBuf.indexOf('\r\n');
|
|
190
232
|
if (idx === -1) return;
|
|
191
233
|
|
|
192
234
|
clientSock.removeListener('data', onHeader);
|
|
193
235
|
|
|
194
|
-
|
|
195
|
-
|
|
236
|
+
var firstLine = headerBuf.substring(0, idx);
|
|
237
|
+
var rest = headerBuf.substring(idx + 2);
|
|
196
238
|
|
|
197
239
|
// CONNECT host:port HTTP/1.1
|
|
198
|
-
|
|
240
|
+
var match = firstLine.match(/^CONNECT\s+([^\s:]+):(\d+)\s+HTTP/i);
|
|
199
241
|
if (match) {
|
|
200
242
|
handleConnect(clientSock, match[1], parseInt(match[2], 10), rest);
|
|
201
243
|
} else {
|
|
@@ -207,37 +249,42 @@ const server = net.createServer({ pauseOnConnect: true }, (clientSock) => {
|
|
|
207
249
|
|
|
208
250
|
function handleConnect(clientSock, targetHost, targetPort, headerRest) {
|
|
209
251
|
// Consume remaining headers until \r\n\r\n
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const consumeHeaders = () => {
|
|
214
|
-
const endIdx = restBuf.indexOf('\r\n\r\n');
|
|
252
|
+
var restBuf = Buffer.from(headerRest);
|
|
253
|
+
var consumeHeaders = function() {
|
|
254
|
+
var endIdx = restBuf.indexOf('\r\n\r\n');
|
|
215
255
|
if (endIdx !== -1) {
|
|
216
|
-
|
|
256
|
+
var trailing = restBuf.slice(endIdx + 4);
|
|
217
257
|
doConnect(trailing);
|
|
218
258
|
return;
|
|
219
259
|
}
|
|
220
|
-
clientSock.once('data', (chunk)
|
|
260
|
+
clientSock.once('data', function(chunk) {
|
|
221
261
|
restBuf = Buffer.concat([restBuf, chunk]);
|
|
222
262
|
consumeHeaders();
|
|
223
263
|
});
|
|
224
264
|
};
|
|
225
265
|
|
|
226
266
|
function doConnect(trailingData) {
|
|
227
|
-
connectUpstream(targetHost, targetPort, (err, upstreamSock, upstreamExtra)
|
|
267
|
+
connectUpstream(targetHost, targetPort, function(err, upstreamSock, upstreamExtra) {
|
|
228
268
|
if (err) {
|
|
229
|
-
clientSock.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
269
|
+
try { clientSock.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); } catch(_) {}
|
|
230
270
|
clientSock.destroy();
|
|
231
271
|
return;
|
|
232
272
|
}
|
|
233
273
|
clientSock.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
234
274
|
|
|
275
|
+
// Reset idle timeout on data activity (keeps streaming alive)
|
|
276
|
+
upstreamSock.on('data', function() {
|
|
277
|
+
try { clientSock.setTimeout(IDLE_TIMEOUT); } catch(_) {}
|
|
278
|
+
});
|
|
279
|
+
clientSock.on('data', function() {
|
|
280
|
+
try { upstreamSock.setTimeout(IDLE_TIMEOUT); } catch(_) {}
|
|
281
|
+
});
|
|
282
|
+
|
|
235
283
|
// Pipe bidirectionally
|
|
236
284
|
clientSock.pipe(upstreamSock);
|
|
237
285
|
upstreamSock.pipe(clientSock);
|
|
238
286
|
|
|
239
287
|
// Send any extra data that came in after handshake
|
|
240
|
-
// upstreamExtra is data from the target server; write it to the client.
|
|
241
288
|
if (upstreamExtra && upstreamExtra.length > 0) {
|
|
242
289
|
clientSock.write(upstreamExtra);
|
|
243
290
|
}
|
|
@@ -245,8 +292,12 @@ function handleConnect(clientSock, targetHost, targetPort, headerRest) {
|
|
|
245
292
|
upstreamSock.write(trailingData);
|
|
246
293
|
}
|
|
247
294
|
|
|
248
|
-
|
|
249
|
-
|
|
295
|
+
// Per-connection errors: destroy peer, don't crash relay
|
|
296
|
+
clientSock.on('error', function() { upstreamSock.destroy(); });
|
|
297
|
+
upstreamSock.on('error', function() { clientSock.destroy(); });
|
|
298
|
+
|
|
299
|
+
// Upstream idle timeout
|
|
300
|
+
upstreamSock.setTimeout(IDLE_TIMEOUT, function() { upstreamSock.destroy(); });
|
|
250
301
|
});
|
|
251
302
|
}
|
|
252
303
|
|
|
@@ -255,24 +306,22 @@ function handleConnect(clientSock, targetHost, targetPort, headerRest) {
|
|
|
255
306
|
|
|
256
307
|
function handlePlainHttp(clientSock, firstLine, headerRest) {
|
|
257
308
|
// For plain HTTP requests, forward directly to upstream proxy
|
|
258
|
-
|
|
259
|
-
|
|
309
|
+
var sock = net.connect(upstreamPort, upstreamHost, function() {
|
|
310
|
+
var authHeader = '';
|
|
260
311
|
if (upstreamUser) {
|
|
261
|
-
|
|
312
|
+
var cred = Buffer.from(upstreamUser + ':' + upstreamPass).toString('base64');
|
|
262
313
|
authHeader = 'Proxy-Authorization: Basic ' + cred + '\r\n';
|
|
263
314
|
}
|
|
264
315
|
sock.write(firstLine + '\r\n' + authHeader + headerRest);
|
|
265
316
|
clientSock.pipe(sock);
|
|
266
317
|
sock.pipe(clientSock);
|
|
267
318
|
});
|
|
268
|
-
sock.on('error', ()
|
|
269
|
-
clientSock.on('error', ()
|
|
319
|
+
sock.on('error', function() { clientSock.destroy(); });
|
|
320
|
+
clientSock.on('error', function() { sock.destroy(); });
|
|
270
321
|
}
|
|
271
322
|
|
|
272
323
|
// ── Lifecycle ───────────────────────────────────────────────────
|
|
273
324
|
|
|
274
|
-
const fs = require('fs');
|
|
275
|
-
|
|
276
325
|
function writePid() {
|
|
277
326
|
if (pidFile) {
|
|
278
327
|
try { fs.writeFileSync(pidFile, String(process.pid)); } catch (_) {}
|
|
@@ -280,6 +329,7 @@ function writePid() {
|
|
|
280
329
|
}
|
|
281
330
|
|
|
282
331
|
function cleanup() {
|
|
332
|
+
clearInterval(_heartbeatTimer);
|
|
283
333
|
if (pidFile) {
|
|
284
334
|
try { fs.unlinkSync(pidFile); } catch (_) {}
|
|
285
335
|
}
|
|
@@ -290,13 +340,27 @@ function cleanup() {
|
|
|
290
340
|
process.on('SIGTERM', cleanup);
|
|
291
341
|
process.on('SIGINT', cleanup);
|
|
292
342
|
|
|
293
|
-
|
|
294
|
-
writePid();
|
|
295
|
-
log('listening on 127.0.0.1:' + listenPort + ' → ' + upstreamHost + ':' + upstreamPort +
|
|
296
|
-
(isSocks5 ? ' (socks5)' : ' (http)'));
|
|
297
|
-
});
|
|
343
|
+
// ── Server start with self-restart on transient errors ──────────
|
|
298
344
|
|
|
299
|
-
|
|
345
|
+
function startServer() {
|
|
346
|
+
server.listen(listenPort, '127.0.0.1', function() {
|
|
347
|
+
writePid();
|
|
348
|
+
log('listening on 127.0.0.1:' + listenPort + ' \u2192 ' + upstreamHost + ':' + upstreamPort +
|
|
349
|
+
(isSocks5 ? ' (socks5)' : ' (http)'));
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
server.on('error', function(err) {
|
|
300
354
|
log('server error: ' + err.message);
|
|
301
|
-
|
|
355
|
+
if (err.code === 'EADDRINUSE') {
|
|
356
|
+
// Port taken — fatal, let watchdog restart us on a new port
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
// Transient error — try to restart after 1s
|
|
360
|
+
setTimeout(function() {
|
|
361
|
+
try { server.close(); } catch(_) {}
|
|
362
|
+
startServer();
|
|
363
|
+
}, 1000);
|
|
302
364
|
});
|
|
365
|
+
|
|
366
|
+
startServer();
|