claw-subagent-service 0.0.4 → 0.0.5
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/service/daemon.js +55 -2
- package/service/worker.js +73 -0
package/package.json
CHANGED
package/service/daemon.js
CHANGED
|
@@ -1,20 +1,70 @@
|
|
|
1
|
-
const { fork } = require('child_process');
|
|
1
|
+
const { fork, execSync } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { createLogger } = require('./logger');
|
|
4
4
|
const { Updater } = require('./updater');
|
|
5
|
+
const { checkPortListening } = require('./modules/port-checker');
|
|
5
6
|
|
|
6
7
|
const log = createLogger('daemon');
|
|
7
8
|
const WORKER_PATH = path.join(__dirname, 'worker.js');
|
|
9
|
+
const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 38765;
|
|
8
10
|
|
|
9
11
|
let worker = null;
|
|
10
12
|
let stopping = false;
|
|
11
13
|
let isRollingBack = false;
|
|
12
14
|
let healthTimer = null;
|
|
13
15
|
let currentBackupDir = null;
|
|
16
|
+
let crashCount = 0;
|
|
17
|
+
let lastCrashTime = 0;
|
|
14
18
|
const updater = new Updater();
|
|
15
19
|
|
|
16
20
|
process.chdir(__dirname);
|
|
17
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 尝试释放占用的端口(杀死占用进程)
|
|
24
|
+
*/
|
|
25
|
+
function freePortIfNeeded(port) {
|
|
26
|
+
try {
|
|
27
|
+
if (process.platform === 'win32') {
|
|
28
|
+
const out = execSync(`netstat -ano | findstr ":${port}"`, {
|
|
29
|
+
encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
30
|
+
});
|
|
31
|
+
for (const line of out.split('\n')) {
|
|
32
|
+
if (line.includes('LISTENING')) {
|
|
33
|
+
const pid = parseInt(line.trim().split(/\s+/).pop(), 10);
|
|
34
|
+
if (pid && pid > 0) {
|
|
35
|
+
log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
|
|
36
|
+
execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
|
|
42
|
+
const pid = parseInt(out.trim(), 10);
|
|
43
|
+
if (pid && pid > 0) {
|
|
44
|
+
log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
|
|
45
|
+
process.kill(pid, 'SIGKILL');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch { /* 端口已被释放或无法查询 */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 获取重启延迟(指数退避,最大 60 秒)
|
|
53
|
+
*/
|
|
54
|
+
function getRestartDelay() {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
// 如果距离上次崩溃超过 30 秒,重置计数器
|
|
57
|
+
if (now - lastCrashTime > 30000) {
|
|
58
|
+
crashCount = 0;
|
|
59
|
+
}
|
|
60
|
+
lastCrashTime = now;
|
|
61
|
+
crashCount++;
|
|
62
|
+
// 指数退避: 1s, 2s, 4s, 8s, 16s, 32s, 60s(封顶)
|
|
63
|
+
const delay = Math.min(1000 * Math.pow(2, crashCount - 1), 60000);
|
|
64
|
+
log.info(`[DAEMON] Worker 连续崩溃 ${crashCount} 次,等待 ${delay/1000}s 后重启`);
|
|
65
|
+
return delay;
|
|
66
|
+
}
|
|
67
|
+
|
|
18
68
|
function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
19
69
|
if (stopping || isRollingBack) return;
|
|
20
70
|
|
|
@@ -68,7 +118,10 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
|
68
118
|
}
|
|
69
119
|
|
|
70
120
|
if (!stopping && !isRollingBack) {
|
|
71
|
-
|
|
121
|
+
// 释放端口后使用指数退避重启
|
|
122
|
+
freePortIfNeeded(PORT);
|
|
123
|
+
const delay = getRestartDelay();
|
|
124
|
+
setTimeout(() => startWorker(false, null), delay);
|
|
72
125
|
}
|
|
73
126
|
});
|
|
74
127
|
|
package/service/worker.js
CHANGED
|
@@ -2,6 +2,7 @@ const http = require('http');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
5
6
|
const { createLogger } = require('./logger');
|
|
6
7
|
const { RongCloudClient, MessageHandler, ensurePluginsAllow } = require('./rongcloud');
|
|
7
8
|
const { RongyunMessageHandler } = require('./modules/rongyun-message-handler');
|
|
@@ -14,6 +15,65 @@ const { startOpencodeService, stopOpencodeService } = require('./modules/opencod
|
|
|
14
15
|
const log = createLogger('worker');
|
|
15
16
|
const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 38765;
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 查找占用指定端口的进程 PID
|
|
20
|
+
*/
|
|
21
|
+
function findPidOnPort(port) {
|
|
22
|
+
try {
|
|
23
|
+
if (process.platform === 'win32') {
|
|
24
|
+
const out = execSync(`netstat -ano | findstr ":${port}"`, {
|
|
25
|
+
encoding: 'utf8', timeout: 5000, windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
for (const line of out.split('\n')) {
|
|
28
|
+
if (line.includes('LISTENING')) {
|
|
29
|
+
const pid = parseInt(line.trim().split(/\s+/).pop(), 10);
|
|
30
|
+
if (!isNaN(pid)) return pid;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
|
|
35
|
+
const pid = parseInt(out.trim(), 10);
|
|
36
|
+
if (!isNaN(pid)) return pid;
|
|
37
|
+
}
|
|
38
|
+
} catch { /* port is free */ }
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 强制终止进程
|
|
44
|
+
*/
|
|
45
|
+
function forceKill(pid) {
|
|
46
|
+
try {
|
|
47
|
+
if (process.platform === 'win32') {
|
|
48
|
+
execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
|
|
49
|
+
} else {
|
|
50
|
+
process.kill(pid, 'SIGKILL');
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 确保端口未被占用:如果端口被占用,尝试杀死占用进程
|
|
58
|
+
* 最多重试 3 轮,每轮间隔 1 秒
|
|
59
|
+
*/
|
|
60
|
+
function ensurePortFree(port) {
|
|
61
|
+
for (let i = 0; i < 3; i++) {
|
|
62
|
+
const pid = findPidOnPort(port);
|
|
63
|
+
if (!pid) return true;
|
|
64
|
+
log.warn(`[WORKER] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
|
|
65
|
+
forceKill(pid);
|
|
66
|
+
// 同步等待端口释放(最多 1.5s)
|
|
67
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1500);
|
|
68
|
+
}
|
|
69
|
+
const finalPid = findPidOnPort(port);
|
|
70
|
+
if (finalPid) {
|
|
71
|
+
log.error(`[WORKER] 端口 ${port} 被进程 ${finalPid} 占用,无法释放`);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
17
77
|
// Timestamp 校验专用日志
|
|
18
78
|
const timestampLogPath = path.join(__dirname, '..', 'logs', 'timestamp-validation.log');
|
|
19
79
|
const logTimestampValidation = (message) => {
|
|
@@ -281,10 +341,23 @@ const server = http.createServer((req, res) => {
|
|
|
281
341
|
res.end('not found');
|
|
282
342
|
});
|
|
283
343
|
|
|
344
|
+
// 启动前确保端口未被占用(防止 EADDRINUSE 导致崩溃循环)
|
|
345
|
+
if (!ensurePortFree(PORT)) {
|
|
346
|
+
log.error(`[WORKER] 端口 ${PORT} 无法使用,进程退出`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
|
|
284
350
|
server.listen(PORT, '127.0.0.1', () => {
|
|
285
351
|
log.info(`[WORKER] HTTP 服务已启动: http://127.0.0.1:${PORT}/health`);
|
|
286
352
|
});
|
|
287
353
|
|
|
354
|
+
server.on('error', (err) => {
|
|
355
|
+
if (err.code === 'EADDRINUSE') {
|
|
356
|
+
log.error(`[WORKER] 端口 ${PORT} 被占用且无法释放,进程退出`);
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
288
361
|
process.on('message', (msg) => {
|
|
289
362
|
if (msg?.type === 'prepare-shutdown') {
|
|
290
363
|
log.info(`[WORKER] 收到${msg.reason || 'unknown'}通知,准备优雅退出...`);
|