befly 3.4.11 → 3.4.13
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/bin/index.ts +8 -20
- package/commands/sync.ts +56 -0
- package/commands/syncApi.ts +21 -1
- package/commands/syncDev.ts +61 -0
- package/commands/syncMenu.ts +21 -1
- package/lib/database.ts +1 -0
- package/lifecycle/lifecycle.ts +1 -8
- package/main.ts +28 -2
- package/package.json +2 -2
- package/bin/launcher.ts +0 -116
- package/commands/dev.ts +0 -80
- package/commands/start.ts +0 -92
- package/lifecycle/cluster-worker.ts +0 -49
- package/lifecycle/cluster.ts +0 -254
package/bin/index.ts
CHANGED
|
@@ -2,25 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Befly CLI - 命令行工具入口
|
|
4
4
|
* 为 Befly 框架提供项目管理和脚本执行功能
|
|
5
|
+
*
|
|
6
|
+
* 环境变量加载:
|
|
7
|
+
* 1. Bun 自动加载:根据 NODE_ENV 自动加载 .env.{NODE_ENV} 文件
|
|
8
|
+
* 2. 手动指定:bun --env-file=.env.xxx befly <command>
|
|
9
|
+
* 3. 默认环境:如未设置 NODE_ENV,Bun 加载 .env.development
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
|
-
/**
|
|
8
|
-
* 环境启动器
|
|
9
|
-
* 必须在任何模块导入前执行
|
|
10
|
-
* 父进程会在这里启动子进程并退出
|
|
11
|
-
* 只有子进程会继续执行后面的代码
|
|
12
|
-
*/
|
|
13
|
-
import { launch } from './launcher.js';
|
|
14
|
-
await launch(import.meta.path);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 以下代码只在子进程中执行
|
|
18
|
-
* 此时环境变量已正确加载
|
|
19
|
-
*/
|
|
20
12
|
import { Command } from 'commander';
|
|
21
|
-
import { devCommand } from '../commands/dev.js';
|
|
22
13
|
import { buildCommand } from '../commands/build.js';
|
|
23
|
-
import {
|
|
14
|
+
import { syncCommand } from '../commands/sync.js';
|
|
24
15
|
import { syncDbCommand } from '../commands/syncDb.js';
|
|
25
16
|
import { syncApiCommand } from '../commands/syncApi.js';
|
|
26
17
|
import { syncMenuCommand } from '../commands/syncMenu.js';
|
|
@@ -130,14 +121,11 @@ function wrapCommand<T extends (...args: any[]) => any>(fn: T): T {
|
|
|
130
121
|
}) as T;
|
|
131
122
|
}
|
|
132
123
|
|
|
133
|
-
// dev 命令 - 开发服务器
|
|
134
|
-
program.command('dev').description('启动开发服务器').option('-p, --port <number>', '端口号', '3000').option('-h, --host <string>', '主机地址', '0.0.0.0').option('--no-sync', '跳过表同步', false).option('-v, --verbose', '详细日志', false).action(wrapCommand(devCommand));
|
|
135
|
-
|
|
136
124
|
// build 命令 - 构建项目
|
|
137
125
|
program.command('build').description('构建项目').option('-o, --outdir <path>', '输出目录', 'dist').option('--minify', '压缩代码', false).option('--sourcemap', '生成 sourcemap', false).action(wrapCommand(buildCommand));
|
|
138
126
|
|
|
139
|
-
//
|
|
140
|
-
program.command('
|
|
127
|
+
// sync 命令 - 一次性执行所有同步
|
|
128
|
+
program.command('sync').description('一次性执行所有同步操作(syncApi + syncMenu + syncDev)').option('--plan', '计划模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncCommand));
|
|
141
129
|
|
|
142
130
|
// syncDb 命令 - 同步数据库
|
|
143
131
|
program.command('syncDb').description('同步数据库表结构').option('-t, --table <name>', '指定表名').option('--dry-run', '预览模式,只显示不执行', false).option('-e, --env <environment>', '指定环境 (development, production, test)').action(wrapCommand(syncDbCommand));
|
package/commands/sync.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync 命令 - 一次性执行所有同步操作
|
|
3
|
+
* 按顺序执行:syncApi → syncMenu → syncDev
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Logger } from '../lib/logger.js';
|
|
7
|
+
import { syncApiCommand } from './syncApi.js';
|
|
8
|
+
import { syncMenuCommand } from './syncMenu.js';
|
|
9
|
+
import { syncDevCommand } from './syncDev.js';
|
|
10
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
11
|
+
|
|
12
|
+
interface SyncOptions {
|
|
13
|
+
env?: string;
|
|
14
|
+
plan?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function syncCommand(options: SyncOptions = {}) {
|
|
18
|
+
try {
|
|
19
|
+
Logger.info('========================================');
|
|
20
|
+
Logger.info('开始执行完整同步流程');
|
|
21
|
+
Logger.info('========================================\n');
|
|
22
|
+
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
|
|
25
|
+
// 确保 logs 目录存在
|
|
26
|
+
if (!existsSync('./logs')) {
|
|
27
|
+
mkdirSync('./logs', { recursive: true });
|
|
28
|
+
Logger.info('✅ 已创建 logs 目录\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 1. 同步接口(并缓存)
|
|
32
|
+
Logger.info('【步骤 1/3】同步接口数据\n');
|
|
33
|
+
await syncApiCommand(options);
|
|
34
|
+
Logger.info('\n✅ 接口同步完成\n');
|
|
35
|
+
|
|
36
|
+
// 2. 同步菜单(并缓存)
|
|
37
|
+
Logger.info('【步骤 2/3】同步菜单数据\n');
|
|
38
|
+
await syncMenuCommand(options);
|
|
39
|
+
Logger.info('\n✅ 菜单同步完成\n');
|
|
40
|
+
|
|
41
|
+
// 3. 同步开发管理员(并缓存角色权限)
|
|
42
|
+
Logger.info('【步骤 3/3】同步开发管理员\n');
|
|
43
|
+
await syncDevCommand(options);
|
|
44
|
+
Logger.info('\n✅ 开发管理员同步完成\n');
|
|
45
|
+
|
|
46
|
+
// 输出总结
|
|
47
|
+
const totalTime = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
48
|
+
Logger.info('========================================');
|
|
49
|
+
Logger.info('🎉 所有同步操作已完成!');
|
|
50
|
+
Logger.info(`总耗时: ${totalTime} 秒`);
|
|
51
|
+
Logger.info('========================================');
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
Logger.error('同步过程中发生错误:', error);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/commands/syncApi.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { Logger } from '../lib/logger.js';
|
|
16
16
|
import { Database } from '../lib/database.js';
|
|
17
|
+
import { RedisHelper } from '../lib/redisHelper.js';
|
|
17
18
|
import { scanAddons, getAddonDir, addonDirExists } from '../util.js';
|
|
18
19
|
import { readdirSync, statSync } from 'node:fs';
|
|
19
20
|
import { join, dirname, relative, basename } from 'pathe';
|
|
@@ -317,7 +318,26 @@ export async function syncApiCommand(options: SyncApiOptions = {}) {
|
|
|
317
318
|
Logger.info(`更新接口: ${stats.updated} 个`);
|
|
318
319
|
Logger.info(`删除接口: ${deletedCount} 个`);
|
|
319
320
|
Logger.info(`当前总接口数: ${apis.length} 个`);
|
|
320
|
-
|
|
321
|
+
|
|
322
|
+
// 6. 缓存接口数据到 Redis
|
|
323
|
+
Logger.info('\n=== 步骤 4: 缓存接口数据到 Redis ===');
|
|
324
|
+
try {
|
|
325
|
+
const apiList = await helper.getAll({
|
|
326
|
+
table: 'core_api',
|
|
327
|
+
fields: ['id', 'name', 'path', 'method', 'description', 'addonName', 'addonTitle'],
|
|
328
|
+
orderBy: ['addonName#ASC', 'path#ASC']
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const result = await RedisHelper.setObject('apis:all', apiList);
|
|
332
|
+
|
|
333
|
+
if (result === null) {
|
|
334
|
+
Logger.warn('⚠️ 接口缓存失败');
|
|
335
|
+
} else {
|
|
336
|
+
Logger.info(`✅ 已缓存 ${apiList.length} 个接口到 Redis (Key: apis:all)`);
|
|
337
|
+
}
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
Logger.error('⚠️ 接口缓存异常:', error);
|
|
340
|
+
}
|
|
321
341
|
} catch (error: any) {
|
|
322
342
|
Logger.error('API 同步失败:', error);
|
|
323
343
|
process.exit(1);
|
package/commands/syncDev.ts
CHANGED
|
@@ -165,6 +165,67 @@ export async function syncDevCommand(options: SyncDevOptions = {}) {
|
|
|
165
165
|
});
|
|
166
166
|
Logger.info(`✅ 开发管理员已初始化:email=${Env.DEV_EMAIL}, username=dev, roleCode=dev, roleType=admin`);
|
|
167
167
|
}
|
|
168
|
+
|
|
169
|
+
// 缓存角色权限数据到 Redis
|
|
170
|
+
Logger.info('\n=== 缓存角色权限到 Redis ===');
|
|
171
|
+
try {
|
|
172
|
+
// 检查必要的表是否存在
|
|
173
|
+
const apiTableExists = await helper.tableExists('core_api');
|
|
174
|
+
const roleTableExists = await helper.tableExists('core_role');
|
|
175
|
+
|
|
176
|
+
if (!apiTableExists || !roleTableExists) {
|
|
177
|
+
Logger.warn('⚠️ 接口或角色表不存在,跳过角色权限缓存');
|
|
178
|
+
} else {
|
|
179
|
+
// 查询所有角色
|
|
180
|
+
const roles = await helper.getAll({
|
|
181
|
+
table: 'core_role',
|
|
182
|
+
fields: ['id', 'code', 'apis']
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 查询所有接口
|
|
186
|
+
const allApis = await helper.getAll({
|
|
187
|
+
table: 'core_api',
|
|
188
|
+
fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const redis = Database.getRedis();
|
|
192
|
+
let cachedRoles = 0;
|
|
193
|
+
|
|
194
|
+
// 为每个角色缓存接口权限
|
|
195
|
+
for (const role of roles) {
|
|
196
|
+
if (!role.apis) continue;
|
|
197
|
+
|
|
198
|
+
// 解析角色的接口 ID 列表
|
|
199
|
+
const apiIds = role.apis
|
|
200
|
+
.split(',')
|
|
201
|
+
.map((id: string) => parseInt(id.trim()))
|
|
202
|
+
.filter((id: number) => !isNaN(id));
|
|
203
|
+
|
|
204
|
+
// 根据 ID 过滤出接口路径
|
|
205
|
+
const roleApiPaths = allApis.filter((api: any) => apiIds.includes(api.id)).map((api: any) => `${api.method}${api.path}`);
|
|
206
|
+
|
|
207
|
+
if (roleApiPaths.length === 0) continue;
|
|
208
|
+
|
|
209
|
+
// 使用 Redis Set 缓存角色权限
|
|
210
|
+
const redisKey = `role:apis:${role.code}`;
|
|
211
|
+
|
|
212
|
+
// 先删除旧数据
|
|
213
|
+
await redis.del(redisKey);
|
|
214
|
+
|
|
215
|
+
// 批量添加到 Set
|
|
216
|
+
const result = await redis.sadd(redisKey, roleApiPaths);
|
|
217
|
+
|
|
218
|
+
if (result > 0) {
|
|
219
|
+
cachedRoles++;
|
|
220
|
+
Logger.debug(` └ 角色 ${role.code}: ${result} 个接口`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Logger.info(`✅ 已缓存 ${cachedRoles} 个角色的接口权限`);
|
|
225
|
+
}
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
Logger.error('⚠️ 角色权限缓存异常:', error);
|
|
228
|
+
}
|
|
168
229
|
} catch (error: any) {
|
|
169
230
|
Logger.error('开发管理员同步失败:', error);
|
|
170
231
|
process.exit(1);
|
package/commands/syncMenu.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { Logger } from '../lib/logger.js';
|
|
18
18
|
import { Database } from '../lib/database.js';
|
|
19
|
+
import { RedisHelper } from '../lib/redisHelper.js';
|
|
19
20
|
import { join } from 'pathe';
|
|
20
21
|
import { existsSync } from 'node:fs';
|
|
21
22
|
import { coreDir, projectDir } from '../paths.js';
|
|
@@ -370,7 +371,26 @@ export async function syncMenuCommand(options: SyncMenuOptions = {}) {
|
|
|
370
371
|
Logger.info(`删除菜单: ${deletedCount} 个`);
|
|
371
372
|
Logger.info(`当前父级菜单: ${allMenus.filter((m: any) => m.pid === 0).length} 个`);
|
|
372
373
|
Logger.info(`当前子级菜单: ${allMenus.filter((m: any) => m.pid !== 0).length} 个`);
|
|
373
|
-
|
|
374
|
+
|
|
375
|
+
// 9. 缓存菜单数据到 Redis
|
|
376
|
+
Logger.info('\n=== 步骤 8: 缓存菜单数据到 Redis ===');
|
|
377
|
+
try {
|
|
378
|
+
const menus = await helper.getAll({
|
|
379
|
+
table: 'core_menu',
|
|
380
|
+
fields: ['id', 'pid', 'name', 'path', 'icon', 'type', 'sort'],
|
|
381
|
+
orderBy: ['sort#ASC', 'id#ASC']
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const result = await RedisHelper.setObject('menus:all', menus);
|
|
385
|
+
|
|
386
|
+
if (result === null) {
|
|
387
|
+
Logger.warn('⚠️ 菜单缓存失败');
|
|
388
|
+
} else {
|
|
389
|
+
Logger.info(`✅ 已缓存 ${menus.length} 个菜单到 Redis (Key: menus:all)`);
|
|
390
|
+
}
|
|
391
|
+
} catch (error: any) {
|
|
392
|
+
Logger.error('⚠️ 菜单缓存异常:', error);
|
|
393
|
+
}
|
|
374
394
|
} catch (error: any) {
|
|
375
395
|
Logger.error('菜单同步失败:', error);
|
|
376
396
|
process.exit(1);
|
package/lib/database.ts
CHANGED
package/lifecycle/lifecycle.ts
CHANGED
|
@@ -50,14 +50,7 @@ export class Lifecycle {
|
|
|
50
50
|
// 3. 加载所有 API(addon + app)
|
|
51
51
|
await this.loadAllApis();
|
|
52
52
|
|
|
53
|
-
// 4.
|
|
54
|
-
if (appContext.cache && appContext.redis) {
|
|
55
|
-
await appContext.cache.cacheAll();
|
|
56
|
-
} else {
|
|
57
|
-
Logger.warn('⚠️ Redis 或 Cache 插件未启用,跳过数据缓存');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 5. 启动 HTTP 服务器
|
|
53
|
+
// 4. 启动 HTTP 服务器
|
|
61
54
|
const totalStartupTime = calcPerfTime(serverStartTime);
|
|
62
55
|
Logger.info(`服务器启动准备完成,总耗时: ${totalStartupTime}`);
|
|
63
56
|
|
package/main.ts
CHANGED
|
@@ -29,11 +29,37 @@ export class Befly {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* 启动服务器并注册优雅关闭处理
|
|
33
33
|
* @param callback - 启动完成后的回调函数
|
|
34
34
|
*/
|
|
35
35
|
async listen(callback?: (server: Server) => void): Promise<Server> {
|
|
36
|
-
|
|
36
|
+
const server = await this.lifecycle.start(this.appContext, callback);
|
|
37
|
+
|
|
38
|
+
// 注册优雅关闭信号处理器
|
|
39
|
+
const gracefulShutdown = async (signal: string) => {
|
|
40
|
+
Logger.info(`\n收到 ${signal} 信号,开始优雅关闭...`);
|
|
41
|
+
|
|
42
|
+
// 1. 停止接收新请求
|
|
43
|
+
server.stop(true);
|
|
44
|
+
Logger.info('✅ HTTP 服务器已停止');
|
|
45
|
+
|
|
46
|
+
// 2. 关闭数据库连接
|
|
47
|
+
try {
|
|
48
|
+
const { Database } = await import('./lib/database.js');
|
|
49
|
+
await Database.disconnect();
|
|
50
|
+
Logger.info('✅ 数据库连接已关闭');
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
Logger.warn('⚠️ 关闭数据库连接时出错:', error.message);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Logger.info('✅ 服务器已优雅关闭');
|
|
56
|
+
process.exit(0);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
60
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
61
|
+
|
|
62
|
+
return server;
|
|
37
63
|
}
|
|
38
64
|
}
|
|
39
65
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.13",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -81,5 +81,5 @@
|
|
|
81
81
|
"ora": "^9.0.0",
|
|
82
82
|
"pathe": "^2.0.3"
|
|
83
83
|
},
|
|
84
|
-
"gitHead": "
|
|
84
|
+
"gitHead": "f37b14e9acad47245d399709829013fda8a0c44e"
|
|
85
85
|
}
|
package/bin/launcher.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly CLI 环境启动器
|
|
3
|
-
* 负责检测环境参数并在正确的环境中启动子进程
|
|
4
|
-
*
|
|
5
|
-
* 工作原理:
|
|
6
|
-
* 1. 父进程:检测 --env 参数 → 启动子进程 → 退出
|
|
7
|
-
* 2. 子进程:跳过启动逻辑 → 继续执行 CLI 命令
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readdirSync, existsSync } from 'node:fs';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 解析环境名称
|
|
15
|
-
* 支持前缀匹配(至少3个字符)
|
|
16
|
-
*
|
|
17
|
-
* @param input 用户输入的环境名称
|
|
18
|
-
* @returns 完整的环境名称
|
|
19
|
-
*/
|
|
20
|
-
function resolveEnvName(input: string): string {
|
|
21
|
-
// 如果输入少于3个字符,直接返回(不做匹配)
|
|
22
|
-
if (input.length < 3) {
|
|
23
|
-
return input;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// 获取项目根目录的所有 .env.* 文件
|
|
27
|
-
const rootDir = process.cwd();
|
|
28
|
-
let envFiles: string[] = [];
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
const files = readdirSync(rootDir);
|
|
32
|
-
envFiles = files.filter((file) => file.startsWith('.env.') && file !== '.env.example').map((file) => file.replace('.env.', ''));
|
|
33
|
-
} catch (error) {
|
|
34
|
-
// 读取失败时直接返回原始输入
|
|
35
|
-
return input;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 精确匹配
|
|
39
|
-
if (envFiles.includes(input)) {
|
|
40
|
-
return input;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 前缀匹配
|
|
44
|
-
const matches = envFiles.filter((env) => env.startsWith(input.toLowerCase()));
|
|
45
|
-
|
|
46
|
-
if (matches.length === 1) {
|
|
47
|
-
return matches[0];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (matches.length > 1) {
|
|
51
|
-
console.error(`❌ 环境名称 "${input}" 匹配到多个环境: ${matches.join(', ')}`);
|
|
52
|
-
console.error('请使用更具体的名称');
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 未匹配到,返回原始输入(让 Bun 处理文件不存在的情况)
|
|
57
|
-
return input;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 启动环境检测和子进程
|
|
62
|
-
* 如果在父进程中,会启动子进程并退出(不返回)
|
|
63
|
-
* 如果在子进程中,直接返回(继续执行)
|
|
64
|
-
*
|
|
65
|
-
* @param entryFile CLI 入口文件的绝对路径(通常是 bin/index.ts)
|
|
66
|
-
*/
|
|
67
|
-
export async function launch(entryFile: string): Promise<void> {
|
|
68
|
-
// 如果已经在子进程中,直接返回
|
|
69
|
-
if (process.env.BEFLY_SUBPROCESS) {
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// 确定环境名称
|
|
74
|
-
const longEnvIndex = process.argv.indexOf('--env');
|
|
75
|
-
const shortEnvIndex = process.argv.indexOf('-e');
|
|
76
|
-
let envInput = 'development'; // 默认环境
|
|
77
|
-
|
|
78
|
-
// 检查 --env 或 -e 参数
|
|
79
|
-
if (longEnvIndex !== -1 && process.argv[longEnvIndex + 1]) {
|
|
80
|
-
envInput = process.argv[longEnvIndex + 1];
|
|
81
|
-
} else if (shortEnvIndex !== -1 && process.argv[shortEnvIndex + 1]) {
|
|
82
|
-
envInput = process.argv[shortEnvIndex + 1];
|
|
83
|
-
} else if (process.env.NODE_ENV) {
|
|
84
|
-
// 如果设置了 NODE_ENV 环境变量
|
|
85
|
-
envInput = process.env.NODE_ENV;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 解析环境名称(支持前缀匹配)
|
|
89
|
-
const envName = resolveEnvName(envInput);
|
|
90
|
-
const envFile = `.env.${envName}`;
|
|
91
|
-
|
|
92
|
-
// 过滤掉 --env/-e 参数(已通过 --env-file 处理)
|
|
93
|
-
const filteredArgs = process.argv.slice(2).filter((arg, i, arr) => {
|
|
94
|
-
if (arg === '--env' || arg === '-e') return false;
|
|
95
|
-
if (arr[i - 1] === '--env' || arr[i - 1] === '-e') return false;
|
|
96
|
-
return true;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// 启动子进程,使用指定的环境文件
|
|
100
|
-
const proc = Bun.spawn(['bun', 'run', `--env-file=${envFile}`, entryFile, ...filteredArgs], {
|
|
101
|
-
cwd: process.cwd(),
|
|
102
|
-
stdio: ['inherit', 'inherit', 'inherit'],
|
|
103
|
-
env: {
|
|
104
|
-
// 不继承父进程的环境变量,只使用以下变量
|
|
105
|
-
BEFLY_SUBPROCESS: '1', // 标记为子进程
|
|
106
|
-
NODE_ENV: envName, // 设置 NODE_ENV
|
|
107
|
-
PATH: process.env.PATH, // 保留 PATH
|
|
108
|
-
SYSTEMROOT: process.env.SYSTEMROOT, // Windows 需要
|
|
109
|
-
TEMP: process.env.TEMP, // Windows 需要
|
|
110
|
-
TMP: process.env.TMP // Windows 需要
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// 等待子进程结束并退出(父进程不会返回)
|
|
115
|
-
process.exit(await proc.exited);
|
|
116
|
-
}
|
package/commands/dev.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev 命令 - 启动开发服务器
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { join } from 'pathe';
|
|
6
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
import { Befly } from '../main.js';
|
|
9
|
-
import { getProjectRoot } from './util.js';
|
|
10
|
-
|
|
11
|
-
interface DevOptions {
|
|
12
|
-
port: string;
|
|
13
|
-
host: string;
|
|
14
|
-
sync: boolean;
|
|
15
|
-
verbose: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function devCommand(options: DevOptions) {
|
|
19
|
-
try {
|
|
20
|
-
const projectRoot = getProjectRoot();
|
|
21
|
-
|
|
22
|
-
// 验证是否在 Befly 项目目录下
|
|
23
|
-
const packageJsonPath = join(projectRoot, 'package.json');
|
|
24
|
-
if (!existsSync(packageJsonPath)) {
|
|
25
|
-
Logger.error('未找到 package.json 文件,请确保在项目目录下');
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 检查并创建 logs 目录
|
|
30
|
-
const logsDir = join(projectRoot, 'logs');
|
|
31
|
-
if (!existsSync(logsDir)) {
|
|
32
|
-
mkdirSync(logsDir, { recursive: true });
|
|
33
|
-
Logger.info('已创建 logs 目录');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 设置环境变量
|
|
37
|
-
process.env.NODE_ENV = 'development';
|
|
38
|
-
process.env.APP_PORT = options.port;
|
|
39
|
-
process.env.APP_HOST = options.host;
|
|
40
|
-
|
|
41
|
-
if (options.verbose) {
|
|
42
|
-
process.env.LOG_DEBUG = '1';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
Logger.info('正在启动开发服务器...\n');
|
|
46
|
-
Logger.info(`端口: ${options.port}`);
|
|
47
|
-
Logger.info(`主机: ${options.host}`);
|
|
48
|
-
Logger.info(`环境: development\n`);
|
|
49
|
-
|
|
50
|
-
// 检查环境变量文件
|
|
51
|
-
const envFile = join(projectRoot, '.env.development');
|
|
52
|
-
if (existsSync(envFile)) {
|
|
53
|
-
Logger.info(`环境变量文件: .env.development\n`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 直接启动 Befly 实例
|
|
57
|
-
const app = new Befly();
|
|
58
|
-
const server = await app.listen();
|
|
59
|
-
|
|
60
|
-
// 设置信号处理,确保优雅关闭
|
|
61
|
-
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
62
|
-
signals.forEach((signal) => {
|
|
63
|
-
process.on(signal, async () => {
|
|
64
|
-
Logger.info(`\n收到 ${signal} 信号,正在关闭开发服务器...`);
|
|
65
|
-
try {
|
|
66
|
-
server.stop(true);
|
|
67
|
-
Logger.info('开发服务器已关闭');
|
|
68
|
-
process.exit(0);
|
|
69
|
-
} catch (error) {
|
|
70
|
-
Logger.error('关闭开发服务器失败:', error);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
} catch (error) {
|
|
76
|
-
Logger.error('启动开发服务器失败:');
|
|
77
|
-
console.error(error);
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
}
|
package/commands/start.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Start 命令 - 启动生产服务器
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { join } from 'pathe';
|
|
6
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
import { ClusterManager } from '../lifecycle/cluster.js';
|
|
9
|
-
import { Befly } from '../main.js';
|
|
10
|
-
import { getProjectRoot } from './util.js';
|
|
11
|
-
|
|
12
|
-
interface StartOptions {
|
|
13
|
-
port: string;
|
|
14
|
-
host: string;
|
|
15
|
-
cluster?: string; // 集群模式:数字或 'max'
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function startCommand(options: StartOptions) {
|
|
19
|
-
try {
|
|
20
|
-
const projectRoot = getProjectRoot();
|
|
21
|
-
|
|
22
|
-
// 验证是否在项目目录下
|
|
23
|
-
const packageJsonPath = join(projectRoot, 'package.json');
|
|
24
|
-
if (!existsSync(packageJsonPath)) {
|
|
25
|
-
Logger.error('未找到 package.json 文件,请确保在项目目录下');
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 检查并创建 logs 目录
|
|
30
|
-
const logsDir = join(projectRoot, 'logs');
|
|
31
|
-
if (!existsSync(logsDir)) {
|
|
32
|
-
mkdirSync(logsDir, { recursive: true });
|
|
33
|
-
Logger.info('已创建 logs 目录');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// 设置生产环境变量
|
|
37
|
-
process.env.NODE_ENV = 'production';
|
|
38
|
-
process.env.APP_PORT = options.port;
|
|
39
|
-
process.env.APP_HOST = options.host;
|
|
40
|
-
|
|
41
|
-
// 检查是否使用集群模式
|
|
42
|
-
if (options.cluster) {
|
|
43
|
-
// 集群模式
|
|
44
|
-
const instances = options.cluster === 'max' ? 'max' : parseInt(options.cluster);
|
|
45
|
-
|
|
46
|
-
const clusterManager = new ClusterManager({
|
|
47
|
-
instances,
|
|
48
|
-
startPort: parseInt(options.port),
|
|
49
|
-
host: options.host,
|
|
50
|
-
projectRoot
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
await clusterManager.start();
|
|
54
|
-
} else {
|
|
55
|
-
// 单进程模式
|
|
56
|
-
Logger.info('正在启动生产服务器...\n');
|
|
57
|
-
Logger.info(`端口: ${options.port}`);
|
|
58
|
-
Logger.info(`主机: ${options.host}`);
|
|
59
|
-
Logger.info(`环境: production\n`);
|
|
60
|
-
|
|
61
|
-
// 检查环境变量文件
|
|
62
|
-
const envFile = join(projectRoot, '.env.production');
|
|
63
|
-
if (existsSync(envFile)) {
|
|
64
|
-
Logger.info(`环境变量文件: .env.production\n`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 直接启动 Befly 实例
|
|
68
|
-
const app = new Befly();
|
|
69
|
-
const server = await app.listen();
|
|
70
|
-
|
|
71
|
-
// 设置信号处理,确保优雅关闭
|
|
72
|
-
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
73
|
-
signals.forEach((signal) => {
|
|
74
|
-
process.on(signal, async () => {
|
|
75
|
-
Logger.info(`\n收到 ${signal} 信号,正在关闭生产服务器...`);
|
|
76
|
-
try {
|
|
77
|
-
server.stop(true);
|
|
78
|
-
Logger.info('生产服务器已关闭');
|
|
79
|
-
process.exit(0);
|
|
80
|
-
} catch (error) {
|
|
81
|
-
Logger.error('关闭生产服务器失败:', error);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
} catch (error) {
|
|
88
|
-
Logger.error('启动失败:');
|
|
89
|
-
console.error(error);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cluster Worker 入口
|
|
3
|
-
* 由 ClusterManager 启动的子进程入口文件
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Befly } from '../main.js';
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
|
|
9
|
-
// 启动 Befly 实例
|
|
10
|
-
const app = new Befly();
|
|
11
|
-
const server = await app.listen();
|
|
12
|
-
|
|
13
|
-
// Bun 原生信号处理:当收到 SIGTERM/SIGINT 时优雅关闭
|
|
14
|
-
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
|
15
|
-
|
|
16
|
-
signals.forEach((signal) => {
|
|
17
|
-
process.on(signal, async () => {
|
|
18
|
-
const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
|
|
19
|
-
Logger.info(`Worker ${workerId} 收到 ${signal} 信号,正在关闭...`);
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
// Bun Server 的 stop() 方法:关闭服务器
|
|
23
|
-
// 参数 true 表示强制关闭(不等待现有连接)
|
|
24
|
-
server.stop(true);
|
|
25
|
-
Logger.info(`Worker ${workerId} HTTP 服务器已关闭`);
|
|
26
|
-
|
|
27
|
-
// 给予短暂时间让资源清理完成
|
|
28
|
-
await Bun.sleep(100);
|
|
29
|
-
|
|
30
|
-
process.exit(0);
|
|
31
|
-
} catch (error) {
|
|
32
|
-
Logger.error(`Worker ${workerId} 关闭失败:`, error);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// 处理未捕获的异常,防止进程意外退出
|
|
39
|
-
process.on('uncaughtException', (error) => {
|
|
40
|
-
const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
|
|
41
|
-
Logger.error(`Worker ${workerId} 发生未捕获异常:`, error);
|
|
42
|
-
// 不退出进程,让 ClusterManager 决定是否重启
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
process.on('unhandledRejection', (reason) => {
|
|
46
|
-
const workerId = process.env.CLUSTER_WORKER_ID || 'unknown';
|
|
47
|
-
Logger.error(`Worker ${workerId} 发生未处理的 Promise 拒绝:`, reason);
|
|
48
|
-
// 不退出进程,让 ClusterManager 决定是否重启
|
|
49
|
-
});
|
package/lifecycle/cluster.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cluster Manager - 集群管理器
|
|
3
|
-
* 负责多进程启动、自动重启、优雅关闭
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { join } from 'pathe';
|
|
7
|
-
import type { Subprocess } from 'bun';
|
|
8
|
-
import { Logger } from '../lib/logger.js';
|
|
9
|
-
import { Befly } from '../main.js';
|
|
10
|
-
|
|
11
|
-
export interface ClusterOptions {
|
|
12
|
-
/** 实例数量(数字或 'max') */
|
|
13
|
-
instances: number | 'max';
|
|
14
|
-
/** 起始端口 */
|
|
15
|
-
startPort: number;
|
|
16
|
-
/** 主机地址 */
|
|
17
|
-
host: string;
|
|
18
|
-
/** 项目根目录 */
|
|
19
|
-
projectRoot: string;
|
|
20
|
-
/** 环境变量 */
|
|
21
|
-
env?: Record<string, string>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface WorkerInfo {
|
|
25
|
-
id: number;
|
|
26
|
-
port: number;
|
|
27
|
-
process: Subprocess;
|
|
28
|
-
restartCount: number;
|
|
29
|
-
lastRestartTime: number;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class ClusterManager {
|
|
33
|
-
private workers: Map<number, WorkerInfo> = new Map();
|
|
34
|
-
private isShuttingDown = false;
|
|
35
|
-
private readonly MAX_RESTARTS = 10; // 最大重启次数
|
|
36
|
-
private readonly RESTART_DELAY = 1000; // 重启延迟(毫秒)
|
|
37
|
-
private readonly RESTART_WINDOW = 60000; // 重启计数窗口(1分钟)
|
|
38
|
-
|
|
39
|
-
constructor(private options: ClusterOptions) {}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 启动集群
|
|
43
|
-
*/
|
|
44
|
-
async start(): Promise<void> {
|
|
45
|
-
const instances = this.getInstanceCount();
|
|
46
|
-
const { startPort, host } = this.options;
|
|
47
|
-
|
|
48
|
-
Logger.info(`启动集群模式: ${instances} 个实例\n`);
|
|
49
|
-
Logger.info(`端口范围: ${startPort} - ${startPort + instances - 1}`);
|
|
50
|
-
Logger.info(`主机地址: ${host}`);
|
|
51
|
-
Logger.info(`环境: production\n`);
|
|
52
|
-
|
|
53
|
-
// 启动所有 Worker
|
|
54
|
-
for (let i = 0; i < instances; i++) {
|
|
55
|
-
const port = startPort + i;
|
|
56
|
-
this.spawnWorker(i, port);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 监听进程信号
|
|
60
|
-
this.setupSignalHandlers();
|
|
61
|
-
|
|
62
|
-
Logger.info(`集群启动成功!\n`);
|
|
63
|
-
this.printWorkerStatus();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* 获取实例数量
|
|
68
|
-
*/
|
|
69
|
-
private getInstanceCount(): number {
|
|
70
|
-
const { instances } = this.options;
|
|
71
|
-
|
|
72
|
-
if (instances === 'max') {
|
|
73
|
-
return navigator.hardwareConcurrency || 4;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const count = typeof instances === 'string' ? parseInt(instances) : instances;
|
|
77
|
-
|
|
78
|
-
if (isNaN(count) || count < 1) {
|
|
79
|
-
Logger.warn(`无效的实例数量 "${instances}",使用默认值 4`);
|
|
80
|
-
return 4;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return count;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 启动单个 Worker
|
|
88
|
-
*/
|
|
89
|
-
private spawnWorker(id: number, port: number): void {
|
|
90
|
-
const { projectRoot, host, env = {} } = this.options;
|
|
91
|
-
|
|
92
|
-
Logger.info(`启动 Worker ${id} (端口 ${port})...`);
|
|
93
|
-
|
|
94
|
-
// 使用内置的 worker 入口文件
|
|
95
|
-
const workerFile = join(import.meta.dir, 'cluster-worker.ts');
|
|
96
|
-
|
|
97
|
-
const proc = Bun.spawn(['bun', 'run', '--env-file=.env.production', workerFile], {
|
|
98
|
-
cwd: projectRoot,
|
|
99
|
-
stdout: 'inherit',
|
|
100
|
-
stderr: 'inherit',
|
|
101
|
-
stdin: 'inherit',
|
|
102
|
-
env: {
|
|
103
|
-
...process.env,
|
|
104
|
-
...env,
|
|
105
|
-
NODE_ENV: 'production',
|
|
106
|
-
APP_PORT: port.toString(),
|
|
107
|
-
APP_HOST: host,
|
|
108
|
-
CLUSTER_MODE: '1',
|
|
109
|
-
CLUSTER_WORKER_ID: id.toString(),
|
|
110
|
-
CLUSTER_INSTANCES: this.getInstanceCount().toString(),
|
|
111
|
-
FORCE_COLOR: '1'
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// 保存 Worker 信息
|
|
116
|
-
this.workers.set(id, {
|
|
117
|
-
id,
|
|
118
|
-
port,
|
|
119
|
-
process: proc,
|
|
120
|
-
restartCount: 0,
|
|
121
|
-
lastRestartTime: 0
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// 监听进程退出
|
|
125
|
-
this.watchWorker(id, port);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 监听 Worker 退出并自动重启
|
|
130
|
-
*/
|
|
131
|
-
private async watchWorker(id: number, port: number): Promise<void> {
|
|
132
|
-
const worker = this.workers.get(id);
|
|
133
|
-
if (!worker) return;
|
|
134
|
-
|
|
135
|
-
const exitCode = await worker.process.exited;
|
|
136
|
-
|
|
137
|
-
// 如果正在关闭,不重启
|
|
138
|
-
if (this.isShuttingDown) {
|
|
139
|
-
Logger.info(`Worker ${id} (端口 ${port}) 已退出`);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
Logger.warn(`Worker ${id} (端口 ${port}) 异常退出,退出码: ${exitCode}`);
|
|
144
|
-
|
|
145
|
-
// 检查重启次数
|
|
146
|
-
if (!this.canRestart(worker)) {
|
|
147
|
-
Logger.error(`Worker ${id} 重启次数过多,停止重启`);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// 延迟重启
|
|
152
|
-
Logger.info(`${this.RESTART_DELAY / 1000} 秒后重启 Worker ${id}...`);
|
|
153
|
-
await Bun.sleep(this.RESTART_DELAY);
|
|
154
|
-
|
|
155
|
-
// 更新重启计数
|
|
156
|
-
this.updateRestartCount(worker);
|
|
157
|
-
|
|
158
|
-
// 重新启动
|
|
159
|
-
this.spawnWorker(id, port);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 检查是否可以重启
|
|
164
|
-
*/
|
|
165
|
-
private canRestart(worker: WorkerInfo): boolean {
|
|
166
|
-
const now = Date.now();
|
|
167
|
-
const timeSinceLastRestart = now - worker.lastRestartTime;
|
|
168
|
-
|
|
169
|
-
// 如果距离上次重启超过窗口期,重置计数
|
|
170
|
-
if (timeSinceLastRestart > this.RESTART_WINDOW) {
|
|
171
|
-
worker.restartCount = 0;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return worker.restartCount < this.MAX_RESTARTS;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 更新重启计数
|
|
179
|
-
*/
|
|
180
|
-
private updateRestartCount(worker: WorkerInfo): void {
|
|
181
|
-
worker.restartCount++;
|
|
182
|
-
worker.lastRestartTime = Date.now();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* 设置信号处理器
|
|
187
|
-
*/
|
|
188
|
-
private setupSignalHandlers(): void {
|
|
189
|
-
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT', 'SIGHUP'];
|
|
190
|
-
|
|
191
|
-
signals.forEach((signal) => {
|
|
192
|
-
process.on(signal, () => {
|
|
193
|
-
this.gracefulShutdown(signal);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* 优雅关闭
|
|
200
|
-
*/
|
|
201
|
-
private async gracefulShutdown(signal: NodeJS.Signals): Promise<void> {
|
|
202
|
-
if (this.isShuttingDown) return;
|
|
203
|
-
this.isShuttingDown = true;
|
|
204
|
-
|
|
205
|
-
Logger.info(`\n收到 ${signal} 信号,正在关闭集群...`);
|
|
206
|
-
|
|
207
|
-
// 向所有 Worker 发送 SIGTERM
|
|
208
|
-
for (const [id, worker] of this.workers.entries()) {
|
|
209
|
-
Logger.info(`关闭 Worker ${id} (端口 ${worker.port})...`);
|
|
210
|
-
try {
|
|
211
|
-
worker.process.kill('SIGTERM');
|
|
212
|
-
} catch (error) {
|
|
213
|
-
Logger.warn(`无法向 Worker ${id} 发送 SIGTERM:`, error);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 等待所有进程退出,最多 3 秒(Bun 的 server.stop() 很快)
|
|
218
|
-
const timeout = setTimeout(() => {
|
|
219
|
-
Logger.warn('等待超时(3秒),强制关闭所有 Worker');
|
|
220
|
-
for (const worker of this.workers.values()) {
|
|
221
|
-
try {
|
|
222
|
-
worker.process.kill('SIGKILL');
|
|
223
|
-
} catch (error) {
|
|
224
|
-
// 忽略 SIGKILL 失败(进程可能已退出)
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
// 强制退出主进程
|
|
228
|
-
setTimeout(() => process.exit(1), 500);
|
|
229
|
-
}, 3000);
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
// 等待所有进程退出
|
|
233
|
-
await Promise.all(Array.from(this.workers.values()).map((w) => w.process.exited));
|
|
234
|
-
clearTimeout(timeout);
|
|
235
|
-
Logger.info('集群已安全关闭');
|
|
236
|
-
process.exit(0);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
clearTimeout(timeout);
|
|
239
|
-
Logger.error('等待 Worker 退出时发生错误:', error);
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 打印 Worker 状态
|
|
246
|
-
*/
|
|
247
|
-
private printWorkerStatus(): void {
|
|
248
|
-
Logger.info('Worker 列表:');
|
|
249
|
-
for (const worker of this.workers.values()) {
|
|
250
|
-
Logger.info(` - Worker ${worker.id}: http://${this.options.host}:${worker.port}`);
|
|
251
|
-
}
|
|
252
|
-
Logger.info('');
|
|
253
|
-
}
|
|
254
|
-
}
|