提交额外资源
This commit is contained in:
BIN
build/icons/icon_backup.ico
Normal file
BIN
build/icons/icon_backup.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
100
electron/controller/java.js
Normal file
100
electron/controller/java.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const path = require('path');
|
||||
|
||||
// 动态获取 scripts 目录路径
|
||||
function getScriptsPath(scriptName) {
|
||||
// 开发环境
|
||||
const devPath = path.join(__dirname, '../../scripts', scriptName);
|
||||
// 生产环境(打包后)
|
||||
const prodPath = path.join(process.resourcesPath, 'scripts', scriptName);
|
||||
|
||||
try {
|
||||
// 先尝试开发环境路径
|
||||
require.resolve(devPath);
|
||||
return devPath;
|
||||
} catch (e) {
|
||||
// 如果开发环境路径不存在,使用生产环境路径
|
||||
return prodPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟加载 JavaRunner
|
||||
let JavaRunner = null;
|
||||
function getJavaRunner() {
|
||||
if (!JavaRunner) {
|
||||
JavaRunner = require(getScriptsPath('java-runner'));
|
||||
}
|
||||
return JavaRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Java 控制器 - 提供 JRE 管理和 Java 程序调用接口
|
||||
*/
|
||||
class JavaController {
|
||||
|
||||
/**
|
||||
* 检查 JRE 是否可用
|
||||
*/
|
||||
async checkAvailability() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const available = javaRunner.isJREAvailable();
|
||||
return { success: true, data: { available } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Java 版本
|
||||
*/
|
||||
async getVersion() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const version = await javaRunner.getVersion();
|
||||
return { success: true, data: { version } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 JRE 路径信息
|
||||
*/
|
||||
async getPathInfo() {
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const pathInfo = javaRunner.getPathInfo();
|
||||
return { success: true, data: pathInfo };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 JAR 文件(异步,等待完成)
|
||||
* @param {Object} params - { jarPath, args }
|
||||
*/
|
||||
async runJar(params) {
|
||||
const { jarPath, args = [] } = params;
|
||||
|
||||
if (!jarPath) {
|
||||
return { success: false, message: 'JAR path is required' };
|
||||
}
|
||||
|
||||
try {
|
||||
const JavaRunnerClass = getJavaRunner();
|
||||
const javaRunner = new JavaRunnerClass();
|
||||
const exitCode = await javaRunner.runJarAsync(jarPath, args);
|
||||
return { success: true, data: { exitCode } };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JavaController.toString = () => '[class JavaController]';
|
||||
module.exports = JavaController;
|
||||
|
||||
75
electron/controller/mysql.js
Normal file
75
electron/controller/mysql.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const path = require('path');
|
||||
|
||||
// 动态获取 scripts 目录路径
|
||||
function getScriptsPath(scriptName) {
|
||||
// 开发环境
|
||||
const devPath = path.join(__dirname, '../../scripts', scriptName);
|
||||
// 生产环境(打包后)
|
||||
const prodPath = path.join(process.resourcesPath, 'scripts', scriptName);
|
||||
|
||||
try {
|
||||
// 先尝试开发环境路径
|
||||
require.resolve(devPath);
|
||||
return devPath;
|
||||
} catch (e) {
|
||||
// 如果开发环境路径不存在,使用生产环境路径
|
||||
return prodPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟加载 MySQLManager
|
||||
let MySQLManager = null;
|
||||
function getMySQLManager() {
|
||||
if (!MySQLManager) {
|
||||
MySQLManager = require(getScriptsPath('start-mysql'));
|
||||
}
|
||||
return MySQLManager;
|
||||
}
|
||||
|
||||
class MySQLController {
|
||||
|
||||
/**
|
||||
* 启动MySQL服务
|
||||
*/
|
||||
async start() {
|
||||
try {
|
||||
const MySQLManagerClass = getMySQLManager();
|
||||
const mysqlManager = new MySQLManagerClass();
|
||||
await mysqlManager.start();
|
||||
return { success: true, message: 'MySQL started successfully' };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止MySQL服务
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
const MySQLManagerClass = getMySQLManager();
|
||||
const mysqlManager = new MySQLManagerClass();
|
||||
await mysqlManager.stop();
|
||||
return { success: true, message: 'MySQL stopped successfully' };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取MySQL连接配置
|
||||
*/
|
||||
async getConnectionConfig() {
|
||||
try {
|
||||
const MySQLManagerClass = getMySQLManager();
|
||||
const mysqlManager = new MySQLManagerClass();
|
||||
const config = mysqlManager.getConnectionConfig();
|
||||
return { success: true, data: config };
|
||||
} catch (error) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MySQLController.toString = () => '[class MySQLController]';
|
||||
module.exports = MySQLController;
|
||||
136
scripts/config-generator.js
Normal file
136
scripts/config-generator.js
Normal file
@@ -0,0 +1,136 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 配置文件生成器
|
||||
* 根据应用实际安装路径生成Spring Boot配置文件
|
||||
*/
|
||||
class ConfigGenerator {
|
||||
constructor() {
|
||||
// 开发环境:项目根目录
|
||||
// 打包后:应用根目录(win-unpacked)
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
|
||||
// 开发环境:build/extraResources/java
|
||||
// 打包后:resources/extraResources/java
|
||||
this.javaPath = isDev
|
||||
? path.join(baseDir, 'build', 'extraResources', 'java')
|
||||
: path.join(process.resourcesPath, 'extraResources', 'java');
|
||||
this.templatePath = path.join(this.javaPath, 'application.yml.template');
|
||||
this.configPath = path.join(this.javaPath, 'application.yml');
|
||||
|
||||
// 数据目录(使用应用所在盘符的根目录下的data文件夹)
|
||||
this.dataPath = this.getDataPath(baseDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据目录路径
|
||||
* @param {string} baseDir 应用基础目录
|
||||
* @returns {string} 数据目录路径
|
||||
*/
|
||||
getDataPath(baseDir) {
|
||||
// 获取应用所在盘符(例如:C:, D:, E:)
|
||||
const driveLetter = path.parse(baseDir).root;
|
||||
// 数据目录设置在盘符根目录下的 NPQS9100_Data 文件夹
|
||||
return path.join(driveLetter, 'NPQS9100_Data');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成配置文件
|
||||
* @param {object} options - 配置选项
|
||||
* @param {number} options.mysqlPort - MySQL 端口
|
||||
* @param {number} options.javaPort - Java 应用端口
|
||||
* @param {string} options.mysqlPassword - MySQL 密码
|
||||
*/
|
||||
generateConfig(options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// 读取模板文件
|
||||
if (!fs.existsSync(this.templatePath)) {
|
||||
throw new Error(`Template file not found: ${this.templatePath}`);
|
||||
}
|
||||
|
||||
let template = fs.readFileSync(this.templatePath, 'utf-8');
|
||||
|
||||
// 替换占位符
|
||||
// Windows路径需要转义反斜杠
|
||||
const dataPathEscaped = this.dataPath.replace(/\\/g, '\\\\');
|
||||
template = template.replace(/\{\{APP_DATA_PATH\}\}/g, dataPathEscaped);
|
||||
|
||||
// 替换MySQL密码
|
||||
const mysqlPassword = options.mysqlPassword || 'njcnpqs';
|
||||
template = template.replace(/\{\{MYSQL_PASSWORD\}\}/g, mysqlPassword);
|
||||
|
||||
// 替换端口(如果提供)
|
||||
if (options.mysqlPort) {
|
||||
// 支持两种格式:localhost:3306 和 {{MYSQL_PORT}}
|
||||
template = template.replace(/\{\{MYSQL_PORT\}\}/g, options.mysqlPort);
|
||||
template = template.replace(/localhost:3306/g, `localhost:${options.mysqlPort}`);
|
||||
}
|
||||
if (options.javaPort) {
|
||||
template = template.replace(/port:\s*18092/g, `port: ${options.javaPort}`);
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
fs.writeFileSync(this.configPath, template, 'utf-8');
|
||||
|
||||
// 创建必要的目录
|
||||
this.createDirectories();
|
||||
|
||||
console.log('[ConfigGenerator] Configuration file generated successfully');
|
||||
console.log('[ConfigGenerator] Data path:', this.dataPath);
|
||||
console.log('[ConfigGenerator] MySQL port:', options.mysqlPort || 3306);
|
||||
console.log('[ConfigGenerator] MySQL password:', options.mysqlPassword || 'njcnpqs');
|
||||
console.log('[ConfigGenerator] Java port:', options.javaPort || 18092);
|
||||
|
||||
resolve({
|
||||
configPath: this.configPath,
|
||||
dataPath: this.dataPath,
|
||||
mysqlPort: options.mysqlPort || 3306,
|
||||
javaPort: options.javaPort || 18092
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ConfigGenerator] Failed to generate config:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建必要的目录
|
||||
*/
|
||||
createDirectories() {
|
||||
const dirs = [
|
||||
this.dataPath,
|
||||
path.join(this.dataPath, 'logs'),
|
||||
path.join(this.dataPath, 'template'),
|
||||
path.join(this.dataPath, 'report'),
|
||||
path.join(this.dataPath, 'data')
|
||||
];
|
||||
|
||||
dirs.forEach(dir => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log('[ConfigGenerator] Created directory:', dir);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
getConfigInfo() {
|
||||
return {
|
||||
javaPath: this.javaPath,
|
||||
templatePath: this.templatePath,
|
||||
configPath: this.configPath,
|
||||
dataPath: this.dataPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ConfigGenerator;
|
||||
|
||||
321
scripts/java-runner.js
Normal file
321
scripts/java-runner.js
Normal file
@@ -0,0 +1,321 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Java 运行器 - 用于调用便携式 JRE 运行 Java 程序
|
||||
*/
|
||||
class JavaRunner {
|
||||
constructor() {
|
||||
// 在开发与打包后均可解析到应用根目录下的 jre 目录
|
||||
// 开发环境:项目根目录
|
||||
// 打包后:应用根目录(win-unpacked)
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
this.jrePath = path.join(baseDir, 'jre');
|
||||
this.binPath = path.join(this.jrePath, 'bin');
|
||||
this.javaExe = path.join(this.binPath, 'java.exe');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 JRE 是否存在
|
||||
*/
|
||||
isJREAvailable() {
|
||||
return fs.existsSync(this.javaExe);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Java 版本
|
||||
*/
|
||||
getVersion() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isJREAvailable()) {
|
||||
reject(new Error('JRE not found at: ' + this.javaExe));
|
||||
return;
|
||||
}
|
||||
|
||||
const versionProcess = spawn(this.javaExe, ['-version'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
versionProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
versionProcess.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
versionProcess.on('close', (code) => {
|
||||
if (code === 0 || errorOutput.includes('version')) {
|
||||
// Java -version 输出到 stderr
|
||||
const versionInfo = (output + errorOutput).trim();
|
||||
resolve(versionInfo);
|
||||
} else {
|
||||
reject(new Error('Failed to get Java version'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 JAR 文件
|
||||
* @param {string} jarPath - JAR 文件的绝对路径
|
||||
* @param {Array<string>} args - Java 程序参数
|
||||
* @param {Object} options - spawn 选项
|
||||
* @returns {ChildProcess}
|
||||
*/
|
||||
runJar(jarPath, args = [], options = {}) {
|
||||
if (!this.isJREAvailable()) {
|
||||
throw new Error('JRE not found at: ' + this.javaExe);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(jarPath)) {
|
||||
throw new Error('JAR file not found at: ' + jarPath);
|
||||
}
|
||||
|
||||
const javaArgs = ['-jar', jarPath, ...args];
|
||||
|
||||
const defaultOptions = {
|
||||
cwd: path.dirname(jarPath),
|
||||
stdio: 'inherit'
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('Running Java:', this.javaExe, javaArgs.join(' '));
|
||||
return spawn(this.javaExe, javaArgs, mergedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 JAR 文件并等待完成
|
||||
* @param {string} jarPath - JAR 文件的绝对路径
|
||||
* @param {Array<string>} args - Java 程序参数
|
||||
* @param {Object} options - spawn 选项
|
||||
* @returns {Promise<number>} 退出代码
|
||||
*/
|
||||
runJarAsync(jarPath, args = [], options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const process = this.runJar(jarPath, args, options);
|
||||
|
||||
process.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve(code);
|
||||
} else {
|
||||
reject(new Error(`Java process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
process.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 Java 类
|
||||
* @param {string} className - Java 类名(包含包名)
|
||||
* @param {string} classPath - classpath 路径
|
||||
* @param {Array<string>} args - 程序参数
|
||||
* @param {Object} options - spawn 选项
|
||||
* @returns {ChildProcess}
|
||||
*/
|
||||
runClass(className, classPath, args = [], options = {}) {
|
||||
if (!this.isJREAvailable()) {
|
||||
throw new Error('JRE not found at: ' + this.javaExe);
|
||||
}
|
||||
|
||||
const javaArgs = ['-cp', classPath, className, ...args];
|
||||
|
||||
const defaultOptions = {
|
||||
stdio: 'inherit'
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('Running Java:', this.javaExe, javaArgs.join(' '));
|
||||
return spawn(this.javaExe, javaArgs, mergedOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行 Spring Boot JAR 文件
|
||||
* @param {string} jarPath - JAR 文件的绝对路径
|
||||
* @param {string} configPath - 配置文件路径
|
||||
* @param {Object} options - 启动选项(需包含 javaPort)
|
||||
* @returns {ChildProcess}
|
||||
*/
|
||||
runSpringBoot(jarPath, configPath, options = {}) {
|
||||
if (!this.isJREAvailable()) {
|
||||
throw new Error('JRE not found at: ' + this.javaExe);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(jarPath)) {
|
||||
throw new Error('JAR file not found at: ' + jarPath);
|
||||
}
|
||||
|
||||
const javaArgs = [
|
||||
'-Dfile.encoding=UTF-8', // 设置文件编码为UTF-8,解决中文乱码
|
||||
'-Duser.language=zh', // 设置语言为中文
|
||||
'-Duser.region=CN', // 设置地区为中国
|
||||
'-jar',
|
||||
jarPath,
|
||||
`--spring.config.location=${configPath}`
|
||||
];
|
||||
|
||||
const defaultOptions = {
|
||||
cwd: path.dirname(jarPath),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
JAVA_TOOL_OPTIONS: '-Dfile.encoding=UTF-8' // 额外确保UTF-8编码
|
||||
}
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('Running Spring Boot:', this.javaExe, javaArgs.join(' '));
|
||||
const javaProcess = spawn(this.javaExe, javaArgs, mergedOptions);
|
||||
|
||||
// 记录PID和端口用于后续停止
|
||||
this.springBootProcess = javaProcess;
|
||||
this.currentJavaPort = options.javaPort;
|
||||
|
||||
// 将Java端口记录到文件,供手动清理脚本使用
|
||||
if (options.javaPort) {
|
||||
this.recordJavaPort(options.javaPort);
|
||||
}
|
||||
|
||||
// 进程退出时清理端口记录
|
||||
javaProcess.on('close', () => {
|
||||
this.cleanupJavaPortFile();
|
||||
});
|
||||
|
||||
return javaProcess;
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 Spring Boot 应用
|
||||
*/
|
||||
stopSpringBoot() {
|
||||
return new Promise((resolve) => {
|
||||
if (this.springBootProcess && !this.springBootProcess.killed) {
|
||||
// 设置3秒超时,如果进程没有正常退出,强制kill
|
||||
const timeout = setTimeout(() => {
|
||||
console.log('[Java] Force killing Spring Boot process');
|
||||
try {
|
||||
this.springBootProcess.kill('SIGKILL');
|
||||
} catch (e) {
|
||||
console.error('[Java] Error force killing:', e);
|
||||
}
|
||||
|
||||
// 清理端口记录文件
|
||||
this.cleanupJavaPortFile();
|
||||
resolve();
|
||||
}, 3000);
|
||||
|
||||
this.springBootProcess.on('close', () => {
|
||||
clearTimeout(timeout);
|
||||
console.log('[Java] Spring Boot application stopped gracefully');
|
||||
|
||||
// 清理端口记录文件
|
||||
this.cleanupJavaPortFile();
|
||||
resolve();
|
||||
});
|
||||
|
||||
// 先尝试优雅关闭
|
||||
console.log('[Java] Sending SIGTERM to Spring Boot');
|
||||
this.springBootProcess.kill('SIGTERM');
|
||||
} else {
|
||||
// 即使没有进程引用,也尝试清理端口记录文件
|
||||
this.cleanupJavaPortFile();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录Java端口到文件
|
||||
*/
|
||||
recordJavaPort(port) {
|
||||
try {
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
const javaDir = path.join(baseDir, 'java');
|
||||
const portFilePath = path.join(javaDir, '.running-port');
|
||||
|
||||
fs.writeFileSync(portFilePath, port.toString(), 'utf-8');
|
||||
console.log(`[Java] Port ${port} recorded to ${portFilePath}`);
|
||||
} catch (error) {
|
||||
console.warn('[Java] Failed to record port:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理Java端口记录文件
|
||||
*/
|
||||
cleanupJavaPortFile() {
|
||||
try {
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
const javaDir = path.join(baseDir, 'java');
|
||||
const portFilePath = path.join(javaDir, '.running-port');
|
||||
|
||||
if (fs.existsSync(portFilePath)) {
|
||||
fs.unlinkSync(portFilePath);
|
||||
console.log('[Java] Port record file cleaned up');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Java] Failed to cleanup port record:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取记录的Java运行端口
|
||||
*/
|
||||
getRecordedJavaPort() {
|
||||
try {
|
||||
const isDev = !process.resourcesPath;
|
||||
const baseDir = isDev
|
||||
? path.join(__dirname, '..')
|
||||
: path.dirname(process.resourcesPath);
|
||||
const javaDir = path.join(baseDir, 'java');
|
||||
const portFilePath = path.join(javaDir, '.running-port');
|
||||
|
||||
if (fs.existsSync(portFilePath)) {
|
||||
const port = fs.readFileSync(portFilePath, 'utf-8').trim();
|
||||
return parseInt(port);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Java] Failed to read port record:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 JRE 路径信息
|
||||
*/
|
||||
getPathInfo() {
|
||||
return {
|
||||
jrePath: this.jrePath,
|
||||
binPath: this.binPath,
|
||||
javaExe: this.javaExe,
|
||||
available: this.isJREAvailable()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JavaRunner;
|
||||
|
||||
325
scripts/log-window-manager.js
Normal file
325
scripts/log-window-manager.js
Normal file
@@ -0,0 +1,325 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* 日志窗口管理器
|
||||
* 显示 MySQL 和 Spring Boot 的实时日志
|
||||
*/
|
||||
class LogWindowManager {
|
||||
constructor() {
|
||||
this.logWindow = null;
|
||||
this.logs = [];
|
||||
this.maxLogs = 1000; // 最多保留1000条日志
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日志窗口
|
||||
*/
|
||||
createLogWindow() {
|
||||
this.logWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 600,
|
||||
title: 'NPQS9100 - 服务日志',
|
||||
backgroundColor: '#1e1e1e',
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
// 加载日志页面
|
||||
const logHtml = this.generateLogHTML();
|
||||
this.logWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(logHtml)}`);
|
||||
|
||||
// 窗口关闭事件 - 只清理引用,不退出应用
|
||||
this.logWindow.on('closed', () => {
|
||||
console.log('[LogWindow] Log window closed by user');
|
||||
this.logWindow = null;
|
||||
});
|
||||
|
||||
// 防止日志窗口关闭时退出应用(但允许隐藏)
|
||||
this.closeHandler = (event) => {
|
||||
// 只是隐藏窗口,不是真正关闭
|
||||
// 这样可以随时再打开
|
||||
event.preventDefault();
|
||||
this.logWindow.hide();
|
||||
console.log('[LogWindow] Log window hidden');
|
||||
};
|
||||
|
||||
this.logWindow.on('close', this.closeHandler);
|
||||
|
||||
return this.logWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成日志HTML页面
|
||||
*/
|
||||
generateLogHTML() {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>NPQS9100 服务日志</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: #2d2d30;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #4ec9b0;
|
||||
}
|
||||
.controls button {
|
||||
background: #0e639c;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.controls button:hover {
|
||||
background: #1177bb;
|
||||
}
|
||||
.log-container {
|
||||
background: #1e1e1e;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
height: calc(100vh - 70px);
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.log-entry {
|
||||
margin-bottom: 2px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.log-mysql { color: #4ec9b0; }
|
||||
.log-java { color: #dcdcaa; }
|
||||
.log-system { color: #569cd6; }
|
||||
.log-error { color: #f48771; }
|
||||
.log-warn { color: #ce9178; }
|
||||
.log-success { color: #608b4e; }
|
||||
.timestamp {
|
||||
color: #858585;
|
||||
margin-right: 8px;
|
||||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #424242;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #4e4e4e;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="title">📝 NPQS9100 服务日志监控</div>
|
||||
<div class="controls">
|
||||
<button onclick="clearLogs()">清空日志</button>
|
||||
<button onclick="toggleAutoScroll()">自动滚动: <span id="autoScrollStatus">开</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-container" id="logContainer"></div>
|
||||
|
||||
<script>
|
||||
const { ipcRenderer } = require('electron');
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
let autoScroll = true;
|
||||
|
||||
// 接收日志
|
||||
ipcRenderer.on('log-message', (event, data) => {
|
||||
addLog(data);
|
||||
});
|
||||
|
||||
function addLog(data) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
|
||||
const timestamp = document.createElement('span');
|
||||
timestamp.className = 'timestamp';
|
||||
timestamp.textContent = data.timestamp;
|
||||
entry.appendChild(timestamp);
|
||||
|
||||
const message = document.createElement('span');
|
||||
message.className = \`log-\${data.type}\`;
|
||||
message.textContent = data.message;
|
||||
entry.appendChild(message);
|
||||
|
||||
logContainer.appendChild(entry);
|
||||
|
||||
// 自动滚动到底部
|
||||
if (autoScroll) {
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
// 限制日志条数
|
||||
while (logContainer.children.length > 1000) {
|
||||
logContainer.removeChild(logContainer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
function toggleAutoScroll() {
|
||||
autoScroll = !autoScroll;
|
||||
document.getElementById('autoScrollStatus').textContent = autoScroll ? '开' : '关';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日志
|
||||
*/
|
||||
addLog(type, message) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
type,
|
||||
message
|
||||
};
|
||||
|
||||
this.logs.push(logEntry);
|
||||
|
||||
// 限制日志数量
|
||||
if (this.logs.length > this.maxLogs) {
|
||||
this.logs.shift();
|
||||
}
|
||||
|
||||
// 发送到窗口
|
||||
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
||||
this.logWindow.webContents.send('log-message', logEntry);
|
||||
}
|
||||
|
||||
// 同时输出到控制台
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示日志窗口
|
||||
*/
|
||||
show() {
|
||||
if (!this.logWindow || this.logWindow.isDestroyed()) {
|
||||
// 窗口已被销毁,重新创建
|
||||
console.log('[LogWindow] Recreating log window...');
|
||||
this.createLogWindow();
|
||||
|
||||
// 重新发送历史日志
|
||||
this.logs.forEach(log => {
|
||||
this.logWindow.webContents.send('log-message', log);
|
||||
});
|
||||
} else {
|
||||
this.logWindow.show();
|
||||
this.logWindow.focus();
|
||||
console.log('[LogWindow] Log window shown');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏日志窗口
|
||||
*/
|
||||
hide() {
|
||||
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
||||
this.logWindow.hide();
|
||||
console.log('[LogWindow] Log window hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查日志窗口是否可见
|
||||
*/
|
||||
isVisible() {
|
||||
return this.logWindow && !this.logWindow.isDestroyed() && this.logWindow.isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换日志窗口显示/隐藏
|
||||
*/
|
||||
toggle() {
|
||||
if (this.isVisible()) {
|
||||
this.hide();
|
||||
} else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭日志窗口(真正销毁)
|
||||
*/
|
||||
close() {
|
||||
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
||||
try {
|
||||
// 移除 close 事件监听,允许真正关闭
|
||||
if (this.closeHandler) {
|
||||
this.logWindow.removeListener('close', this.closeHandler);
|
||||
}
|
||||
this.logWindow.removeAllListeners('close');
|
||||
this.logWindow.removeAllListeners('closed');
|
||||
|
||||
// 强制销毁窗口
|
||||
this.logWindow.destroy();
|
||||
console.log('[LogWindow] Log window destroyed');
|
||||
} catch (error) {
|
||||
console.error('[LogWindow] Error closing log window:', error);
|
||||
} finally {
|
||||
this.logWindow = null;
|
||||
this.closeHandler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有日志
|
||||
*/
|
||||
getLogs() {
|
||||
return this.logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空日志
|
||||
*/
|
||||
clearLogs() {
|
||||
this.logs = [];
|
||||
if (this.logWindow && !this.logWindow.isDestroyed()) {
|
||||
this.logWindow.webContents.send('clear-logs');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LogWindowManager;
|
||||
|
||||
154
scripts/port-checker.js
Normal file
154
scripts/port-checker.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const net = require('net');
|
||||
|
||||
/**
|
||||
* 端口检测工具
|
||||
*/
|
||||
class PortChecker {
|
||||
/**
|
||||
* 检查端口是否可用(检测0.0.0.0,确保能绑定到所有地址)
|
||||
* @param {number} port - 端口号
|
||||
* @param {string} host - 主机地址,默认 0.0.0.0(所有地址)
|
||||
* @returns {Promise<boolean>} true 表示端口可用,false 表示已被占用
|
||||
*/
|
||||
static checkPort(port, host = '0.0.0.0') {
|
||||
return new Promise((resolve) => {
|
||||
// 先尝试连接,看是否有服务在监听
|
||||
const testSocket = new net.Socket();
|
||||
testSocket.setTimeout(200);
|
||||
|
||||
testSocket.on('connect', () => {
|
||||
// 能连接上,说明端口被占用
|
||||
console.log(`[PortChecker] Port ${port} is in use (connection successful)`);
|
||||
testSocket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
testSocket.on('timeout', () => {
|
||||
// 超时,再用绑定方式检测
|
||||
testSocket.destroy();
|
||||
this._checkPortByBinding(port, host, resolve);
|
||||
});
|
||||
|
||||
testSocket.on('error', (err) => {
|
||||
testSocket.destroy();
|
||||
if (err.code === 'ECONNREFUSED') {
|
||||
// 连接被拒绝,说明没有服务监听,再用绑定方式确认
|
||||
this._checkPortByBinding(port, host, resolve);
|
||||
} else {
|
||||
// 其他错误,认为端口可用
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
testSocket.connect(port, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
static _checkPortByBinding(port, host, resolve) {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.log(`[PortChecker] Port ${port} is in use (EADDRINUSE)`);
|
||||
resolve(false);
|
||||
} else {
|
||||
console.log(`[PortChecker] Port ${port} check error: ${err.code}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
server.once('listening', () => {
|
||||
server.close();
|
||||
console.log(`[PortChecker] Port ${port} is available`);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
server.listen(port, host);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找可用端口(从指定端口开始递增查找)
|
||||
* @param {number} startPort - 起始端口
|
||||
* @param {number} maxAttempts - 最大尝试次数,默认100
|
||||
* @param {string} host - 主机地址,默认 0.0.0.0
|
||||
* @returns {Promise<number>} 返回可用的端口号,如果都不可用则返回 -1
|
||||
*/
|
||||
static async findAvailablePort(startPort, maxAttempts = 100, host = '0.0.0.0') {
|
||||
console.log(`[PortChecker] Searching for available port starting from ${startPort}...`);
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const port = startPort + i;
|
||||
const isAvailable = await this.checkPort(port, host);
|
||||
|
||||
if (isAvailable) {
|
||||
console.log(`[PortChecker] ✓ Found available port: ${port}`);
|
||||
return port;
|
||||
} else {
|
||||
console.log(`[PortChecker] ✗ Port ${port} is in use, trying ${port + 1}...`);
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`[PortChecker] ✗ No available port found from ${startPort} to ${startPort + maxAttempts - 1}`);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待端口开始监听(用于检测服务是否启动成功)
|
||||
* @param {number} port - 端口号
|
||||
* @param {number} timeout - 超时时间(毫秒),默认30秒
|
||||
* @param {string} host - 主机地址
|
||||
* @returns {Promise<boolean>} true 表示端口已开始监听
|
||||
*/
|
||||
static async waitForPort(port, timeout = 30000, host = '127.0.0.1') {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
const isListening = await this.isPortListening(port, host);
|
||||
|
||||
if (isListening) {
|
||||
console.log(`[PortChecker] Port ${port} is now listening`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 等待500ms后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
console.error(`[PortChecker] Timeout waiting for port ${port} to listen`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查端口是否正在监听
|
||||
* @param {number} port - 端口号
|
||||
* @param {string} host - 主机地址
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static isPortListening(port, host = '127.0.0.1') {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
|
||||
socket.setTimeout(1000);
|
||||
|
||||
socket.once('connect', () => {
|
||||
socket.destroy();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
socket.once('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.once('error', () => {
|
||||
socket.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
socket.connect(port, host);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PortChecker;
|
||||
|
||||
373
scripts/start-mysql.js
Normal file
373
scripts/start-mysql.js
Normal file
@@ -0,0 +1,373 @@
|
||||
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;
|
||||
116
scripts/startup-manager.js
Normal file
116
scripts/startup-manager.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const { BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 启动状态管理器
|
||||
* 管理启动流程和显示启动进度
|
||||
*/
|
||||
class StartupManager {
|
||||
constructor() {
|
||||
this.loadingWindow = null;
|
||||
this.steps = [
|
||||
{ id: 'init', label: '正在初始化应用...', progress: 0 },
|
||||
{ id: 'check-mysql-port', label: '正在检测MySQL端口...', progress: 15 },
|
||||
{ id: 'start-mysql', label: '正在启动MySQL数据库...', progress: 30 },
|
||||
{ id: 'wait-mysql', label: '等待MySQL就绪...', progress: 45 },
|
||||
{ id: 'check-java-port', label: '正在检测后端服务端口...', progress: 60 },
|
||||
{ id: 'generate-config', label: '正在生成配置文件...', progress: 70 },
|
||||
{ id: 'start-java', label: '正在启动后端服务...', progress: 80 },
|
||||
{ id: 'wait-java', label: '等待后端服务就绪...', progress: 90 },
|
||||
{ id: 'done', label: '启动完成!', progress: 100 }
|
||||
];
|
||||
this.currentStep = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Loading 窗口
|
||||
*/
|
||||
createLoadingWindow() {
|
||||
this.loadingWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 300,
|
||||
frame: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
skipTaskbar: true, // 不在任务栏显示
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
// 加载 loading 页面
|
||||
const loadingHtml = path.join(__dirname, '../public/html/loading.html');
|
||||
this.loadingWindow.loadFile(loadingHtml);
|
||||
|
||||
return this.loadingWindow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新启动进度
|
||||
* @param {string} stepId - 步骤ID
|
||||
* @param {object} extraInfo - 额外信息
|
||||
*/
|
||||
updateProgress(stepId, extraInfo = {}) {
|
||||
const stepIndex = this.steps.findIndex(s => s.id === stepId);
|
||||
|
||||
if (stepIndex !== -1) {
|
||||
this.currentStep = stepIndex;
|
||||
const step = this.steps[stepIndex];
|
||||
|
||||
const progressData = {
|
||||
step: stepId,
|
||||
label: step.label,
|
||||
progress: step.progress,
|
||||
...extraInfo
|
||||
};
|
||||
|
||||
// 发送进度到 loading 窗口
|
||||
if (this.loadingWindow && !this.loadingWindow.isDestroyed()) {
|
||||
this.loadingWindow.webContents.send('startup-progress', progressData);
|
||||
}
|
||||
|
||||
console.log(`[StartupManager] ${step.label} (${step.progress}%)`, extraInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误信息
|
||||
* @param {string} error - 错误信息
|
||||
*/
|
||||
showError(error) {
|
||||
if (this.loadingWindow && !this.loadingWindow.isDestroyed()) {
|
||||
this.loadingWindow.webContents.send('startup-error', { error });
|
||||
}
|
||||
console.error('[StartupManager] Error:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 Loading 窗口
|
||||
*/
|
||||
closeLoadingWindow() {
|
||||
if (this.loadingWindow && !this.loadingWindow.isDestroyed()) {
|
||||
// 使用 destroy() 而不是 close() 确保窗口被完全销毁
|
||||
this.loadingWindow.destroy();
|
||||
this.loadingWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有步骤
|
||||
*/
|
||||
getSteps() {
|
||||
return this.steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前步骤
|
||||
*/
|
||||
getCurrentStep() {
|
||||
return this.steps[this.currentStep];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StartupManager;
|
||||
|
||||
Reference in New Issue
Block a user