@@ -1,20 +1,484 @@
'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 ) ) ;
}
/**
@@ -30,29 +494,58 @@ class Lifecycle {
async windowReady ( ) {
logger . info ( '[lifecycle] window-ready' ) ;
// 延迟加载,无白屏
const win = getMainWindow ( ) ;
const { windowsOption } = getConfig ( ) ;
if ( windowsOption . show == false ) {
win . once ( 'ready-to-show' , ( ) => {
// 创建日志窗口
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 ( ) ;
} )
} else {
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
* before app close (框架生命周期钩子)
*/
async beforeClose ( ) {
logger . info ( '[lifecycle] before-close' ) ;
logger . info ( '[lifecycle] before-close hook triggered ' ) ;
await this . cleanup ( ) ;
}
}
Lifecycle . toString = ( ) => '[class Lifecycle]' ;
module . exports = {
Lifecycle
} ;
// 导出实例而不是类
module . exports = new Lifecycle ( ) ;