From 12af3c74f26f9a7cb6c82e4d31fa0d9bdb180cc4 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Thu, 16 Oct 2025 20:18:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E9=A2=9D=E5=A4=96=E8=B5=84?= =?UTF-8?q?=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/icons/icon_backup.ico | Bin 0 -> 27047 bytes electron/controller/java.js | 100 +++++++++ electron/controller/mysql.js | 75 +++++++ scripts/config-generator.js | 136 +++++++++++++ scripts/java-runner.js | 321 +++++++++++++++++++++++++++++ scripts/log-window-manager.js | 325 +++++++++++++++++++++++++++++ scripts/port-checker.js | 154 ++++++++++++++ scripts/start-mysql.js | 373 ++++++++++++++++++++++++++++++++++ scripts/startup-manager.js | 116 +++++++++++ 9 files changed, 1600 insertions(+) create mode 100644 build/icons/icon_backup.ico create mode 100644 electron/controller/java.js create mode 100644 electron/controller/mysql.js create mode 100644 scripts/config-generator.js create mode 100644 scripts/java-runner.js create mode 100644 scripts/log-window-manager.js create mode 100644 scripts/port-checker.js create mode 100644 scripts/start-mysql.js create mode 100644 scripts/startup-manager.js diff --git a/build/icons/icon_backup.ico b/build/icons/icon_backup.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8b5a24e387e9420fb6a1807458f10352f9e500 GIT binary patch literal 27047 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

