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;