mm_os 5.0.2 → 5.0.4
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/README.md +20 -10
- package/index.js +12 -13
- package/lib/config.js +104 -0
- package/lib/os.js +854 -571
- package/package.json +49 -32
package/lib/os.js
CHANGED
|
@@ -1,572 +1,855 @@
|
|
|
1
|
-
const path = require('path');
|
|
2
|
-
const {
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
*
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*
|
|
417
|
-
* @param {object}
|
|
418
|
-
* @
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
};
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { createRequire } = require('module');
|
|
3
|
+
const { Mod } = require('mm_mod');
|
|
4
|
+
const { containerAdmin } = require('mm_container');
|
|
5
|
+
const { getLogger } = require('mm_logger');
|
|
6
|
+
const { Loader } = require('mm_loader');
|
|
7
|
+
const { appAdmin } = require('mm_apps');
|
|
8
|
+
const os_config = require('./config');
|
|
9
|
+
const pkg = require('../package.json');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 实例序号:容器默认按实例隔离(见构造函数),序号保证同一进程内唯一
|
|
13
|
+
* @type {number}
|
|
14
|
+
*/
|
|
15
|
+
let os_seq = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 需要按 root 解析为绝对路径的配置项(相对路径一律相对 config.root)
|
|
19
|
+
* @type {Array<Array<string>>}
|
|
20
|
+
*/
|
|
21
|
+
const ROOT_PATHS = [
|
|
22
|
+
['jsondb', 'dir'],
|
|
23
|
+
['sqlite', 'dir'],
|
|
24
|
+
['tpl', 'dir'],
|
|
25
|
+
['log', 'dir']
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* OS类
|
|
30
|
+
* 系统只负责加载:装载默认配置、初始化各管理器与适配器、加载应用;
|
|
31
|
+
* 默认配置见 lib/config.js;mm_mqtt 等资源模块由应用管理器按资源清单(mqtt.json 等)
|
|
32
|
+
* 统一装配,宿主服务由模块自身从服务器实例解析;
|
|
33
|
+
* Web 请求到事件钩子的调用由适配器 mm_web_server 自身完成
|
|
34
|
+
*/
|
|
35
|
+
class OS extends Mod {
|
|
36
|
+
static config = os_config;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* OS类构造函数
|
|
40
|
+
* @param {object} config 配置对象
|
|
41
|
+
*/
|
|
42
|
+
constructor(config) {
|
|
43
|
+
super(config);
|
|
44
|
+
// 容器默认按实例隔离:历史实现按固定字符串 'os' 取单例,
|
|
45
|
+
// 同进程起第二个 OS(集成测试、双环境共存)时后建实例会覆盖先建实例
|
|
46
|
+
// 的 server 与全部注册项,故障现象是「A 实例在用 B 实例的库」。
|
|
47
|
+
// 默认隔离、显式共享:需要共用容器时传入 config.container_scope
|
|
48
|
+
const scope = this.config.container_scope || `${this.config.name || 'os'}#${++os_seq}`;
|
|
49
|
+
/**
|
|
50
|
+
* 实例容器(默认按实例隔离,config.container_scope 可显式共享)
|
|
51
|
+
* @type {object}
|
|
52
|
+
*/
|
|
53
|
+
this.container = containerAdmin(scope, { name: scope });
|
|
54
|
+
this.container.server = this;
|
|
55
|
+
/**
|
|
56
|
+
* 管理器作用域:app / static / event / 适配器管理器的注册表命名空间
|
|
57
|
+
*
|
|
58
|
+
* 默认 'os' 是历史约定的兼容值——应用层(事件钩子、插件)通过
|
|
59
|
+
* `appAdmin('os')` 取应用集合,改成实例唯一值会让这些调用全部取空。
|
|
60
|
+
* 多实例共存时显式配置 `scope`,该值会随之贯穿全链路:
|
|
61
|
+
* 内核用它建管理器、注入入站适配器配置,适配器再经 `ctx.app_scope`
|
|
62
|
+
* 下发给事件钩子,应用层据此取到本实例的管理器。
|
|
63
|
+
* @type {string}
|
|
64
|
+
*/
|
|
65
|
+
this.scope = this.config.scope || 'os';
|
|
66
|
+
/**
|
|
67
|
+
* 初始化阶段创建的全局管理器列表
|
|
68
|
+
* @type {Array<object>}
|
|
69
|
+
*/
|
|
70
|
+
this._admin_managers = [];
|
|
71
|
+
/**
|
|
72
|
+
* 已启动的全局管理器集合,避免重复启动
|
|
73
|
+
* @type {Set<object>}
|
|
74
|
+
*/
|
|
75
|
+
this._started_admins = new Set();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 生命周期初始化钩子
|
|
80
|
+
* @returns {void} 无返回值
|
|
81
|
+
*/
|
|
82
|
+
async onInit() {
|
|
83
|
+
// 路径锚定必须先于配置装载:之后所有模块读到的都是绝对路径
|
|
84
|
+
this._resolveRoot();
|
|
85
|
+
this.initConfig();
|
|
86
|
+
this.initLogger();
|
|
87
|
+
// 对已废弃配置输出告警(out_adapters / tools,见 README)
|
|
88
|
+
this._warnDeprecated();
|
|
89
|
+
// 初始化事件总线
|
|
90
|
+
this.initEventBus();
|
|
91
|
+
// 初始化模块通道:先于清单装配,清单模块可经通道 cwd 解析(模块可装在宿主目录)
|
|
92
|
+
await this.initModuleChannel();
|
|
93
|
+
// 存储需先于管理器初始化:mm_dao 等管理器会通过 mm_sql 的 getSql() 复用同名 SQL 实例,
|
|
94
|
+
// 若管理器先创建,则会以默认(空库名)配置占用实例,导致后续 SQL 报 No database selected
|
|
95
|
+
await this.initpStorage();
|
|
96
|
+
await this.initAdmins();
|
|
97
|
+
await this.initDaos();
|
|
98
|
+
await this.initAdapter();
|
|
99
|
+
await this.initLoader();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 对已废弃配置输出一次告警(对齐 README:out_adapters 与 tools 已废弃并忽略)
|
|
105
|
+
* @returns {void} 无返回值
|
|
106
|
+
*/
|
|
107
|
+
OS.prototype._warnDeprecated = function () {
|
|
108
|
+
const deprecated = [];
|
|
109
|
+
if (this.config.out_adapters !== undefined) {
|
|
110
|
+
deprecated.push('out_adapters');
|
|
111
|
+
}
|
|
112
|
+
if (this.config.tools !== undefined) {
|
|
113
|
+
deprecated.push('tools');
|
|
114
|
+
}
|
|
115
|
+
if (!deprecated.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.log('warn', `配置项 ${deprecated.join('、')} 已废弃:存储统一走 cache / sql,语言与模板由各模块自行初始化,本次启动将忽略`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 路径锚定:把所有相对路径配置解析为相对 config.root 的绝对路径
|
|
123
|
+
* 历史实现里 jsondb/sqlite/tpl/loader 的目录都按 process.cwd 解析,
|
|
124
|
+
* 于是 systemd 工作目录、PM2 工作目录、IDE 启动各不相同,插件写文件失败却只打一行日志继续跑。
|
|
125
|
+
* 现在统一以 config.root(默认 process.cwd,宿主可覆盖)为基准,启动目录不再影响行为。
|
|
126
|
+
* @returns {void} 无返回值
|
|
127
|
+
*/
|
|
128
|
+
OS.prototype._resolveRoot = function () {
|
|
129
|
+
const root = path.resolve(this.config.root || process.cwd());
|
|
130
|
+
this.config.root = root;
|
|
131
|
+
const resolveItem = (item) => {
|
|
132
|
+
if (item && typeof item.dir === 'string' && item.dir && !path.isAbsolute(item.dir)) {
|
|
133
|
+
item.dir = path.resolve(root, item.dir);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
for (const [group] of ROOT_PATHS) {
|
|
137
|
+
resolveItem(this.config[group]);
|
|
138
|
+
}
|
|
139
|
+
// 各数据域的 SQLite 目录(如 dao_org 按机构分库)
|
|
140
|
+
for (const key of Object.keys(this.config)) {
|
|
141
|
+
if (key.indexOf('dao_') !== 0) continue;
|
|
142
|
+
const conf = this.config[key];
|
|
143
|
+
if (conf && conf.sql) {
|
|
144
|
+
resolveItem(conf.sql.sqlite);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 初始化配置管理器
|
|
151
|
+
*/
|
|
152
|
+
OS.prototype.initConfig = function () {
|
|
153
|
+
const { admin } = require('mm_config');
|
|
154
|
+
admin.setServer(this);
|
|
155
|
+
admin.get('os', this.config);
|
|
156
|
+
admin.get('cache', { ...this.config.cache, redis: this.config.redis, mongodb: this.config.mongodb });
|
|
157
|
+
admin.get('sql', { ...this.config.sql, mysql: this.config.mysql, sqlite: this.config.sqlite, jsondb: this.config.jsondb });
|
|
158
|
+
admin.get('web', this.config.web);
|
|
159
|
+
admin.get('mqtt', this.config.mqtt);
|
|
160
|
+
admin.get('socket', this.config.socket);
|
|
161
|
+
admin.get('websocket', this.config.websocket);
|
|
162
|
+
admin.get('jsondb', this.config.jsondb);
|
|
163
|
+
admin.get('redis', this.config.redis);
|
|
164
|
+
admin.get('mongodb', this.config.mongodb);
|
|
165
|
+
admin.get('sqlite', this.config.sqlite);
|
|
166
|
+
admin.get('mysql', this.config.mysql);
|
|
167
|
+
admin.get('ret', this.config.ret);
|
|
168
|
+
admin.get('lang', this.config.lang);
|
|
169
|
+
admin.get('tpl', this.config.tpl);
|
|
170
|
+
admin.get('loader', this.config.loader);
|
|
171
|
+
this.reg('config', admin);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 初始化日志记录器
|
|
176
|
+
*/
|
|
177
|
+
OS.prototype.initLogger = function () {
|
|
178
|
+
this.reg('logger', getLogger(this.config.log || { level: 'info' }));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 记录日志
|
|
183
|
+
* @param {string} type 日志类型
|
|
184
|
+
* @param {string} msg 日志消息
|
|
185
|
+
* @param {...*} args 日志参数
|
|
186
|
+
*/
|
|
187
|
+
OS.prototype.log = function (type, msg, ...args) {
|
|
188
|
+
this.container.get('logger')[type](msg, ...args);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 注册管理
|
|
193
|
+
* @param {string} name 管理名称
|
|
194
|
+
* @param {object} admin 管理对象
|
|
195
|
+
*/
|
|
196
|
+
OS.prototype.reg = function (name, admin) {
|
|
197
|
+
this.container.set(name, admin);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 加载模块
|
|
202
|
+
* @param {string} mod_name 模块名称
|
|
203
|
+
* @returns {object} 模块对象
|
|
204
|
+
*/
|
|
205
|
+
OS.prototype.require = function (mod_name) {
|
|
206
|
+
return require(mod_name);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 解析清单模块:模块通道启用时优先按通道 cwd 解析(模块可安装在宿主目录
|
|
211
|
+
* 而非 mm_os 包内,见 D-17),失败时回退默认 require(兼容既有装配与测试桩)。
|
|
212
|
+
* 回退只针对「通道目录解析不到该模块」;模块已解析但加载失败(如自身依赖缺失)
|
|
213
|
+
* 必须原样上抛——否则回退到默认路径会得到误导性的「模块未安装」错误
|
|
214
|
+
* @param {string} mod_name 模块名称
|
|
215
|
+
* @returns {object} 模块对象
|
|
216
|
+
*/
|
|
217
|
+
OS.prototype._requireModule = function (mod_name) {
|
|
218
|
+
const channel = this._module_channel;
|
|
219
|
+
const cwd = channel && channel.config && channel.config.cwd;
|
|
220
|
+
if (cwd) {
|
|
221
|
+
const req = createRequire(path.join(cwd, 'package.json'));
|
|
222
|
+
try {
|
|
223
|
+
// 先 resolve 不执行:区分「找不到模块」与「模块加载失败」
|
|
224
|
+
req.resolve(mod_name);
|
|
225
|
+
return req(mod_name);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (!(error.code === 'MODULE_NOT_FOUND' && String(error.message).includes(`'${mod_name}'`))) {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
// 通道目录解析不到时回退默认路径
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return this.require(mod_name);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 初始化模块通道(可选,基于 mm_module)
|
|
238
|
+
* 通道只负责「包」的生命周期:本地清单、npm 安装/卸载/升级、require 缓存管理;
|
|
239
|
+
* 管理器实例的装配与启停仍由 OS 按清单驱动(moduleAssemble / moduleDisassemble)。
|
|
240
|
+
* 通道是增强能力:未安装 mm_module 或初始化失败时降级为纯静态装配,不阻断启动
|
|
241
|
+
* @returns {Promise<void>} 无返回值
|
|
242
|
+
*/
|
|
243
|
+
OS.prototype.initModuleChannel = async function () {
|
|
244
|
+
const conf = this.config.modules;
|
|
245
|
+
if (conf === false) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let mod;
|
|
249
|
+
try {
|
|
250
|
+
mod = require('mm_module');
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.log('info', '未安装 mm_module,动态模块通道停用(npm install mm_module 启用)');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const cfg = Object.assign(
|
|
256
|
+
{ cwd: this.config.root },
|
|
257
|
+
conf && typeof conf === 'object' ? conf : {}
|
|
258
|
+
);
|
|
259
|
+
try {
|
|
260
|
+
const channel = new mod.ModuleManager(cfg, this);
|
|
261
|
+
await channel.init();
|
|
262
|
+
await channel.start();
|
|
263
|
+
this._module_channel = channel;
|
|
264
|
+
this.reg('module', channel);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
this.log('error', `模块通道初始化失败,已降级为静态装配: ${error.message || error}`, error);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 收集引用某模块的清单角色(admins / in_adapters 条目)
|
|
272
|
+
* @param {string} module_name 模块名
|
|
273
|
+
* @returns {Array<object>} 角色列表,元素含 kind(管理/适配器)与清单条目字段
|
|
274
|
+
*/
|
|
275
|
+
OS.prototype._rolesOf = function (module_name) {
|
|
276
|
+
const roles = [];
|
|
277
|
+
for (const m of this.config.admins || []) {
|
|
278
|
+
if (m && m.module === module_name) {
|
|
279
|
+
roles.push(Object.assign({ kind: '管理' }, m));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const m of this.config.in_adapters || []) {
|
|
283
|
+
if (m && m.module === module_name) {
|
|
284
|
+
roles.push(Object.assign({ kind: '适配器' }, m));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return roles;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 装配某模块承担的全部角色:按清单段初始化管理器、注册容器并注入作用域/探针;
|
|
292
|
+
* OS 已处于运行态时(运行期热装)新管理器立即启动
|
|
293
|
+
* @param {string} module_name 模块名
|
|
294
|
+
* @returns {Promise<Array<object>>} 本次新创建的管理器列表
|
|
295
|
+
*/
|
|
296
|
+
OS.prototype.moduleAssemble = async function (module_name) {
|
|
297
|
+
const roles = this._rolesOf(module_name);
|
|
298
|
+
if (!roles.length) {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
const created = [];
|
|
302
|
+
const before = new Set(this._admin_managers);
|
|
303
|
+
await this._initModules(roles.filter((r) => r.kind === '管理'), '管理', true);
|
|
304
|
+
await this._initModules(roles.filter((r) => r.kind === '适配器'), '适配器', false);
|
|
305
|
+
this._injectAppScope();
|
|
306
|
+
for (const role of roles) {
|
|
307
|
+
if (role.kind !== '适配器') continue;
|
|
308
|
+
const manager = this.container.get(role.name);
|
|
309
|
+
if (manager) {
|
|
310
|
+
this._injectProbe(manager);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const manager of this._admin_managers) {
|
|
314
|
+
if (!before.has(manager)) {
|
|
315
|
+
created.push(manager);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (this._started) {
|
|
319
|
+
for (const manager of created) {
|
|
320
|
+
if (typeof manager.start === 'function') {
|
|
321
|
+
await manager.start();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return created;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 拆除某模块承担的全部角色:逆序停止管理器、移出启动列表并摘除容器注册项
|
|
330
|
+
* @param {string} module_name 模块名
|
|
331
|
+
* @returns {Promise<Array<string>>} 已拆除的注册名列表
|
|
332
|
+
*/
|
|
333
|
+
OS.prototype.moduleDisassemble = async function (module_name) {
|
|
334
|
+
const roles = this._rolesOf(module_name).reverse();
|
|
335
|
+
const removed = [];
|
|
336
|
+
for (const role of roles) {
|
|
337
|
+
const manager = this.container.get(role.name);
|
|
338
|
+
if (!manager) {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (typeof manager.stop === 'function') {
|
|
342
|
+
try {
|
|
343
|
+
await manager.stop();
|
|
344
|
+
} catch (error) {
|
|
345
|
+
this.log('error', `停止${role.kind} ${role.name} 失败: ${error.message || error}`, error);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
this._admin_managers = this._admin_managers.filter((m) => m !== manager);
|
|
349
|
+
this._started_admins.delete(manager);
|
|
350
|
+
this.container.del(role.name);
|
|
351
|
+
this.container.del(role.name + '_admin');
|
|
352
|
+
removed.push(role.name);
|
|
353
|
+
}
|
|
354
|
+
return removed;
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 动态安装模块:经模块通道 npm 安装 → 装配其清单角色;
|
|
359
|
+
* OS 运行中时新管理器直接启动。通道登记为幂等,保证后续卸载必清 require 缓存
|
|
360
|
+
* @param {string} name 模块名
|
|
361
|
+
* @param {object} [options] 安装选项(透传 ModuleManager.install:force/version/load)
|
|
362
|
+
* @returns {Promise<Array<object>>} 新装配的管理器列表
|
|
363
|
+
*/
|
|
364
|
+
OS.prototype.moduleInstall = async function (name, options = {}) {
|
|
365
|
+
const channel = this._module_channel;
|
|
366
|
+
if (!channel) {
|
|
367
|
+
throw new Error('模块通道未启用(安装 mm_module 并确认 config.modules 未关闭)');
|
|
368
|
+
}
|
|
369
|
+
await channel.install(name, Object.assign({ load: false }, options));
|
|
370
|
+
const created = await this.moduleAssemble(name);
|
|
371
|
+
if (!channel.has(name)) {
|
|
372
|
+
await channel.load([name]);
|
|
373
|
+
}
|
|
374
|
+
return created;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 动态卸载模块:先拆除其管理器(stop + 摘注册),再经通道卸载包并清理 require 缓存
|
|
379
|
+
* @param {string} name 模块名
|
|
380
|
+
* @returns {Promise<Array<string>>} 已拆除的注册名列表
|
|
381
|
+
*/
|
|
382
|
+
OS.prototype.moduleUnload = async function (name) {
|
|
383
|
+
const channel = this._module_channel;
|
|
384
|
+
if (!channel) {
|
|
385
|
+
throw new Error('模块通道未启用(安装 mm_module 并确认 config.modules 未关闭)');
|
|
386
|
+
}
|
|
387
|
+
const removed = await this.moduleDisassemble(name);
|
|
388
|
+
// init 阶段的静态装配不经过通道登记:补登记以挂上 disposer,
|
|
389
|
+
// 否则 unload 不清 require 缓存,随后的 reload 会命中旧代码
|
|
390
|
+
if (!channel.has(name)) {
|
|
391
|
+
try {
|
|
392
|
+
await channel.load([name]);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
this.log('warn', `模块 ${name} 通道登记失败,按普通卸载处理: ${error.message || error}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
await channel.unload(name);
|
|
398
|
+
return removed;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* 热更新模块:卸载(停管理器 + 清 require 缓存)→ 重新 require → 重新装配。
|
|
403
|
+
* 边界:清缓存只保证「下次 require 拿到新代码」;跨模块单例(configAdmin、
|
|
404
|
+
* 静态 Admin 注册表、getLogger)不在清理范围;持连接/端口的旧对象以 stop 释放
|
|
405
|
+
* @param {string} name 模块名
|
|
406
|
+
* @returns {Promise<Array<object>>} 重装配后的管理器列表
|
|
407
|
+
*/
|
|
408
|
+
OS.prototype.moduleReload = async function (name) {
|
|
409
|
+
await this.moduleUnload(name);
|
|
410
|
+
const channel = this._module_channel;
|
|
411
|
+
await channel.load([name]);
|
|
412
|
+
return this.moduleAssemble(name);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* 按清单初始化模块:加载模块 → 绑定服务器 → 取管理器 → 初始化 → 注册
|
|
417
|
+
* @param {Array<object>} list 模块清单,元素形如 { name, module, scope }
|
|
418
|
+
* @param {string} label 日志标签(适配器 / 管理)
|
|
419
|
+
* @param {boolean} collect 是否计入生命周期启动列表
|
|
420
|
+
* @returns {Promise<void>} 无返回值
|
|
421
|
+
*/
|
|
422
|
+
OS.prototype._initModules = async function (list, label, collect) {
|
|
423
|
+
for (let mod of list || []) {
|
|
424
|
+
// 解析模块:required(默认 true)缺失即 fail-fast 并给出安装指引;
|
|
425
|
+
// required: false 的可选角色缺失时降级跳过,不阻断启动
|
|
426
|
+
let mod_obj;
|
|
427
|
+
try {
|
|
428
|
+
mod_obj = this._requireModule(mod.module);
|
|
429
|
+
} catch (error) {
|
|
430
|
+
const missing = error.code === 'MODULE_NOT_FOUND' && String(error.message).includes(`'${mod.module}'`);
|
|
431
|
+
if (!missing) {
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
if (mod.required === false) {
|
|
435
|
+
this.log('info', `跳过可选${label} ${mod.name}(${mod.module} 未安装;需要时 npm install ${mod.module} 或 os.moduleInstall('${mod.module}'))`);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
throw new Error(
|
|
439
|
+
`装配失败:${label} ${mod.name} 的模块 ${mod.module} 未安装。` +
|
|
440
|
+
`安装:npm install ${mod.module},或运行期 await os.moduleInstall('${mod.module}');` +
|
|
441
|
+
`若该角色可选,清单条目加 required: false 跳过`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
let admin = mod_obj.admin;
|
|
446
|
+
this._setAdminServer(admin);
|
|
447
|
+
let manager = admin.get(mod.scope || this.scope, this.config[mod.name]);
|
|
448
|
+
if (typeof manager.init === 'function') {
|
|
449
|
+
await manager.init();
|
|
450
|
+
}
|
|
451
|
+
this.reg(mod.name, manager);
|
|
452
|
+
// 暴露管理器工厂:供 mm_nav、mm_dbs 等模块按作用域解析宿主管理器(如 api_admin)
|
|
453
|
+
this.reg(mod.name + '_admin', admin);
|
|
454
|
+
if (collect) {
|
|
455
|
+
this._admin_managers.push(manager);
|
|
456
|
+
}
|
|
457
|
+
} catch (error) {
|
|
458
|
+
// 第二参传 Error:日志器会完整输出 stack,否则只剩一行 message,50 依赖下无法定位
|
|
459
|
+
this.log('error', `初始化${label} ${mod.name} 失败: ${error.message || error}`, error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* 初始化入站适配器
|
|
466
|
+
* @returns {void} 无返回值
|
|
467
|
+
*/
|
|
468
|
+
OS.prototype.initAdapter = async function () {
|
|
469
|
+
await this._initModules(this.config.in_adapters, '适配器', false);
|
|
470
|
+
this._injectAppScope();
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 把实例的管理器作用域下发给入站适配器
|
|
475
|
+
* 适配器不认识 OS、也不需要认识:它只是把配置里的 app_scope 原样放进 ctx,
|
|
476
|
+
* 事件钩子据此取到本实例的 appAdmin,而不是撞上进程里另一个实例的注册表
|
|
477
|
+
* @returns {void} 无返回值
|
|
478
|
+
*/
|
|
479
|
+
OS.prototype._injectAppScope = function () {
|
|
480
|
+
for (const mod of this.config.in_adapters || []) {
|
|
481
|
+
const manager = this.container.get(mod.name);
|
|
482
|
+
if (manager && manager.config && manager.config.app_scope === undefined) {
|
|
483
|
+
manager.config.app_scope = this.scope;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 初始化管理
|
|
490
|
+
* @returns {void} 无返回值
|
|
491
|
+
*/
|
|
492
|
+
OS.prototype.initAdmins = async function () {
|
|
493
|
+
await this._initModules(this.config.admins, '管理', true);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* 初始化数据域
|
|
498
|
+
* DAO 层按域组织,域清单由宿主声明(config.daos),此处提前建好各域管理器,
|
|
499
|
+
* 业务侧直接按域取用;os 域已由 initAdmins 装配,不重复初始化。
|
|
500
|
+
* DAO 模块经 admins 清单解析(name === 'dao' 的条目),内核不硬编码模块名,
|
|
501
|
+
* 宿主替换或移除清单中的 dao 时,数据域初始化随之变化。
|
|
502
|
+
* @returns {Promise<void>} 无返回值
|
|
503
|
+
*/
|
|
504
|
+
OS.prototype.initDaos = async function () {
|
|
505
|
+
const entry = (this.config.admins || []).find((m) => m && m.name === 'dao');
|
|
506
|
+
if (!entry || !entry.module) {
|
|
507
|
+
this.log('info', 'admins 清单未包含 dao,跳过数据域初始化');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const mod = this.require(entry.module);
|
|
511
|
+
this._setAdminServer(mod.admin);
|
|
512
|
+
const ready = ['os'];
|
|
513
|
+
for (const name of this.config.daos || []) {
|
|
514
|
+
if (name === 'os') continue;
|
|
515
|
+
try {
|
|
516
|
+
const manager = mod.daoAdmin(name, this.config['dao_' + name]);
|
|
517
|
+
// 模板域(按机构分库)只登记配置,实例在 daoAdmin(域, { org_id }) 时派生
|
|
518
|
+
if (!manager) {
|
|
519
|
+
ready.push(name + '(按机构分库)');
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (typeof manager.init === 'function') {
|
|
523
|
+
await manager.init();
|
|
524
|
+
}
|
|
525
|
+
this.reg('dao:' + name, manager);
|
|
526
|
+
ready.push(name);
|
|
527
|
+
} catch (error) {
|
|
528
|
+
this.log('error', `初始化数据域 dao:${name} 失败: ${error.message || error}`, error);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
this.log('info', `数据域已就绪: ${ready.join('、')}`);
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 初始化存储
|
|
537
|
+
* @returns {void} 无返回值
|
|
538
|
+
*/
|
|
539
|
+
OS.prototype.initpStorage = async function () {
|
|
540
|
+
// 初始化缓存存储
|
|
541
|
+
let cacheAdmin = this.require('mm_cache').admin;
|
|
542
|
+
this._setAdminServer(cacheAdmin);
|
|
543
|
+
let cache = cacheAdmin.get('os', Object.assign({}, this.config.cache, { redis: this.config.redis, mongodb: this.config.mongodb, cachebase: this.config.cachebase }));
|
|
544
|
+
if (typeof cache.init === 'function') {
|
|
545
|
+
await cache.init();
|
|
546
|
+
}
|
|
547
|
+
this.reg('cache', cache);
|
|
548
|
+
|
|
549
|
+
// 初始化数据库
|
|
550
|
+
let sqlAdmin = this.require('mm_sql').admin;
|
|
551
|
+
this._setAdminServer(sqlAdmin);
|
|
552
|
+
let sql = sqlAdmin.get('os', Object.assign({}, this.config.sql, { mysql: this.config.mysql, sqlite: this.config.sqlite, jsondb: this.config.jsondb }));
|
|
553
|
+
if (typeof sql.init === 'function') {
|
|
554
|
+
await sql.init();
|
|
555
|
+
}
|
|
556
|
+
this.reg('sql', sql);
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 设置管理器服务器:已设置时跳过,避免同一 Admin 实例重复绑定
|
|
561
|
+
* @param {object} admin 管理器工厂实例
|
|
562
|
+
*/
|
|
563
|
+
OS.prototype._setAdminServer = function (admin) {
|
|
564
|
+
if (!admin.getServer()) {
|
|
565
|
+
admin.setServer(this);
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 初始化应用加载器(协调层:只创建加载器与应用管理器,装配逻辑在 mm_apps/mm_plugins)
|
|
571
|
+
* @returns {void} 无返回值
|
|
572
|
+
*/
|
|
573
|
+
OS.prototype.initLoader = async function () {
|
|
574
|
+
const conf = this.config.loader || {};
|
|
575
|
+
// loader.dir 必填:历史默认回退到包内 example/app,生产环境会静默加载示例应用
|
|
576
|
+
// (或者加载一个不存在的空目录),故障表现为「服务起来了但没有任何路由」。
|
|
577
|
+
// 显式传 false 表示本实例不加载应用(纯 API/网关形态)
|
|
578
|
+
const dir = conf.dir;
|
|
579
|
+
if (dir === false) {
|
|
580
|
+
this.log('info', 'loader.dir=false,跳过应用加载');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (!dir) {
|
|
584
|
+
throw new Error('未配置 loader.dir:请显式指定应用目录(示例见 example/demo.js);内核不再回退到包内 example/app');
|
|
585
|
+
}
|
|
586
|
+
// 配置加载器:注入 socket/mqtt 类型,其余类型由 mm_loader 默认提供
|
|
587
|
+
const loader = new Loader(conf);
|
|
588
|
+
loader.addType('socket', 'socket.json');
|
|
589
|
+
loader.addType('mqtt', 'mqtt.json');
|
|
590
|
+
this.reg('loader', loader);
|
|
591
|
+
|
|
592
|
+
const app = appAdmin(this.scope);
|
|
593
|
+
app.setServer(this);
|
|
594
|
+
app.setLogger(this.container.get('logger'));
|
|
595
|
+
this.reg('app', app);
|
|
596
|
+
|
|
597
|
+
// 路径已由 _resolveRoot 统一锚定 root,绝对路径原样使用
|
|
598
|
+
await app.load(path.resolve(this.config.root || process.cwd(), dir), loader);
|
|
599
|
+
await app.init();
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* 初始化事件总线
|
|
604
|
+
* @returns {void} 无返回值
|
|
605
|
+
*/
|
|
606
|
+
OS.prototype.initEventBus = function () {
|
|
607
|
+
let admin = this.require('mm_eventer').admin;
|
|
608
|
+
admin.setServer(this);
|
|
609
|
+
this.reg('eventer', admin.get('os'));
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* 启动适配器:按配置中的入站、出站适配器名称从容器取出管理器并启动
|
|
614
|
+
* 任一适配器启动失败(含初始化阶段已失败导致管理器缺失)即整体抛错,
|
|
615
|
+
* 避免「进程存活但端口未监听」的假成功状态(fail-fast,交由宿主决定退出/重启)
|
|
616
|
+
* @throws {Error} 任一适配器启动失败时抛出,error.causes 携带明细
|
|
617
|
+
* @returns {Promise<void>} 无返回值
|
|
618
|
+
*/
|
|
619
|
+
OS.prototype._startAdapters = async function () {
|
|
620
|
+
let mods = this.config.in_adapters;
|
|
621
|
+
const failed = [];
|
|
622
|
+
for (let mod of mods) {
|
|
623
|
+
let manager = this.container.get(mod.name);
|
|
624
|
+
if (!manager || typeof manager.start !== 'function') {
|
|
625
|
+
failed.push({ name: mod.name, error: new Error('管理器缺失或不可启动(初始化阶段可能已失败)') });
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
await manager.start();
|
|
630
|
+
this._printAddress(mod.name, manager);
|
|
631
|
+
this._injectProbe(manager);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
this.log('error', `启动适配器 ${mod.name} 失败: ${error.message || error}`, error);
|
|
634
|
+
failed.push({ name: mod.name, error });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (failed.length) {
|
|
638
|
+
const names = failed.map((f) => f.name).join('、');
|
|
639
|
+
const error = new Error(`适配器启动失败: ${names}(端口被占用或配置错误时请检查 config.web / mqtt / socket / websocket)`);
|
|
640
|
+
error.causes = failed;
|
|
641
|
+
throw error;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* 启动初始化阶段创建的全局管理器
|
|
647
|
+
* 仅启动一次,避免重复执行管理器生命周期
|
|
648
|
+
* @returns {void} 无返回值
|
|
649
|
+
*/
|
|
650
|
+
OS.prototype._startAdmins = async function () {
|
|
651
|
+
let managers = this._admin_managers || [];
|
|
652
|
+
for (let manager of managers) {
|
|
653
|
+
if (!manager || typeof manager.start !== 'function') continue;
|
|
654
|
+
if (this._started_admins.has(manager)) continue;
|
|
655
|
+
try {
|
|
656
|
+
await manager.start();
|
|
657
|
+
this._started_admins.add(manager);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
let name = manager.config && manager.config.name;
|
|
660
|
+
this.log('error', `启动管理 ${name} 失败: ${error.message || error}`, error);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 启动
|
|
667
|
+
* @throws {Error} 适配器或存储启动失败时抛出
|
|
668
|
+
* @returns {Promise<void>} 无返回值
|
|
669
|
+
*/
|
|
670
|
+
OS.prototype.start = async function () {
|
|
671
|
+
// 启动适配器(任一失败即抛错,避免假成功)
|
|
672
|
+
await this._startAdapters();
|
|
673
|
+
|
|
674
|
+
// 启动缓存存储与数据库:初始化失败时容器内无实例,此处必须判空并给出明确原因
|
|
675
|
+
const cache = this.container.get('cache');
|
|
676
|
+
const sql = this.container.get('sql');
|
|
677
|
+
if (!cache || !sql) {
|
|
678
|
+
throw new Error('缓存或数据库未初始化(initpStorage 阶段失败),无法启动');
|
|
679
|
+
}
|
|
680
|
+
await cache.start();
|
|
681
|
+
await sql.start();
|
|
682
|
+
|
|
683
|
+
// 启动应用
|
|
684
|
+
const app = this.container.get('app');
|
|
685
|
+
if (app) {
|
|
686
|
+
await app.start();
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// 启动初始化阶段创建的全局管理器(含 os 作用域静态资源)
|
|
690
|
+
await this._startAdmins();
|
|
691
|
+
|
|
692
|
+
// 运行态标志:OS.prototype.start 覆盖了基类状态机,不自动推进 state,
|
|
693
|
+
// 模块通道据此判断「运行期热装的管理器应立即启动」
|
|
694
|
+
this._started = true;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 是否处于运行态(start 之后、stop 之前)
|
|
699
|
+
* @returns {boolean} 运行态返回 true
|
|
700
|
+
*/
|
|
701
|
+
OS.prototype.isRunning = function () {
|
|
702
|
+
return !!this._started;
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* 停止(生命周期 onStop 钩子,由基类 stop() 调用,状态机会推进到 stopped)
|
|
707
|
+
* 与 start 逆序:应用 → 全局管理器 → 适配器 → 存储;
|
|
708
|
+
* 每一步失败仅记录并继续,保证尽量多的资源被释放(连接、WAL、日志缓冲)
|
|
709
|
+
* @returns {Promise<void>} 无返回值
|
|
710
|
+
*/
|
|
711
|
+
OS.prototype.onStop = async function () {
|
|
712
|
+
this._started = false;
|
|
713
|
+
const tryStop = async (label, obj) => {
|
|
714
|
+
if (!obj || typeof obj.stop !== 'function') {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
await obj.stop();
|
|
719
|
+
} catch (error) {
|
|
720
|
+
this.log('error', `停止${label}失败: ${error.message || error}`, error);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// 1. 应用先停:不再产生新业务
|
|
725
|
+
await tryStop('应用', this.container.get('app'));
|
|
726
|
+
|
|
727
|
+
// 2. 全局管理器(定时器、静态资源等)逆序停
|
|
728
|
+
const managers = [...(this._admin_managers || [])].reverse();
|
|
729
|
+
for (const manager of managers) {
|
|
730
|
+
await tryStop('管理', manager);
|
|
731
|
+
}
|
|
732
|
+
this._started_admins.clear();
|
|
733
|
+
|
|
734
|
+
// 3. 适配器逆序停:关闭监听与存量连接
|
|
735
|
+
const adapters = [...(this.config.in_adapters || [])].reverse();
|
|
736
|
+
for (const mod of adapters) {
|
|
737
|
+
await tryStop('适配器 ' + mod.name, this.container.get(mod.name));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// 4. 存储最后停:保证前面的收尾还能落库、刷缓冲
|
|
741
|
+
await tryStop('缓存存储', this.container.get('cache'));
|
|
742
|
+
await tryStop('数据库', this.container.get('sql'));
|
|
743
|
+
|
|
744
|
+
// 5. 模块通道最后停:逆序触发各模块 disposer(清理 require 缓存)
|
|
745
|
+
await tryStop('模块通道', this._module_channel);
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* 标记预热状态:预热期间就绪探针应返回 503,
|
|
750
|
+
* 让编排层在冷启动渲染完成前不放量(否则第一批用户承担全部冷启动开销)
|
|
751
|
+
* @param {boolean} warming 是否处于预热中
|
|
752
|
+
* @returns {void} 无返回值
|
|
753
|
+
*/
|
|
754
|
+
OS.prototype.setWarming = function (warming) {
|
|
755
|
+
this._warming = !!warming;
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* 就绪状态快照:所有入站适配器均已启动且不在预热中才算就绪
|
|
760
|
+
* 供宿主 /readyz 探针、容器编排与发布脚本使用,避免「进程存活但端口未监听」不可见
|
|
761
|
+
* @returns {Object} { ready: boolean, reason: string, adapters: [{ name, state }] }
|
|
762
|
+
*/
|
|
763
|
+
OS.prototype.getReadiness = function () {
|
|
764
|
+
const adapters = (this.config.in_adapters || []).map((mod) => {
|
|
765
|
+
const manager = this.container.get(mod.name);
|
|
766
|
+
return { name: mod.name, state: manager ? manager.state : 'missing' };
|
|
767
|
+
});
|
|
768
|
+
const missing = adapters.filter((a) => a.state === 'missing').map((a) => a.name);
|
|
769
|
+
const pending = adapters.filter((a) => a.state !== 'started' && a.state !== 'missing')
|
|
770
|
+
.map((a) => `${a.name}:${a.state}`);
|
|
771
|
+
const started = adapters.length > 0 && missing.length === 0 && pending.length === 0;
|
|
772
|
+
const ready = started && !this._warming;
|
|
773
|
+
let reason = '';
|
|
774
|
+
if (!adapters.length) {
|
|
775
|
+
reason = '未配置入站适配器';
|
|
776
|
+
} else if (missing.length) {
|
|
777
|
+
reason = `适配器未创建(初始化阶段失败): ${missing.join('、')}`;
|
|
778
|
+
} else if (pending.length) {
|
|
779
|
+
reason = `适配器未就绪: ${pending.join('、')}`;
|
|
780
|
+
} else if (this._warming) {
|
|
781
|
+
reason = '预热中(冷启动渲染未完成)';
|
|
782
|
+
}
|
|
783
|
+
return { ready, reason, adapters };
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* 健康快照:进程级存活信息,供 /healthz 探针与监控采集
|
|
788
|
+
* 与 getReadiness 的区别:健康只看「进程还能不能响应」,就绪看「能不能接流量」
|
|
789
|
+
* @returns {Object} { status, name, version, uptime, memory, check_at }
|
|
790
|
+
*/
|
|
791
|
+
OS.prototype.getHealth = function () {
|
|
792
|
+
const mem = process.memoryUsage();
|
|
793
|
+
return {
|
|
794
|
+
status: 'ok',
|
|
795
|
+
name: this.config.name,
|
|
796
|
+
version: pkg.version,
|
|
797
|
+
uptime: Math.round(process.uptime()),
|
|
798
|
+
memory: {
|
|
799
|
+
rss: Math.round(mem.rss / 1048576),
|
|
800
|
+
heap_used: Math.round(mem.heapUsed / 1048576)
|
|
801
|
+
},
|
|
802
|
+
check_at: new Date().toISOString()
|
|
803
|
+
};
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* 向适配器注入探针提供者:适配器据此暴露 /healthz、/readyz
|
|
808
|
+
* 采用能力探测(duck typing)而不是按模块名判断,新增适配器只要实现 setProbe 即可接入
|
|
809
|
+
* @param {object} manager 适配器管理器
|
|
810
|
+
* @returns {void} 无返回值
|
|
811
|
+
*/
|
|
812
|
+
OS.prototype._injectProbe = function (manager) {
|
|
813
|
+
if (!manager || typeof manager.setProbe !== 'function') {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
manager.setProbe(() => ({ health: this.getHealth(), readiness: this.getReadiness() }));
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* 打印适配器监听地址
|
|
821
|
+
* 协议自描述:取适配器配置中的 protocol(web→http / websocket→ws / socket→tcp / mqtt→mqtt),
|
|
822
|
+
* 内核不维护名称→协议的映射表,新增适配器无需改内核;
|
|
823
|
+
* 地址以 manager.getAddress() 的真实绑定结果优先(port:0 时拿到的是系统分配后的端口),
|
|
824
|
+
* 避免把「已启动」打成一条无法连接的伪装地址
|
|
825
|
+
* @param {string} name 适配器名称
|
|
826
|
+
* @param {object} manager 适配器管理器实例
|
|
827
|
+
*/
|
|
828
|
+
OS.prototype._printAddress = function (name, manager) {
|
|
829
|
+
const cfg = manager.config || {};
|
|
830
|
+
const protocol = cfg.protocol || name;
|
|
831
|
+
let host = cfg.host;
|
|
832
|
+
let port = cfg.port;
|
|
833
|
+
if (typeof manager.getAddress === 'function') {
|
|
834
|
+
const addr = manager.getAddress();
|
|
835
|
+
if (addr && addr.port) {
|
|
836
|
+
port = addr.port;
|
|
837
|
+
if (addr.address && addr.address !== '0.0.0.0' && addr.address !== '::') {
|
|
838
|
+
host = addr.address;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (!host || host === '0.0.0.0' || host === '::') {
|
|
843
|
+
host = 'localhost';
|
|
844
|
+
}
|
|
845
|
+
let address = `${protocol}://${host}`;
|
|
846
|
+
if (port) {
|
|
847
|
+
address += `:${port}`;
|
|
848
|
+
this.log('info', `适配器 ${name} 已启动: ${address}`);
|
|
849
|
+
} else {
|
|
850
|
+
// 端口未定(配置缺省或随机分配):明确说明而不是伪装成可连接地址
|
|
851
|
+
this.log('info', `适配器 ${name} 已启动: ${address}(未监听固定端口,无法给出可连接地址)`);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
572
855
|
module.exports = OS;
|