alemonjs 2.1.0-alpha.42 → 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 +197 -91
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -1,114 +1,220 @@
|
|
|
1
1
|
import { fork } from 'child_process';
|
|
2
2
|
import { createRequire } from 'module';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
3
5
|
|
|
4
6
|
const require = createRequire(import.meta.url);
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
let currentChild;
|
|
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
|
+
}
|
|
25
|
+
function terminateChild(reason) {
|
|
26
|
+
if (currentChild && !currentChild.killed) {
|
|
27
|
+
try {
|
|
28
|
+
logger?.debug?.(`终止平台子进程 pid=${currentChild.pid}${reason ? ' reason=' + reason : ''}`);
|
|
29
|
+
try {
|
|
30
|
+
currentChild.send?.(JSON.stringify({ type: 'shutdown' }));
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
currentChild.removeAllListeners();
|
|
34
|
+
currentChild.kill('SIGTERM');
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
if (currentChild && !currentChild.killed) {
|
|
37
|
+
try {
|
|
38
|
+
currentChild.kill('SIGKILL');
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
}, 2000).unref?.();
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
logger?.warn?.('终止子进程失败', e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
currentChild = undefined;
|
|
49
|
+
}
|
|
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
|
+
});
|
|
59
|
+
process.on('disconnect', () => {
|
|
60
|
+
externalSignal = true;
|
|
61
|
+
shuttingDown = true;
|
|
62
|
+
terminateChild('parent disconnect');
|
|
63
|
+
setTimeout(() => process.exit(0), 10);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
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
|
+
}
|
|
87
|
+
}
|
|
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]));
|
|
95
|
+
try {
|
|
96
|
+
currentChild = fork(modulePath, {
|
|
97
|
+
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
|
98
|
+
execArgv,
|
|
99
|
+
env: { ...process.env, ALEMON_PARENT_PID: String(process.pid) }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger?.warn?.('fork 启动失败,降级 import', err);
|
|
104
|
+
void startByImport(modulePath);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
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(() => { });
|
|
115
|
+
}
|
|
116
|
+
}, 2000);
|
|
117
|
+
currentChild.on('message', msg => {
|
|
118
|
+
try {
|
|
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
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
logger?.error?.('子进程消息解析失败', e);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
currentChild.on('exit', (code, signal) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
if (shuttingDown || externalSignal || imported) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (process.env.ALEMON_DISABLE_RESTART === '1') {
|
|
137
|
+
logger?.warn?.(`子进程退出 code=${code} signal=${signal},已禁用自动重启 (ALEMON_DISABLE_RESTART=1)`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
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);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
12
156
|
function startAdapterWithFallback() {
|
|
157
|
+
if (started) {
|
|
158
|
+
return; // 避免重复调用
|
|
159
|
+
}
|
|
160
|
+
started = true;
|
|
13
161
|
let modulePath = '';
|
|
14
162
|
try {
|
|
15
163
|
modulePath = require.resolve(process.env.platform);
|
|
16
164
|
}
|
|
17
165
|
catch {
|
|
18
166
|
void import(process.env.platform).then(res => res?.default());
|
|
19
|
-
logger?.warn?.('
|
|
167
|
+
logger?.warn?.('平台连接包不支持 require,降级 import');
|
|
20
168
|
return;
|
|
21
169
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
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 指令
|
|
179
|
+
if (typeof process.send === 'function') {
|
|
180
|
+
process.on('message', msg => {
|
|
181
|
+
try {
|
|
182
|
+
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
183
|
+
if (data?.type === 'shutdown') {
|
|
184
|
+
shuttingDown = true;
|
|
185
|
+
terminateChild('parent-shutdown');
|
|
37
186
|
try {
|
|
38
|
-
|
|
187
|
+
process.send?.(JSON.stringify({ type: 'shutdown-ack' }));
|
|
39
188
|
}
|
|
40
189
|
catch { }
|
|
190
|
+
setTimeout(() => process.exit(0), 60);
|
|
41
191
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!imported) {
|
|
63
|
-
logger?.warn?.(`平台连接子进程已退出,code=${code}, signal=${signal},3秒后自动重启`);
|
|
64
|
-
restart();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
child.on('message', msg => {
|
|
68
|
-
try {
|
|
69
|
-
const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
|
|
70
|
-
if (data?.type === 'ready') {
|
|
71
|
-
ready = true;
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
logger?.debug?.('平台连接已就绪(子进程 fork 模式)');
|
|
74
|
-
// 发送启动
|
|
75
|
-
child?.send(JSON.stringify({ type: 'start' }));
|
|
192
|
+
}
|
|
193
|
+
catch { }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// stdin 指令触发优雅关闭(供 Go 或其他语言写入)
|
|
197
|
+
try {
|
|
198
|
+
if (!process.stdin.destroyed) {
|
|
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;
|
|
76
212
|
}
|
|
77
213
|
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
logger?.error?.('平台连接进程通信数据格式错误', err);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
logger?.warn?.('fork 启动平台连接失败,将尝试 import 加载', err);
|
|
85
|
-
void startByImport();
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
const startByImport = async () => {
|
|
89
|
-
if (imported) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
imported = true;
|
|
93
|
-
try {
|
|
94
|
-
let importPath = modulePath;
|
|
95
|
-
if (!importPath.startsWith('file://')) {
|
|
96
|
-
importPath = 'file://' + importPath;
|
|
97
|
-
}
|
|
98
|
-
const mod = await import(importPath);
|
|
99
|
-
if (typeof mod.default === 'function') {
|
|
100
|
-
await mod.default();
|
|
101
|
-
logger?.debug?.('通过 import 启动平台连接完成');
|
|
102
214
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
logger?.error?.('import 启动平台连接失败', err);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
startByFork();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
112
217
|
}
|
|
218
|
+
catch { }
|
|
113
219
|
|
|
114
220
|
export { startAdapterWithFallback };
|