绿色包保存

This commit is contained in:
2025-11-27 20:50:59 +08:00
parent a7487c24bf
commit 2322858bc4
20 changed files with 2441 additions and 249 deletions

View File

@@ -0,0 +1,680 @@
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<boolean>} 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<number>} 返回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;