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;
|