diff --git a/build/icons/icon_backup.ico b/build/icons/icon_backup.ico new file mode 100644 index 0000000..da8b5a2 Binary files /dev/null and b/build/icons/icon_backup.ico differ diff --git a/electron/controller/java.js b/electron/controller/java.js new file mode 100644 index 0000000..5063ca5 --- /dev/null +++ b/electron/controller/java.js @@ -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; + 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; +