alemonjs 2.1.0-alpha.44 → 2.1.0-alpha.45

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.
Files changed (2) hide show
  1. package/lib/adapter.js +99 -197
  2. package/package.json +1 -1
package/lib/adapter.js CHANGED
@@ -1,220 +1,122 @@
1
1
  import { fork } from 'child_process';
2
2
  import { createRequire } from 'module';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
3
 
6
4
  const require = createRequire(import.meta.url);
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
- }
5
+ /**
6
+ * 启动平台平台连接,优先使用子进程(fork),如不支持或平台连接未响应,则自动降级为 import 动态加载。
7
+ * 自动兼容新老版本平台连接。
8
+ * @param modulePath 平台连接模块绝对路径(require.resolve 得到的)
9
+ * @param env 环境变量对象
10
+ * @param logger 日志对象(需实现 info/warn/error)
11
+ */
156
12
  function startAdapterWithFallback() {
157
- if (started) {
158
- return; // 避免重复调用
159
- }
160
- started = true;
161
13
  let modulePath = '';
162
14
  try {
163
15
  modulePath = require.resolve(process.env.platform);
164
16
  }
165
17
  catch {
166
18
  void import(process.env.platform).then(res => res?.default());
167
- logger?.warn?.('平台连接包不支持 require,降级 import');
19
+ logger?.warn?.('平台连接包未支持 require,降级为 import 加载, 请升级对应的平台连接包以提高进程稳定性');
168
20
  return;
169
21
  }
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');
22
+ let imported = false; // 标记 import 是否已成功,避免重复
23
+ const startByFork = () => {
24
+ if (imported) {
25
+ return; // 如果已经 import 成功,不再 fork
26
+ }
27
+ let restarted = false;
28
+ let ready = false;
29
+ let child;
30
+ const restart = () => {
31
+ if (restarted || imported) {
32
+ return;
33
+ }
34
+ restarted = true;
35
+ if (child) {
36
+ child.removeAllListeners();
186
37
  try {
187
- process.send?.(JSON.stringify({ type: 'shutdown-ack' }));
38
+ child.kill();
188
39
  }
189
40
  catch { }
190
- setTimeout(() => process.exit(0), 60);
191
41
  }
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;
42
+ setTimeout(() => {
43
+ startByFork();
44
+ }, 3000);
45
+ };
46
+ try {
47
+ child = fork(modulePath);
48
+ // 超时
49
+ const checkTimeout = async () => {
50
+ if (!ready && !imported) {
51
+ logger?.warn?.('平台连接未及时响应(未发送 ready 消息),降级为 import 加载, 请升级对应的平台连接包以提高进程稳定性');
52
+ try {
53
+ child?.kill();
54
+ }
55
+ catch { }
56
+ await startByImport();
57
+ }
58
+ };
59
+ const timer = setTimeout(() => void checkTimeout(), 2000);
60
+ child.on('exit', (code, signal) => {
61
+ clearTimeout(timer);
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
+ child?.send?.({ type: 'start' });
212
75
  }
213
76
  }
77
+ catch (err) {
78
+ logger?.error?.('平台连接进程通信数据格式错误', err);
79
+ }
80
+ });
81
+ }
82
+ catch (err) {
83
+ logger?.warn?.('fork 启动平台连接失败,将尝试 import 加载', err);
84
+ void startByImport();
85
+ }
86
+ };
87
+ const startByImport = async () => {
88
+ if (imported) {
89
+ return;
90
+ }
91
+ imported = true;
92
+ try {
93
+ let importPath = modulePath;
94
+ if (!importPath.startsWith('file://')) {
95
+ importPath = 'file://' + importPath;
214
96
  }
215
- });
216
- }
97
+ const mod = await import(importPath);
98
+ if (typeof mod.default === 'function') {
99
+ await mod.default();
100
+ logger?.debug?.('通过 import 启动平台连接完成');
101
+ }
102
+ else {
103
+ logger?.warn?.('通过 import 启动平台连接,但未找到默认导出函数');
104
+ }
105
+ }
106
+ catch (err) {
107
+ logger?.error?.('import 启动平台连接失败', err);
108
+ }
109
+ };
110
+ startByFork();
217
111
  }
218
- catch { }
112
+ ['SIGINT', 'SIGTERM', 'SIGQUIT', 'disconnect'].forEach(sig => {
113
+ process?.on?.(sig, () => {
114
+ logger?.info?.(`[${sig}] 收到信号,正在关闭...`);
115
+ setImmediate(() => process.exit(0));
116
+ });
117
+ });
118
+ process?.on?.('exit', (code) => {
119
+ logger?.info?.(`[exit] 进程退出,code=${code}`);
120
+ });
219
121
 
220
122
  export { startAdapterWithFallback };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs",
3
- "version": "2.1.0-alpha.44",
3
+ "version": "2.1.0-alpha.45",
4
4
  "description": "bot script",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",