diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e9b7653..ed02e20 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -7,7 +7,8 @@ "Bash(taskkill:*)", "Bash(iconv:*)", "Bash(powershell:*)", - "Bash(del:*)" + "Bash(del:*)", + "Bash(git -C \"c:\\code\\gitea\\NPQS-9100\\pqs-9100_client\" log -1 --oneline)" ], "deny": [], "ask": [] diff --git a/electron/preload/lifecycle.js b/electron/preload/lifecycle.js index 3a8b2c5..3dfd3da 100644 --- a/electron/preload/lifecycle.js +++ b/electron/preload/lifecycle.js @@ -67,7 +67,6 @@ class Lifecycle { this.mysqlPort = null; this.javaPort = null; this.autoRefreshTimer = null; - this.isRestartingForAdmin = false; // 权限提升重启标记 } /** @@ -119,23 +118,8 @@ class Lifecycle { this.startupManager.updateProgress('wait-mysql', { mysqlPort: this.mysqlPort }); await this.sleep(500); } catch (error) { - logger.error('[lifecycle] MySQL service error:', error); - this.logWindowManager.addLog('error', `MySQL 服务错误: ${error.message}`); - // 检查是否是权限问题 - if (error.message && (error.message.includes('administrator') || error.message.includes('Denied'))) { - logger.error('[lifecycle] Need administrator privileges'); - this.logWindowManager.addLog('error', '检测到需要管理员权限来安装 MySQL 服务'); - this.logWindowManager.addLog('system', '应用将自动请求管理员权限并重启...'); - - // 等待用户看清日志 - await this.sleep(2000); - - // 自动以管理员身份重启应用 - await this.restartAsAdmin(); - - // 退出当前实例 - return; - } + logger.error('[lifecycle] MySQL error:', error); + this.logWindowManager.addLog('error', `MySQL 错误: ${error.message}`); throw error; } } @@ -350,12 +334,6 @@ class Lifecycle { async cleanup() { logger.info('[lifecycle] Starting cleanup...'); - // 如果是权限提升重启,跳过清理(服务需要继续运行) - if (this.isRestartingForAdmin) { - logger.info('[lifecycle] Restarting for admin, skip cleanup'); - return; - } - // 清除自动刷新定时器 if (this.autoRefreshTimer) { clearTimeout(this.autoRefreshTimer); @@ -397,10 +375,26 @@ class Lifecycle { } } - // MySQL 作为Windows服务运行,应用关闭时保持运行 - logger.info('[lifecycle] MySQL service keeps running as Windows service'); + // 停止 MySQL 进程(进程模式) + if (this.mysqlServiceManager) { + try { + logger.info('[lifecycle] Stopping MySQL process...'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('system', '正在停止 MySQL...'); + } + + await this.mysqlServiceManager.stopMySQLProcess(); + + logger.info('[lifecycle] MySQL process stopped'); + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { + this.logWindowManager.addLog('success', 'MySQL 已停止'); + } + } catch (error) { + logger.error('[lifecycle] Failed to stop MySQL:', error); + } + } + if (this.logWindowManager && this.logWindowManager.logWindow && !this.logWindowManager.logWindow.isDestroyed()) { - this.logWindowManager.addLog('system', 'MySQL 服务保持运行'); this.logWindowManager.addLog('system', '清理完成,应用即将退出'); this.logWindowManager.addLog('system', '='.repeat(60)); } @@ -485,72 +479,6 @@ class Lifecycle { } } - /** - * 以管理员身份重启应用 - */ - async restartAsAdmin() { - const { app, dialog } = require('electron'); - const { spawn } = require('child_process'); - const path = require('path'); - - logger.info('[lifecycle] Requesting administrator privileges...'); - - try { - // 显示提示对话框 - await dialog.showMessageBox({ - type: 'warning', - title: '告警', - message: '需要管理员权限', - buttons: ['确定'] - }); - - // 用户点击确定,以管理员身份重启 - - // 获取应用可执行文件路径 - const exePath = app.getPath('exe'); - logger.info('[lifecycle] Restarting with admin privileges:', exePath); - - // 使用 PowerShell 以管理员身份启动 - const psCommand = `Start-Process -FilePath "${exePath}" -Verb RunAs`; - - const child = spawn('powershell.exe', ['-Command', psCommand], { - detached: true, - stdio: 'ignore', - windowsHide: true - }); - - // 分离子进程,父进程退出不影响子进程 - child.unref(); - - // 立即退出当前实例,释放单实例锁 - // 必须立即退出,否则新实例会因为单实例锁无法启动 - logger.info('[lifecycle] Quitting current instance to release lock...'); - - // 设置标记,跳过清理流程(这只是重启,不是真正退出) - this.isRestartingForAdmin = true; - - // 关闭所有窗口 - const BrowserWindow = require('electron').BrowserWindow; - BrowserWindow.getAllWindows().forEach(win => { - try { - win.destroy(); - } catch (e) { - // 忽略错误 - } - }); - - // 立即强制退出,释放锁 - process.exit(0); - } catch (error) { - logger.error('[lifecycle] Failed to restart as admin:', error); - this.logWindowManager.addLog('error', '自动提升权限失败,请手动以管理员身份运行'); - - // 等待5秒后关闭 - await this.sleep(5000); - app.quit(); - } - } - /** * 睡眠函数 */ @@ -558,24 +486,6 @@ class Lifecycle { return new Promise(resolve => setTimeout(resolve, ms)); } - /** - * 检查是否有管理员权限(仅 Windows) - */ - checkAdminPrivileges() { - if (process.platform !== 'win32') { - return true; // 非 Windows 系统不需要检查 - } - - try { - const { execSync } = require('child_process'); - // 尝试执行需要管理员权限的命令 - execSync('net session', { stdio: 'ignore' }); - return true; - } catch (error) { - return false; - } - } - /** * electron app ready */ @@ -589,16 +499,7 @@ class Lifecycle { async windowReady() { logger.info('[lifecycle] window-ready hook triggered'); - // 在创建任何窗口之前,先检查管理员权限 - const hasAdminPrivileges = this.checkAdminPrivileges(); - logger.info('[lifecycle] Has admin privileges:', hasAdminPrivileges); - - if (!hasAdminPrivileges) { - logger.warn('[lifecycle] No admin privileges, requesting elevation'); - // 调用已有的 restartAsAdmin 方法,避免代码重复 - await this.restartAsAdmin(); - return; // 阻止后续代码执行 - } + // 进程模式不需要管理员权限检查 // 创建日志管理器(但不显示窗口,仅用于写日志文件) logger.info('[lifecycle] Creating log window manager...'); diff --git a/scripts/mysql-service-manager.js b/scripts/mysql-service-manager.js index 3d45fe3..6b99e2e 100644 --- a/scripts/mysql-service-manager.js +++ b/scripts/mysql-service-manager.js @@ -3,22 +3,25 @@ const path = require('path'); const fs = require('fs'); /** - * MySQL Windows 服务管理器 - * 将MySQL安装为Windows服务 mysql9100,持久运行 + * MySQL 进程管理器 + * 使用进程模式启动 MySQL,无需管理员权限 */ class MySQLServiceManager { constructor(logWindowManager = null) { const isDev = !process.resourcesPath; - const baseDir = isDev - ? path.join(__dirname, '..') + 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; // 用于输出日志到文件 + this.configFile = path.join(this.mysqlPath, 'my.ini'); + this.logWindowManager = logWindowManager; + + // 进程模式:保存 MySQL 进程引用 + this.mysqlProcess = null; + this.currentPort = null; } // 睡眠函数 @@ -28,7 +31,7 @@ class MySQLServiceManager { // 日志输出辅助方法 log(type, message) { - console.log(`[MySQL Service] ${message}`); + console.log(`[MySQL] ${message}`); if (this.logWindowManager) { this.logWindowManager.addLog(type, `[MySQL] ${message}`); } @@ -41,13 +44,10 @@ class MySQLServiceManager { 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 }); @@ -56,70 +56,6 @@ class MySQLServiceManager { }); } - /** - * 检查服务是否存在 - */ - 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; - } - } - /** * 读取配置文件中的端口 */ @@ -131,7 +67,7 @@ class MySQLServiceManager { return match ? parseInt(match[1]) : 3306; } } catch (error) { - console.error('[MySQL Service] Failed to read port from config:', error); + console.error('[MySQL] Failed to read port from config:', error); } return 3306; } @@ -142,7 +78,7 @@ class MySQLServiceManager { generateMyIni(port) { const basedir = this.mysqlPath.replace(/\\/g, '/'); const datadir = this.dataPath.replace(/\\/g, '/'); - + const config = `[mysqld] # 基础路径配置 basedir=${basedir} @@ -175,7 +111,7 @@ default-character-set=utf8mb4 `; fs.writeFileSync(this.configFile, config, 'utf-8'); - console.log('[MySQL Service] Generated my.ini for port:', port); + this.log('system', `生成配置文件,端口: ${port}`); } /** @@ -183,217 +119,40 @@ default-character-set=utf8mb4 */ async initializeDatabase() { return new Promise((resolve, reject) => { - console.log('[MySQL Service] Initializing MySQL database...'); - + 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: 'inherit' + 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) { - console.log('[MySQL Service] Database initialized successfully'); + this.log('success', '数据库初始化成功'); resolve(); } else { - reject(new Error(`Database initialization failed with code ${code}`)); + reject(new Error(`数据库初始化失败,退出码: ${code}`)); } }); + + initProcess.on('error', (err) => { + reject(err); + }); }); } - /** - * 安装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'); - } - /** * 检查数据库是否已初始化 */ @@ -402,279 +161,229 @@ default-character-set=utf8mb4 } /** - * 清空数据库目录(重新初始化时使用) + * 检查是否有残留的 mysqld 进程 */ - 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); + 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', '残留进程已强制终止'); } - }; - rimraf(this.dataPath); + } + } catch (error) { + // tasklist 命令失败通常意味着没有找到进程,这是正常的 } - // 重新创建空目录 - fs.mkdirSync(this.dataPath, { recursive: true }); - console.log('[MySQL Service] Database directory cleared'); } /** - * 主流程:确保MySQL服务运行 + * 进程模式:启动 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运行的端口 + * @returns {Promise} 返回 MySQL 运行的端口 */ async ensureServiceRunning(findAvailablePort, waitForPort) { - this.log('system', '开始MySQL服务检查流程'); - this.log('system', `MySQL路径: ${this.mysqlPath}`); + 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 流程 - } + // 1. 检查是否已有 MySQL 进程在运行 + if (this.isMySQLRunning()) { + const port = this.currentPort || this.readPortFromConfig(); + this.log('success', `MySQL 进程已在运行,端口: ${port}`); + return port; } - // 重新获取服务状态(如果删除了旧服务,状态会变为 NOT_EXISTS) - const currentStatus = await this.getServiceStatus(); + // 2. 检查并清理残留进程 + await this.checkAndKillOrphanProcess(); - 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; + // 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); } - - throw new Error('Unknown service status: ' + currentStatus); + + // 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; -