@maiyunnet/kebab 9.17.0 → 9.17.2
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/doc/kebab-rag.md +174 -98
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +10 -0
- package/lib/core.js +51 -0
- package/lib/db/conn.d.ts +11 -3
- package/lib/db/conn.js +20 -7
- package/lib/db/pool.js +10 -3
- package/lib/undici/response.d.ts +1 -1
- package/lib/undici.d.ts +2 -0
- package/lib/undici.js +3 -2
- package/package.json +2 -2
- package/sys/child.js +55 -41
- package/sys/master.js +90 -7
- package/www/example/ctr/test.d.ts +1 -0
- package/www/example/ctr/test.js +26 -2
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.17.
|
|
9
|
+
export const VER = '9.17.2';
|
|
10
10
|
// --- 服务端用的路径 ---
|
|
11
11
|
const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
|
|
12
12
|
/** --- /xxx/xxx --- */
|
package/lib/core.d.ts
CHANGED
|
@@ -206,6 +206,15 @@ export declare function sendRestart(hosts?: string[] | 'config'): Promise<Record
|
|
|
206
206
|
'result': boolean;
|
|
207
207
|
'return': string;
|
|
208
208
|
}>>;
|
|
209
|
+
/**
|
|
210
|
+
* --- 向主进程(或局域网同代码机子)发送 stop 操作,停止接收新连接并在现有连接全部结束后退出 ---
|
|
211
|
+
* @param hosts 局域网主机列表,config 表示使用全局配置;不传时通知当前 master
|
|
212
|
+
* @returns 各主机发送结果
|
|
213
|
+
*/
|
|
214
|
+
export declare function sendStop(hosts?: string[] | 'config'): Promise<Record<string, {
|
|
215
|
+
'result': boolean;
|
|
216
|
+
'return': string;
|
|
217
|
+
}>>;
|
|
209
218
|
/** --- PM2 操作类型 --- */
|
|
210
219
|
export type TPm2Action = 'start' | 'stop' | 'restart';
|
|
211
220
|
/**
|
|
@@ -213,6 +222,7 @@ export type TPm2Action = 'start' | 'stop' | 'restart';
|
|
|
213
222
|
* @param name PM2 进程名称
|
|
214
223
|
* @param action PM2 操作类型
|
|
215
224
|
* @param hosts 局域网列表
|
|
225
|
+
* @returns 各主机是否已接收并排队,不代表 PM2 操作最终执行成功
|
|
216
226
|
*/
|
|
217
227
|
export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<Record<string, {
|
|
218
228
|
'result': boolean;
|
package/lib/core.js
CHANGED
|
@@ -713,11 +713,62 @@ export async function sendRestart(hosts) {
|
|
|
713
713
|
}
|
|
714
714
|
return rtn;
|
|
715
715
|
}
|
|
716
|
+
/**
|
|
717
|
+
* --- 向主进程(或局域网同代码机子)发送 stop 操作,停止接收新连接并在现有连接全部结束后退出 ---
|
|
718
|
+
* @param hosts 局域网主机列表,config 表示使用全局配置;不传时通知当前 master
|
|
719
|
+
* @returns 各主机发送结果
|
|
720
|
+
*/
|
|
721
|
+
export async function sendStop(hosts) {
|
|
722
|
+
if (!hosts) {
|
|
723
|
+
// --- 本地模式 ---
|
|
724
|
+
// eslint-disable-next-line no-console
|
|
725
|
+
console.log('[ Child] Sending stop request...');
|
|
726
|
+
process.send({
|
|
727
|
+
'action': 'stop'
|
|
728
|
+
});
|
|
729
|
+
return {
|
|
730
|
+
'127.0.0.1': { 'result': true, 'return': 'Done' }
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
if (hosts === 'config') {
|
|
734
|
+
hosts = globalConfig.hosts;
|
|
735
|
+
}
|
|
736
|
+
// --- 未传或 config 展开后为空数组,均回退到本机 ---
|
|
737
|
+
if (!hosts?.length) {
|
|
738
|
+
hosts = ['127.0.0.1'];
|
|
739
|
+
}
|
|
740
|
+
// --- 局域网模式 ---
|
|
741
|
+
const time = lTime.stamp();
|
|
742
|
+
/** --- 返回成功的 host --- */
|
|
743
|
+
const rtn = {};
|
|
744
|
+
for (const host of hosts) {
|
|
745
|
+
const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
|
|
746
|
+
'action': 'stop',
|
|
747
|
+
'time': time
|
|
748
|
+
}), globalConfig.rpcSecret), {
|
|
749
|
+
'timeout': 5
|
|
750
|
+
});
|
|
751
|
+
const content = await res.getContent();
|
|
752
|
+
if (!content) {
|
|
753
|
+
rtn[host] = { 'result': false, 'return': 'Timeout' };
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const str = content.toString();
|
|
757
|
+
if (str === 'Done') {
|
|
758
|
+
rtn[host] = { 'result': true, 'return': 'Done' };
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
rtn[host] = { 'result': false, 'return': str };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return rtn;
|
|
765
|
+
}
|
|
716
766
|
/**
|
|
717
767
|
* --- 向本机或局域网 RPC 发送 PM2 操作 ---
|
|
718
768
|
* @param name PM2 进程名称
|
|
719
769
|
* @param action PM2 操作类型
|
|
720
770
|
* @param hosts 局域网列表
|
|
771
|
+
* @returns 各主机是否已接收并排队,不代表 PM2 操作最终执行成功
|
|
721
772
|
*/
|
|
722
773
|
export async function sendPm2(name, action = 'restart', hosts) {
|
|
723
774
|
if (hosts === 'config') {
|
package/lib/db/conn.d.ts
CHANGED
|
@@ -53,9 +53,12 @@ export declare class Connection {
|
|
|
53
53
|
*/
|
|
54
54
|
isUsing(): boolean;
|
|
55
55
|
/**
|
|
56
|
-
* --- 判断是否可用(丢失的也算不可用),返回 true
|
|
56
|
+
* --- 判断是否可用(丢失的也算不可用),返回 true 代表获取成功 ---
|
|
57
|
+
* @param opt 获取选项,连接巡检独占时不刷新最后使用时间
|
|
57
58
|
*/
|
|
58
|
-
using(
|
|
59
|
+
using(opt?: {
|
|
60
|
+
'refreshLast'?: boolean;
|
|
61
|
+
}): boolean;
|
|
59
62
|
/**
|
|
60
63
|
* --- 取消占用 ---
|
|
61
64
|
*/
|
|
@@ -85,7 +88,12 @@ export declare class Connection {
|
|
|
85
88
|
* --- 关闭连接,一般情况下不使用 ---
|
|
86
89
|
*/
|
|
87
90
|
end(): Promise<boolean>;
|
|
88
|
-
|
|
91
|
+
/**
|
|
92
|
+
* --- 开启事务,只能在独占连接中使用 ---
|
|
93
|
+
* @param logError 失败时是否记录错误,连接池在非最后一次重试时传 false
|
|
94
|
+
* @returns 是否开启成功
|
|
95
|
+
*/
|
|
96
|
+
beginTransaction(logError?: boolean): Promise<boolean>;
|
|
89
97
|
commit(): Promise<boolean>;
|
|
90
98
|
rollback(): Promise<boolean>;
|
|
91
99
|
}
|
package/lib/db/conn.js
CHANGED
|
@@ -99,14 +99,17 @@ export class Connection {
|
|
|
99
99
|
return this._using;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* --- 判断是否可用(丢失的也算不可用),返回 true
|
|
102
|
+
* --- 判断是否可用(丢失的也算不可用),返回 true 代表获取成功 ---
|
|
103
|
+
* @param opt 获取选项,连接巡检独占时不刷新最后使用时间
|
|
103
104
|
*/
|
|
104
|
-
using() {
|
|
105
|
+
using(opt = {}) {
|
|
105
106
|
if (this._lost || this._using) {
|
|
106
107
|
return false;
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
|
-
|
|
110
|
+
if (opt.refreshLast !== false) {
|
|
111
|
+
this.refreshLast();
|
|
112
|
+
}
|
|
110
113
|
this._using = true;
|
|
111
114
|
return true;
|
|
112
115
|
}
|
|
@@ -281,6 +284,8 @@ export class Connection {
|
|
|
281
284
|
* --- 关闭连接,一般情况下不使用 ---
|
|
282
285
|
*/
|
|
283
286
|
async end() {
|
|
287
|
+
// --- 驱动的 end 事件可能在异步关闭完成后才触发,提前标记避免连接池在关闭期间重新取到本连接 ---
|
|
288
|
+
this._lost = true;
|
|
284
289
|
try {
|
|
285
290
|
await this._link.end();
|
|
286
291
|
return true;
|
|
@@ -289,8 +294,12 @@ export class Connection {
|
|
|
289
294
|
return false;
|
|
290
295
|
}
|
|
291
296
|
}
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
/**
|
|
298
|
+
* --- 开启事务,只能在独占连接中使用 ---
|
|
299
|
+
* @param logError 失败时是否记录错误,连接池在非最后一次重试时传 false
|
|
300
|
+
* @returns 是否开启成功
|
|
301
|
+
*/
|
|
302
|
+
async beginTransaction(logError = true) {
|
|
294
303
|
if (this._using) {
|
|
295
304
|
try {
|
|
296
305
|
this._transaction = true;
|
|
@@ -306,12 +315,16 @@ export class Connection {
|
|
|
306
315
|
this._transaction = false;
|
|
307
316
|
this._using = false;
|
|
308
317
|
this._lost = true;
|
|
309
|
-
|
|
318
|
+
if (logError) {
|
|
319
|
+
lCore.log({}, '[DB][Connection][beginTransaction] ' + lText.stringifyError(e), '-error');
|
|
320
|
+
}
|
|
310
321
|
return false;
|
|
311
322
|
}
|
|
312
323
|
}
|
|
313
324
|
else {
|
|
314
|
-
|
|
325
|
+
if (logError) {
|
|
326
|
+
lCore.log({}, '[DB][Connection][beginTransaction] connection is not in use', '-error');
|
|
327
|
+
}
|
|
315
328
|
return false;
|
|
316
329
|
}
|
|
317
330
|
}
|
package/lib/db/pool.js
CHANGED
|
@@ -67,6 +67,10 @@ async function checkConnection() {
|
|
|
67
67
|
}
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
|
+
// --- 巡检探活或关闭前先独占连接,避免业务在异步操作期间取到同一连接 ---
|
|
71
|
+
if (!connection.using({ 'refreshLast': false })) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
70
74
|
if (connection.getLast() <= now - 30) {
|
|
71
75
|
// --- 超 30 秒未被使用,则关闭 ---
|
|
72
76
|
await connection.end();
|
|
@@ -75,8 +79,9 @@ async function checkConnection() {
|
|
|
75
79
|
continue;
|
|
76
80
|
}
|
|
77
81
|
// --- 30 秒内使用过,看看连接是否正常 ---
|
|
78
|
-
if (await connection.isAvailable(false)) {
|
|
82
|
+
if (await connection.isAvailable(false) && !connection.isLost()) {
|
|
79
83
|
// --- 正常 ---
|
|
84
|
+
connection.used();
|
|
80
85
|
continue;
|
|
81
86
|
}
|
|
82
87
|
// --- 连接有问题,直接关闭 ---
|
|
@@ -120,7 +125,7 @@ export class Pool {
|
|
|
120
125
|
*/
|
|
121
126
|
async query(sql, values) {
|
|
122
127
|
++this._queries;
|
|
123
|
-
// --- 获取并自动 using
|
|
128
|
+
// --- 获取并自动 using ---
|
|
124
129
|
const conn = await this._getConnection();
|
|
125
130
|
if (!conn) {
|
|
126
131
|
return {
|
|
@@ -167,7 +172,9 @@ export class Pool {
|
|
|
167
172
|
lCore.log(ctr ?? {}, `[DB][Pool][beginTransaction] failed to get connection, service: ${lDb.ESERVICE[this._service]}, database: ${this._etc.name ?? ''}`, '-error');
|
|
168
173
|
return null;
|
|
169
174
|
}
|
|
170
|
-
|
|
175
|
+
/** --- 只有最后一次尝试仍失败时才记录原始错误,避免已恢复的重试被误判为业务失败 --- */
|
|
176
|
+
const logError = i === BEGIN_TRANSACTION_MAX_ATTEMPTS - 1;
|
|
177
|
+
if (!await conn.beginTransaction(logError)) {
|
|
171
178
|
continue;
|
|
172
179
|
}
|
|
173
180
|
return new Transaction(ctr, conn);
|
package/lib/undici/response.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare class Response {
|
|
|
31
31
|
/**
|
|
32
32
|
* --- 获取响应读取流对象 ---
|
|
33
33
|
*/
|
|
34
|
-
getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) |
|
|
34
|
+
getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
|
|
35
35
|
/**
|
|
36
36
|
* --- 获取原生响应读取流对象 ---
|
|
37
37
|
*/
|
package/lib/undici.d.ts
CHANGED
|
@@ -133,6 +133,8 @@ export interface IRequestOptions {
|
|
|
133
133
|
'retry'?: number;
|
|
134
134
|
/** --- JSON 解析失败后的重试次数,默认 0;仅适用于 ResponseJson 快捷方法,非幂等请求需由调用方保证安全 --- */
|
|
135
135
|
'retryJson'?: number;
|
|
136
|
+
/** --- JSON 重试验证方法;返回 false 时按 retryJson 的设置重试,仅适用于 ResponseJson 快捷方法 --- */
|
|
137
|
+
'retryJsonHandler'?: (json: kebab.Json) => boolean;
|
|
136
138
|
/** --- 追踪 location 次数,0 为不追踪,默认为 0 --- */
|
|
137
139
|
'follow'?: number;
|
|
138
140
|
/** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
|
package/lib/undici.js
CHANGED
|
@@ -152,7 +152,7 @@ async function requestResponseJson(u, data, opt, action) {
|
|
|
152
152
|
}
|
|
153
153
|
const rtnStr = rtn.toString();
|
|
154
154
|
const json = lText.parseJson(rtnStr);
|
|
155
|
-
if (json) {
|
|
155
|
+
if (json && (opt.retryJsonHandler?.(json) ?? true)) {
|
|
156
156
|
return json;
|
|
157
157
|
}
|
|
158
158
|
if (i < retryJson) {
|
|
@@ -162,7 +162,8 @@ async function requestResponseJson(u, data, opt, action) {
|
|
|
162
162
|
}
|
|
163
163
|
if (opt.log === undefined || opt.log) {
|
|
164
164
|
const requestData = data === undefined ? '' : `, data: ${lText.stringifyJson(data)}`;
|
|
165
|
-
|
|
165
|
+
const reason = json ? 'retry json handler returned false' : 'parse json failed';
|
|
166
|
+
lCore.log({}, `[UNDICI][${action}] ${reason}, url: ${u}${requestData}, content: ${rtnStr}`, '-neterror');
|
|
166
167
|
}
|
|
167
168
|
return false;
|
|
168
169
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maiyunnet/kebab",
|
|
3
|
-
"version": "9.17.
|
|
3
|
+
"version": "9.17.2",
|
|
4
4
|
"description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@tailwindcss/cli": "^4.3.3",
|
|
36
36
|
"@types/ssh2": "^1.15.5",
|
|
37
37
|
"ajv": "^8.20.0",
|
|
38
|
-
"ajv-formats": "^3.0.1",
|
|
38
|
+
"ajv-formats": "^3.0.1",
|
|
39
39
|
"class-variance-authority": "^0.7.1",
|
|
40
40
|
"clsx": "^2.1.1",
|
|
41
41
|
"ejs": "^6.0.1",
|
package/sys/child.js
CHANGED
|
@@ -47,6 +47,10 @@ let http2Server;
|
|
|
47
47
|
const linkCount = {};
|
|
48
48
|
/** --- 是否正在停止,停止时对 HTTP/1.1 响应追加 Connection: close,避免请求完成后连接回到保活池 --- */
|
|
49
49
|
let stopping = false;
|
|
50
|
+
/** --- 当前缓动停止任务,避免重复 IPC 消息触发停止流程 --- */
|
|
51
|
+
let stopPromise = null;
|
|
52
|
+
/** --- 缓动停止是否必须一直等到现有连接全部结束 --- */
|
|
53
|
+
let stopWaitForever = false;
|
|
50
54
|
/** --- 当前活跃的 HTTP/2 会话集合,用于 graceful shutdown 时发送 GOAWAY 帧 --- */
|
|
51
55
|
const http2Sessions = new Set();
|
|
52
56
|
/**
|
|
@@ -552,56 +556,66 @@ async function reloadCert() {
|
|
|
552
556
|
defaultSc = certList.size > 0 ? certList.values().next().value.sc : undefined;
|
|
553
557
|
certHostIndex = {};
|
|
554
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* --- 停止接收新连接,并在现有请求、上传、异步任务和 WebSocket 全部结束后退出 worker ---
|
|
561
|
+
* @param waitForever 是否取消原有的一小时退出兜底,仅完整 stop 命令使用
|
|
562
|
+
* @returns worker 退出时结束
|
|
563
|
+
*/
|
|
564
|
+
async function gracefulStop(waitForever = false) {
|
|
565
|
+
if (waitForever) {
|
|
566
|
+
stopWaitForever = true;
|
|
567
|
+
}
|
|
568
|
+
if (stopPromise) {
|
|
569
|
+
return stopPromise;
|
|
570
|
+
}
|
|
571
|
+
stopping = true;
|
|
572
|
+
stopPromise = (async () => {
|
|
573
|
+
// --- 向所有 HTTP/2 会话发送 GOAWAY 帧,通知客户端停止在该连接上发送新请求并切换到新连接 ---
|
|
574
|
+
for (const session of http2Sessions) {
|
|
575
|
+
try {
|
|
576
|
+
session.goaway(http2.constants.NGHTTP2_NO_ERROR);
|
|
577
|
+
}
|
|
578
|
+
catch { }
|
|
579
|
+
}
|
|
580
|
+
httpServer?.close();
|
|
581
|
+
http2Server?.close();
|
|
582
|
+
// --- 立即关闭空闲保活连接,活跃连接会继续执行并在响应后关闭 ---
|
|
583
|
+
httpServer?.closeIdleConnections();
|
|
584
|
+
clearInterval(hbTimer);
|
|
585
|
+
sMonitor.stop();
|
|
586
|
+
// --- 等待活跃请求全部完成;WebSocket 的路由任务会一直保留到连接关闭 ---
|
|
587
|
+
/** --- 当前已等待时间,restart 最多等待一小时,完整 stop 不限制 --- */
|
|
588
|
+
let waiting = 0;
|
|
589
|
+
while (Object.keys(linkCount).length) {
|
|
590
|
+
const str = [];
|
|
591
|
+
for (const key in linkCount) {
|
|
592
|
+
str.push(key + ':' + linkCount[key].toString());
|
|
593
|
+
}
|
|
594
|
+
lCore.debug(`[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`);
|
|
595
|
+
lCore.log({}, `[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`, '-warning');
|
|
596
|
+
await lCore.sleep(5_000);
|
|
597
|
+
waiting += 5_000;
|
|
598
|
+
// --- 再次清理已变为空闲的保活连接 ---
|
|
599
|
+
httpServer?.closeIdleConnections();
|
|
600
|
+
if (!stopWaitForever && (waiting > 3_600_000)) {
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
process.exit();
|
|
605
|
+
})();
|
|
606
|
+
return stopPromise;
|
|
607
|
+
}
|
|
555
608
|
// --- 接收主进程回传信号,主要用来 reload,restart ---
|
|
556
609
|
process.on('message', function (msg) {
|
|
557
610
|
(async function () {
|
|
558
611
|
switch (msg.action) {
|
|
559
612
|
case 'reload': {
|
|
560
613
|
await reload();
|
|
561
|
-
|
|
562
|
-
console.log(`[child] Worker ${process.pid} reload execution succeeded.`);
|
|
614
|
+
lCore.display(`[CHILD] Worker ${process.pid} reload execution succeeded.`);
|
|
563
615
|
break;
|
|
564
616
|
}
|
|
565
617
|
case 'stop': {
|
|
566
|
-
|
|
567
|
-
stopping = true;
|
|
568
|
-
// --- 向所有 HTTP/2 会话发送 GOAWAY 帧,通知 CDN 客户端停止在该连接上发送新请求并切换到新连接 ---
|
|
569
|
-
for (const session of http2Sessions) {
|
|
570
|
-
try {
|
|
571
|
-
session.goaway(http2.constants.NGHTTP2_NO_ERROR);
|
|
572
|
-
}
|
|
573
|
-
catch { }
|
|
574
|
-
}
|
|
575
|
-
httpServer?.close();
|
|
576
|
-
http2Server?.close();
|
|
577
|
-
// --- 立即关闭空闲保活连接(无活跃请求的 keep-alive socket),避免进程长时间等待 ---
|
|
578
|
-
httpServer?.closeIdleConnections();
|
|
579
|
-
clearInterval(hbTimer);
|
|
580
|
-
sMonitor.stop();
|
|
581
|
-
// --- 等待活跃请求全部完成 ---
|
|
582
|
-
/** --- 当前已等待时间,等待不超过 1 小时 --- */
|
|
583
|
-
let waiting = 0;
|
|
584
|
-
while (true) {
|
|
585
|
-
if (!Object.keys(linkCount).length) {
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
// --- 有活跃连接,等待中 ---
|
|
589
|
-
const str = [];
|
|
590
|
-
for (const key in linkCount) {
|
|
591
|
-
str.push(key + ':' + linkCount[key].toString());
|
|
592
|
-
}
|
|
593
|
-
lCore.debug(`[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`);
|
|
594
|
-
lCore.log({}, `[CHILD] Worker ${process.pid} busy: ${str.join(',')}.`, '-warning');
|
|
595
|
-
await lCore.sleep(5_000);
|
|
596
|
-
waiting += 5_000;
|
|
597
|
-
// --- 再次清理已变为空闲的保活连接 ---
|
|
598
|
-
httpServer?.closeIdleConnections();
|
|
599
|
-
if (waiting > 3600_000) {
|
|
600
|
-
break;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
// --- 链接全部断开 ---
|
|
604
|
-
process.exit();
|
|
618
|
+
await gracefulStop(msg.waitForever === true);
|
|
605
619
|
break;
|
|
606
620
|
}
|
|
607
621
|
case 'global': {
|
package/sys/master.js
CHANGED
|
@@ -19,6 +19,10 @@ import * as lTime from '#kebab/lib/time.js';
|
|
|
19
19
|
import * as lZip from '#kebab/lib/zip.js';
|
|
20
20
|
/** --- 当前运行中的子进程列表 --- */
|
|
21
21
|
const workerList = {};
|
|
22
|
+
/** --- master 是否正在缓动停止 --- */
|
|
23
|
+
let stopping = false;
|
|
24
|
+
/** --- 当前缓动停止任务,避免重复 stop 命令触发 --- */
|
|
25
|
+
let stopPromise = null;
|
|
22
26
|
/**
|
|
23
27
|
* --- 判断候选路径是否位于指定根目录内 ---
|
|
24
28
|
* @param rootPath 已解析的真实根目录
|
|
@@ -246,6 +250,12 @@ function createRpcListener() {
|
|
|
246
250
|
await restartWorkers();
|
|
247
251
|
break;
|
|
248
252
|
}
|
|
253
|
+
case 'stop': {
|
|
254
|
+
gracefulStop().catch((e) => {
|
|
255
|
+
lCore.display('[master] [gracefulStop]', e);
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
249
259
|
case 'global': {
|
|
250
260
|
// --- 为所有子进程更新 global 变量 ---
|
|
251
261
|
for (const pid in workerList) {
|
|
@@ -264,7 +274,7 @@ function createRpcListener() {
|
|
|
264
274
|
break;
|
|
265
275
|
}
|
|
266
276
|
case 'pm2': {
|
|
267
|
-
// ---
|
|
277
|
+
// --- 接收 PM2 操作 ---
|
|
268
278
|
if (!msg.name || typeof msg.name !== 'string') {
|
|
269
279
|
res.end('Invalid name');
|
|
270
280
|
return;
|
|
@@ -279,12 +289,18 @@ function createRpcListener() {
|
|
|
279
289
|
res.end('Invalid pm2Action');
|
|
280
290
|
return;
|
|
281
291
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
// --- 先确认已接收,让调用方完成当前 HTTP 响应,再执行可能终止本进程的 PM2 操作 ---
|
|
293
|
+
res.end('Done');
|
|
294
|
+
setTimeout(() => {
|
|
295
|
+
lCore.exec(`pm2 ${msg.pm2Action} ${msg.name}`).then((rtn) => {
|
|
296
|
+
if (rtn === false) {
|
|
297
|
+
lCore.display(`[master] PM2 ${msg.pm2Action} ${msg.name} failed.`);
|
|
298
|
+
}
|
|
299
|
+
}).catch((e) => {
|
|
300
|
+
lCore.display(`[master] PM2 ${msg.pm2Action} ${msg.name} failed.`, e);
|
|
301
|
+
});
|
|
302
|
+
}, 1_000);
|
|
303
|
+
return;
|
|
288
304
|
}
|
|
289
305
|
case 'npm': {
|
|
290
306
|
// --- 执行 npm install 操作 ---
|
|
@@ -931,10 +947,59 @@ function createRpcListener() {
|
|
|
931
947
|
});
|
|
932
948
|
}).listen(lCore.globalConfig.rpcPort);
|
|
933
949
|
}
|
|
950
|
+
/**
|
|
951
|
+
* --- 通知 worker 停止,并等待其现有连接全部结束后退出 ---
|
|
952
|
+
* @param worker 要停止的 worker
|
|
953
|
+
* @returns worker 退出后完成
|
|
954
|
+
*/
|
|
955
|
+
function stopWorker(worker) {
|
|
956
|
+
if (worker.isDead()) {
|
|
957
|
+
return Promise.resolve();
|
|
958
|
+
}
|
|
959
|
+
return new Promise((resolve) => {
|
|
960
|
+
worker.once('exit', () => { resolve(); });
|
|
961
|
+
try {
|
|
962
|
+
if (worker.isConnected()) {
|
|
963
|
+
worker.send({
|
|
964
|
+
'action': 'stop',
|
|
965
|
+
'waitForever': true,
|
|
966
|
+
});
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
worker.process.kill('SIGTERM');
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
lCore.display(`[master] Failed to notify worker ${worker.process.pid ?? 'undefined'} to stop, using SIGTERM.`, e);
|
|
973
|
+
worker.process.kill('SIGTERM');
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* --- 停止命令入口和所有 worker,等待整套 Kebab 进程排空后退出 master ---
|
|
979
|
+
* @returns 全部 worker 退出后完成
|
|
980
|
+
*/
|
|
981
|
+
async function gracefulStop() {
|
|
982
|
+
if (stopPromise) {
|
|
983
|
+
return stopPromise;
|
|
984
|
+
}
|
|
985
|
+
stopping = true;
|
|
986
|
+
stopPromise = (async () => {
|
|
987
|
+
lCore.display('[master] Graceful stop started.');
|
|
988
|
+
// --- 包括 restart 时已从 workerList 移除、但仍在排空的旧 worker ---
|
|
989
|
+
const workers = Object.values(cluster.workers ?? {}).filter((worker) => worker !== undefined);
|
|
990
|
+
await Promise.all(workers.map(worker => stopWorker(worker)));
|
|
991
|
+
lCore.display('[master] Graceful stop completed.');
|
|
992
|
+
process.exit(0);
|
|
993
|
+
})();
|
|
994
|
+
return stopPromise;
|
|
995
|
+
}
|
|
934
996
|
/**
|
|
935
997
|
* --- 零宕机重启所有 worker 进程:先创建新进程并等待监听就绪,再停止旧进程 ---
|
|
936
998
|
*/
|
|
937
999
|
async function restartWorkers() {
|
|
1000
|
+
if (stopping) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
938
1003
|
// --- 收集旧进程信息(先快照,避免迭代过程中修改 workerList) ---
|
|
939
1004
|
const oldEntries = [];
|
|
940
1005
|
for (const pid in workerList) {
|
|
@@ -946,6 +1011,9 @@ async function restartWorkers() {
|
|
|
946
1011
|
}
|
|
947
1012
|
// --- 阶段 1:创建新进程并等待其监听端口就绪 ---
|
|
948
1013
|
for (const entry of oldEntries) {
|
|
1014
|
+
if (stopping) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
949
1017
|
const newWorker = await createChildProcess(entry.cpu);
|
|
950
1018
|
if (newWorker) {
|
|
951
1019
|
await waitForWorkerListening(newWorker);
|
|
@@ -953,6 +1021,9 @@ async function restartWorkers() {
|
|
|
953
1021
|
}
|
|
954
1022
|
// --- 阶段 2:停止旧进程 ---
|
|
955
1023
|
for (const entry of oldEntries) {
|
|
1024
|
+
if (stopping) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
956
1027
|
entry.worker.send({
|
|
957
1028
|
'action': 'stop'
|
|
958
1029
|
});
|
|
@@ -963,6 +1034,9 @@ async function restartWorkers() {
|
|
|
963
1034
|
* --- 检测是否有死掉丢失的子进程,复活之 ---
|
|
964
1035
|
*/
|
|
965
1036
|
async function checkWorkerLost() {
|
|
1037
|
+
if (stopping) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
966
1040
|
const now = Date.now();
|
|
967
1041
|
for (const pid in workerList) {
|
|
968
1042
|
if (now - workerList[pid].hbtime < 30000) {
|
|
@@ -984,6 +1058,9 @@ async function checkWorkerLost() {
|
|
|
984
1058
|
* @param cpu CPU ID
|
|
985
1059
|
*/
|
|
986
1060
|
async function createChildProcess(cpu) {
|
|
1061
|
+
if (stopping) {
|
|
1062
|
+
return null;
|
|
1063
|
+
}
|
|
987
1064
|
const worker = cluster.fork();
|
|
988
1065
|
if (!worker.process.pid) {
|
|
989
1066
|
return null;
|
|
@@ -1017,6 +1094,12 @@ async function createChildProcess(cpu) {
|
|
|
1017
1094
|
await restartWorkers();
|
|
1018
1095
|
break;
|
|
1019
1096
|
}
|
|
1097
|
+
case 'stop': {
|
|
1098
|
+
gracefulStop().catch((e) => {
|
|
1099
|
+
lCore.display('[master] [gracefulStop]', e);
|
|
1100
|
+
});
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1020
1103
|
case 'global': {
|
|
1021
1104
|
// --- 为所有子进程更新 global 变量 ---
|
|
1022
1105
|
for (const pid in workerList) {
|
|
@@ -56,6 +56,7 @@ export default class extends sCtr.Ctr {
|
|
|
56
56
|
coreUpdatecode(): Promise<string>;
|
|
57
57
|
coreReload(): Promise<string>;
|
|
58
58
|
coreRestart(): Promise<string>;
|
|
59
|
+
coreStop(): Promise<string>;
|
|
59
60
|
corePm2(): Promise<string>;
|
|
60
61
|
coreNpm(): Promise<any>;
|
|
61
62
|
coreGlobal(): Promise<string>;
|
package/www/example/ctr/test.js
CHANGED
|
@@ -169,6 +169,7 @@ export default class extends sCtr.Ctr {
|
|
|
169
169
|
`<br><a href="${this._config.const.urlBase}test/core-ls">View "test/core-ls"</a>`,
|
|
170
170
|
`<br><a href="${this._config.const.urlBase}test/core-reload">View "test/core-reload"</a>`,
|
|
171
171
|
`<br><a href="${this._config.const.urlBase}test/core-restart">View "test/core-restart"</a>`,
|
|
172
|
+
`<br><a href="${this._config.const.urlBase}test/core-stop">View "test/core-stop"</a>`,
|
|
172
173
|
`<br><a href="${this._config.const.urlBase}test/core-pm2?name=cron&action=restart">View "test/core-pm2"</a>`,
|
|
173
174
|
`<br><a href="${this._config.const.urlBase}test/core-npm">View "test/core-npm"</a>`,
|
|
174
175
|
`<br><a href="${this._config.const.urlBase}test/core-global">View "test/core-global"</a>`,
|
|
@@ -1572,6 +1573,10 @@ to: ${to}`
|
|
|
1572
1573
|
const list = await lCore.sendRestart();
|
|
1573
1574
|
return `The restart request has been sent, please review the console.<br>Hosts: ${JSON.stringify(list)}<br><br>` + this._getEnd();
|
|
1574
1575
|
}
|
|
1576
|
+
async coreStop() {
|
|
1577
|
+
const list = await lCore.sendStop();
|
|
1578
|
+
return `The stop request has been sent, please review the console.<br>Hosts: ${lText.stringifyJson(list)}<br><br>` + this._getEnd();
|
|
1579
|
+
}
|
|
1575
1580
|
async corePm2() {
|
|
1576
1581
|
const name = this._get['name'] ?? '';
|
|
1577
1582
|
const action = (this._get['action'] ?? 'restart');
|
|
@@ -2499,10 +2504,29 @@ error: <pre>${JSON.stringify(res.error, null, 4)}</pre>`);
|
|
|
2499
2504
|
const echo = [];
|
|
2500
2505
|
const json = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json1');
|
|
2501
2506
|
echo.push(`<pre>const json = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json1');</pre>
|
|
2502
|
-
result: <pre>${lText.htmlescape(JSON.stringify(json, null, 4))}</pre>`);
|
|
2507
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json, null, 4))}</pre><hr>`);
|
|
2503
2508
|
const json2 = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json2');
|
|
2504
2509
|
echo.push(`<pre>const json2 = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json2');</pre>
|
|
2505
|
-
result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre>`);
|
|
2510
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json2, null, 4))}</pre><hr>`);
|
|
2511
|
+
/** --- JSON 重试验证方法的执行次数 --- */
|
|
2512
|
+
let retryJsonHandlerCount = 0;
|
|
2513
|
+
const json3 = await lUndici.getResponseJson(this._internalUrl + 'test/undici-get-response-json1', {
|
|
2514
|
+
'retryJson': 1,
|
|
2515
|
+
'retryJsonHandler': (value) => {
|
|
2516
|
+
++retryJsonHandlerCount;
|
|
2517
|
+
return (retryJsonHandlerCount > 1) && (value.result === 1);
|
|
2518
|
+
},
|
|
2519
|
+
});
|
|
2520
|
+
echo.push(`<pre>let retryJsonHandlerCount = 0;
|
|
2521
|
+
const json3 = await lUndici.getResponseJson('${this._internalUrl}test/undici-get-response-json1', {
|
|
2522
|
+
'retryJson': 1,
|
|
2523
|
+
'retryJsonHandler': (value) => {
|
|
2524
|
+
++retryJsonHandlerCount;
|
|
2525
|
+
return (retryJsonHandlerCount > 1) && (value.result === 1);
|
|
2526
|
+
},
|
|
2527
|
+
});</pre>
|
|
2528
|
+
handler count: ${retryJsonHandlerCount}
|
|
2529
|
+
result: <pre>${lText.htmlescape(JSON.stringify(json3, null, 4))}</pre>`);
|
|
2506
2530
|
return echo.join('') + '<br>' + this._getEnd();
|
|
2507
2531
|
}
|
|
2508
2532
|
undiciGetResponseJson1() {
|