Files
pqs-9100_client/scripts/mysql-service-manager.js

390 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
// 监听 stderrMySQL 主要输出在 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<number>} 返回 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;