const { spawn, exec } = require('child_process'); const path = require('path'); const fs = require('fs'); /** * MySQL 进程管理器 * 使用进程模式启动 MySQL,无需管理员权限 */ class MySQLServiceManager { constructor(logWindowManager = null) { 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.configFile = path.join(this.mysqlPath, 'my.ini'); this.logWindowManager = logWindowManager; // 进程模式:保存 MySQL 进程引用 this.mysqlProcess = null; this.currentPort = null; } // 睡眠函数 sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 日志输出辅助方法 log(type, message) { console.log(`[MySQL] ${message}`); if (this.logWindowManager) { this.logWindowManager.addLog(type, `[MySQL] ${message}`); } } /** * 执行命令并返回Promise */ execCommand(command) { return new Promise((resolve, reject) => { exec(command, { encoding: 'utf8' }, (error, stdout, stderr) => { if (error) { const err = new Error(error.message || stderr || stdout || 'Command execution failed'); err.code = error.code; err.stdout = stdout; err.stderr = stderr; reject(err); } else { resolve({ stdout, stderr }); } }); }); } /** * 读取配置文件中的端口 */ readPortFromConfig() { try { if (fs.existsSync(this.configFile)) { const content = fs.readFileSync(this.configFile, 'utf-8'); const match = content.match(/port\s*=\s*(\d+)/); return match ? parseInt(match[1]) : 3306; } } catch (error) { console.error('[MySQL] Failed to read port from config:', error); } return 3306; } /** * 生成 my.ini 配置文件 */ generateMyIni(port) { const basedir = this.mysqlPath.replace(/\\/g, '/'); const datadir = this.dataPath.replace(/\\/g, '/'); const config = `[mysqld] # 基础路径配置 basedir=${basedir} datadir=${datadir} # 网络配置 port=${port} bind-address=0.0.0.0 # 字符集配置 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci # 性能优化 max_connections=200 innodb_buffer_pool_size=128M innodb_log_file_size=64M # 日志配置 log_error=${basedir}/mysql_error.log slow_query_log=1 slow_query_log_file=${basedir}/mysql_slow.log long_query_time=2 # 安全配置 sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION [client] default-character-set=utf8mb4 `; fs.writeFileSync(this.configFile, config, 'utf-8'); this.log('system', `生成配置文件,端口: ${port}`); } /** * 初始化数据库 */ async initializeDatabase() { return new Promise((resolve, reject) => { this.log('system', '正在初始化 MySQL 数据库...'); const mysqld = path.join(this.binPath, 'mysqld.exe'); const initProcess = spawn(mysqld, [ '--defaults-file=' + this.configFile, '--initialize-insecure' ], { cwd: this.mysqlPath, stdio: 'pipe' }); initProcess.stdout.on('data', (data) => { this.log('system', data.toString().trim()); }); initProcess.stderr.on('data', (data) => { this.log('system', data.toString().trim()); }); initProcess.on('close', (code) => { if (code === 0) { this.log('success', '数据库初始化成功'); resolve(); } else { reject(new Error(`数据库初始化失败,退出码: ${code}`)); } }); initProcess.on('error', (err) => { reject(err); }); }); } /** * 检查数据库是否已初始化 */ isDatabaseInitialized() { return fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0; } /** * 检查是否有残留的 mysqld 进程 */ async checkAndKillOrphanProcess() { try { // 检查是否有 mysqld.exe 进程在运行 const { stdout } = await this.execCommand('tasklist /FI "IMAGENAME eq mysqld.exe" /FO CSV /NH'); if (stdout.includes('mysqld.exe')) { this.log('warn', '检测到残留的 MySQL 进程,正在清理...'); // 尝试优雅关闭 try { const mysqladmin = path.join(this.binPath, 'mysqladmin.exe'); await this.execCommand(`"${mysqladmin}" -u root shutdown`); await this.sleep(2000); this.log('success', '残留进程已优雅关闭'); } catch (e) { // 优雅关闭失败,强制杀死 this.log('warn', '优雅关闭失败,强制终止...'); await this.execCommand('taskkill /F /IM mysqld.exe'); await this.sleep(1000); this.log('success', '残留进程已强制终止'); } } } catch (error) { // tasklist 命令失败通常意味着没有找到进程,这是正常的 } } /** * 进程模式:启动 MySQL 进程 */ async startMySQLProcess() { return new Promise((resolve, reject) => { const mysqld = path.join(this.binPath, 'mysqld.exe'); this.log('system', '正在启动 MySQL 进程...'); this.log('system', `可执行文件: ${mysqld}`); this.log('system', `配置文件: ${this.configFile}`); // 检查文件是否存在 if (!fs.existsSync(mysqld)) { return reject(new Error(`mysqld.exe 不存在: ${mysqld}`)); } if (!fs.existsSync(this.configFile)) { return reject(new Error(`配置文件不存在: ${this.configFile}`)); } this.mysqlProcess = spawn(mysqld, [ `--defaults-file=${this.configFile}`, '--console' ], { cwd: this.mysqlPath, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); let startupComplete = false; let startupTimeout = null; // 监听 stderr(MySQL 主要输出在 stderr) this.mysqlProcess.stderr.on('data', (data) => { const msg = data.toString().trim(); if (msg) { // 检查启动成功标志 if (msg.includes('ready for connections') || msg.includes('port:')) { if (!startupComplete) { startupComplete = true; if (startupTimeout) clearTimeout(startupTimeout); this.log('success', 'MySQL 进程已就绪'); resolve(true); } } // 输出日志(过滤一些不重要的信息) if (!msg.includes('[Note]') || msg.includes('error') || msg.includes('ready')) { this.log('system', `[mysqld] ${msg}`); } } }); this.mysqlProcess.stdout.on('data', (data) => { const msg = data.toString().trim(); if (msg) { this.log('system', `[mysqld] ${msg}`); } }); this.mysqlProcess.on('error', (err) => { this.log('error', `MySQL 进程启动失败: ${err.message}`); this.mysqlProcess = null; reject(err); }); this.mysqlProcess.on('exit', (code, signal) => { this.log('system', `MySQL 进程退出,代码: ${code}, 信号: ${signal}`); this.mysqlProcess = null; // 如果还没完成启动就退出了,说明启动失败 if (!startupComplete) { reject(new Error(`MySQL 进程异常退出,代码: ${code}`)); } }); // 超时处理(30秒) startupTimeout = setTimeout(() => { if (!startupComplete && this.mysqlProcess) { // 进程还在运行,认为启动成功 startupComplete = true; this.log('success', 'MySQL 进程启动完成(超时检测)'); resolve(true); } }, 30000); }); } /** * 进程模式:停止 MySQL 进程 */ async stopMySQLProcess() { if (!this.mysqlProcess) { this.log('system', 'MySQL 进程未运行'); return; } this.log('system', '正在停止 MySQL 进程...'); try { // 方式1:通过 mysqladmin 优雅关闭 const mysqladmin = path.join(this.binPath, 'mysqladmin.exe'); await this.execCommand(`"${mysqladmin}" -u root shutdown`); // 等待进程退出 await this.sleep(3000); if (this.mysqlProcess && !this.mysqlProcess.killed) { // 如果还没退出,强制杀死 this.log('warn', '优雅关闭超时,强制终止...'); this.mysqlProcess.kill('SIGTERM'); } this.log('success', 'MySQL 进程已关闭'); } catch (error) { // mysqladmin 关闭失败,强制杀死进程 this.log('warn', `优雅关闭失败: ${error.message},强制终止...`); if (this.mysqlProcess) { this.mysqlProcess.kill('SIGTERM'); } // 确保进程被杀死 try { await this.execCommand('taskkill /F /IM mysqld.exe'); } catch (e) { // 忽略 } } this.mysqlProcess = null; this.currentPort = null; } /** * 检查 MySQL 进程是否在运行 */ isMySQLRunning() { return this.mysqlProcess !== null && !this.mysqlProcess.killed; } /** * 主流程:确保 MySQL 运行(进程模式) * @param {function} findAvailablePort - 查找可用端口的函数 * @param {function} waitForPort - 等待端口就绪的函数 * @returns {Promise} 返回 MySQL 运行的端口 */ async ensureServiceRunning(findAvailablePort, waitForPort) { this.log('system', '开始 MySQL 进程检查流程(进程模式)'); this.log('system', `MySQL 路径: ${this.mysqlPath}`); this.log('system', `配置文件: ${this.configFile}`); // 1. 检查是否已有 MySQL 进程在运行 if (this.isMySQLRunning()) { const port = this.currentPort || this.readPortFromConfig(); this.log('success', `MySQL 进程已在运行,端口: ${port}`); return port; } // 2. 检查并清理残留进程 await this.checkAndKillOrphanProcess(); // 3. 查找可用端口 const port = await findAvailablePort(3306, 100); this.log('system', `找到可用端口: ${port}`); // 4. 检查数据库是否已初始化 if (!this.isDatabaseInitialized()) { this.log('system', '数据库未初始化,开始初始化...'); this.generateMyIni(port); await this.initializeDatabase(); } else { this.log('system', '检测到已有数据库目录'); // 更新配置文件(可能端口变了) this.generateMyIni(port); } // 5. 启动 MySQL 进程 this.log('system', '启动 MySQL 进程...'); await this.startMySQLProcess(); // 6. 等待端口就绪 const finalPort = this.readPortFromConfig(); this.currentPort = finalPort; this.log('system', `等待端口 ${finalPort} 就绪...`); const portReady = await waitForPort(finalPort, 30000); if (!portReady) { throw new Error(`MySQL 端口 ${finalPort} 未能在 30 秒内就绪`); } this.log('success', `MySQL 进程启动成功,端口: ${finalPort}`); return finalPort; } } module.exports = MySQLServiceManager;