初始化
This commit is contained in:
85
electron/config/config.default.js
Normal file
85
electron/config/config.default.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const {getBaseDir} = require('ee-core/ps');
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
module.exports = () => {
|
||||
return {
|
||||
openDevTools: false,
|
||||
singleLock: true,
|
||||
windowsOption: {
|
||||
title: 'CN_Tool 灿能运维工具',
|
||||
menuBarVisible: false, // 隐藏菜单栏
|
||||
width: 1920,
|
||||
height: 1000,
|
||||
minWidth: 1024,
|
||||
minHeight: 640,
|
||||
webPreferences: {
|
||||
//webSecurity: false,
|
||||
contextIsolation: false, // false -> 可在渲染进程中使用electron的api,true->需要bridge.js(contextBridge)
|
||||
nodeIntegration: true,
|
||||
//preload: path.join(getElectronDir(), 'preload', 'bridge.js'),
|
||||
},
|
||||
frame: true,
|
||||
show: false, // 初始不显示,等待服务启动完成后再显示
|
||||
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
|
||||
},
|
||||
logger: {
|
||||
level: 'INFO',
|
||||
outputJSON: false,
|
||||
appLogName: '9100.log',
|
||||
coreLogName: '9100-core.log',
|
||||
errorLogName: '9100-error.log'
|
||||
},
|
||||
// 远程web地址
|
||||
remote: {
|
||||
enable: false,
|
||||
url: ''
|
||||
},
|
||||
socketServer: {
|
||||
enable: false,
|
||||
port: 7070,
|
||||
path: "/socket.io/",
|
||||
connectTimeout: 45000,
|
||||
pingTimeout: 30000,
|
||||
pingInterval: 25000,
|
||||
maxHttpBufferSize: 1e8,
|
||||
transports: ["polling", "websocket"],
|
||||
cors: {
|
||||
origin: true,
|
||||
},
|
||||
channel: 'socket-channel'
|
||||
},
|
||||
httpServer: {
|
||||
enable: false,
|
||||
https: {
|
||||
enable: false,
|
||||
key: '/public/ssl/localhost+1.key',
|
||||
cert: '/public/ssl/localhost+1.pem'
|
||||
},
|
||||
host: '127.0.0.1',
|
||||
port: 7071,
|
||||
},
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
electron/config/config.local.js
Normal file
13
electron/config/config.local.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Development environment configuration, coverage config.default.js
|
||||
*/
|
||||
module.exports = () => {
|
||||
return {
|
||||
openDevTools: false,
|
||||
jobs: {
|
||||
messageLog: false
|
||||
}
|
||||
};
|
||||
};
|
||||
10
electron/config/config.prod.js
Normal file
10
electron/config/config.prod.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* coverage config.default.js
|
||||
*/
|
||||
module.exports = () => {
|
||||
return {
|
||||
openDevTools: false,
|
||||
};
|
||||
};
|
||||
30
electron/controller/example.js
Normal file
30
electron/controller/example.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const { logger } = require('ee-core/log');
|
||||
const { exampleService } = require('../service/example');
|
||||
|
||||
/**
|
||||
* example
|
||||
* @class
|
||||
*/
|
||||
class ExampleController {
|
||||
|
||||
/**
|
||||
* 所有方法接收两个参数
|
||||
* @param args 前端传的参数
|
||||
* @param event - ipc通信时才有值。详情见:控制器文档
|
||||
*/
|
||||
|
||||
/**
|
||||
* test
|
||||
*/
|
||||
async test () {
|
||||
const result = await exampleService.test('electron');
|
||||
logger.info('service result:', result);
|
||||
|
||||
return 'hello electron-egg';
|
||||
}
|
||||
}
|
||||
ExampleController.toString = () => '[class ExampleController]';
|
||||
|
||||
module.exports = ExampleController;
|
||||
100
electron/controller/java.js
Normal file
100
electron/controller/java.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const path = require('path');
|
||||
|
||||
// 动态获取 scripts 目录路径
|
||||
function getScriptsPath(scriptName) {
|
||||
// 开发环境
|
||||
const devPath = path.join(__dirname, '../../scripts', scriptName);
|
||||
// 生产环境(打包后)
|
||||
const prodPath = path.join(process.resourcesPath, 'scripts', scriptName);
|
||||
|
||||
try {
|
||||
// 先尝试开发环境路径
|
||||
require.resolve(devPath);
|
||||
return devPath;
|
||||
} catch (e) {
|
||||
// 如果开发环境路径不存在,使用生产环境路径
|
||||
return prodPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟加载 JavaRunner
|
||||
let JavaRunner = null;
|
||||
function getJavaRunner() {
|
||||
if (!JavaRunner) {
|
||||
JavaRunner = require(getScriptsPath('java-runner'));
|
||||
}
|
||||
return JavaRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java 控制器 - 提供 JRE 管理和 Java 程序调用接口
|
||||
*/
|
||||
class JavaController {
|
||||
|
||||
/**
|
||||
* 检查 JRE 是否可用
|
||||
*/
|
||||
async checkAvailability() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const available = javaRunner.isJREAvailable();
|
||||
return { success: true, data: { available } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Java 版本
|
||||
*/
|
||||
async getVersion() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const version = await javaRunner.getVersion();
|
||||
return { success: true, data: { version } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 JRE 路径信息
|
||||
*/
|
||||
async getPathInfo() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const pathInfo = javaRunner.getPathInfo();
|
||||
return { success: true, data: pathInfo };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 JAR 文件(异步,等待完成)
|
||||
* @param {Object} params - { jarPath, args }
|
||||
*/
|
||||
async runJar(params) {
|
||||
const { jarPath, args = [] } = params;
|
||||
|
||||
if (!jarPath) {
|
||||
return { success: false, message: 'JAR path is required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const exitCode = await javaRunner.runJarAsync(jarPath, args);
|
||||
return { success: true, data: { exitCode } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JavaController.toString = () => '[class JavaController]';
|
||||
module.exports = JavaController;
|
||||
|
||||
278
electron/main.js
Normal file
278
electron/main.js
Normal file
@@ -0,0 +1,278 @@
|
||||
const { ElectronEgg } = require('ee-core');
|
||||
const { app, Menu, ipcMain, Tray, dialog, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const lifecycle = require('./preload/lifecycle');
|
||||
const { preload } = require('./preload');
|
||||
|
||||
const APP_DISPLAY_NAME = 'CN_Tool 灿能运维工具';
|
||||
|
||||
// new app
|
||||
const electronApp = new ElectronEgg();
|
||||
|
||||
// 全局变量
|
||||
let tray = null;
|
||||
let isQuitting = false;
|
||||
|
||||
/**
|
||||
* 统一定位当前应用自己的主窗口,避免继续依赖旧品牌关键字。
|
||||
* 优先按当前主窗口标题精确匹配;如果主窗口尚未完全就绪,再回退到顶层可用窗口。
|
||||
*/
|
||||
function findAppMainWindow() {
|
||||
const windows = BrowserWindow.getAllWindows().filter(win => !win.isDestroyed());
|
||||
|
||||
return windows.find(win => win.getTitle() === APP_DISPLAY_NAME)
|
||||
|| windows.find(win => !win.getParentWindow())
|
||||
|| null;
|
||||
}
|
||||
|
||||
// 创建系统托盘
|
||||
function createTray() {
|
||||
try {
|
||||
// 开发环境和生产环境的图标路径
|
||||
const isDev = !process.resourcesPath;
|
||||
const iconPath = isDev
|
||||
? path.join(__dirname, '..', 'public', 'images', 'tray.png')
|
||||
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'tray.png');
|
||||
|
||||
console.log('[Tray] Icon path:', iconPath);
|
||||
|
||||
// 检查图标文件是否存在
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(iconPath)) {
|
||||
console.error('[Tray] Icon file not found:', iconPath);
|
||||
// 如果找不到,尝试使用备用路径(主图标)
|
||||
const fallbackIcon = isDev
|
||||
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
|
||||
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
|
||||
|
||||
if (fs.existsSync(fallbackIcon)) {
|
||||
console.log('[Tray] Using fallback icon:', fallbackIcon);
|
||||
tray = new Tray(fallbackIcon);
|
||||
} else {
|
||||
console.error('[Tray] No icon available, tray not created');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
tray = new Tray(iconPath);
|
||||
}
|
||||
|
||||
tray.setToolTip(APP_DISPLAY_NAME);
|
||||
console.log('[Tray] Tray created successfully');
|
||||
|
||||
// 创建托盘菜单
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: '显示主窗口',
|
||||
click: () => {
|
||||
const mainWindow = findAppMainWindow();
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: '退出',
|
||||
click: async () => {
|
||||
// 弹出确认对话框
|
||||
const { response } = await dialog.showMessageBox({
|
||||
type: 'question',
|
||||
title: '退出确认',
|
||||
message: '确定退出应用吗?',
|
||||
buttons: ['取消', '确定退出'],
|
||||
defaultId: 0,
|
||||
cancelId: 0
|
||||
});
|
||||
|
||||
if (response === 1) {
|
||||
// 用户点击了"确定退出"
|
||||
isQuitting = true;
|
||||
|
||||
// 获取主窗口
|
||||
const mainWindow = findAppMainWindow();
|
||||
|
||||
// 移除所有 close 监听器,避免阻止关闭
|
||||
if (mainWindow) {
|
||||
mainWindow.removeAllListeners('close');
|
||||
}
|
||||
|
||||
// 执行清理
|
||||
await lifecycle.cleanup();
|
||||
|
||||
// 退出应用
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
// 双击托盘图标显示窗口
|
||||
tray.on('double-click', () => {
|
||||
const mainWindow = findAppMainWindow();
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Tray] Failed to create tray:', error);
|
||||
tray = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建应用菜单
|
||||
function createApplicationMenu() {
|
||||
// 生产环境隐藏菜单栏
|
||||
Menu.setApplicationMenu(null);
|
||||
|
||||
// 调试时可以使用下面的菜单(取消注释)
|
||||
/*
|
||||
const template = [
|
||||
{
|
||||
label: '查看',
|
||||
submenu: [
|
||||
{ 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();
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否正在退出
|
||||
ipcMain.handle('is-quitting', () => {
|
||||
return isQuitting;
|
||||
});
|
||||
|
||||
// 处理单实例:当尝试启动第二个实例时,聚焦已有窗口
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
console.log('[Main] Second instance detected, focusing main window...');
|
||||
|
||||
// 单实例激活时只聚焦当前应用自己的主窗口,避免误用历史命名判断。
|
||||
const mainWindow = findAppMainWindow();
|
||||
|
||||
if (mainWindow) {
|
||||
// 如果窗口最小化,恢复它
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
|
||||
// 如果窗口隐藏,显示它
|
||||
if (!mainWindow.isVisible()) {
|
||||
mainWindow.show();
|
||||
}
|
||||
|
||||
// 聚焦窗口
|
||||
mainWindow.focus();
|
||||
|
||||
console.log('[Main] Main window focused');
|
||||
} else {
|
||||
console.warn('[Main] Main window not found');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听应用退出前事件(确保清理托盘)
|
||||
app.on('before-quit', () => {
|
||||
console.log('[Main] App before-quit, destroying tray...');
|
||||
|
||||
// 销毁托盘图标
|
||||
if (tray && !tray.isDestroyed()) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 will-quit 事件(强制退出时)
|
||||
app.on('will-quit', () => {
|
||||
console.log('[Main] App will-quit');
|
||||
|
||||
// 确保托盘被销毁
|
||||
if (tray && !tray.isDestroyed()) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
}
|
||||
});
|
||||
|
||||
// register lifecycle (绑定 this 上下文)
|
||||
electronApp.register("ready", lifecycle.ready.bind(lifecycle));
|
||||
electronApp.register("electron-app-ready", () => {
|
||||
lifecycle.electronAppReady.bind(lifecycle)();
|
||||
createApplicationMenu();
|
||||
createTray(); // 创建系统托盘
|
||||
});
|
||||
electronApp.register("window-ready", lifecycle.windowReady.bind(lifecycle));
|
||||
electronApp.register("before-close", async () => {
|
||||
// 如果不是真正退出,不执行清理
|
||||
if (!isQuitting) {
|
||||
console.log('[Main] Not quitting, skip cleanup');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Main] Quitting, execute cleanup');
|
||||
|
||||
// 销毁托盘图标
|
||||
if (tray && !tray.isDestroyed()) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
}
|
||||
|
||||
await lifecycle.beforeClose.bind(lifecycle)();
|
||||
});
|
||||
|
||||
// register preload
|
||||
electronApp.register("preload", preload);
|
||||
|
||||
// run
|
||||
electronApp.run();
|
||||
14
electron/preload/bridge.js
Normal file
14
electron/preload/bridge.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* 如果启用了上下文隔离,渲染进程无法使用electron的api,
|
||||
* 可通过contextBridge 导出api给渲染进程使用
|
||||
*/
|
||||
|
||||
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'),
|
||||
})
|
||||
16
electron/preload/index.js
Normal file
16
electron/preload/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*************************************************
|
||||
** preload为预加载模块,该文件将会在程序启动时加载 **
|
||||
*************************************************/
|
||||
|
||||
const { logger } = require('ee-core/log');
|
||||
|
||||
function preload() {
|
||||
logger.info('[preload] load 1');
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载模块入口
|
||||
*/
|
||||
module.exports = {
|
||||
preload
|
||||
}
|
||||
570
electron/preload/lifecycle.js
Normal file
570
electron/preload/lifecycle.js
Normal file
@@ -0,0 +1,570 @@
|
||||
'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 MySQLProcessManager, JavaRunner, ConfigGenerator, PortChecker, StartupManager, LogWindowManager;
|
||||
|
||||
function loadScripts() {
|
||||
if (!MySQLProcessManager) {
|
||||
MySQLProcessManager = require(getScriptsPath('mysql-process-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.mysqlProcessManager = 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.mysqlProcessManager = new MySQLProcessManager(this.logWindowManager);
|
||||
this.logWindowManager.addLog('system', '正在检查 MySQL 进程状态...');
|
||||
|
||||
try {
|
||||
// 使用进程管理器确保 MySQL 进程运行
|
||||
this.logWindowManager.addLog('system', '▶ 步骤6: 确保 MySQL 进程运行中...');
|
||||
this.mysqlPort = await this.mysqlProcessManager.ensureServiceRunning(
|
||||
PortChecker.findAvailablePort.bind(PortChecker),
|
||||
PortChecker.waitForPort.bind(PortChecker)
|
||||
);
|
||||
|
||||
logger.info(`[lifecycle] MySQL process 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, dataPath);
|
||||
|
||||
// 步骤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', '✓ CN_Tool 灿能运维工具 启动完成!所有服务正常运行');
|
||||
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.mysqlProcessManager) {
|
||||
try {
|
||||
logger.info('[lifecycle] Stopping MySQL process...');
|
||||
if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) {
|
||||
this.logWindowManager.addLog('system', '正在停止 MySQL...');
|
||||
}
|
||||
|
||||
await this.mysqlProcessManager.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, dataPath) {
|
||||
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 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', 'CN_Tool 灿能运维工具 应用启动');
|
||||
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', 'CN_Tool 灿能运维工具 启动中...');
|
||||
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();
|
||||
30
electron/service/example.js
Normal file
30
electron/service/example.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const { logger } = require('ee-core/log');
|
||||
|
||||
/**
|
||||
* 示例服务
|
||||
* @class
|
||||
*/
|
||||
class ExampleService {
|
||||
|
||||
/**
|
||||
* test
|
||||
*/
|
||||
async test(args) {
|
||||
let obj = {
|
||||
status:'ok',
|
||||
params: args
|
||||
}
|
||||
|
||||
logger.info('ExampleService obj:', obj);
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
ExampleService.toString = () => '[class ExampleService]';
|
||||
|
||||
module.exports = {
|
||||
ExampleService,
|
||||
exampleService: new ExampleService()
|
||||
};
|
||||
Reference in New Issue
Block a user