mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-18 14:30:29 +00:00
Removed excessive logging and streamlined process restart and shutdown flows in napcat.ts. Added isShuttingDown flag to prevent unintended worker restarts during shutdown. Improved forceKillProcess to handle Windows-specific process termination. Updated IWorkerProcess interface and implementations to include the 'off' event method for better event management.
334 lines
9.5 KiB
TypeScript
334 lines
9.5 KiB
TypeScript
import { NCoreInitShell } from './base';
|
||
import { NapCatPathWrapper } from '@/napcat-common/src/path';
|
||
import { LogWrapper } from '@/napcat-core/helper/log';
|
||
import { connectToNamedPipe } from './pipe';
|
||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||
import { AuthHelper } from '@/napcat-webui-backend/src/helper/SignToken';
|
||
import { createProcessManager, type IProcessManager, type IWorkerProcess } from './process-api';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
|
||
// ES 模块中获取 __dirname
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
// 环境变量配置
|
||
const ENV = {
|
||
isWorkerProcess: process.env['NAPCAT_WORKER_PROCESS'] === '1',
|
||
isMultiProcessDisabled: process.env['NAPCAT_DISABLE_MULTI_PROCESS'] === '1',
|
||
isPipeDisabled: process.env['NAPCAT_DISABLE_PIPE'] === '1',
|
||
} as const;
|
||
|
||
// 初始化日志
|
||
const pathWrapper = new NapCatPathWrapper();
|
||
const logger = new LogWrapper(pathWrapper.logsPath);
|
||
|
||
// 进程管理器和当前 Worker 进程引用
|
||
let processManager: IProcessManager | null = null;
|
||
let currentWorker: IWorkerProcess | null = null;
|
||
let isElectron = false;
|
||
let isRestarting = false;
|
||
let isShuttingDown = false;
|
||
|
||
/**
|
||
* 获取进程类型名称(用于日志)
|
||
*/
|
||
function getProcessTypeName (): string {
|
||
return isElectron ? 'UtilityProcess' : 'Fork';
|
||
}
|
||
|
||
/**
|
||
* 获取 Worker 脚本路径
|
||
*/
|
||
function getWorkerScriptPath (): string {
|
||
return __filename.endsWith('.mjs')
|
||
? path.join(__dirname, 'napcat.mjs')
|
||
: path.join(__dirname, 'napcat.js');
|
||
}
|
||
|
||
/**
|
||
* 检查进程是否存在
|
||
*/
|
||
function isProcessAlive (pid: number): boolean {
|
||
try {
|
||
process.kill(pid, 0);
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 强制终止进程
|
||
*/
|
||
function forceKillProcess (pid: number): void {
|
||
try {
|
||
process.kill(pid, 'SIGKILL');
|
||
} catch (error) {
|
||
// SIGKILL 失败,在 Windows 上使用 taskkill 兜底
|
||
if (process.platform === 'win32') {
|
||
try {
|
||
require('child_process').execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
||
} catch {
|
||
logger.logError(`[NapCat] [Process] 强制终止进程失败: PID ${pid}`);
|
||
}
|
||
} else {
|
||
logger.logError(`[NapCat] [Process] 强制终止进程失败:`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 重启 Worker 进程
|
||
*/
|
||
export async function restartWorker (secretKey?: string): Promise<void> {
|
||
isRestarting = true;
|
||
|
||
if (!currentWorker) {
|
||
logger.logWarn('[NapCat] [Process] 没有运行中的Worker进程');
|
||
await startWorker(false);
|
||
isRestarting = false;
|
||
return;
|
||
}
|
||
|
||
const workerPid = currentWorker.pid;
|
||
|
||
// 1. 通知旧进程准备重启(旧进程会自行退出)
|
||
currentWorker.postMessage({ type: 'restart-prepare' });
|
||
|
||
// 2. 等待进程退出(最多 5 秒,给更多时间让进程自行清理)
|
||
await new Promise<void>((resolve) => {
|
||
const timeout = setTimeout(() => {
|
||
logger.logWarn('[NapCat] [Process] Worker进程未在 5 秒内退出,尝试发送强制关闭信号');
|
||
currentWorker?.postMessage({ type: 'shutdown' });
|
||
|
||
// 再等待 2 秒
|
||
setTimeout(() => {
|
||
logger.logWarn('[NapCat] [Process] Worker进程仍未退出,尝试 kill');
|
||
currentWorker?.kill();
|
||
resolve();
|
||
}, 2000);
|
||
}, 5000);
|
||
|
||
currentWorker?.once('exit', () => {
|
||
clearTimeout(timeout);
|
||
resolve();
|
||
});
|
||
});
|
||
|
||
// 3. 二次确认进程是否真的被终止(兜底检查)
|
||
if (workerPid && isProcessAlive(workerPid)) {
|
||
logger.logWarn(`[NapCat] [Process] 进程 ${workerPid} 仍在运行,尝试强制杀掉`);
|
||
forceKillProcess(workerPid);
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
if (isProcessAlive(workerPid)) {
|
||
logger.logError(`[NapCat] [Process] 进程 ${workerPid} 无法终止,可能需要手动处理`);
|
||
}
|
||
}
|
||
|
||
// 4. 等待后启动新进程
|
||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||
|
||
// 5. 启动新进程(重启模式不传递快速登录参数,传递密钥)
|
||
await startWorker(false, secretKey);
|
||
isRestarting = false;
|
||
}
|
||
|
||
/**
|
||
* 启动 Worker 进程
|
||
* @param passQuickLogin 是否传递快速登录参数,默认为 true,重启时为 false
|
||
*/
|
||
async function startWorker (passQuickLogin: boolean = true, secretKey?: string): Promise<void> {
|
||
if (!processManager) {
|
||
throw new Error('进程管理器未初始化');
|
||
}
|
||
|
||
const workerScript = getWorkerScriptPath();
|
||
const processType = getProcessTypeName();
|
||
|
||
// 只在首次启动时传递 -q 或 --qq 参数给 worker 进程
|
||
const workerArgs: string[] = [];
|
||
if (passQuickLogin) {
|
||
const args = process.argv.slice(2);
|
||
const qIndex = args.findIndex(arg => arg === '-q' || arg === '--qq');
|
||
if (qIndex !== -1 && qIndex + 1 < args.length) {
|
||
const qFlag = args[qIndex];
|
||
const qValue = args[qIndex + 1];
|
||
if (qFlag && qValue) {
|
||
workerArgs.push(qFlag, qValue);
|
||
}
|
||
}
|
||
}
|
||
|
||
const child = processManager.createWorker(workerScript, workerArgs, {
|
||
env: {
|
||
...process.env,
|
||
NAPCAT_WORKER_PROCESS: '1',
|
||
...(secretKey ? { NAPCAT_WEBUI_JWT_SECRET_KEY: secretKey } : {}),
|
||
},
|
||
stdio: isElectron ? 'pipe' : ['inherit', 'pipe', 'pipe', 'ipc'],
|
||
});
|
||
|
||
currentWorker = child;
|
||
|
||
// 监听标准输出(直接转发)
|
||
if (child.stdout) {
|
||
child.stdout.on('data', (data: Buffer) => {
|
||
process.stdout.write(data);
|
||
});
|
||
}
|
||
|
||
// 监听标准错误(直接转发)
|
||
if (child.stderr) {
|
||
child.stderr.on('data', (data: Buffer) => {
|
||
process.stderr.write(data);
|
||
});
|
||
}
|
||
|
||
// 监听子进程消息
|
||
child.on('message', (msg: unknown) => {
|
||
// 处理重启请求
|
||
if (typeof msg === 'object' && msg !== null && 'type' in msg && msg.type === 'restart') {
|
||
const secretKey = 'secretKey' in msg ? (msg as any).secretKey : undefined;
|
||
restartWorker(secretKey).catch(e => {
|
||
logger.logError(`[NapCat] [${processType}] 重启Worker进程失败:`, e);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 监听子进程退出
|
||
child.on('exit', (code: unknown) => {
|
||
const exitCode = typeof code === 'number' ? code : 0;
|
||
if (exitCode !== 0) {
|
||
logger.logError(`[NapCat] [${processType}] Worker进程退出,退出码: ${exitCode}`);
|
||
}
|
||
// 如果不是由于主动重启或关闭引起的退出,尝试自动重新拉起
|
||
if (!isRestarting && !isShuttingDown) {
|
||
logger.logWarn(`[NapCat] [${processType}] Worker进程意外退出,正在尝试重新拉起...`);
|
||
startWorker(true).catch(e => {
|
||
logger.logError(`[NapCat] [${processType}] 重新拉起Worker进程失败:`, e);
|
||
});
|
||
}
|
||
});
|
||
|
||
// 等待进程成功 spawn
|
||
await new Promise<void>((resolve, reject) => {
|
||
const onSpawn = () => {
|
||
child.off('error', onError);
|
||
resolve();
|
||
};
|
||
const onError = (...args: unknown[]) => {
|
||
const err = args[0] as Error;
|
||
logger.logError(`[NapCat] [${processType}] Worker进程启动失败:`, err);
|
||
child.off('spawn', onSpawn);
|
||
reject(err);
|
||
};
|
||
child.once('spawn', onSpawn);
|
||
child.once('error', onError);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 启动 Master 进程
|
||
*/
|
||
async function startMasterProcess (): Promise<void> {
|
||
// 连接命名管道(可通过环境变量禁用)
|
||
if (!ENV.isPipeDisabled) {
|
||
await connectToNamedPipe(logger).catch(e =>
|
||
logger.logError('命名管道连接失败', e)
|
||
);
|
||
}
|
||
|
||
// 启动 Worker 进程
|
||
await startWorker();
|
||
|
||
// 优雅关闭处理
|
||
const shutdown = () => {
|
||
isShuttingDown = true;
|
||
if (currentWorker) {
|
||
currentWorker.postMessage({ type: 'shutdown' });
|
||
setTimeout(() => {
|
||
currentWorker?.kill();
|
||
process.exit(0);
|
||
}, 1000);
|
||
} else {
|
||
process.exit(0);
|
||
}
|
||
};
|
||
|
||
process.on('SIGINT', () => shutdown());
|
||
process.on('SIGTERM', () => shutdown());
|
||
}
|
||
|
||
/**
|
||
* 启动 Worker 进程(子进程入口)
|
||
*/
|
||
async function startWorkerProcess (): Promise<void> {
|
||
if (!processManager) {
|
||
throw new Error('进程管理器未初始化');
|
||
}
|
||
|
||
// 监听来自父进程的消息
|
||
processManager.onParentMessage((msg: unknown) => {
|
||
if (typeof msg === 'object' && msg !== null && 'type' in msg) {
|
||
if (msg.type === 'restart-prepare' || msg.type === 'shutdown') {
|
||
setTimeout(() => {
|
||
process.exit(0);
|
||
}, 100);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 注册重启进程函数到 WebUI
|
||
WebUiDataRuntime.setRestartProcessCall(async () => {
|
||
try {
|
||
const success = processManager!.sendToParent({ type: 'restart', secretKey: AuthHelper.getSecretKey() });
|
||
|
||
|
||
if (success) {
|
||
return { result: true, message: '进程重启请求已发送' };
|
||
} else {
|
||
return { result: false, message: '无法与主进程通信' };
|
||
}
|
||
} catch (e) {
|
||
logger.logError('[NapCat] [Process] 发送重启请求失败:', e);
|
||
return {
|
||
result: false,
|
||
message: '发送重启请求失败: ' + (e as Error).message
|
||
};
|
||
}
|
||
});
|
||
|
||
// 启动 NapCat 核心
|
||
await NCoreInitShell();
|
||
}
|
||
|
||
/**
|
||
* 主入口
|
||
*/
|
||
async function main (): Promise<void> {
|
||
// 单进程模式:直接启动核心
|
||
if (ENV.isMultiProcessDisabled) {
|
||
await NCoreInitShell();
|
||
return;
|
||
}
|
||
|
||
// 多进程模式:初始化进程管理器
|
||
const result = await createProcessManager();
|
||
processManager = result.manager;
|
||
isElectron = result.isElectron;
|
||
|
||
// 根据进程类型启动
|
||
if (ENV.isWorkerProcess) {
|
||
await startWorkerProcess();
|
||
} else {
|
||
await startMasterProcess();
|
||
}
|
||
}
|
||
|
||
// 启动应用
|
||
main().catch((e: Error) => {
|
||
logger.logError('[NapCat] [Process] 启动失败:', e);
|
||
process.exit(1);
|
||
});
|