'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; this.isRestartingForAdmin = false; // 权限提升重启标记 } /** * 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 service error:', error); this.logWindowManager.addLog('error', `MySQL 服务错误: ${error.message}`); // 检查是否是权限问题 if (error.message && (error.message.includes('administrator') || error.message.includes('Denied'))) { logger.error('[lifecycle] Need administrator privileges'); this.logWindowManager.addLog('error', '检测到需要管理员权限来安装 MySQL 服务'); this.logWindowManager.addLog('system', '应用将自动请求管理员权限并重启...'); // 等待用户看清日志 await this.sleep(2000); // 自动以管理员身份重启应用 await this.restartAsAdmin(); // 退出当前实例 return; } throw error; } } // 步骤5: 检测 Java 端口 this.logWindowManager.addLog('system', '▶ 步骤7: 检测可用的 Java 端口(从18092开始)...'); this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort }); this.javaPort = await PortChecker.findAvailablePort(18092, 100); if (this.javaPort === -1) { this.logWindowManager.addLog('error', 'Java 端口检测失败:18092-18191 全部被占用'); throw new Error('无法找到可用的后端服务端口(18092-18191 全部被占用)'); } // 步骤5.5: 检测 WebSocket 端口 this.logWindowManager.addLog('system', '▶ 步骤8: 检测可用的 WebSocket 端口(从7777开始)...'); this.websocketPort = await PortChecker.findAvailablePort(7777, 100); if (this.websocketPort === -1) { this.logWindowManager.addLog('error', 'WebSocket 端口检测失败:7777-7876 全部被占用'); throw new Error('无法找到可用的 WebSocket 端口(7777-7876 全部被占用)'); } if (this.javaPort !== 18092) { this.logWindowManager.addLog('warn', `⚠ Java 默认端口 18092 已被占用,自动切换到端口: ${this.javaPort}`); } else { this.logWindowManager.addLog('success', `✓ Java 将使用默认端口: ${this.javaPort}`); } if (this.websocketPort !== 7777) { this.logWindowManager.addLog('warn', `⚠ WebSocket 默认端口 7777 已被占用,自动切换到端口: ${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(); // 添加主窗口关闭事件监听 win.on('close', async (event) => { // 总是弹出确认对话框 event.preventDefault(); const { dialog } = require('electron'); const { app } = require('electron'); const response = await dialog.showMessageBox(win, { type: 'question', title: '退出确认', message: '确定要退出应用吗?', buttons: ['取消', '退出'], defaultId: 0, cancelId: 0 }); if (response.response === 1) { // 用户确认退出 logger.info('[lifecycle] User confirmed exit'); // 移除所有监听器避免循环 win.removeAllListeners('close'); // 执行清理 await this.cleanup(); // 退出应用 app.quit(); } // 用户取消,什么都不做(已经 preventDefault) }); // 立即刷新一次,确保显示最新内容 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.isRestartingForAdmin) { logger.info('[lifecycle] Restarting for admin, skip cleanup'); return; } // 清除自动刷新定时器 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 作为Windows服务运行,应用关闭时保持运行 logger.info('[lifecycle] MySQL service keeps running as Windows service'); if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { this.logWindowManager.addLog('system', 'MySQL 服务保持运行'); 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'); const javaProcess = this.javaRunner.runSpringBoot(jarPath, configPath, { javaPort: this.javaPort }); // 监听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; } } /** * 以管理员身份重启应用 */ async restartAsAdmin() { const { app, dialog } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); logger.info('[lifecycle] Requesting administrator privileges...'); try { // 显示提示对话框 const { response } = await dialog.showMessageBox({ type: 'warning', title: '需要管理员权限', message: '安装 MySQL 服务需要管理员权限', detail: '应用将以管理员身份重新启动', buttons: ['取消', '确定'], defaultId: 1, cancelId: 0 }); if (response === 0) { // 用户取消,关闭应用 logger.info('[lifecycle] User cancelled admin elevation'); app.quit(); return; } // 获取应用可执行文件路径 const exePath = app.getPath('exe'); logger.info('[lifecycle] Restarting with admin privileges:', exePath); // 使用 PowerShell 以管理员身份启动 const psCommand = `Start-Process -FilePath "${exePath}" -Verb RunAs`; const child = spawn('powershell.exe', ['-Command', psCommand], { detached: true, stdio: 'ignore', windowsHide: true }); // 分离子进程,父进程退出不影响子进程 child.unref(); // 立即退出当前实例,释放单实例锁 // 必须立即退出,否则新实例会因为单实例锁无法启动 logger.info('[lifecycle] Quitting current instance to release lock...'); // 设置标记,跳过清理流程(这只是重启,不是真正退出) this.isRestartingForAdmin = true; // 关闭所有窗口 const BrowserWindow = require('electron').BrowserWindow; BrowserWindow.getAllWindows().forEach(win => { try { win.destroy(); } catch (e) { // 忽略错误 } }); // 立即强制退出,释放锁 process.exit(0); } catch (error) { logger.error('[lifecycle] Failed to restart as admin:', error); this.logWindowManager.addLog('error', '自动提升权限失败,请手动以管理员身份运行'); // 等待5秒后关闭 await this.sleep(5000); app.quit(); } } /** * 睡眠函数 */ 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();