Files
pqs-9100_client/electron/preload/lifecycle.js

552 lines
20 KiB
JavaScript
Raw 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');
// 判断是否是打包后的环境
const isProd = process.resourcesPath && process.resourcesPath.includes('win-unpacked');
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 MySQLManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager;
function loadScripts() {
if (!MySQLManager) {
MySQLManager = require(getScriptsPath('start-mysql'));
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.mysqlManager = 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() {
const config = getConfig();
// 步骤1: 初始化
this.startupManager.updateProgress('init');
await this.sleep(500);
// 步骤2: 检测 MySQL 端口
this.startupManager.updateProgress('check-mysql-port');
this.logWindowManager.addLog('system', '正在检测可用的 MySQL 端口从3306开始...');
this.mysqlPort = await PortChecker.findAvailablePort(3306, 100);
if (this.mysqlPort === -1) {
this.logWindowManager.addLog('error', 'MySQL 端口检测失败3306-3405 全部被占用');
throw new Error('无法找到可用的 MySQL 端口3306-3405 全部被占用)');
}
if (this.mysqlPort !== 3306) {
this.logWindowManager.addLog('warn', `MySQL 默认端口 3306 已被占用,自动切换到端口: ${this.mysqlPort}`);
} else {
this.logWindowManager.addLog('success', `MySQL 将使用默认端口: ${this.mysqlPort}`);
}
logger.info(`[lifecycle] MySQL will use port: ${this.mysqlPort}`);
this.startupManager.updateProgress('check-mysql-port', { mysqlPort: this.mysqlPort });
await this.sleep(500);
// 步骤3: 启动 MySQL
if (config.mysql && config.mysql.enable && config.mysql.autoStart) {
this.startupManager.updateProgress('start-mysql', { mysqlPort: this.mysqlPort });
this.logWindowManager.addLog('mysql', `正在启动 MySQL端口: ${this.mysqlPort}`);
this.mysqlManager = new MySQLManager();
// 监听 MySQL 输出
const actualPort = await this.mysqlManager.start(this.mysqlPort);
this.setupMySQLLogging(this.mysqlManager.process);
logger.info(`[lifecycle] MySQL started on port: ${actualPort}`);
this.logWindowManager.addLog('success', `MySQL 已启动,端口: ${actualPort}`);
// 步骤4: 等待 MySQL 就绪
this.startupManager.updateProgress('wait-mysql', { mysqlPort: actualPort });
this.logWindowManager.addLog('mysql', '等待 MySQL 就绪...');
const mysqlReady = await PortChecker.waitForPort(actualPort, 30000);
if (!mysqlReady) {
this.logWindowManager.addLog('error', `MySQL 启动超时(端口 ${actualPort} 未响应)`);
throw new Error(`MySQL 启动超时(端口 ${actualPort} 未响应)`);
}
logger.info('[lifecycle] MySQL is ready');
this.logWindowManager.addLog('success', 'MySQL 已就绪!');
await this.sleep(500);
}
// 步骤5: 检测 Java 端口
this.startupManager.updateProgress('check-java-port', { mysqlPort: this.mysqlPort });
this.logWindowManager.addLog('system', '正在检测可用的 Java 端口从18092开始...');
this.javaPort = await PortChecker.findAvailablePort(18092, 100);
if (this.javaPort === -1) {
this.logWindowManager.addLog('error', 'Java 端口检测失败18092-18191 全部被占用');
throw new Error('无法找到可用的后端服务端口18092-18191 全部被占用)');
}
if (this.javaPort !== 18092) {
this.logWindowManager.addLog('warn', `Java 默认端口 18092 已被占用,自动切换到端口: ${this.javaPort}`);
} else {
this.logWindowManager.addLog('success', `Java 将使用默认端口: ${this.javaPort}`);
}
logger.info(`[lifecycle] Spring Boot will use port: ${this.javaPort}`);
this.startupManager.updateProgress('check-java-port', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort
});
await this.sleep(500);
// 步骤6: 生成配置文件
this.startupManager.updateProgress('generate-config', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort
});
const configGenerator = new ConfigGenerator();
const { configPath, dataPath } = await configGenerator.generateConfig({
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
mysqlPassword: 'njcnpqs'
});
logger.info(`[lifecycle] Configuration generated at: ${configPath}`);
logger.info(`[lifecycle] Data directory: ${dataPath}`);
this.startupManager.updateProgress('generate-config', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
dataPath: dataPath
});
await this.sleep(500);
// 步骤7: 启动 Spring Boot
this.startupManager.updateProgress('start-java', {
mysqlPort: this.mysqlPort,
javaPort: this.javaPort,
dataPath: dataPath
});
await this.startSpringBoot(configPath);
// 步骤8: 等待 Spring Boot 就绪
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 启动超时,但继续启动应用`);
} else {
logger.info('[lifecycle] Spring Boot is ready');
}
await this.sleep(1000);
// 步骤9: 完成
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', `✓ 数据目录: ${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 () => {
logger.info('[lifecycle] Main window closing, cleaning up...');
await this.cleanup();
});
// 立即刷新一次,确保显示最新内容
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...');
}
// 设置超时3秒后强制继续
const stopPromise = this.javaRunner.stopSpringBoot();
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000));
await Promise.race([stopPromise, timeoutPromise]);
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
const config = getConfig();
if (config.mysql && config.mysql.enable && config.mysql.autoStart) {
try {
logger.info('[lifecycle] Stopping MySQL...');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('system', '正在停止 MySQL最多等待10秒...');
}
if (this.mysqlManager) {
// MySQL 的 stop() 方法内部使用 mysqladmin shutdown最多10秒
// 设置12秒超时作为保险
const stopPromise = this.mysqlManager.stop();
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 12000));
await Promise.race([stopPromise, timeoutPromise]);
}
logger.info('[lifecycle] MySQL stopped');
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('success', 'MySQL 已停止');
this.logWindowManager.addLog('system', '清理完成,应用即将退出');
this.logWindowManager.addLog('system', '='.repeat(60));
}
} catch (error) {
logger.error('[lifecycle] Failed to stop MySQL:', error);
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
this.logWindowManager.addLog('error', 'MySQL 停止失败: ' + error.message);
}
}
}
// 等待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');
}
/**
* 设置 MySQL 日志监听
*/
setupMySQLLogging(mysqlProcess) {
if (!mysqlProcess) return;
mysqlProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
if (output) {
// 根据内容判断日志类型
if (output.includes('[ERROR]') || output.includes('ERROR')) {
this.logWindowManager.addLog('error', `[MySQL] ${output}`);
} else if (output.includes('[Warning]') || output.includes('WARNING')) {
this.logWindowManager.addLog('warn', `[MySQL] ${output}`);
} else if (output.includes('ready for connections')) {
this.logWindowManager.addLog('success', `[MySQL] ${output}`);
} else {
this.logWindowManager.addLog('mysql', `[MySQL] ${output}`);
}
}
});
mysqlProcess.stderr.on('data', (data) => {
const output = data.toString().trim();
if (output) {
this.logWindowManager.addLog('warn', `[MySQL Error] ${output}`);
}
});
}
/**
* 启动 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;
}
}
/**
* 睡眠函数
*/
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');
// 创建日志窗口
this.logWindowManager = new LogWindowManager();
this.logWindowManager.createLogWindow();
this.logWindowManager.addLog('system', '='.repeat(60));
this.logWindowManager.addLog('system', 'NPQS9100 启动中...');
this.logWindowManager.addLog('system', '='.repeat(60));
// 创建 Loading 窗口
this.startupManager = new StartupManager();
this.startupManager.createLoadingWindow();
// 开始启动流程
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 () => {
logger.info('[lifecycle] Main window closing (after error), cleaning up...');
await this.cleanup();
});
this.logWindowManager.addLog('warn', '应用已启动,但部分服务可能未正常运行');
}, 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();