373 lines
12 KiB
JavaScript
373 lines
12 KiB
JavaScript
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; |