alemonjs 2.1.0-alpha.41 → 2.1.0-alpha.43

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 +107 -13
  2. package/package.json +1 -1
package/lib/adapter.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { fork } from 'child_process';
2
2
  import { createRequire } from 'module';
3
+ import { createInterface } from 'readline';
3
4
 
4
5
  const require = createRequire(import.meta.url);
5
6
  /**
@@ -9,6 +10,59 @@ const require = createRequire(import.meta.url);
9
10
  * @param env 环境变量对象
10
11
  * @param logger 日志对象(需实现 info/warn/error)
11
12
  */
13
+ let currentChild;
14
+ let cleanupRegistered = false;
15
+ let shuttingDown = false; // 标记是否正在整体退出,避免触发重启
16
+ function terminateChild(reason) {
17
+ if (currentChild && !currentChild.killed) {
18
+ try {
19
+ logger?.debug?.(`即将终止平台连接子进程 pid=${currentChild.pid}${reason ? ' reason=' + reason : ''}`);
20
+ // 先尝试发送一个 shutdown 指令(如果对端支持)
21
+ try {
22
+ currentChild.send?.(JSON.stringify({ type: 'shutdown' }));
23
+ }
24
+ catch { /* ignore */ }
25
+ currentChild.removeAllListeners();
26
+ currentChild.kill('SIGTERM');
27
+ // 兜底强杀
28
+ setTimeout(() => {
29
+ if (currentChild && !currentChild.killed) {
30
+ try {
31
+ currentChild.kill('SIGKILL');
32
+ }
33
+ catch { /* ignore */ }
34
+ }
35
+ }, 3000).unref?.();
36
+ }
37
+ catch (e) {
38
+ logger?.warn?.('终止子进程失败', e);
39
+ }
40
+ }
41
+ currentChild = undefined;
42
+ }
43
+ function registerProcessCleanup() {
44
+ if (cleanupRegistered) {
45
+ return;
46
+ }
47
+ cleanupRegistered = true;
48
+ const signals = ['SIGINT', 'SIGTERM', 'SIGQUIT'];
49
+ const handle = (sig) => {
50
+ shuttingDown = true;
51
+ terminateChild(`parent ${sig}`);
52
+ };
53
+ signals.forEach(s => process.once(s, handle));
54
+ process.once('beforeExit', () => handle('beforeExit'));
55
+ process.once('exit', () => handle('exit'));
56
+ // 如果父进程与其父断开(例如被主控杀掉),也尝试清理
57
+ process.on('disconnect', () => {
58
+ shuttingDown = true;
59
+ terminateChild('parent disconnect');
60
+ });
61
+ }
62
+ function stopAdapter() {
63
+ shuttingDown = true;
64
+ terminateChild('manual stop');
65
+ }
12
66
  function startAdapterWithFallback() {
13
67
  let modulePath = '';
14
68
  try {
@@ -26,51 +80,51 @@ function startAdapterWithFallback() {
26
80
  }
27
81
  let restarted = false;
28
82
  let ready = false;
29
- let child;
30
83
  const restart = () => {
31
84
  if (restarted || imported) {
32
85
  return;
33
86
  }
34
87
  restarted = true;
35
- if (child) {
36
- child.removeAllListeners();
37
- try {
38
- child.kill();
39
- }
40
- catch { }
41
- }
88
+ terminateChild('restart');
42
89
  setTimeout(() => {
43
90
  startByFork();
44
91
  }, 3000);
45
92
  };
46
93
  try {
47
- child = fork(modulePath);
94
+ currentChild = fork(modulePath);
95
+ registerProcessCleanup();
48
96
  // 超时
49
97
  const checkTimeout = async () => {
50
98
  if (!ready && !imported) {
51
99
  logger?.warn?.('平台连接未及时响应(未发送 ready 消息),降级为 import 加载, 请升级对应的平台连接包以提高进程稳定性');
52
100
  try {
53
- child?.kill();
101
+ currentChild?.kill();
54
102
  }
55
103
  catch { }
56
104
  await startByImport();
57
105
  }
58
106
  };
59
107
  const timer = setTimeout(() => void checkTimeout(), 2000);
60
- child.on('exit', (code, signal) => {
108
+ currentChild.on('exit', (code, signal) => {
61
109
  clearTimeout(timer);
110
+ if (shuttingDown) {
111
+ logger?.debug?.('父进程正在关闭,忽略子进程退出重启逻辑');
112
+ return;
113
+ }
62
114
  if (!imported) {
63
115
  logger?.warn?.(`平台连接子进程已退出,code=${code}, signal=${signal},3秒后自动重启`);
64
116
  restart();
65
117
  }
66
118
  });
67
- child.on('message', msg => {
119
+ currentChild.on('message', msg => {
68
120
  try {
69
121
  const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
70
122
  if (data?.type === 'ready') {
71
123
  ready = true;
72
124
  clearTimeout(timer);
73
125
  logger?.debug?.('平台连接已就绪(子进程 fork 模式)');
126
+ // 发送启动
127
+ currentChild?.send(JSON.stringify({ type: 'start' }));
74
128
  }
75
129
  }
76
130
  catch (err) {
@@ -108,5 +162,45 @@ function startAdapterWithFallback() {
108
162
  };
109
163
  startByFork();
110
164
  }
165
+ // 若当前进程本身也是被上层 fork/spawn 的子进程,可接收父进程的 shutdown 指令并级联关闭其内部子进程。
166
+ if (typeof process.send === 'function') {
167
+ process.on('message', msg => {
168
+ try {
169
+ const data = typeof msg === 'string' ? JSON.parse(msg) : msg;
170
+ if (data?.type === 'shutdown') {
171
+ shuttingDown = true;
172
+ stopAdapter();
173
+ // 给父进程一个确认
174
+ try {
175
+ process.send?.(JSON.stringify({ type: 'shutdown-ack' }));
176
+ }
177
+ catch {
178
+ // ignore
179
+ }
180
+ // 适当延迟确保子进程杀干净
181
+ setTimeout(() => process.exit(0), 100);
182
+ }
183
+ }
184
+ catch { /* ignore parse errors */ }
185
+ });
186
+ }
187
+ // 兼容由 Go / 其它语言通过 exec 启动:向 stdin 写入 "shutdown"(换行) 触发优雅退出
188
+ try {
189
+ if (!process.stdin.destroyed) {
190
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
191
+ rl.on('line', line => {
192
+ const cmd = line.trim().toLowerCase();
193
+ if (cmd === 'shutdown' || cmd === 'quit' || cmd === 'exit') {
194
+ shuttingDown = true;
195
+ stopAdapter();
196
+ rl.close();
197
+ setTimeout(() => process.exit(0), 50);
198
+ }
199
+ });
200
+ // 不阻止进程退出
201
+ rl.on('close', () => { });
202
+ }
203
+ }
204
+ catch { /* ignore */ }
111
205
 
112
- export { startAdapterWithFallback };
206
+ export { startAdapterWithFallback, stopAdapter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs",
3
- "version": "2.1.0-alpha.41",
3
+ "version": "2.1.0-alpha.43",
4
4
  "description": "bot script",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",