alemonjs 2.1.0-alpha.43 → 2.1.0-alpha.44
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/lib/adapter.js +155 -141
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -1,38 +1,45 @@
|
|
|
1
1
|
import { fork } from 'child_process';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
-
import
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
4
5
|
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* 自动兼容新老版本平台连接。
|
|
9
|
-
* @param modulePath 平台连接模块绝对路径(require.resolve 得到的)
|
|
10
|
-
* @param env 环境变量对象
|
|
11
|
-
* @param logger 日志对象(需实现 info/warn/error)
|
|
12
|
-
*/
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
13
9
|
let currentChild;
|
|
14
|
-
let
|
|
15
|
-
let shuttingDown = false;
|
|
10
|
+
let imported = false;
|
|
11
|
+
let shuttingDown = false;
|
|
12
|
+
let externalSignal = false;
|
|
13
|
+
let restartScheduled = false;
|
|
14
|
+
let started = false; // 防止重复 start
|
|
15
|
+
let restartAttempts = 0; // 记录连续重启次数
|
|
16
|
+
const RESTART_BASE_DELAY = 3000; // ms
|
|
17
|
+
const RESTART_MAX_DELAY = 20000; // ms
|
|
18
|
+
function calcBackoffDelay() {
|
|
19
|
+
if (restartAttempts <= 1) {
|
|
20
|
+
return RESTART_BASE_DELAY;
|
|
21
|
+
}
|
|
22
|
+
const delay = RESTART_BASE_DELAY * Math.pow(2, restartAttempts - 1);
|
|
23
|
+
return Math.min(delay, RESTART_MAX_DELAY);
|
|
24
|
+
}
|
|
16
25
|
function terminateChild(reason) {
|
|
17
26
|
if (currentChild && !currentChild.killed) {
|
|
18
27
|
try {
|
|
19
|
-
logger?.debug?.(
|
|
20
|
-
// 先尝试发送一个 shutdown 指令(如果对端支持)
|
|
28
|
+
logger?.debug?.(`终止平台子进程 pid=${currentChild.pid}${reason ? ' reason=' + reason : ''}`);
|
|
21
29
|
try {
|
|
22
30
|
currentChild.send?.(JSON.stringify({ type: 'shutdown' }));
|
|
23
31
|
}
|
|
24
|
-
catch {
|
|
32
|
+
catch { }
|
|
25
33
|
currentChild.removeAllListeners();
|
|
26
34
|
currentChild.kill('SIGTERM');
|
|
27
|
-
// 兜底强杀
|
|
28
35
|
setTimeout(() => {
|
|
29
36
|
if (currentChild && !currentChild.killed) {
|
|
30
37
|
try {
|
|
31
38
|
currentChild.kill('SIGKILL');
|
|
32
39
|
}
|
|
33
|
-
catch {
|
|
40
|
+
catch { }
|
|
34
41
|
}
|
|
35
|
-
},
|
|
42
|
+
}, 2000).unref?.();
|
|
36
43
|
}
|
|
37
44
|
catch (e) {
|
|
38
45
|
logger?.warn?.('终止子进程失败', e);
|
|
@@ -40,167 +47,174 @@ function terminateChild(reason) {
|
|
|
40
47
|
}
|
|
41
48
|
currentChild = undefined;
|
|
42
49
|
}
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
53
|
-
signals.forEach(s => process.once(s, handle));
|
|
54
|
-
process.once('beforeExit', () => handle('beforeExit'));
|
|
55
|
-
process.once('exit', () => handle('exit'));
|
|
56
|
-
// 如果父进程与其父断开(例如被主控杀掉),也尝试清理
|
|
50
|
+
function setupSignals() {
|
|
51
|
+
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(sig => {
|
|
52
|
+
process.once(sig, s => {
|
|
53
|
+
externalSignal = true;
|
|
54
|
+
shuttingDown = true;
|
|
55
|
+
terminateChild(`parent ${s}`);
|
|
56
|
+
setImmediate(() => process.exit(0));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
57
59
|
process.on('disconnect', () => {
|
|
60
|
+
externalSignal = true;
|
|
58
61
|
shuttingDown = true;
|
|
59
62
|
terminateChild('parent disconnect');
|
|
63
|
+
setTimeout(() => process.exit(0), 10);
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
async function startByImport(modulePath) {
|
|
67
|
+
if (imported) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
imported = true;
|
|
71
|
+
try {
|
|
72
|
+
if (!modulePath.startsWith('file://')) {
|
|
73
|
+
modulePath = 'file://' + modulePath;
|
|
74
|
+
}
|
|
75
|
+
const mod = await import(modulePath);
|
|
76
|
+
if (typeof mod.default === 'function') {
|
|
77
|
+
await mod.default();
|
|
78
|
+
logger?.debug?.('平台连接已就绪(import 模式)');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
logger?.warn?.('import 启动,但未找到默认导出函数');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
logger?.error?.('import 启动平台连接失败', err);
|
|
86
|
+
}
|
|
65
87
|
}
|
|
66
|
-
function
|
|
67
|
-
|
|
88
|
+
function startByFork(modulePath) {
|
|
89
|
+
if (imported) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let ready = false;
|
|
93
|
+
const watchdog = path.join(__dirname, 'child-watchdog.cjs');
|
|
94
|
+
const execArgv = Array.from(new Set([...(process.execArgv || []), '--require', watchdog]));
|
|
68
95
|
try {
|
|
69
|
-
|
|
96
|
+
currentChild = fork(modulePath, {
|
|
97
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
98
|
+
execArgv,
|
|
99
|
+
env: { ...process.env, ALEMON_PARENT_PID: String(process.pid) }
|
|
100
|
+
});
|
|
70
101
|
}
|
|
71
|
-
catch {
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger?.warn?.('fork 启动失败,降级 import', err);
|
|
104
|
+
void startByImport(modulePath);
|
|
74
105
|
return;
|
|
75
106
|
}
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
if (imported) {
|
|
79
|
-
|
|
107
|
+
// 超时检测
|
|
108
|
+
const timeout = setTimeout(() => {
|
|
109
|
+
if (!ready && !imported && !shuttingDown) {
|
|
110
|
+
(async () => {
|
|
111
|
+
logger?.warn?.('子进程未及时 ready,降级 import');
|
|
112
|
+
terminateChild('timeout');
|
|
113
|
+
await startByImport(modulePath);
|
|
114
|
+
})().catch(() => { });
|
|
80
115
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const restart = () => {
|
|
84
|
-
if (restarted || imported) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
restarted = true;
|
|
88
|
-
terminateChild('restart');
|
|
89
|
-
setTimeout(() => {
|
|
90
|
-
startByFork();
|
|
91
|
-
}, 3000);
|
|
92
|
-
};
|
|
116
|
+
}, 2000);
|
|
117
|
+
currentChild.on('message', msg => {
|
|
93
118
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
currentChild?.kill();
|
|
102
|
-
}
|
|
103
|
-
catch { }
|
|
104
|
-
await startByImport();
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
const timer = setTimeout(() => void checkTimeout(), 2000);
|
|
108
|
-
currentChild.on('exit', (code, signal) => {
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
if (shuttingDown) {
|
|
111
|
-
logger?.debug?.('父进程正在关闭,忽略子进程退出重启逻辑');
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!imported) {
|
|
115
|
-
logger?.warn?.(`平台连接子进程已退出,code=${code}, signal=${signal},3秒后自动重启`);
|
|
116
|
-
restart();
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
currentChild.on('message', msg => {
|
|
120
|
-
try {
|
|
121
|
-
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
122
|
-
if (data?.type === 'ready') {
|
|
123
|
-
ready = true;
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
logger?.debug?.('平台连接已就绪(子进程 fork 模式)');
|
|
126
|
-
// 发送启动
|
|
127
|
-
currentChild?.send(JSON.stringify({ type: 'start' }));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
logger?.error?.('平台连接进程通信数据格式错误', err);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
119
|
+
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
120
|
+
if (data?.type === 'ready') {
|
|
121
|
+
ready = true;
|
|
122
|
+
clearTimeout(timeout);
|
|
123
|
+
logger?.debug?.('平台连接已就绪(fork 模式)');
|
|
124
|
+
currentChild?.send(JSON.stringify({ type: 'start' }));
|
|
125
|
+
}
|
|
134
126
|
}
|
|
135
|
-
catch (
|
|
136
|
-
logger?.
|
|
137
|
-
void startByImport();
|
|
127
|
+
catch (e) {
|
|
128
|
+
logger?.error?.('子进程消息解析失败', e);
|
|
138
129
|
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
|
|
130
|
+
});
|
|
131
|
+
currentChild.on('exit', (code, signal) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
if (shuttingDown || externalSignal || imported) {
|
|
142
134
|
return;
|
|
143
135
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!importPath.startsWith('file://')) {
|
|
148
|
-
importPath = 'file://' + importPath;
|
|
149
|
-
}
|
|
150
|
-
const mod = await import(importPath);
|
|
151
|
-
if (typeof mod.default === 'function') {
|
|
152
|
-
await mod.default();
|
|
153
|
-
logger?.debug?.('通过 import 启动平台连接完成');
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
logger?.warn?.('通过 import 启动平台连接,但未找到默认导出函数');
|
|
157
|
-
}
|
|
136
|
+
if (process.env.ALEMON_DISABLE_RESTART === '1') {
|
|
137
|
+
logger?.warn?.(`子进程退出 code=${code} signal=${signal},已禁用自动重启 (ALEMON_DISABLE_RESTART=1)`);
|
|
138
|
+
return;
|
|
158
139
|
}
|
|
159
|
-
|
|
160
|
-
|
|
140
|
+
if (!restartScheduled) {
|
|
141
|
+
restartScheduled = true;
|
|
142
|
+
restartAttempts++;
|
|
143
|
+
const delay = calcBackoffDelay();
|
|
144
|
+
logger?.warn?.(`子进程退出 code=${code} signal=${signal},${delay / 1000}s 后重启 (attempt=${restartAttempts})`);
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
if (shuttingDown || externalSignal) {
|
|
147
|
+
restartScheduled = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
restartScheduled = false;
|
|
151
|
+
startByFork(modulePath);
|
|
152
|
+
}, delay);
|
|
161
153
|
}
|
|
162
|
-
};
|
|
163
|
-
startByFork();
|
|
154
|
+
});
|
|
164
155
|
}
|
|
165
|
-
|
|
156
|
+
function startAdapterWithFallback() {
|
|
157
|
+
if (started) {
|
|
158
|
+
return; // 避免重复调用
|
|
159
|
+
}
|
|
160
|
+
started = true;
|
|
161
|
+
let modulePath = '';
|
|
162
|
+
try {
|
|
163
|
+
modulePath = require.resolve(process.env.platform);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
void import(process.env.platform).then(res => res?.default());
|
|
167
|
+
logger?.warn?.('平台连接包不支持 require,降级 import');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
setupSignals();
|
|
171
|
+
if (process.env.ALEMON_NO_FORK === '1') {
|
|
172
|
+
logger?.info?.('ALEMON_NO_FORK=1 -> 直接 import 启动');
|
|
173
|
+
void startByImport(modulePath);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
startByFork(modulePath);
|
|
177
|
+
}
|
|
178
|
+
// 父进程(如果本身也是子进程)收到上层 shutdown 指令
|
|
166
179
|
if (typeof process.send === 'function') {
|
|
167
180
|
process.on('message', msg => {
|
|
168
181
|
try {
|
|
169
182
|
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
170
183
|
if (data?.type === 'shutdown') {
|
|
171
184
|
shuttingDown = true;
|
|
172
|
-
|
|
173
|
-
// 给父进程一个确认
|
|
185
|
+
terminateChild('parent-shutdown');
|
|
174
186
|
try {
|
|
175
187
|
process.send?.(JSON.stringify({ type: 'shutdown-ack' }));
|
|
176
188
|
}
|
|
177
|
-
catch {
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
// 适当延迟确保子进程杀干净
|
|
181
|
-
setTimeout(() => process.exit(0), 100);
|
|
189
|
+
catch { }
|
|
190
|
+
setTimeout(() => process.exit(0), 60);
|
|
182
191
|
}
|
|
183
192
|
}
|
|
184
|
-
catch {
|
|
193
|
+
catch { }
|
|
185
194
|
});
|
|
186
195
|
}
|
|
187
|
-
//
|
|
196
|
+
// stdin 指令触发优雅关闭(供 Go 或其他语言写入)
|
|
188
197
|
try {
|
|
189
198
|
if (!process.stdin.destroyed) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
199
|
+
let buffer = '';
|
|
200
|
+
process.stdin.on('data', chunk => {
|
|
201
|
+
buffer += chunk.toString();
|
|
202
|
+
if (buffer.includes('\n')) {
|
|
203
|
+
const lines = buffer.split(/\r?\n/);
|
|
204
|
+
buffer = lines.pop() || '';
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const text = line.trim().toLowerCase();
|
|
207
|
+
if (['shutdown', 'quit', 'exit'].includes(text)) {
|
|
208
|
+
shuttingDown = true;
|
|
209
|
+
terminateChild('stdin-cmd');
|
|
210
|
+
setTimeout(() => process.exit(0), 30);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
198
214
|
}
|
|
199
215
|
});
|
|
200
|
-
// 不阻止进程退出
|
|
201
|
-
rl.on('close', () => { });
|
|
202
216
|
}
|
|
203
217
|
}
|
|
204
|
-
catch {
|
|
218
|
+
catch { }
|
|
205
219
|
|
|
206
|
-
export { startAdapterWithFallback
|
|
220
|
+
export { startAdapterWithFallback };
|