const { spawn, exec } = require('child_process'); const path = require('path'); const fs = require('fs'); /** * MySQL Windows 服务管理器 * 将MySQL安装为Windows服务 mysql9100,持久运行 */ 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.serviceName = 'mysql9100'; this.configFile = path.join(this.mysqlPath, 'my.ini'); // 使用标准的 my.ini this.logWindowManager = logWindowManager; // 用于输出日志到文件 } // 睡眠函数 sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // 日志输出辅助方法 log(type, message) { console.log(`[MySQL Service] ${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) { // 创建标准 Error 对象,确保有 message 属性 const err = new Error(error.message || stderr || stdout || 'Command execution failed'); err.code = error.code; err.stdout = stdout; err.stderr = stderr; err.command = command; err.originalError = error; reject(err); } else { resolve({ stdout, stderr }); } }); }); } /** * 检查服务是否存在 */ async checkServiceExists() { try { const { stdout } = await this.execCommand(`sc query ${this.serviceName}`); return stdout.includes('SERVICE_NAME'); } catch (error) { return false; } } /** * 获取服务状态 * @returns {string} 'RUNNING' | 'STOPPED' | 'NOT_EXISTS' */ async getServiceStatus() { try { const { stdout } = await this.execCommand(`sc query ${this.serviceName}`); if (stdout.includes('RUNNING')) { return 'RUNNING'; } else if (stdout.includes('STOPPED')) { return 'STOPPED'; } else { return 'UNKNOWN'; } } catch (error) { return 'NOT_EXISTS'; } } /** * 检查服务路径是否匹配当前环境 * @returns {Promise} true=路径匹配,false=路径不匹配 */ async checkServicePath() { try { const { stdout } = await this.execCommand(`sc qc ${this.serviceName}`); // 从输出中提取 BINARY_PATH_NAME const match = stdout.match(/BINARY_PATH_NAME\s*:\s*(.+)/i); if (!match) { return false; } const binaryPath = match[1].trim(); const expectedPath = path.join(this.binPath, 'mysqld.exe'); // 检查路径是否包含当前的 MySQL 目录 const pathMatches = binaryPath.toLowerCase().includes(this.mysqlPath.toLowerCase()); this.log('system', `服务路径检查:`); this.log('system', ` 当前环境: ${this.mysqlPath}`); this.log('system', ` 服务路径: ${binaryPath}`); this.log('system', ` 路径匹配: ${pathMatches ? '是' : '否'}`); return pathMatches; } catch (error) { this.log('warn', `检查服务路径失败: ${error.message}`); return false; } } /** * 读取配置文件中的端口 */ 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 Service] 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'); console.log('[MySQL Service] Generated my.ini for port:', port); } /** * 初始化数据库 */ async initializeDatabase() { return new Promise((resolve, reject) => { console.log('[MySQL Service] Initializing MySQL database...'); const mysqld = path.join(this.binPath, 'mysqld.exe'); const initProcess = spawn(mysqld, [ '--defaults-file=' + this.configFile, '--initialize-insecure' ], { cwd: this.mysqlPath, stdio: 'inherit' }); initProcess.on('close', (code) => { if (code === 0) { console.log('[MySQL Service] Database initialized successfully'); resolve(); } else { reject(new Error(`Database initialization failed with code ${code}`)); } }); }); } /** * 安装MySQL服务 */ async installService() { try { const mysqld = path.join(this.binPath, 'mysqld.exe'); // 使用绝对路径和完整参数,避免环境变量干扰 // 关键:使用 --defaults-file 的绝对路径,确保不会读取系统 MySQL 的配置 const absoluteConfigPath = path.resolve(this.configFile); const absoluteMysqldPath = path.resolve(mysqld); // 构建完全隔离的安装命令 const command = `"${absoluteMysqldPath}" --install ${this.serviceName} --defaults-file="${absoluteConfigPath}"`; this.log('system', `准备安装服务: ${this.serviceName}`); this.log('system', `MySQL可执行文件: ${absoluteMysqldPath}`); this.log('system', `配置文件: ${absoluteConfigPath}`); // 检查文件是否存在 if (!fs.existsSync(mysqld)) { throw new Error(`mysqld.exe not found at: ${mysqld}`); } if (!fs.existsSync(this.configFile)) { throw new Error(`Config file not found at: ${this.configFile}`); } // 在安装前清理环境变量(避免继承系统 MySQL 的环境变量) const cleanEnv = { ...process.env }; delete cleanEnv.MYSQL_HOME; delete cleanEnv.MYSQL_TCP_PORT; // 使用 spawn 代替 exec,更好地控制环境 const { spawn } = require('child_process'); await new Promise((resolve, reject) => { const installProcess = spawn(absoluteMysqldPath, [ '--install', this.serviceName, `--defaults-file=${absoluteConfigPath}` ], { env: cleanEnv, cwd: this.mysqlPath, stdio: 'pipe' }); let stdout = ''; let stderr = ''; if (installProcess.stdout) { installProcess.stdout.on('data', (data) => { stdout += data.toString(); }); } if (installProcess.stderr) { installProcess.stderr.on('data', (data) => { stderr += data.toString(); }); } installProcess.on('close', (code) => { if (code === 0) { this.log('system', '服务安装命令执行成功'); if (stdout) this.log('system', `输出: ${stdout}`); resolve(); } else { const errorMsg = stderr || stdout || `Exit code: ${code}`; this.log('error', `安装命令失败: ${errorMsg}`); reject(new Error(errorMsg)); } }); installProcess.on('error', (err) => { this.log('error', `进程启动失败: ${err.message}`); reject(err); }); }); return true; } catch (error) { console.error('[MySQL Service] Failed to install service:', error); this.log('error', `服务安装失败: ${error.message}`); throw new Error(`Failed to install MySQL service: ${error.message}. Please run as administrator.`); } } /** * 检查是否有其他MySQL服务 */ async checkOtherMySQLServices() { try { const { stdout } = await this.execCommand('sc query type= service state= all | findstr /i "mysql"'); const services = []; const lines = stdout.split('\n'); for (const line of lines) { const match = line.match(/SERVICE_NAME:\s*(\S+)/); if (match && match[1] !== this.serviceName) { services.push(match[1]); } } return services; } catch (error) { // 如果没找到任何MySQL服务,命令会失败,这是正常的 return []; } } /** * 启动服务 */ async startService() { try { console.log('[MySQL Service] Starting service...'); await this.execCommand(`net start ${this.serviceName}`); console.log('[MySQL Service] Service started successfully'); // 等待MySQL完全启动 await new Promise(resolve => setTimeout(resolve, 2000)); return true; } catch (error) { console.error('[MySQL Service] Failed to start service:', error); console.error('[MySQL Service] This may be due to invalid service configuration'); throw error; } } /** * 停止服务 */ async stopService() { try { console.log('[MySQL Service] Stopping service...'); await this.execCommand(`net stop ${this.serviceName}`); console.log('[MySQL Service] Service stopped successfully'); return true; } catch (error) { console.error('[MySQL Service] Failed to stop service:', error); // 停止失败不抛异常,因为可能服务本来就是停止状态 return false; } } /** * 删除服务 */ async removeService() { try { // 先尝试停止服务(如果正在运行) try { await this.stopService(); } catch (e) { // 服务可能已经停止,忽略错误 console.log('[MySQL Service] Service already stopped or not running'); } // 删除服务 await this.execCommand(`sc delete ${this.serviceName}`); console.log('[MySQL Service] Service removed successfully'); // 等待服务完全删除 await new Promise(resolve => setTimeout(resolve, 1000)); return true; } catch (error) { console.error('[MySQL Service] Failed to remove service:', error); // 即使删除失败也返回 true,因为可能服务本来就不存在 return true; } } /** * 更新服务端口 * 需要停止服务、修改配置、重启服务 */ async updateServicePort(newPort) { console.log('[MySQL Service] Updating service port to:', newPort); // 1. 停止服务 await this.stopService(); // 2. 更新配置文件 this.generateMyIni(newPort); // 3. 启动服务 await this.startService(); console.log('[MySQL Service] Port updated successfully'); } /** * 检查数据库是否已初始化 */ isDatabaseInitialized() { return fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0; } /** * 清空数据库目录(重新初始化时使用) */ clearDatabase() { console.log('[MySQL Service] Clearing database directory...'); if (fs.existsSync(this.dataPath)) { // 递归删除目录 const rimraf = (dir) => { if (fs.existsSync(dir)) { fs.readdirSync(dir).forEach((file) => { const curPath = path.join(dir, file); if (fs.lstatSync(curPath).isDirectory()) { rimraf(curPath); } else { fs.unlinkSync(curPath); } }); fs.rmdirSync(dir); } }; rimraf(this.dataPath); } // 重新创建空目录 fs.mkdirSync(this.dataPath, { recursive: true }); console.log('[MySQL Service] Database directory cleared'); } /** * 主流程:确保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}`); const serviceStatus = await this.getServiceStatus(); this.log('system', `服务状态: ${serviceStatus}`); // 如果服务存在,检查路径是否匹配 if (serviceStatus !== 'NOT_EXISTS') { const pathMatches = await this.checkServicePath(); if (!pathMatches) { this.log('warn', '检测到服务路径与当前环境不匹配'); this.log('warn', '这可能是从其他环境(开发/打包)安装的旧服务'); this.log('system', '正在删除旧服务并重新安装...'); // 删除旧服务 await this.removeService(); // 重置状态,后续将按新安装流程处理 this.log('system', '旧服务已删除,准备重新安装'); // 继续执行下面的 NOT_EXISTS 流程 } } // 重新获取服务状态(如果删除了旧服务,状态会变为 NOT_EXISTS) const currentStatus = await this.getServiceStatus(); if (currentStatus === 'NOT_EXISTS') { // ========== 服务不存在,全新安装 ========== this.log('system', '服务不存在,开始安装流程'); // 0. 检查是否有其他MySQL服务(仅作提示,不影响安装) const otherServices = await this.checkOtherMySQLServices(); if (otherServices.length > 0) { this.log('system', `检测到系统中有其他MySQL服务: ${otherServices.join(', ')}`); this.log('system', 'mysql9100 将使用独立的配置和端口,不会冲突'); } // 1. 查找可用端口 const port = await findAvailablePort(3306, 100); this.log('system', `找到可用端口: ${port}`); // 2. 检查数据库是否已初始化 if (!this.isDatabaseInitialized()) { // 数据库未初始化,正常初始化流程 this.log('system', '数据库未初始化,开始初始化...'); this.generateMyIni(port); await this.initializeDatabase(); } else { // 数据库已存在(可能是从其他机器拷贝过来的) this.log('system', '检测到已有数据库目录'); // 无论端口是什么,都使用现有数据库,只修改配置文件 if (port !== 3306) { this.log('system', `端口3306被占用,将使用端口${port}`); this.log('system', '保留现有数据库,仅更新端口配置'); } else { this.log('system', '端口3306可用,使用现有数据库'); } // 只生成新配置,不删除数据 this.generateMyIni(port); } // 3. 安装服务 this.log('system', '开始安装MySQL服务...'); await this.installService(); // 3.1 验证服务是否真的安装成功 await this.sleep(2000); // 等待2秒让服务注册完成 const verifyStatus = await this.getServiceStatus(); if (verifyStatus === 'NOT_EXISTS') { this.log('error', '服务安装失败:服务未创建'); this.log('error', '可能的原因:'); this.log('error', '1. 权限不足(请确保以管理员身份运行)'); this.log('error', '2. MySQL可执行文件损坏'); this.log('error', '3. 配置文件路径有误'); // 检查是否有其他MySQL服务 const otherServices = await this.checkOtherMySQLServices(); if (otherServices.length > 0) { this.log('system', `提示:系统中存在其他MySQL服务 ${otherServices.join(', ')}`); this.log('system', '如果问题持续,请查看文档:doc/MySQL服务冲突处理方案.md'); } throw new Error('MySQL服务安装失败,请检查管理员权限和日志'); } this.log('success', '服务安装成功,已验证'); // 3.2 设置服务为开机自启 this.log('system', '正在配置服务为开机自启...'); try { await this.execCommand(`sc config ${this.serviceName} start= auto`); this.log('success', '服务已配置为开机自启'); // 验证配置是否生效 const { stdout: qcOutput } = await this.execCommand(`sc qc ${this.serviceName}`); if (qcOutput.includes('AUTO_START')) { this.log('success', '开机自启配置验证成功'); } else { this.log('warn', '开机自启配置可能未生效,请手动检查'); } } catch (error) { this.log('warn', `设置开机自启失败: ${error.message}`); this.log('warn', '服务已安装,但需要手动设置启动类型'); } // 4. 启动服务 this.log('system', '开始启动MySQL服务...'); await this.startService(); const finalPort = this.readPortFromConfig(); this.log('success', `MySQL服务安装并启动成功,端口: ${finalPort}`); // 4. 等待端口就绪 await waitForPort(finalPort, 30000); return finalPort; } else if (currentStatus === 'STOPPED') { // ========== 服务存在但停止,尝试启动 ========== this.log('system', '服务已停止,尝试启动...'); try { // 先读取配置端口 const configPort = this.readPortFromConfig(); // 重新生成配置文件,确保使用绝对路径(修复相对路径问题) this.log('system', '更新配置文件,确保路径正确...'); this.generateMyIni(configPort); // 尝试启动服务 await this.startService(); this.log('success', `服务启动成功,端口: ${configPort}`); // 等待端口就绪 await waitForPort(configPort, 30000); return configPort; } catch (startError) { // 启动失败,先诊断问题 this.log('error', '服务启动失败'); this.log('system', `失败原因: ${startError.message}`); // 检查是否是端口被占用导致的启动失败 const configPort = this.readPortFromConfig(); this.log('system', `配置的端口: ${configPort}`); // 检查端口是否被占用 const PortChecker = require('./port-checker'); const portAvailable = await PortChecker.findAvailablePort(configPort, 1) === configPort; if (!portAvailable) { // 端口被占用,需要更换端口并重新安装 this.log('warn', `端口${configPort}已被占用,尝试重新配置服务`); // 1. 删除旧服务 this.log('system', '正在删除旧服务...'); await this.removeService(); // 2. 查找可用端口 const port = await findAvailablePort(3306, 100); this.log('system', `找到可用端口: ${port}`); // 3. 保留现有数据库,只更新配置 if (this.isDatabaseInitialized()) { this.log('system', '保留现有数据库,更新端口配置'); this.generateMyIni(port); } else { this.log('system', '数据库未初始化,开始初始化...'); this.generateMyIni(port); await this.initializeDatabase(); } // 4. 重新安装服务 this.log('system', '开始重新安装MySQL服务...'); await this.installService(); // 4.1 设置开机自启 this.log('system', '正在配置服务为开机自启...'); try { await this.execCommand(`sc config ${this.serviceName} start= auto`); this.log('success', '服务已配置为开机自启'); } catch (error) { this.log('warn', `设置开机自启失败: ${error.message}`); } // 5. 启动服务 this.log('system', '开始启动MySQL服务...'); await this.startService(); const finalPort = this.readPortFromConfig(); this.log('success', `MySQL服务重新安装并启动成功,端口: ${finalPort}`); // 6. 等待端口就绪 await waitForPort(finalPort, 30000); return finalPort; } else { // 端口没被占用,但服务启动失败,可能是配置问题或权限问题 this.log('error', '服务启动失败,但端口未被占用'); this.log('error', '可能的原因:'); this.log('error', '1. 配置文件路径错误或损坏'); this.log('error', '2. 数据库文件损坏'); this.log('error', '3. 权限不足'); this.log('error', '4. MySQL可执行文件问题'); this.log('system', '建议:检查日志文件 mysql/mysql_error.log'); throw new Error(`MySQL服务启动失败: ${startError.message}。请检查配置和日志。`); } } } else if (currentStatus === 'RUNNING') { // ========== 服务正在运行 ========== const configPort = this.readPortFromConfig(); this.log('success', `MySQL服务已在运行,端口: ${configPort}`); // 验证端口是否真的在监听 const PortChecker = require('./port-checker'); const portReady = await PortChecker.checkPort(configPort); if (!portReady) { this.log('warn', '服务运行中但端口未就绪,重新启动...'); // 重新生成配置文件 this.generateMyIni(configPort); await this.stopService(); await this.startService(); await waitForPort(configPort, 30000); } return configPort; } throw new Error('Unknown service status: ' + currentStatus); } } module.exports = MySQLServiceManager;