rVEsjTZm_2{lPXK~%)$tG6)UXrj>$0l9TK zm}MLOZyT9KAq5ymY>jO&!Q-Ou-IRhaYNPLQmNb0{RQ$V>TXy0c8-f&^WxfRGOm>Ce zt|f^|zQkuvjNmh;*S?2$gl+>^J3Q(*`yf8v{HiDSwA~N^E?JyEC$N`Uc7g7-OAMsa4t_H4c$7 zZZXofKHtRs$q5Yr>w^;jQq8^ztXp?x;u;v&+}W?g0h|dKqdwu7egv>cz=1QTIa~~< zjWY@yXnUggz!N+W064R$5TY$So=P}e{_;QGu~{Fx6&qI)5lP889uLEOG~>YCy0a=K zxIHnwW~jX})mQ*fUB52{kF&3>&#dwhYDz2;o6Wfr2NYhyp(A(OhhO5z=3=KnPR$*T<^*TPB~>>^ex6sw>?Mj1_*B z@;sk?n9aIh*kdp|HH(XYdnSkH;8Q+CJM9 zi>zMtgV}7KkMFN$dN{w3#L8H!l|`bI3F1UGTY+6zY~ay+!z{FuZvzrjGY@|GbFW|g zt?qsQ{O8wyM+gy0-&20UL&5ihfkL$|eJ_+!KnjsaF58~RQ|U=?CfZ!E2XM9xXjtg< zFaP7OeIEec&0bcHU4YClUE4IFoZBh1f(~OF#(?hwlDl1+<_sJkI|QVw%=5MBR@`W` z(z!>E+7pxa*Z8AE%DlCH|MkHM0H6EJxqrI8?ZMoV{kn6oS|f$VFTV2YZ*Rplv`SI6t^| zzok=EKYH}V%0tcZ?2rji!FuItP>PBL(cp;oZ}&Ci*d1Vn-+FUx)gQ-yr>Np%%OB%H?F|k) zio^md<16>??Yjo$?~hC3{gT5LPS}2s=Aq{vdX0CP-bLEp-wW@aI3|1 zuYxmv{Feux~)_hXZEY`XS)K8sn69)+!Afrq>7dR!`l$Go7Z|B94-<*=z-2yB)?!iZF_! zW;%>On}yWpxHHO`Ko<6VcxGiX~p#Fof%xcGlSp$wJ&USAA81d zwYcAbgL97kgV>*1YZVgcR(yT=A@fIn@*Q2DpGOdch{6PE8pB!(!8rg9r43Z7FuJcN zK~Ihx&lK4al$psU@J4rA!Kv>g@P4J21HSprI6imjf@3a#qQ!9{KzE?YQ(K+nn0Cbw%T)+KcZ+Z^^eEt)!9XNUP z0EVhX43x6-<3YmDDsE>zgzEFrn&_FVx2L06$8oBVrb!S*(OeRSp^8JOFbLvSYc^U~ z2-?jCZoc`I-+R*a<;c;`c%l}UBLyr6PyiPk9z0lMq0=lg+l}x2)#V41_YWL6bL=I2 z;+5XZ!{5655J3>39YzSFL`Shg7%LN}8c}NEL>VMnLum~LhJr64;D9ti(rjRHY6_D- z$pbUT?!c49^3L29fDVkhT9opa8H2FQ)m_IO??dxERC*jNMK^P+)r zdHzutefMxheku*ZX!0jt`A=JOYjWh+e)O#S5x?=xzq`K8CH7+u{BL)+El+W~v1f4L zz0r|D)GB3oQUcCGr3y)s>O`fnPP8$`z!;ZWXWL3Cl_&#abdYL0mns#;%EBlUI&11F zOeUW(_~U=|=e=G_jvPA$J*S(Q(yecZ==9&}ZnIL~fKLApUVjd1{e}_mP=ZW^O;o3wg#Y$ z4ZvN!;@YRS_&(&w(U*9_2 '[class JavaController]'; +module.exports = JavaController; + diff --git a/electron/controller/mysql.js b/electron/controller/mysql.js new file mode 100644 index 0000000..b925bca --- /dev/null +++ b/electron/controller/mysql.js @@ -0,0 +1,75 @@ +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; + } +} + +// 延迟加载 MySQLManager +let MySQLManager = null; +function getMySQLManager() { + if (!MySQLManager) { + MySQLManager = require(getScriptsPath('start-mysql')); + } + return MySQLManager; +} + +class MySQLController { + + /** + * 启动MySQL服务 + */ + async start() { + try { + const MySQLManagerClass = getMySQLManager(); + const mysqlManager = new MySQLManagerClass(); + await mysqlManager.start(); + return { success: true, message: 'MySQL started successfully' }; + } catch (error) { + return { success: false, message: error.message }; + } + } + + /** + * 停止MySQL服务 + */ + async stop() { + try { + const MySQLManagerClass = getMySQLManager(); + const mysqlManager = new MySQLManagerClass(); + await mysqlManager.stop(); + return { success: true, message: 'MySQL stopped successfully' }; + } catch (error) { + return { success: false, message: error.message }; + } + } + + /** + * 获取MySQL连接配置 + */ + async getConnectionConfig() { + try { + const MySQLManagerClass = getMySQLManager(); + const mysqlManager = new MySQLManagerClass(); + const config = mysqlManager.getConnectionConfig(); + return { success: true, data: config }; + } catch (error) { + return { success: false, message: error.message }; + } + } +} + +MySQLController.toString = () => '[class MySQLController]'; +module.exports = MySQLController; \ No newline at end of file diff --git a/scripts/config-generator.js b/scripts/config-generator.js new file mode 100644 index 0000000..16a3e0c --- /dev/null +++ b/scripts/config-generator.js @@ -0,0 +1,136 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * 配置文件生成器 + * 根据应用实际安装路径生成Spring Boot配置文件 + */ +class ConfigGenerator { + constructor() { + // 开发环境:项目根目录 + // 打包后:应用根目录(win-unpacked) + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + + // 开发环境:build/extraResources/java + // 打包后:resources/extraResources/java + this.javaPath = isDev + ? path.join(baseDir, 'build', 'extraResources', 'java') + : path.join(process.resourcesPath, 'extraResources', 'java'); + this.templatePath = path.join(this.javaPath, 'application.yml.template'); + this.configPath = path.join(this.javaPath, 'application.yml'); + + // 数据目录(使用应用所在盘符的根目录下的data文件夹) + this.dataPath = this.getDataPath(baseDir); + } + + /** + * 获取数据目录路径 + * @param {string} baseDir 应用基础目录 + * @returns {string} 数据目录路径 + */ + getDataPath(baseDir) { + // 获取应用所在盘符(例如:C:, D:, E:) + const driveLetter = path.parse(baseDir).root; + // 数据目录设置在盘符根目录下的 NPQS9100_Data 文件夹 + return path.join(driveLetter, 'NPQS9100_Data'); + } + + /** + * 生成配置文件 + * @param {object} options - 配置选项 + * @param {number} options.mysqlPort - MySQL 端口 + * @param {number} options.javaPort - Java 应用端口 + * @param {string} options.mysqlPassword - MySQL 密码 + */ + generateConfig(options = {}) { + return new Promise((resolve, reject) => { + try { + // 读取模板文件 + if (!fs.existsSync(this.templatePath)) { + throw new Error(`Template file not found: ${this.templatePath}`); + } + + let template = fs.readFileSync(this.templatePath, 'utf-8'); + + // 替换占位符 + // Windows路径需要转义反斜杠 + const dataPathEscaped = this.dataPath.replace(/\\/g, '\\\\'); + template = template.replace(/\{\{APP_DATA_PATH\}\}/g, dataPathEscaped); + + // 替换MySQL密码 + const mysqlPassword = options.mysqlPassword || 'njcnpqs'; + template = template.replace(/\{\{MYSQL_PASSWORD\}\}/g, mysqlPassword); + + // 替换端口(如果提供) + if (options.mysqlPort) { + // 支持两种格式:localhost:3306 和 {{MYSQL_PORT}} + template = template.replace(/\{\{MYSQL_PORT\}\}/g, options.mysqlPort); + template = template.replace(/localhost:3306/g, `localhost:${options.mysqlPort}`); + } + if (options.javaPort) { + template = template.replace(/port:\s*18092/g, `port: ${options.javaPort}`); + } + + // 写入配置文件 + fs.writeFileSync(this.configPath, template, 'utf-8'); + + // 创建必要的目录 + this.createDirectories(); + + console.log('[ConfigGenerator] Configuration file generated successfully'); + console.log('[ConfigGenerator] Data path:', this.dataPath); + console.log('[ConfigGenerator] MySQL port:', options.mysqlPort || 3306); + console.log('[ConfigGenerator] MySQL password:', options.mysqlPassword || 'njcnpqs'); + console.log('[ConfigGenerator] Java port:', options.javaPort || 18092); + + resolve({ + configPath: this.configPath, + dataPath: this.dataPath, + mysqlPort: options.mysqlPort || 3306, + javaPort: options.javaPort || 18092 + }); + } catch (error) { + console.error('[ConfigGenerator] Failed to generate config:', error); + reject(error); + } + }); + } + + /** + * 创建必要的目录 + */ + createDirectories() { + const dirs = [ + this.dataPath, + path.join(this.dataPath, 'logs'), + path.join(this.dataPath, 'template'), + path.join(this.dataPath, 'report'), + path.join(this.dataPath, 'data') + ]; + + dirs.forEach(dir => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + console.log('[ConfigGenerator] Created directory:', dir); + } + }); + } + + /** + * 获取配置信息 + */ + getConfigInfo() { + return { + javaPath: this.javaPath, + templatePath: this.templatePath, + configPath: this.configPath, + dataPath: this.dataPath + }; + } +} + +module.exports = ConfigGenerator; + diff --git a/scripts/java-runner.js b/scripts/java-runner.js new file mode 100644 index 0000000..803068c --- /dev/null +++ b/scripts/java-runner.js @@ -0,0 +1,321 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +/** + * Java 运行器 - 用于调用便携式 JRE 运行 Java 程序 + */ +class JavaRunner { + constructor() { + // 在开发与打包后均可解析到应用根目录下的 jre 目录 + // 开发环境:项目根目录 + // 打包后:应用根目录(win-unpacked) + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + this.jrePath = path.join(baseDir, 'jre'); + this.binPath = path.join(this.jrePath, 'bin'); + this.javaExe = path.join(this.binPath, 'java.exe'); + } + + /** + * 检查 JRE 是否存在 + */ + isJREAvailable() { + return fs.existsSync(this.javaExe); + } + + /** + * 获取 Java 版本 + */ + getVersion() { + return new Promise((resolve, reject) => { + if (!this.isJREAvailable()) { + reject(new Error('JRE not found at: ' + this.javaExe)); + return; + } + + const versionProcess = spawn(this.javaExe, ['-version'], { + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let output = ''; + let errorOutput = ''; + + versionProcess.stdout.on('data', (data) => { + output += data.toString(); + }); + + versionProcess.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + versionProcess.on('close', (code) => { + if (code === 0 || errorOutput.includes('version')) { + // Java -version 输出到 stderr + const versionInfo = (output + errorOutput).trim(); + resolve(versionInfo); + } else { + reject(new Error('Failed to get Java version')); + } + }); + }); + } + + /** + * 运行 JAR 文件 + * @param {string} jarPath - JAR 文件的绝对路径 + * @param {Array} args - Java 程序参数 + * @param {Object} options - spawn 选项 + * @returns {ChildProcess} + */ + runJar(jarPath, args = [], options = {}) { + if (!this.isJREAvailable()) { + throw new Error('JRE not found at: ' + this.javaExe); + } + + if (!fs.existsSync(jarPath)) { + throw new Error('JAR file not found at: ' + jarPath); + } + + const javaArgs = ['-jar', jarPath, ...args]; + + const defaultOptions = { + cwd: path.dirname(jarPath), + stdio: 'inherit' + }; + + const mergedOptions = { ...defaultOptions, ...options }; + + console.log('Running Java:', this.javaExe, javaArgs.join(' ')); + return spawn(this.javaExe, javaArgs, mergedOptions); + } + + /** + * 运行 JAR 文件并等待完成 + * @param {string} jarPath - JAR 文件的绝对路径 + * @param {Array} args - Java 程序参数 + * @param {Object} options - spawn 选项 + * @returns {Promise} 退出代码 + */ + runJarAsync(jarPath, args = [], options = {}) { + return new Promise((resolve, reject) => { + try { + const process = this.runJar(jarPath, args, options); + + process.on('close', (code) => { + if (code === 0) { + resolve(code); + } else { + reject(new Error(`Java process exited with code ${code}`)); + } + }); + + process.on('error', (error) => { + reject(error); + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * 运行 Java 类 + * @param {string} className - Java 类名(包含包名) + * @param {string} classPath - classpath 路径 + * @param {Array} args - 程序参数 + * @param {Object} options - spawn 选项 + * @returns {ChildProcess} + */ + runClass(className, classPath, args = [], options = {}) { + if (!this.isJREAvailable()) { + throw new Error('JRE not found at: ' + this.javaExe); + } + + const javaArgs = ['-cp', classPath, className, ...args]; + + const defaultOptions = { + stdio: 'inherit' + }; + + const mergedOptions = { ...defaultOptions, ...options }; + + console.log('Running Java:', this.javaExe, javaArgs.join(' ')); + return spawn(this.javaExe, javaArgs, mergedOptions); + } + + /** + * 运行 Spring Boot JAR 文件 + * @param {string} jarPath - JAR 文件的绝对路径 + * @param {string} configPath - 配置文件路径 + * @param {Object} options - 启动选项(需包含 javaPort) + * @returns {ChildProcess} + */ + runSpringBoot(jarPath, configPath, options = {}) { + if (!this.isJREAvailable()) { + throw new Error('JRE not found at: ' + this.javaExe); + } + + if (!fs.existsSync(jarPath)) { + throw new Error('JAR file not found at: ' + jarPath); + } + + const javaArgs = [ + '-Dfile.encoding=UTF-8', // 设置文件编码为UTF-8,解决中文乱码 + '-Duser.language=zh', // 设置语言为中文 + '-Duser.region=CN', // 设置地区为中国 + '-jar', + jarPath, + `--spring.config.location=${configPath}` + ]; + + const defaultOptions = { + cwd: path.dirname(jarPath), + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + JAVA_TOOL_OPTIONS: '-Dfile.encoding=UTF-8' // 额外确保UTF-8编码 + } + }; + + const mergedOptions = { ...defaultOptions, ...options }; + + console.log('Running Spring Boot:', this.javaExe, javaArgs.join(' ')); + const javaProcess = spawn(this.javaExe, javaArgs, mergedOptions); + + // 记录PID和端口用于后续停止 + this.springBootProcess = javaProcess; + this.currentJavaPort = options.javaPort; + + // 将Java端口记录到文件,供手动清理脚本使用 + if (options.javaPort) { + this.recordJavaPort(options.javaPort); + } + + // 进程退出时清理端口记录 + javaProcess.on('close', () => { + this.cleanupJavaPortFile(); + }); + + return javaProcess; + } + + /** + * 停止 Spring Boot 应用 + */ + stopSpringBoot() { + return new Promise((resolve) => { + if (this.springBootProcess && !this.springBootProcess.killed) { + // 设置3秒超时,如果进程没有正常退出,强制kill + const timeout = setTimeout(() => { + console.log('[Java] Force killing Spring Boot process'); + try { + this.springBootProcess.kill('SIGKILL'); + } catch (e) { + console.error('[Java] Error force killing:', e); + } + + // 清理端口记录文件 + this.cleanupJavaPortFile(); + resolve(); + }, 3000); + + this.springBootProcess.on('close', () => { + clearTimeout(timeout); + console.log('[Java] Spring Boot application stopped gracefully'); + + // 清理端口记录文件 + this.cleanupJavaPortFile(); + resolve(); + }); + + // 先尝试优雅关闭 + console.log('[Java] Sending SIGTERM to Spring Boot'); + this.springBootProcess.kill('SIGTERM'); + } else { + // 即使没有进程引用,也尝试清理端口记录文件 + this.cleanupJavaPortFile(); + resolve(); + } + }); + } + + /** + * 记录Java端口到文件 + */ + recordJavaPort(port) { + try { + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + const javaDir = path.join(baseDir, 'java'); + const portFilePath = path.join(javaDir, '.running-port'); + + fs.writeFileSync(portFilePath, port.toString(), 'utf-8'); + console.log(`[Java] Port ${port} recorded to ${portFilePath}`); + } catch (error) { + console.warn('[Java] Failed to record port:', error); + } + } + + /** + * 清理Java端口记录文件 + */ + cleanupJavaPortFile() { + try { + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + const javaDir = path.join(baseDir, 'java'); + const portFilePath = path.join(javaDir, '.running-port'); + + if (fs.existsSync(portFilePath)) { + fs.unlinkSync(portFilePath); + console.log('[Java] Port record file cleaned up'); + } + } catch (error) { + console.warn('[Java] Failed to cleanup port record:', error); + } + } + + /** + * 获取记录的Java运行端口 + */ + getRecordedJavaPort() { + try { + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + const javaDir = path.join(baseDir, 'java'); + const portFilePath = path.join(javaDir, '.running-port'); + + if (fs.existsSync(portFilePath)) { + const port = fs.readFileSync(portFilePath, 'utf-8').trim(); + return parseInt(port); + } + } catch (error) { + console.warn('[Java] Failed to read port record:', error); + } + return null; + } + + /** + * 获取 JRE 路径信息 + */ + getPathInfo() { + return { + jrePath: this.jrePath, + binPath: this.binPath, + javaExe: this.javaExe, + available: this.isJREAvailable() + }; + } +} + +module.exports = JavaRunner; + diff --git a/scripts/log-window-manager.js b/scripts/log-window-manager.js new file mode 100644 index 0000000..e52d6b2 --- /dev/null +++ b/scripts/log-window-manager.js @@ -0,0 +1,325 @@ +const { BrowserWindow } = require('electron'); +const path = require('path'); +const fs = require('fs'); + +/** + * 日志窗口管理器 + * 显示 MySQL 和 Spring Boot 的实时日志 + */ +class LogWindowManager { + constructor() { + this.logWindow = null; + this.logs = []; + this.maxLogs = 1000; // 最多保留1000条日志 + } + + /** + * 创建日志窗口 + */ + createLogWindow() { + this.logWindow = new BrowserWindow({ + width: 900, + height: 600, + title: 'NPQS9100 - 服务日志', + backgroundColor: '#1e1e1e', + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + // 加载日志页面 + const logHtml = this.generateLogHTML(); + this.logWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(logHtml)}`); + + // 窗口关闭事件 - 只清理引用,不退出应用 + this.logWindow.on('closed', () => { + console.log('[LogWindow] Log window closed by user'); + this.logWindow = null; + }); + + // 防止日志窗口关闭时退出应用(但允许隐藏) + this.closeHandler = (event) => { + // 只是隐藏窗口,不是真正关闭 + // 这样可以随时再打开 + event.preventDefault(); + this.logWindow.hide(); + console.log('[LogWindow] Log window hidden'); + }; + + this.logWindow.on('close', this.closeHandler); + + return this.logWindow; + } + + /** + * 生成日志HTML页面 + */ + generateLogHTML() { + return ` + + + + + NPQS9100 服务日志 + + + +

+
📝 NPQS9100 服务日志监控
+
+ + +
+
+
+ + + + + `; + } + + /** + * 添加日志 + */ + addLog(type, message) { + const timestamp = new Date().toLocaleTimeString(); + const logEntry = { + timestamp, + type, + message + }; + + this.logs.push(logEntry); + + // 限制日志数量 + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + + // 发送到窗口 + if (this.logWindow && !this.logWindow.isDestroyed()) { + this.logWindow.webContents.send('log-message', logEntry); + } + + // 同时输出到控制台 + console.log(`[${type.toUpperCase()}] ${message}`); + } + + /** + * 显示日志窗口 + */ + show() { + if (!this.logWindow || this.logWindow.isDestroyed()) { + // 窗口已被销毁,重新创建 + console.log('[LogWindow] Recreating log window...'); + this.createLogWindow(); + + // 重新发送历史日志 + this.logs.forEach(log => { + this.logWindow.webContents.send('log-message', log); + }); + } else { + this.logWindow.show(); + this.logWindow.focus(); + console.log('[LogWindow] Log window shown'); + } + } + + /** + * 隐藏日志窗口 + */ + hide() { + if (this.logWindow && !this.logWindow.isDestroyed()) { + this.logWindow.hide(); + console.log('[LogWindow] Log window hidden'); + } + } + + /** + * 检查日志窗口是否可见 + */ + isVisible() { + return this.logWindow && !this.logWindow.isDestroyed() && this.logWindow.isVisible(); + } + + /** + * 切换日志窗口显示/隐藏 + */ + toggle() { + if (this.isVisible()) { + this.hide(); + } else { + this.show(); + } + } + + /** + * 关闭日志窗口(真正销毁) + */ + close() { + if (this.logWindow && !this.logWindow.isDestroyed()) { + try { + // 移除 close 事件监听,允许真正关闭 + if (this.closeHandler) { + this.logWindow.removeListener('close', this.closeHandler); + } + this.logWindow.removeAllListeners('close'); + this.logWindow.removeAllListeners('closed'); + + // 强制销毁窗口 + this.logWindow.destroy(); + console.log('[LogWindow] Log window destroyed'); + } catch (error) { + console.error('[LogWindow] Error closing log window:', error); + } finally { + this.logWindow = null; + this.closeHandler = null; + } + } + } + + /** + * 获取所有日志 + */ + getLogs() { + return this.logs; + } + + /** + * 清空日志 + */ + clearLogs() { + this.logs = []; + if (this.logWindow && !this.logWindow.isDestroyed()) { + this.logWindow.webContents.send('clear-logs'); + } + } +} + +module.exports = LogWindowManager; + diff --git a/scripts/port-checker.js b/scripts/port-checker.js new file mode 100644 index 0000000..ef424bf --- /dev/null +++ b/scripts/port-checker.js @@ -0,0 +1,154 @@ +const net = require('net'); + +/** + * 端口检测工具 + */ +class PortChecker { + /** + * 检查端口是否可用(检测0.0.0.0,确保能绑定到所有地址) + * @param {number} port - 端口号 + * @param {string} host - 主机地址,默认 0.0.0.0(所有地址) + * @returns {Promise} true 表示端口可用,false 表示已被占用 + */ + static checkPort(port, host = '0.0.0.0') { + return new Promise((resolve) => { + // 先尝试连接,看是否有服务在监听 + const testSocket = new net.Socket(); + testSocket.setTimeout(200); + + testSocket.on('connect', () => { + // 能连接上,说明端口被占用 + console.log(`[PortChecker] Port ${port} is in use (connection successful)`); + testSocket.destroy(); + resolve(false); + }); + + testSocket.on('timeout', () => { + // 超时,再用绑定方式检测 + testSocket.destroy(); + this._checkPortByBinding(port, host, resolve); + }); + + testSocket.on('error', (err) => { + testSocket.destroy(); + if (err.code === 'ECONNREFUSED') { + // 连接被拒绝,说明没有服务监听,再用绑定方式确认 + this._checkPortByBinding(port, host, resolve); + } else { + // 其他错误,认为端口可用 + resolve(true); + } + }); + + testSocket.connect(port, '127.0.0.1'); + }); + } + + static _checkPortByBinding(port, host, resolve) { + const server = net.createServer(); + + server.once('error', (err) => { + if (err.code === 'EADDRINUSE') { + console.log(`[PortChecker] Port ${port} is in use (EADDRINUSE)`); + resolve(false); + } else { + console.log(`[PortChecker] Port ${port} check error: ${err.code}`); + resolve(false); + } + }); + + server.once('listening', () => { + server.close(); + console.log(`[PortChecker] Port ${port} is available`); + resolve(true); + }); + + server.listen(port, host); + } + + /** + * 查找可用端口(从指定端口开始递增查找) + * @param {number} startPort - 起始端口 + * @param {number} maxAttempts - 最大尝试次数,默认100 + * @param {string} host - 主机地址,默认 0.0.0.0 + * @returns {Promise} 返回可用的端口号,如果都不可用则返回 -1 + */ + static async findAvailablePort(startPort, maxAttempts = 100, host = '0.0.0.0') { + console.log(`[PortChecker] Searching for available port starting from ${startPort}...`); + + for (let i = 0; i < maxAttempts; i++) { + const port = startPort + i; + const isAvailable = await this.checkPort(port, host); + + if (isAvailable) { + console.log(`[PortChecker] ✓ Found available port: ${port}`); + return port; + } else { + console.log(`[PortChecker] ✗ Port ${port} is in use, trying ${port + 1}...`); + } + } + + console.error(`[PortChecker] ✗ No available port found from ${startPort} to ${startPort + maxAttempts - 1}`); + return -1; + } + + /** + * 等待端口开始监听(用于检测服务是否启动成功) + * @param {number} port - 端口号 + * @param {number} timeout - 超时时间(毫秒),默认30秒 + * @param {string} host - 主机地址 + * @returns {Promise} true 表示端口已开始监听 + */ + static async waitForPort(port, timeout = 30000, host = '127.0.0.1') { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const isListening = await this.isPortListening(port, host); + + if (isListening) { + console.log(`[PortChecker] Port ${port} is now listening`); + return true; + } + + // 等待500ms后重试 + await new Promise(resolve => setTimeout(resolve, 500)); + } + + console.error(`[PortChecker] Timeout waiting for port ${port} to listen`); + return false; + } + + /** + * 检查端口是否正在监听 + * @param {number} port - 端口号 + * @param {string} host - 主机地址 + * @returns {Promise} + */ + static isPortListening(port, host = '127.0.0.1') { + return new Promise((resolve) => { + const socket = new net.Socket(); + + socket.setTimeout(1000); + + socket.once('connect', () => { + socket.destroy(); + resolve(true); + }); + + socket.once('timeout', () => { + socket.destroy(); + resolve(false); + }); + + socket.once('error', () => { + socket.destroy(); + resolve(false); + }); + + socket.connect(port, host); + }); + } +} + +module.exports = PortChecker; + diff --git a/scripts/start-mysql.js b/scripts/start-mysql.js new file mode 100644 index 0000000..f46ab48 --- /dev/null +++ b/scripts/start-mysql.js @@ -0,0 +1,373 @@ +const { spawn, exec } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +class MySQLManager { + constructor() { + // 在开发与打包后均可解析到应用根目录下的 mysql 目录 + // 开发环境:项目根目录 + // 打包后:应用根目录(win-unpacked) + const isDev = !process.resourcesPath; + const baseDir = isDev + ? path.join(__dirname, '..') + : path.dirname(process.resourcesPath); + this.mysqlPath = path.join(baseDir, 'mysql'); + this.binPath = path.join(this.mysqlPath, 'bin'); + this.dataPath = path.join(this.mysqlPath, 'data'); + this.process = null; + this.currentPort = null; + } + + // 检查MySQL是否已初始化 + isInitialized() { + return fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0; + } + + // 初始化MySQL数据库 + async initialize() { + if (this.isInitialized()) { + console.log('MySQL already initialized'); + return Promise.resolve(); + } + + return new Promise(async (resolve, reject) => { + const mysqld = path.join(this.binPath, 'mysqld.exe'); + + // 创建初始化SQL文件(授权127.0.0.1和所有主机) + const initSqlPath = path.join(this.mysqlPath, 'init_grant.sql'); + const initSql = ` +CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION; +CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +`; + + try { + fs.writeFileSync(initSqlPath, initSql, 'utf-8'); + console.log('[MySQL] Created init SQL file for granting permissions'); + } catch (error) { + console.error('[MySQL] Failed to create init SQL file:', error); + } + + // 使用 --init-file 参数初始化并授权 + const initProcess = spawn(mysqld, [ + '--initialize-insecure', + `--init-file=${initSqlPath}` + ], { + cwd: this.mysqlPath, + stdio: 'inherit' + }); + + initProcess.on('close', (code) => { + if (code === 0) { + console.log('[MySQL] Initialized successfully with permissions granted'); + // 删除临时SQL文件 + try { + if (fs.existsSync(initSqlPath)) { + fs.unlinkSync(initSqlPath); + } + } catch (e) { + // 忽略删除失败 + } + resolve(); + } else { + reject(new Error(`MySQL initialization failed with code ${code}`)); + } + }); + }); + } + + // 启动MySQL服务 + start(port = 3306) { + return new Promise(async (resolve, reject) => { + try { + // 确保数据库已初始化 + await this.initialize(); + + const mysqld = path.join(this.binPath, 'mysqld.exe'); + + // 启动MySQL,指定端口 + this.process = spawn(mysqld, [ + '--console', + `--port=${port}` + ], { + cwd: this.mysqlPath, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + this.currentPort = port; + + // 将当前端口写入文件,供停止脚本使用 + try { + const portFilePath = path.join(this.mysqlPath, '.running-port'); + fs.writeFileSync(portFilePath, port.toString(), 'utf-8'); + console.log(`[MySQL] Port ${port} recorded to ${portFilePath}`); + } catch (error) { + console.warn('[MySQL] Failed to record port:', error); + } + + let output = ''; + this.process.stdout.on('data', (data) => { + output += data.toString(); + console.log('MySQL:', data.toString()); + + // MySQL启动完成的标志 + if (output.includes('ready for connections') || output.includes('MySQL Community Server')) { + console.log(`MySQL started successfully on port ${port}`); + + // 自动授权 root 用户从任何主机连接 + setTimeout(async () => { + try { + console.log('[MySQL] Waiting 3 seconds before granting permissions...'); + await this.grantRootAccess(); + } catch (error) { + console.warn('[MySQL] Failed to grant root access, but MySQL is running:', error.message); + } + resolve(port); + }, 3000); + } + }); + + this.process.stderr.on('data', (data) => { + console.error('MySQL Error:', data.toString()); + }); + + this.process.on('close', (code) => { + console.log(`MySQL process exited with code ${code}`); + this.process = null; + this.currentPort = null; + + // 删除端口记录文件 + try { + const portFilePath = path.join(this.mysqlPath, '.running-port'); + if (fs.existsSync(portFilePath)) { + fs.unlinkSync(portFilePath); + console.log('[MySQL] Port record file removed'); + } + } catch (error) { + console.warn('[MySQL] Failed to remove port record:', error); + } + }); + + // 超时处理 + setTimeout(async () => { + if (this.process && !this.process.killed) { + console.log(`MySQL started on port ${port} (timeout reached, assuming success)`); + + // 自动授权 root 用户从任何主机连接 + try { + console.log('[MySQL] Granting permissions...'); + await this.grantRootAccess(); + } catch (error) { + console.warn('[MySQL] Failed to grant root access, but MySQL is running:', error.message); + } + + resolve(port); + } + }, 18000); + + } catch (error) { + reject(error); + } + }); + } + + // 授权 root 用户从 127.0.0.1 访问 + grantRootAccess() { + return new Promise((resolve, reject) => { + // 创建 SQL 文件 + const sqlFilePath = path.join(this.mysqlPath, 'grant_root.sql'); + const sqlContent = ` +CREATE USER IF NOT EXISTS 'root'@'127.0.0.1' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' WITH GRANT OPTION; +CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +`; + + try { + fs.writeFileSync(sqlFilePath, sqlContent, 'utf-8'); + } catch (error) { + console.error('[MySQL] Failed to create grant SQL file:', error); + return resolve(); // 继续启动 + } + + const mysqlExe = path.join(this.binPath, 'mysql.exe'); + const grantProcess = spawn(mysqlExe, [ + '--host=localhost', + `--port=${this.currentPort}`, + '--user=root' + ], { + cwd: this.mysqlPath, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + // 通过 stdin 输入 SQL + grantProcess.stdin.write(sqlContent); + grantProcess.stdin.end(); + + let output = ''; + let errorOutput = ''; + + grantProcess.stdout.on('data', (data) => { + output += data.toString(); + }); + + grantProcess.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + grantProcess.on('close', (code) => { + if (code === 0) { + console.log('[MySQL] Root user granted access from 127.0.0.1 and all hosts'); + resolve(); + } else { + console.error('[MySQL] Grant access failed (code:', code, ')'); + console.error('[MySQL] Error output:', errorOutput); + console.error('[MySQL] Standard output:', output); + // 即使失败也 resolve,让应用继续启动 + resolve(); + } + }); + }); + } + + // 获取当前MySQL端口 + getCurrentPort() { + return this.currentPort || 3306; + } + + // 停止MySQL服务 + stop() { + return new Promise(async (resolve) => { + if (this.process && !this.process.killed) { + console.log('[MySQL] Stopping MySQL...'); + + // 方法1: 尝试使用 mysqladmin shutdown 优雅关闭 + try { + console.log('[MySQL] Trying mysqladmin shutdown...'); + const mysqladmin = path.join(this.binPath, 'mysqladmin.exe'); + + if (fs.existsSync(mysqladmin)) { + const shutdownProcess = spawn(mysqladmin, [ + '-u', 'root', + '-pnjcnpqs', + '--port=' + this.currentPort, + 'shutdown' + ], { + cwd: this.mysqlPath, + stdio: 'ignore' + }); + + // 等待 mysqladmin 执行完成(最多5秒) + const shutdownPromise = new Promise((res) => { + shutdownProcess.on('close', (code) => { + console.log(`[MySQL] mysqladmin shutdown exited with code ${code}`); + res(code === 0); + }); + }); + + const timeoutPromise = new Promise((res) => setTimeout(() => res(false), 5000)); + const shutdownSuccess = await Promise.race([shutdownPromise, timeoutPromise]); + + if (shutdownSuccess) { + console.log('[MySQL] Shutdown successful via mysqladmin'); + // 等待进程真正退出 + await new Promise((res) => { + if (this.process && !this.process.killed) { + this.process.on('close', res); + setTimeout(res, 2000); // 最多等2秒 + } else { + res(); + } + }); + this.cleanupPortFile(); + return resolve(); + } + } + } catch (error) { + console.warn('[MySQL] mysqladmin shutdown failed:', error.message); + } + + // 方法2: 如果 mysqladmin 失败,尝试 SIGTERM + console.log('[MySQL] Trying SIGTERM...'); + const killTimeout = setTimeout(() => { + // 方法3: 5秒后强制 SIGKILL + console.log('[MySQL] Force killing with SIGKILL'); + try { + if (this.process && !this.process.killed) { + this.process.kill('SIGKILL'); + } + } catch (e) { + console.error('[MySQL] Error force killing:', e); + } + this.cleanupPortFile(); + resolve(); + }, 5000); + + this.process.on('close', () => { + clearTimeout(killTimeout); + console.log('[MySQL] Process closed'); + this.cleanupPortFile(); + resolve(); + }); + + try { + this.process.kill('SIGTERM'); + } catch (e) { + console.error('[MySQL] Error sending SIGTERM:', e); + clearTimeout(killTimeout); + this.cleanupPortFile(); + resolve(); + } + } else { + // 没有进程引用,说明MySQL已经停止或不在我们控制下 + console.log('[MySQL] No process reference, MySQL may already be stopped'); + console.log('[MySQL] If MySQL is still running, please use kill-running-port.bat to clean up'); + this.cleanupPortFile(); + resolve(); + } + }); + } + + // 清理端口记录文件 + cleanupPortFile() { + try { + const portFilePath = path.join(this.mysqlPath, '.running-port'); + if (fs.existsSync(portFilePath)) { + fs.unlinkSync(portFilePath); + console.log('[MySQL] Port record file cleaned up'); + } + } catch (error) { + console.warn('[MySQL] Failed to cleanup port record:', error); + } + } + + // 获取记录的运行端口 + getRecordedPort() { + try { + const portFilePath = path.join(this.mysqlPath, '.running-port'); + if (fs.existsSync(portFilePath)) { + const port = fs.readFileSync(portFilePath, 'utf-8').trim(); + return parseInt(port); + } + } catch (error) { + console.warn('[MySQL] Failed to read port record:', error); + } + return null; + } + + // 获取MySQL连接配置 + getConnectionConfig() { + return { + host: 'localhost', + port: 3306, + user: 'root', + password: '', + database: 'app_db' + }; + } +} + +module.exports = MySQLManager; \ No newline at end of file diff --git a/scripts/startup-manager.js b/scripts/startup-manager.js new file mode 100644 index 0000000..cb293c0 --- /dev/null +++ b/scripts/startup-manager.js @@ -0,0 +1,116 @@ +const { BrowserWindow } = require('electron'); +const path = require('path'); + +/** + * 启动状态管理器 + * 管理启动流程和显示启动进度 + */ +class StartupManager { + constructor() { + this.loadingWindow = null; + this.steps = [ + { id: 'init', label: '正在初始化应用...', progress: 0 }, + { id: 'check-mysql-port', label: '正在检测MySQL端口...', progress: 15 }, + { id: 'start-mysql', label: '正在启动MySQL数据库...', progress: 30 }, + { id: 'wait-mysql', label: '等待MySQL就绪...', progress: 45 }, + { id: 'check-java-port', label: '正在检测后端服务端口...', progress: 60 }, + { id: 'generate-config', label: '正在生成配置文件...', progress: 70 }, + { id: 'start-java', label: '正在启动后端服务...', progress: 80 }, + { id: 'wait-java', label: '等待后端服务就绪...', progress: 90 }, + { id: 'done', label: '启动完成!', progress: 100 } + ]; + this.currentStep = 0; + } + + /** + * 创建 Loading 窗口 + */ + createLoadingWindow() { + this.loadingWindow = new BrowserWindow({ + width: 500, + height: 300, + frame: false, + transparent: true, + resizable: false, + alwaysOnTop: true, + skipTaskbar: true, // 不在任务栏显示 + webPreferences: { + nodeIntegration: true, + contextIsolation: false + } + }); + + // 加载 loading 页面 + const loadingHtml = path.join(__dirname, '../public/html/loading.html'); + this.loadingWindow.loadFile(loadingHtml); + + return this.loadingWindow; + } + + /** + * 更新启动进度 + * @param {string} stepId - 步骤ID + * @param {object} extraInfo - 额外信息 + */ + updateProgress(stepId, extraInfo = {}) { + const stepIndex = this.steps.findIndex(s => s.id === stepId); + + if (stepIndex !== -1) { + this.currentStep = stepIndex; + const step = this.steps[stepIndex]; + + const progressData = { + step: stepId, + label: step.label, + progress: step.progress, + ...extraInfo + }; + + // 发送进度到 loading 窗口 + if (this.loadingWindow && !this.loadingWindow.isDestroyed()) { + this.loadingWindow.webContents.send('startup-progress', progressData); + } + + console.log(`[StartupManager] ${step.label} (${step.progress}%)`, extraInfo); + } + } + + /** + * 显示错误信息 + * @param {string} error - 错误信息 + */ + showError(error) { + if (this.loadingWindow && !this.loadingWindow.isDestroyed()) { + this.loadingWindow.webContents.send('startup-error', { error }); + } + console.error('[StartupManager] Error:', error); + } + + /** + * 关闭 Loading 窗口 + */ + closeLoadingWindow() { + if (this.loadingWindow && !this.loadingWindow.isDestroyed()) { + // 使用 destroy() 而不是 close() 确保窗口被完全销毁 + this.loadingWindow.destroy(); + this.loadingWindow = null; + } + } + + /** + * 获取所有步骤 + */ + getSteps() { + return this.steps; + } + + /** + * 获取当前步骤 + */ + getCurrentStep() { + return this.steps[this.currentStep]; + } +} + +module.exports = StartupManager; +