升级electron egg脚手架版本

This commit is contained in:
2025-10-16 20:14:55 +08:00
parent c04be0264a
commit 7dac2b8c7d
18 changed files with 860 additions and 148 deletions

View File

@@ -12,7 +12,7 @@ module.exports = () => {
singleLock: true,
windowsOption: {
title: 'NPQS9100-自动检测平台',
menuBarVisible: false,
menuBarVisible: true, // 显示菜单栏,方便查看日志
width: 1920,
height: 1000,
minWidth: 1024,
@@ -24,7 +24,7 @@ module.exports = () => {
//preload: path.join(getElectronDir(), 'preload', 'bridge.js'),
},
frame: true,
show: true,
show: false, // 初始不显示,等待服务启动完成后再显示
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
},
logger: {
@@ -66,6 +66,20 @@ module.exports = () => {
mainServer: {
indexPath: '/public/dist/index.html',
channelSeparator: '/',
},
// MySQL配置
mysql: {
enable: true,
autoStart: true, // 应用启动时自动启动MySQL
path: path.join(getBaseDir(), 'mysql'),
connection: {
host: 'localhost',
port: 3306,
user: 'root',
password: 'njcnpqs',
database: 'npqs9100_db',
charset: 'utf8mb4'
}
}
}
}

View File

@@ -1,19 +1,87 @@
const { ElectronEgg } = require('ee-core');
const { Lifecycle } = require('./preload/lifecycle');
const { app, Menu, ipcMain } = require('electron');
const lifecycle = require('./preload/lifecycle');
const { preload } = require('./preload');
// new app
const app = new ElectronEgg();
const electronApp = new ElectronEgg();
// register lifecycle
const life = new Lifecycle();
app.register("ready", life.ready);
app.register("electron-app-ready", life.electronAppReady);
app.register("window-ready", life.windowReady);
app.register("before-close", life.beforeClose);
// 创建应用菜单
function createApplicationMenu() {
const template = [
{
label: '查看',
submenu: [
{
label: '显示/隐藏服务日志',
accelerator: 'F12',
click: () => {
if (lifecycle.logWindowManager) {
lifecycle.logWindowManager.toggle();
}
}
},
{ type: 'separator' },
{ role: 'reload', label: '刷新' },
{ role: 'forceReload', label: '强制刷新' },
{ type: 'separator' },
{ role: 'toggleDevTools', label: '开发者工具' }
]
},
{
label: '帮助',
submenu: [
{
label: '使用说明',
click: () => {
// 可以打开帮助文档
}
},
{ type: 'separator' },
{
label: '关于',
click: () => {
// 可以显示关于信息
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
// 注册 IPC 处理器
ipcMain.handle('show-log-window', () => {
if (lifecycle.logWindowManager) {
lifecycle.logWindowManager.show();
}
});
ipcMain.handle('hide-log-window', () => {
if (lifecycle.logWindowManager) {
lifecycle.logWindowManager.hide();
}
});
ipcMain.handle('toggle-log-window', () => {
if (lifecycle.logWindowManager) {
lifecycle.logWindowManager.toggle();
}
});
// register lifecycle (绑定 this 上下文)
electronApp.register("ready", lifecycle.ready.bind(lifecycle));
electronApp.register("electron-app-ready", () => {
lifecycle.electronAppReady.bind(lifecycle)();
createApplicationMenu();
});
electronApp.register("window-ready", lifecycle.windowReady.bind(lifecycle));
electronApp.register("before-close", lifecycle.beforeClose.bind(lifecycle));
// register preload
app.register("preload", preload);
electronApp.register("preload", preload);
// run
app.run();
electronApp.run();

View File

@@ -7,4 +7,8 @@ const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: ipcRenderer,
// 日志窗口控制
showLogWindow: () => ipcRenderer.invoke('show-log-window'),
hideLogWindow: () => ipcRenderer.invoke('hide-log-window'),
toggleLogWindow: () => ipcRenderer.invoke('toggle-log-window'),
})

View File

@@ -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();