Files
pqs-9100_client/electron/preload/lifecycle.js
2025-12-10 18:47:17 +08:00

577 lines
22 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const path = require('path');
const { logger } = require('ee-core/log');
const { getConfig } = require('ee-core/config');
const { getMainWindow } = require('ee-core/electron');
const { getBaseDir } = require('ee-core/ps');
const { app } = require('electron');
// 动态获取 scripts 目录路径
function getScriptsPath(scriptName) {
const fs = require('fs');
// 判断是否是打包后的环境
// 只要 process.resourcesPath 存在,就是打包后的环境(无论在哪个目录)
const isProd = !!process.resourcesPath;
if (isProd) {
// 生产环境(打包后):从 resources 目录
const prodPath = path.join(process.resourcesPath, 'scripts', scriptName);
console.log(`[getScriptsPath] Production mode, using: ${prodPath}`);
return prodPath;
} else {
// 开发环境:从项目根目录
// __dirname 是 electron/preload 或 public/electron/preload
// 需要找到项目根目录
let currentDir = __dirname;
let scriptsPath = null;
// 向上查找,直到找到 scripts 目录
for (let i = 0; i < 5; i++) {
currentDir = path.join(currentDir, '..');
const testPath = path.join(currentDir, 'scripts', scriptName + '.js');
if (fs.existsSync(testPath)) {
scriptsPath = path.join(currentDir, 'scripts', scriptName);
console.log(`[getScriptsPath] Development mode, found at: ${scriptsPath}`);
return scriptsPath;
}
}
// 如果找不到,返回一个默认路径
console.warn(`[getScriptsPath] Cannot find ${scriptName}, returning default path`);
return path.join(__dirname, '../../../scripts', scriptName);
}
}
// 延迟加载 scripts
let MySQLServiceManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager;
function loadScripts() {
if (!MySQLServiceManager) {
MySQLServiceManager = require(getScriptsPath('mysql-service-manager'));
JavaRunner = require(getScriptsPath('java-runner'));
ConfigGenerator = require(getScriptsPath('config-generator'));
PortChecker = require(getScriptsPath('port-checker'));
StartupManager = require(getScriptsPath('startup-manager'));
LogWindowManager = require(getScriptsPath('log-window-manager'));
}
}
class Lifecycle {
constructor() {
this.mysqlServiceManager = null;
this.javaRunner = null;
this.startupManager = null;
this.logWindowManager = null;
this.mysqlPort = null;
this.javaPort = null;
this.autoRefreshTimer = null;
}
/**
* core app have been loaded
*/
async ready() {
logger.info('[lifecycle] ready');
// 延迟加载 scripts
loadScripts();
}
/**
* 完整的应用启动流程
*/
async startApplication() {
this.logWindowManager.addLog('system', '▶ 步骤1: 开始启动流程...');
logger.info('[lifecycle] Starting application...');
this.logWindowManager.addLog('system', '▶ 步骤2: 加载配置信息...');
const config = getConfig();
logger.info('[lifecycle] Config loaded:', JSON.stringify(config));
// 步骤1: 初始化
this.logWindowManager.addLog('system', '▶ 步骤3: 初始化启动管理器...');
this.startupManager.updateProgress('init');
await this.sleep(500);
// 步骤2-4: 确保 MySQL 服务运行
this.logWindowManager.addLog('system', '▶ 步骤4: 检查 MySQL 配置...');
logger.info('[lifecycle] MySQL config check - enable:', config.mysql?.enable, 'autoStart:', config.mysql?.autoStart);
if (config.mysql && config.mysql.enable && config.mysql.autoStart) {
this.startupManager.updateProgress('check-mysql-port');
this.logWindowManager.addLog('system', '▶ 步骤5: 启动 MySQL 服务管理器...');
this.mysqlServiceManager = new MySQLServiceManager(this.logWindowManager);
this.logWindowManager.addLog('system', '正在检查 MySQL 服务状态...');
try {
// 使用服务管理器确保MySQL服务运行
this.logWindowManager.addLog('system', '▶ 步骤6: 确保 MySQL 服务运行中...');
this.mysqlPort = await this.mysqlServiceManager.ensureServiceRunning(
PortChecker.findAvailablePort.bind(PortChecker),
PortChecker.waitForPort.bind(PortChecker)
);
logger.info(`[lifecycle] MySQL service running on port: ${this.mysqlPort}`);
this.logWindowManager.addLog('success', `✓ MySQL 服务已就绪,端口: ${this.mysqlPort}`);
this.startupManager.updateProgress('wait-mysql', { mysqlPort: this.mysqlPort });
await this.sleep(500);
} catch (error) {
logger.error('[lifecycle] MySQL error:', error);
this.logWindowManager.addLog('error', `MySQL 错误: ${error.message}`);
throw error;
}
}
// 步骤5: 检测 Java 端口
this.logWindowManager.addLog('system', '▶ 步骤7: 检测可用的 Java 端口从18093开始...');
this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort });
this.javaPort = await PortChecker.findAvailablePort(18093, 100);
if (this.javaPort === -1) {
this.logWindowManager.addLog('error', 'Java 端口检测失败18093-18192 全部被占用');
throw new Error('无法找到可用的后端服务端口18093-18192 全部被占用)');
}
// 步骤5.5: 检测 WebSocket 端口
this.logWindowManager.addLog('system', '▶ 步骤8: 检测可用的 WebSocket 端口从7778开始...');
this.websocketPort = await PortChecker.findAvailablePort(7778, 100);
if (this.websocketPort === -1) {
this.logWindowManager.addLog('error', 'WebSocket 端口检测失败7778-7877 全部被占用');
throw new Error('无法找到可用的 WebSocket 端口7778-7877 全部被占用)');
}
if (this.javaPort !== 18093) {
this.logWindowManager.addLog('warn', `⚠ Java 默认端口 18093 已被占用,自动切换到端口: ${this.javaPort}`);
} else {
this.logWindowManager.addLog('success', `✓ Java 将使用默认端口: ${this.javaPort}`);
}
if (this.websocketPort !== 7778) {
this.logWindowManager.addLog('warn', `⚠ WebSocket 默认端口 7778 已被占用,自动切换到端口: ${this.websocketPort}`);
} else {
this.logWindowManager.addLog('success', `✓ WebSocket 将使用默认端口: ${this.websocketPort}`);
}
logger.info(`[lifecycle] Spring Boot will use port: ${this.javaPort}`);
logger.info(`[lifecycle] WebSocket will use port: ${this.websocketPort}`);
this.startupManager.updateProgress('check-java-port', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort
});
await this.sleep(500);
// 步骤6: 生成配置文件
this.logWindowManager.addLog('system', '▶ 步骤9: 生成 Spring Boot 配置文件...');
this.startupManager.updateProgress('generate-config', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
websocketPort: this.websocketPort
});
const configGenerator = new ConfigGenerator();
const { configPath, dataPath } = await configGenerator.generateConfig({
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
websocketPort: this.websocketPort,
mysqlPassword: 'njcnpqs'
});
logger.info(`[lifecycle] Configuration generated at: ${configPath}`);
logger.info(`[lifecycle] Data directory: ${dataPath}`);
this.logWindowManager.addLog('success', `✓ 配置文件已生成: ${configPath}`);
this.logWindowManager.addLog('system', ` 数据目录: ${dataPath}`);
this.startupManager.updateProgress('generate-config', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
websocketPort: this.websocketPort,
dataPath: dataPath
});
await this.sleep(500);
// 步骤7: 启动 Spring Boot
this.logWindowManager.addLog('system', '▶ 步骤10: 启动 Spring Boot 应用...');
this.startupManager.updateProgress('start-java', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
dataPath: dataPath
});
await this.startSpringBoot(configPath);
// 步骤8: 等待 Spring Boot 就绪
this.logWindowManager.addLog('system', '▶ 步骤11: 等待 Spring Boot 就绪最多60秒...');
this.startupManager.updateProgress('wait-java', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
dataPath: dataPath
});
const javaReady = await PortChecker.waitForPort(this.javaPort, 60000);
if (!javaReady) {
logger.warn(`[lifecycle] Spring Boot 启动超时,但继续启动应用`);
this.logWindowManager.addLog('warn', '⚠ Spring Boot 启动超时60秒但应用将继续启动');
} else {
logger.info('[lifecycle] Spring Boot is ready');
this.logWindowManager.addLog('success', '✓ Spring Boot 启动成功!');
}
await this.sleep(1000);
// 步骤9: 完成
this.logWindowManager.addLog('system', '▶ 步骤12: 启动完成,准备显示主窗口...');
this.startupManager.updateProgress('done', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
dataPath: dataPath
});
logger.info('[lifecycle] Application startup completed');
this.logWindowManager.addLog('system', '='.repeat(60));
this.logWindowManager.addLog('success', '✓ NPQS9100 启动完成!所有服务正常运行');
this.logWindowManager.addLog('system', `✓ MySQL 端口: ${this.mysqlPort}`);
this.logWindowManager.addLog('system', `✓ Java 端口: ${this.javaPort}`);
this.logWindowManager.addLog('system', `✓ WebSocket 端口: ${this.websocketPort}`);
this.logWindowManager.addLog('system', `✓ 数据目录: ${dataPath}`);
this.logWindowManager.addLog('system', '='.repeat(60));
this.logWindowManager.addLog('system', '应用即将启动...');
// 先关闭 Loading 窗口
this.startupManager.closeLoadingWindow();
// 等待3秒让用户看清日志信息然后再显示主窗口
await this.sleep(3000);
// 显示主窗口
const win = getMainWindow();
win.show();
win.focus();
// 窗口关闭时,使用 Electron 原生对话框确认
win.on('close', async (event) => {
if (global.isConfirmedExit) {
logger.info('[lifecycle] Exit already confirmed, proceeding');
return;
}
event.preventDefault();
logger.info('[lifecycle] Window close intercepted, showing exit confirm dialog');
const { dialog } = require('electron');
const { response } = await dialog.showMessageBox(win, {
type: 'question',
buttons: ['取消', '退出'],
defaultId: 1,
cancelId: 0,
title: '退出确认',
message: '确定要退出应用吗?',
noLink: true
});
if (response === 1) {
logger.info('[lifecycle] User confirmed exit');
global.isConfirmedExit = true;
await this.cleanup();
if (win && !win.isDestroyed()) {
win.destroy();
}
app.quit();
} else {
logger.info('[lifecycle] User cancelled exit');
}
});
// 立即刷新一次,确保显示最新内容
setTimeout(() => {
if (win && !win.isDestroyed()) {
logger.info('[lifecycle] Reloading main window to ensure fresh content');
this.logWindowManager.addLog('system', '正在加载应用界面...');
win.reload();
}
}, 500);
// 设置主窗口加载超时自动刷新30秒
this.setupAutoRefresh(win);
// 提示用户
this.logWindowManager.addLog('system', '主应用已打开!此日志窗口可随时关闭');
}
/**
* 设置主窗口自动刷新机制
*/
setupAutoRefresh(win) {
// 30秒后检查页面是否还在loading如果是则刷新
this.autoRefreshTimer = setTimeout(() => {
if (win && !win.isDestroyed()) {
// 检查页面是否还在loading状态
win.webContents.executeJavaScript('document.readyState').then(state => {
logger.info(`[lifecycle] Page readyState after 30s: ${state}`);
if (state !== 'complete') {
logger.warn('[lifecycle] Page still loading after 30s, reloading...');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('warn', '页面加载超时,自动刷新...');
}
win.reload();
} else {
logger.info('[lifecycle] Page loaded successfully');
}
}).catch(err => {
logger.error('[lifecycle] Failed to check page state:', err);
});
}
}, 30000); // 30秒超时
}
/**
* 清理所有资源
*/
async cleanup() {
logger.info('[lifecycle] Starting cleanup...');
// 清除自动刷新定时器
if (this.autoRefreshTimer) {
clearTimeout(this.autoRefreshTimer);
this.autoRefreshTimer = null;
}
// 在日志窗口显示清理信息(在关闭之前)
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('system', '='.repeat(60));
this.logWindowManager.addLog('system', '应用正在关闭,清理资源中...');
}
// 关闭 Loading 窗口
if (this.startupManager) {
try {
this.startupManager.closeLoadingWindow();
} catch (error) {
// 忽略
}
}
// 停止 Spring Boot
if (this.javaRunner) {
try {
logger.info('[lifecycle] Stopping Spring Boot...');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('system', '正在停止 Spring Boot...');
}
// 停止 Java 进程(内部已有完整的等待和清理逻辑)
await this.javaRunner.stopSpringBoot();
logger.info('[lifecycle] Spring Boot stopped');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('success', 'Spring Boot 已停止');
}
} catch (error) {
logger.error('[lifecycle] Failed to stop Spring Boot:', error);
}
}
// 停止 MySQL 进程(进程模式)
if (this.mysqlServiceManager) {
try {
logger.info('[lifecycle] Stopping MySQL process...');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('system', '正在停止 MySQL...');
}
await this.mysqlServiceManager.stopMySQLProcess();
logger.info('[lifecycle] MySQL process stopped');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('success', 'MySQL 已停止');
}
} catch (error) {
logger.error('[lifecycle] Failed to stop MySQL:', error);
}
}
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('system', '清理完成,应用即将退出');
this.logWindowManager.addLog('system', '='.repeat(60));
}
// 等待500ms让用户看到清理日志
await this.sleep(500);
// 最后关闭日志窗口
if (this.logWindowManager) {
try {
logger.info('[lifecycle] Closing log window...');
this.logWindowManager.close();
} catch (error) {
logger.error('[lifecycle] Failed to close log window:', error);
}
}
logger.info('[lifecycle] Cleanup completed');
}
/**
* 启动 Spring Boot 应用
*/
async startSpringBoot(configPath) {
try {
logger.info('[lifecycle] Starting Spring Boot application...');
this.logWindowManager.addLog('java', '正在启动 Spring Boot 应用...');
// 启动Java应用
this.javaRunner = new JavaRunner();
// 开发环境build/extraResources/java/entrance.jar
// 打包后resources/extraResources/java/entrance.jar
const isDev = !process.resourcesPath;
const jarPath = isDev
? path.join(__dirname, '..', 'build', 'extraResources', 'java', 'entrance.jar')
: path.join(process.resourcesPath, 'extraResources', 'java', 'entrance.jar');
// 获取日志路径(与 config-generator.js 中的 dataPath 保持一致)
const isDev2 = !process.resourcesPath;
const baseDir = isDev2
? path.join(__dirname, '..', '..')
: path.dirname(process.resourcesPath);
const dataPath = path.join(baseDir, 'NPQS9100_Data');
const logPath = path.join(dataPath, 'logs');
const javaProcess = this.javaRunner.runSpringBoot(jarPath, configPath, {
javaPort: this.javaPort,
logPath: logPath // 传递日志路径
});
// 监听Java进程输出
javaProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
if (output) {
logger.info('[SpringBoot]', output);
// 根据内容判断日志类型
if (output.includes('ERROR') || output.includes('Exception')) {
this.logWindowManager.addLog('error', `[Java] ${output}`);
} else if (output.includes('WARN')) {
this.logWindowManager.addLog('warn', `[Java] ${output}`);
} else if (output.includes('Started') || output.includes('Completed')) {
this.logWindowManager.addLog('success', `[Java] ${output}`);
} else {
this.logWindowManager.addLog('java', `[Java] ${output}`);
}
}
});
javaProcess.stderr.on('data', (data) => {
const output = data.toString().trim();
if (output) {
logger.error('[SpringBoot]', output);
this.logWindowManager.addLog('error', `[Java Error] ${output}`);
}
});
javaProcess.on('close', (code) => {
logger.info(`[SpringBoot] Process exited with code ${code}`);
this.logWindowManager.addLog('system', `Spring Boot 进程退出,代码: ${code}`);
});
logger.info('[lifecycle] Spring Boot application started');
this.logWindowManager.addLog('success', 'Spring Boot 应用已启动!');
} catch (error) {
logger.error('[lifecycle] Failed to start Spring Boot:', error);
this.logWindowManager.addLog('error', `Spring Boot 启动失败: ${error.message}`);
throw error;
}
}
/**
* 睡眠函数
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* electron app ready
*/
async electronAppReady() {
logger.info('[lifecycle] electron-app-ready');
}
/**
* main window have been loaded
*/
async windowReady() {
logger.info('[lifecycle] window-ready hook triggered');
// 进程模式不需要管理员权限检查
// 创建日志管理器(但不显示窗口,仅用于写日志文件)
logger.info('[lifecycle] Creating log window manager...');
this.logWindowManager = new LogWindowManager();
// this.logWindowManager.createLogWindow(); // ← 注释掉,不创建窗口
this.logWindowManager.addLog('system', '='.repeat(80));
this.logWindowManager.addLog('system', 'NPQS9100 应用启动');
this.logWindowManager.addLog('system', '='.repeat(80));
// 创建 Loading 窗口
logger.info('[lifecycle] Creating startup manager and loading window...');
this.startupManager = new StartupManager();
this.startupManager.createLoadingWindow();
this.logWindowManager.addLog('system', '='.repeat(60));
this.logWindowManager.addLog('system', 'NPQS9100 启动中...');
this.logWindowManager.addLog('system', '='.repeat(60));
// 开始启动流程
logger.info('[lifecycle] Starting application flow...');
try {
await this.startApplication();
} catch (error) {
logger.error('[lifecycle] Failed to start application:', error);
this.logWindowManager.addLog('error', `启动失败: ${error.message}`);
this.logWindowManager.addLog('system', '请检查日志窗口了解详细错误信息');
this.startupManager.showError(error.message || '启动失败,请查看日志');
// 显示错误5秒后关闭Loading窗口但不关闭日志窗口
setTimeout(() => {
this.startupManager.closeLoadingWindow();
// 即使启动失败,也显示主窗口(但用户可能需要手动修复问题)
const win = getMainWindow();
win.show();
win.focus();
// 启动失败时,允许用户正常关闭窗口(不强制托盘)
// 因为可能托盘未创建,或用户想直接退出
win.on('close', async (event) => {
logger.info('[lifecycle] Window closing (after error), cleaning up...');
// 不阻止关闭,执行清理
await this.cleanup();
});
this.logWindowManager.addLog('warn', '应用已启动,但部分服务可能未正常运行');
this.logWindowManager.addLog('system', '您可以点击 X 关闭应用');
}, 5000);
}
// 主窗口初始化但不显示,等待启动流程完成后再显示
// 在 startApplication() 中会调用 win.show()
}
/**
* before app close (框架生命周期钩子)
*/
async beforeClose() {
logger.info('[lifecycle] before-close hook triggered');
await this.cleanup();
}
}
Lifecycle.toString = () => '[class Lifecycle]';
// 导出实例而不是类
module.exports = new Lifecycle();