628 lines
19 KiB
JavaScript
628 lines
19 KiB
JavaScript
|
|
const { spawn, exec, execFile } = require('child_process');
|
|||
|
|
const path = require('path');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const { resolveRuntimeStrategy } = require('./path-utils');
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* MySQL 进程管理器
|
|||
|
|
* 使用进程模式启动 MySQL,无需管理员权限
|
|||
|
|
*/
|
|||
|
|
class MySQLProcessManager {
|
|||
|
|
constructor(logWindowManager = null) {
|
|||
|
|
const isDev = !process.resourcesPath;
|
|||
|
|
const baseDir = isDev
|
|||
|
|
? path.join(__dirname, '..')
|
|||
|
|
: path.dirname(process.resourcesPath);
|
|||
|
|
const pathStrategy = resolveRuntimeStrategy(baseDir);
|
|||
|
|
|
|||
|
|
this.baseDir = baseDir;
|
|||
|
|
this.pathStrategy = pathStrategy;
|
|||
|
|
this.sourceMysqlPath = path.join(baseDir, 'mysql');
|
|||
|
|
this.mysqlPath = pathStrategy.usesSafePaths
|
|||
|
|
? path.join(pathStrategy.safeRuntimeRoot, 'mysql')
|
|||
|
|
: this.sourceMysqlPath;
|
|||
|
|
this.binPath = path.join(this.mysqlPath, 'bin');
|
|||
|
|
this.dataPath = path.join(this.mysqlPath, 'data');
|
|||
|
|
this.configFile = path.join(this.mysqlPath, 'my.ini');
|
|||
|
|
this.processRecordFile = path.join(this.mysqlPath, '.running-process.json');
|
|||
|
|
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}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
normalizeComparablePath(target = '') {
|
|||
|
|
return String(target).replace(/"/g, '').replace(/\//g, '\\').toLowerCase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
saveProcessRecord(record = {}) {
|
|||
|
|
try {
|
|||
|
|
fs.mkdirSync(path.dirname(this.processRecordFile), { recursive: true });
|
|||
|
|
fs.writeFileSync(this.processRecordFile, JSON.stringify(record, null, 2), 'utf-8');
|
|||
|
|
} catch (error) {
|
|||
|
|
this.log('warn', `写入 MySQL 进程标记失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
getProcessRecord() {
|
|||
|
|
try {
|
|||
|
|
if (!fs.existsSync(this.processRecordFile)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return JSON.parse(fs.readFileSync(this.processRecordFile, 'utf-8'));
|
|||
|
|
} catch (error) {
|
|||
|
|
this.log('warn', `读取 MySQL 进程标记失败: ${error.message}`);
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clearProcessRecord() {
|
|||
|
|
try {
|
|||
|
|
if (fs.existsSync(this.processRecordFile)) {
|
|||
|
|
fs.unlinkSync(this.processRecordFile);
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
this.log('warn', `清理 MySQL 进程标记失败: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
execFileCommand(command, args = []) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
execFile(command, args, { encoding: 'utf8', windowsHide: true }, (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 });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async getProcessInfoByPid(pid) {
|
|||
|
|
if (!pid) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const script = `$proc = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($proc) { [PSCustomObject]@{ executablePath = $proc.ExecutablePath; commandLine = $proc.CommandLine; name = $proc.Name } | ConvertTo-Json -Compress }`;
|
|||
|
|
const { stdout } = await this.execFileCommand('powershell.exe', ['-NoProfile', '-Command', script]);
|
|||
|
|
const raw = (stdout || '').trim();
|
|||
|
|
return raw ? JSON.parse(raw) : null;
|
|||
|
|
} catch (error) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async isOwnMySQLProcess(pid, record = this.getProcessRecord()) {
|
|||
|
|
const processInfo = await this.getProcessInfoByPid(pid);
|
|||
|
|
if (!processInfo) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const expectedExe = this.normalizeComparablePath(path.join(this.binPath, 'mysqld.exe'));
|
|||
|
|
const expectedConfig = this.normalizeComparablePath((record && record.configFile) || this.configFile);
|
|||
|
|
const executablePath = this.normalizeComparablePath(processInfo.executablePath);
|
|||
|
|
const commandLine = this.normalizeComparablePath(processInfo.commandLine);
|
|||
|
|
|
|||
|
|
if (executablePath !== expectedExe) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 关键校验:仅在命令行绑定到当前应用自己的 my.ini 时才允许清理。
|
|||
|
|
return !expectedConfig || commandLine.includes(`--defaults-file=${expectedConfig}`) || commandLine.includes(expectedConfig);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async waitForProcessExit(pid, timeoutMs = 3000) {
|
|||
|
|
const deadline = Date.now() + timeoutMs;
|
|||
|
|
|
|||
|
|
while (Date.now() < deadline) {
|
|||
|
|
const processInfo = await this.getProcessInfoByPid(pid);
|
|||
|
|
if (!processInfo) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
await this.sleep(200);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async terminateTrackedProcess(record = this.getProcessRecord(), reason = '正在停止 MySQL 进程...') {
|
|||
|
|
const pid = this.mysqlProcess?.pid || record?.pid;
|
|||
|
|
const port = this.currentPort || record?.port || this.readPortFromConfig();
|
|||
|
|
|
|||
|
|
if (!pid) {
|
|||
|
|
this.log('system', '未找到可清理的 MySQL 进程标记');
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isOwnProcess = await this.isOwnMySQLProcess(pid, record);
|
|||
|
|
if (!isOwnProcess) {
|
|||
|
|
this.log('warn', `PID ${pid} 不是当前应用的 MySQL 进程,跳过清理`);
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
if (this.mysqlProcess && this.mysqlProcess.pid === pid) {
|
|||
|
|
this.mysqlProcess = null;
|
|||
|
|
}
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log('system', `${reason} PID=${pid}${port ? `, 端口=${port}` : ''}`);
|
|||
|
|
|
|||
|
|
// 优先按当前应用自己的端口优雅关闭,避免强杀带来的数据损坏风险。
|
|||
|
|
if (port) {
|
|||
|
|
try {
|
|||
|
|
const mysqladmin = path.join(this.binPath, 'mysqladmin.exe');
|
|||
|
|
await this.execCommand(`"${mysqladmin}" -u root -P ${port} shutdown`);
|
|||
|
|
const exited = await this.waitForProcessExit(pid, 5000);
|
|||
|
|
if (exited) {
|
|||
|
|
this.log('success', 'MySQL 进程已优雅关闭');
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
if (this.mysqlProcess && this.mysqlProcess.pid === pid) {
|
|||
|
|
this.mysqlProcess = null;
|
|||
|
|
}
|
|||
|
|
this.currentPort = null;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
this.log('warn', `优雅关闭失败,准备按 PID 清理: ${error.message}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await this.execCommand(`taskkill /F /T /PID ${pid}`);
|
|||
|
|
await this.waitForProcessExit(pid, 3000);
|
|||
|
|
this.log('success', 'MySQL 进程已按 PID 清理');
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
if (this.mysqlProcess && this.mysqlProcess.pid === pid) {
|
|||
|
|
this.mysqlProcess = null;
|
|||
|
|
}
|
|||
|
|
this.currentPort = null;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 递归复制目录。
|
|||
|
|
* 仅当文件不存在、大小变化或源文件时间更新时才覆盖,避免每次启动全量重写。
|
|||
|
|
*/
|
|||
|
|
copyDirectorySync(sourceDir, targetDir, options = {}) {
|
|||
|
|
const {
|
|||
|
|
excludeTopLevelDirs = new Set(),
|
|||
|
|
excludeFileNames = new Set(),
|
|||
|
|
excludeFileExtensions = new Set()
|
|||
|
|
} = options;
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(sourceDir)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!fs.existsSync(targetDir)) {
|
|||
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|||
|
|
|
|||
|
|
entries.forEach((entry) => {
|
|||
|
|
const sourcePath = path.join(sourceDir, entry.name);
|
|||
|
|
const targetPath = path.join(targetDir, entry.name);
|
|||
|
|
const isTopLevelEntry = sourceDir === this.sourceMysqlPath;
|
|||
|
|
|
|||
|
|
if (entry.isDirectory()) {
|
|||
|
|
if (isTopLevelEntry && excludeTopLevelDirs.has(entry.name)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.copyDirectorySync(sourcePath, targetPath, options);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const extension = path.extname(entry.name).toLowerCase();
|
|||
|
|
if (excludeFileNames.has(entry.name) || excludeFileExtensions.has(extension)) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let shouldCopy = !fs.existsSync(targetPath);
|
|||
|
|
if (!shouldCopy) {
|
|||
|
|
const sourceStat = fs.statSync(sourcePath);
|
|||
|
|
const targetStat = fs.statSync(targetPath);
|
|||
|
|
shouldCopy = sourceStat.size !== targetStat.size || sourceStat.mtimeMs > targetStat.mtimeMs;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (shouldCopy) {
|
|||
|
|
fs.copyFileSync(sourcePath, targetPath);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 非 ASCII 路径下,将 MySQL 运行环境同步到英文安全目录。
|
|||
|
|
*/
|
|||
|
|
ensureRuntimeEnvironment() {
|
|||
|
|
if (!this.pathStrategy.usesSafePaths) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log('system', '检测到应用路径包含非 ASCII 字符,启用 MySQL 英文安全运行目录');
|
|||
|
|
this.log('system', `MySQL 源目录: ${this.sourceMysqlPath}`);
|
|||
|
|
this.log('system', `MySQL 运行目录: ${this.mysqlPath}`);
|
|||
|
|
this.log('system', `MySQL 数据目录: ${this.dataPath}`);
|
|||
|
|
|
|||
|
|
fs.mkdirSync(this.mysqlPath, { recursive: true });
|
|||
|
|
fs.mkdirSync(path.dirname(this.dataPath), { recursive: true });
|
|||
|
|
|
|||
|
|
this.copyDirectorySync(this.sourceMysqlPath, this.mysqlPath, {
|
|||
|
|
excludeTopLevelDirs: new Set(['data', 'data_backup']),
|
|||
|
|
excludeFileNames: new Set(['my.ini', 'mysql_error.log', 'mysql_slow.log']),
|
|||
|
|
excludeFileExtensions: new Set(['.pid', '.err'])
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.ensureSeedData();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 首次进入英文安全路径模式时,将打包内预置数据库复制到安全数据目录。
|
|||
|
|
*/
|
|||
|
|
ensureSeedData() {
|
|||
|
|
const hasTargetData = fs.existsSync(this.dataPath) && fs.readdirSync(this.dataPath).length > 0;
|
|||
|
|
if (hasTargetData) {
|
|||
|
|
this.log('system', '检测到已有 MySQL 安全数据目录,直接复用');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sourceDataPath = path.join(this.sourceMysqlPath, 'data');
|
|||
|
|
if (fs.existsSync(sourceDataPath) && fs.readdirSync(sourceDataPath).length > 0) {
|
|||
|
|
this.log('system', '首次启动,正在复制预置 MySQL 数据到安全数据目录...');
|
|||
|
|
this.copyDirectorySync(sourceDataPath, this.dataPath);
|
|||
|
|
this.log('success', '预置 MySQL 数据复制完成');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs.mkdirSync(this.dataPath, { recursive: true });
|
|||
|
|
this.log('warn', '未找到预置 MySQL 数据目录,后续将按空库初始化');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 执行命令并返回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() {
|
|||
|
|
const record = this.getProcessRecord();
|
|||
|
|
if (!record || !record.pid) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const isOwnProcess = await this.isOwnMySQLProcess(record.pid, record);
|
|||
|
|
if (!isOwnProcess) {
|
|||
|
|
this.log('warn', `检测到失效的 MySQL 进程标记 PID=${record.pid},已跳过清理并移除标记`);
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.log('warn', `检测到当前应用上次遗留的 MySQL 进程 PID=${record.pid},开始定向清理...`);
|
|||
|
|
await this.terminateTrackedProcess(record, '正在清理当前应用遗留的 MySQL 进程...');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 进程模式:启动 MySQL 进程
|
|||
|
|
*/
|
|||
|
|
async startMySQLProcess(port) {
|
|||
|
|
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}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mysqlProcess = spawn(mysqld, [
|
|||
|
|
`--defaults-file=${this.configFile}`,
|
|||
|
|
'--console'
|
|||
|
|
], {
|
|||
|
|
cwd: this.mysqlPath,
|
|||
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|||
|
|
windowsHide: true
|
|||
|
|
});
|
|||
|
|
this.mysqlProcess = mysqlProcess;
|
|||
|
|
this.currentPort = port;
|
|||
|
|
|
|||
|
|
this.saveProcessRecord({
|
|||
|
|
pid: mysqlProcess.pid,
|
|||
|
|
port,
|
|||
|
|
executablePath: mysqld,
|
|||
|
|
configFile: this.configFile,
|
|||
|
|
mysqlPath: this.mysqlPath,
|
|||
|
|
createdAt: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
let startupComplete = false;
|
|||
|
|
let startupTimeout = null;
|
|||
|
|
|
|||
|
|
// 监听 stderr(MySQL 主要输出在 stderr)
|
|||
|
|
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}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
mysqlProcess.stdout.on('data', (data) => {
|
|||
|
|
const msg = data.toString().trim();
|
|||
|
|
if (msg) {
|
|||
|
|
this.log('system', `[mysqld] ${msg}`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
mysqlProcess.on('error', (err) => {
|
|||
|
|
this.log('error', `MySQL 进程启动失败: ${err.message}`);
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
this.mysqlProcess = null;
|
|||
|
|
reject(err);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
mysqlProcess.on('exit', (code, signal) => {
|
|||
|
|
this.log('system', `MySQL 进程退出,代码: ${code}, 信号: ${signal}`);
|
|||
|
|
const record = this.getProcessRecord();
|
|||
|
|
if (record && record.pid === mysqlProcess.pid) {
|
|||
|
|
this.clearProcessRecord();
|
|||
|
|
}
|
|||
|
|
this.mysqlProcess = null;
|
|||
|
|
this.currentPort = null;
|
|||
|
|
|
|||
|
|
// 如果还没完成启动就退出了,说明启动失败
|
|||
|
|
if (!startupComplete) {
|
|||
|
|
reject(new Error(`MySQL 进程异常退出,代码: ${code}`));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 超时处理(30秒)
|
|||
|
|
startupTimeout = setTimeout(() => {
|
|||
|
|
if (!startupComplete && mysqlProcess) {
|
|||
|
|
// 进程还在运行,认为启动成功
|
|||
|
|
startupComplete = true;
|
|||
|
|
this.log('success', 'MySQL 进程启动完成(超时检测)');
|
|||
|
|
resolve(true);
|
|||
|
|
}
|
|||
|
|
}, 30000);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 进程模式:停止 MySQL 进程
|
|||
|
|
*/
|
|||
|
|
async stopMySQLProcess() {
|
|||
|
|
const record = this.getProcessRecord();
|
|||
|
|
if (!this.mysqlProcess && !record) {
|
|||
|
|
this.log('system', 'MySQL 进程未运行');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await this.terminateTrackedProcess(record, '正在停止当前应用自己的 MySQL 进程...');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查 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.ensureRuntimeEnvironment();
|
|||
|
|
|
|||
|
|
this.log('system', '开始 MySQL 进程检查流程(进程模式)');
|
|||
|
|
this.log('system', `路径策略: ${this.pathStrategy.usesSafePaths ? '英文安全路径模式' : '应用目录直启模式'}`);
|
|||
|
|
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(port);
|
|||
|
|
|
|||
|
|
// 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 = MySQLProcessManager;
|