初始化

This commit is contained in:
2026-04-13 17:32:58 +08:00
commit c6ee0d5243
1342 changed files with 96426 additions and 0 deletions

220
scripts/config-generator.js Normal file
View File

@@ -0,0 +1,220 @@
const fs = require('fs');
const path = require('path');
const { resolveRuntimeStrategy } = require('./path-utils');
/**
* 配置文件生成器
* 根据应用实际安装路径生成Spring Boot配置文件
*/
class ConfigGenerator {
constructor() {
// 开发环境:项目根目录
// 打包后:应用根目录(最终交付目录)
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
const pathStrategy = resolveRuntimeStrategy(baseDir);
// 开发环境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');
this.pathStrategy = pathStrategy;
// 数据目录:
// - ASCII 路径:使用当前应用目录内的 CN_Tool_Data
// - 非 ASCII 路径:切到 CN_Tool 英文安全运行根目录下,避免与历史客户端混用目录
this.dataPath = this.getDataPath(baseDir, pathStrategy);
}
/**
* 获取数据目录路径
* @param {string} baseDir 应用基础目录
* @returns {string} 数据目录路径
*/
getDataPath(baseDir, pathStrategy = resolveRuntimeStrategy(baseDir)) {
if (pathStrategy.usesSafePaths) {
return path.join(pathStrategy.safeRuntimeRoot, 'data');
}
return path.join(baseDir, 'CN_Tool_Data');
}
/**
* 生成配置文件
* @param {object} options - 配置选项
* @param {number} options.mysqlPort - MySQL 端口
* @param {number} options.javaPort - Java 应用端口
* @param {number} options.websocketPort - WebSocket 端口
* @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}`);
}
// 替换 WebSocket 端口
if (options.websocketPort) {
template = template.replace(/port:\s*7777/g, `port: ${options.websocketPort}`);
}
// 写入配置文件
fs.writeFileSync(this.configPath, template, 'utf-8');
// 创建必要的目录
this.createDirectories();
console.log('[ConfigGenerator] Configuration file generated successfully');
console.log('[ConfigGenerator] Path mode:', this.pathStrategy.usesSafePaths ? 'safe-data-root' : 'app-local-data');
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 || 18093);
console.log('[ConfigGenerator] WebSocket port:', options.websocketPort || 7778);
resolve({
configPath: this.configPath,
dataPath: this.dataPath,
mysqlPort: options.mysqlPort || 3306,
javaPort: options.javaPort || 18093,
websocketPort: options.websocketPort || 7778
});
} 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);
}
});
// 复制内置的报告模板文件
this.copyBuiltInTemplates();
}
/**
* 复制内置的报告模板文件到用户数据目录
*/
copyBuiltInTemplates() {
try {
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
// 内置模板源路径
const templateSource = isDev
? path.join(baseDir, 'build', 'extraResources', 'templates')
: path.join(process.resourcesPath, 'extraResources', 'templates');
// 目标路径:用户数据目录/template/
const templateDest = path.join(this.dataPath, 'template');
// 检查源模板是否存在
if (!fs.existsSync(templateSource)) {
console.log('[ConfigGenerator] Built-in templates not found, skipping copy');
return;
}
// 递归复制模板文件(只复制不存在的文件,不覆盖已有文件)
this.copyTemplatesRecursive(templateSource, templateDest);
console.log('[ConfigGenerator] Built-in templates copied successfully');
} catch (error) {
console.error('[ConfigGenerator] Failed to copy templates:', error);
}
}
/**
* 递归复制模板文件(不覆盖已存在的文件)
* @param {string} src 源目录
* @param {string} dest 目标目录
*/
copyTemplatesRecursive(src, dest) {
// 确保目标目录存在
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
// 读取源目录内容
const entries = fs.readdirSync(src, { withFileTypes: true });
entries.forEach(entry => {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
// 递归复制子目录
this.copyTemplatesRecursive(srcPath, destPath);
} else {
// 只复制不存在的文件(不覆盖用户修改过的文件)
if (!fs.existsSync(destPath)) {
fs.copyFileSync(srcPath, destPath);
console.log('[ConfigGenerator] Copied template:', destPath);
} else {
console.log('[ConfigGenerator] Template already exists, skipping:', destPath);
}
}
});
}
/**
* 获取配置信息
*/
getConfigInfo() {
return {
javaPath: this.javaPath,
templatePath: this.templatePath,
configPath: this.configPath,
dataPath: this.dataPath
};
}
}
module.exports = ConfigGenerator;

485
scripts/java-runner.js Normal file
View File

@@ -0,0 +1,485 @@
const { spawn, exec, execFile } = require('child_process');
const path = require('path');
const fs = require('fs');
/**
* Java 运行器 - 用于调用便携式 JRE 运行 Java 程序
*/
class JavaRunner {
constructor() {
// 在开发与打包后均可解析到应用根目录下的 jre 目录
// 开发环境:项目根目录
// 打包后:应用根目录(最终交付目录)
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
this.baseDir = baseDir;
this.jrePath = path.join(baseDir, 'jre');
this.binPath = path.join(this.jrePath, 'bin');
this.javaExe = path.join(this.binPath, 'java.exe');
this.javaRuntimeDir = path.join(this.baseDir, 'java');
this.processRecordPath = path.join(this.javaRuntimeDir, '.running-process.json');
}
normalizeComparablePath(target = '') {
return String(target).replace(/"/g, '').replace(/\//g, '\\').toLowerCase();
}
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 });
}
});
});
}
getRecordedJavaProcess() {
try {
if (!fs.existsSync(this.processRecordPath)) {
return null;
}
return JSON.parse(fs.readFileSync(this.processRecordPath, 'utf-8'));
} catch (error) {
console.warn('[Java] Failed to read process record:', error);
return null;
}
}
recordJavaProcess(record = {}) {
try {
fs.mkdirSync(this.javaRuntimeDir, { recursive: true });
fs.writeFileSync(this.processRecordPath, JSON.stringify(record, null, 2), 'utf-8');
} catch (error) {
console.warn('[Java] Failed to record process metadata:', error);
}
}
cleanupJavaProcessRecord() {
try {
if (fs.existsSync(this.processRecordPath)) {
fs.unlinkSync(this.processRecordPath);
}
} catch (error) {
console.warn('[Java] Failed to cleanup process record:', error);
}
}
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 isOwnJavaProcess(pid, record = this.getRecordedJavaProcess()) {
const processInfo = await this.getProcessInfoByPid(pid);
if (!processInfo) {
return false;
}
const executablePath = this.normalizeComparablePath(processInfo.executablePath);
const commandLine = this.normalizeComparablePath(processInfo.commandLine);
const expectedJavaExe = this.normalizeComparablePath(this.javaExe);
const expectedJarPath = this.normalizeComparablePath((record && record.jarPath) || this.currentJarPath || '');
if (executablePath !== expectedJavaExe) {
return false;
}
if (!expectedJarPath) {
return true;
}
// 关键校验:仅当命令行仍指向当前应用自己的 JAR 时,才允许清理。
return commandLine.includes(expectedJarPath);
}
async findPidsByPort(port) {
return new Promise(resolve => {
exec(`netstat -ano | findstr :${port}`, (error, stdout) => {
if (error || !stdout) {
resolve([]);
return;
}
const pids = new Set();
stdout.trim().split('\n').forEach(line => {
const parts = line.trim().split(/\s+/);
const pid = parts[parts.length - 1];
if (pid && pid !== '0') {
pids.add(Number(pid));
}
});
resolve(Array.from(pids).filter(Boolean));
});
});
}
async killProcessByPid(pid, label = 'Java') {
return new Promise(resolve => {
exec(`taskkill /F /T /PID ${pid}`, error => {
if (error) {
console.warn(`[${label}] Failed to kill PID ${pid}:`, error);
} else {
console.log(`[${label}] Killed PID ${pid}`);
}
resolve();
});
});
}
/**
* 检查 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', // 设置地区为中国
];
// 如果提供了 logPath通过 JVM 系统属性传递给 logback
if (options.logPath) {
// Windows 路径使用正斜杠或保持原样JVM 会自动处理
// 方案1转换为正斜杠跨平台兼容
const normalizedLogPath = options.logPath.replace(/\\/g, '/');
javaArgs.push(`-DlogHomeDir=${normalizedLogPath}`);
console.log('[Java] Setting log path to:', normalizedLogPath);
console.log('[Java] Original log path:', options.logPath);
}
javaArgs.push('-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;
this.currentJarPath = jarPath;
this.recordJavaProcess({
pid: javaProcess.pid,
port: options.javaPort || null,
jarPath,
javaExe: this.javaExe,
createdAt: new Date().toISOString()
});
// 将Java端口记录到文件供手动清理脚本使用
if (options.javaPort) {
this.recordJavaPort(options.javaPort);
}
// 进程退出时清理端口记录
javaProcess.on('close', () => {
this.cleanupJavaProcessRecord();
this.cleanupJavaPortFile();
});
return javaProcess;
}
/**
* 停止 Spring Boot 应用
*/
async stopSpringBoot() {
const recordedProcess = this.getRecordedJavaProcess();
const candidateProcesses = [];
if (this.springBootProcess && !this.springBootProcess.killed) {
candidateProcesses.push({
pid: this.springBootProcess.pid,
port: this.currentJavaPort || null,
jarPath: this.currentJarPath || null
});
}
if (recordedProcess && recordedProcess.pid && !candidateProcesses.some(item => item.pid === recordedProcess.pid)) {
candidateProcesses.push(recordedProcess);
}
for (const candidate of candidateProcesses) {
const isOwnProcess = await this.isOwnJavaProcess(candidate.pid, candidate);
if (!isOwnProcess) {
continue;
}
console.log(`[Java] Stopping tracked Spring Boot process PID ${candidate.pid}`);
await this.killProcessByPid(candidate.pid, 'Java');
this.cleanupJavaProcessRecord();
this.cleanupJavaPortFile();
this.springBootProcess = null;
this.currentJavaPort = null;
this.currentJarPath = null;
console.log('[Java] Spring Boot stop process completed');
console.log('[Java] Only the current application JAR process was cleaned');
return;
}
const fallbackPort = this.currentJavaPort || recordedProcess?.port || this.getRecordedJavaPort();
if (fallbackPort) {
const pids = await this.findPidsByPort(fallbackPort);
for (const pid of pids) {
const isOwnProcess = await this.isOwnJavaProcess(pid, recordedProcess || { jarPath: this.currentJarPath });
if (!isOwnProcess) {
continue;
}
console.log(`[Java] Stopping fallback JAR process on port ${fallbackPort}, PID ${pid}`);
await this.killProcessByPid(pid, 'Java');
this.cleanupJavaProcessRecord();
this.cleanupJavaPortFile();
this.springBootProcess = null;
this.currentJavaPort = null;
this.currentJarPath = null;
console.log('[Java] Spring Boot stop process completed');
console.log('[Java] Only the current application JAR process was cleaned');
return;
}
}
this.cleanupJavaProcessRecord();
this.cleanupJavaPortFile();
this.springBootProcess = null;
this.currentJavaPort = null;
this.currentJarPath = null;
console.log('[Java] No tracked Spring Boot process found, process record cleaned');
}
/**
* 记录Java端口到文件
*/
recordJavaPort(port) {
try {
const javaDir = this.javaRuntimeDir;
const portFilePath = path.join(javaDir, '.running-port');
fs.mkdirSync(javaDir, { recursive: true });
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 portFilePath = path.join(this.javaRuntimeDir, '.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 recordedProcess = this.getRecordedJavaProcess();
if (recordedProcess && recordedProcess.port) {
return parseInt(recordedProcess.port);
}
const portFilePath = path.join(this.javaRuntimeDir, '.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;

View File

@@ -0,0 +1,422 @@
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条日志
// 初始化日志文件路径
this.initLogFile();
}
/**
* 初始化日志文件路径(按天滚动)
*/
initLogFile() {
// 开发环境:项目根目录的 logs 文件夹
// 打包后:应用根目录的 logs 文件夹
const isDev = !process.resourcesPath;
const baseDir = isDev
? path.join(__dirname, '..')
: path.dirname(process.resourcesPath);
this.logsDir = path.join(baseDir, 'logs');
// 确保 logs 目录存在
if (!fs.existsSync(this.logsDir)) {
fs.mkdirSync(this.logsDir, { recursive: true });
}
// 生成当天的日志文件名startup-YYYYMMDD.log
const today = new Date();
const dateStr = today.getFullYear() +
String(today.getMonth() + 1).padStart(2, '0') +
String(today.getDate()).padStart(2, '0');
this.logFilePath = path.join(this.logsDir, `startup-${dateStr}.log`);
console.log('[LogWindowManager] Log file:', this.logFilePath);
// 写入启动标记
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', 'CN_Tool 灿能运维工具 应用启动');
this.writeToFile(new Date().toISOString().replace('T', ' ').substring(0, 19), 'SYSTEM', '=' .repeat(80));
// 清理超过30天的旧日志
this.cleanOldLogs(30);
}
/**
* 清理旧日志文件
* @param {number} days - 保留天数
*/
cleanOldLogs(days) {
try {
const now = Date.now();
const maxAge = days * 24 * 60 * 60 * 1000; // 转换为毫秒
const files = fs.readdirSync(this.logsDir);
let deletedCount = 0;
files.forEach(file => {
if (file.startsWith('startup-') && file.endsWith('.log')) {
const filePath = path.join(this.logsDir, file);
const stats = fs.statSync(filePath);
const age = now - stats.mtimeMs;
if (age > maxAge) {
fs.unlinkSync(filePath);
deletedCount++;
console.log(`[LogWindowManager] Deleted old log: ${file}`);
}
}
});
if (deletedCount > 0) {
console.log(`[LogWindowManager] Cleaned up ${deletedCount} old log file(s)`);
}
} catch (error) {
console.error('[LogWindowManager] Failed to clean old logs:', error);
}
}
/**
* 写入日志到文件
*/
writeToFile(timestamp, type, message) {
try {
const logLine = `[${timestamp}] [${type.toUpperCase()}] ${message}\n`;
fs.appendFileSync(this.logFilePath, logLine, 'utf-8');
} catch (error) {
console.error('[LogWindowManager] Failed to write log to file:', error);
}
}
/**
* 创建日志窗口
*/
createLogWindow() {
const isDev = !process.resourcesPath;
const iconPath = isDev
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
this.logWindow = new BrowserWindow({
width: 900,
height: 600,
title: 'CN_Tool 灿能运维工具 - 服务日志',
backgroundColor: '#1e1e1e',
icon: iconPath,
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>CN_Tool 灿能运维工具 服务日志</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">📝 CN_Tool 灿能运维工具 服务日志监控</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 now = new Date();
const timestamp = now.toLocaleTimeString();
const fullTimestamp = now.toISOString().replace('T', ' ').substring(0, 19);
const logEntry = {
timestamp,
type,
message
};
this.logs.push(logEntry);
// 限制日志数量
if (this.logs.length > this.maxLogs) {
this.logs.shift();
}
// 写入文件(使用完整时间戳)
this.writeToFile(fullTimestamp, type, message);
// 发送到窗口
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;

View File

@@ -0,0 +1,627 @@
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;
// 监听 stderrMySQL 主要输出在 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;

42
scripts/path-utils.js Normal file
View File

@@ -0,0 +1,42 @@
const path = require('path');
/**
* 判断路径中是否包含非 ASCII 字符。
* 这里不只判断中文,其他非 ASCII 字符同样视为不安全路径。
*/
function hasNonAscii(targetPath = '') {
return /[^\x00-\x7F]/.test(targetPath);
}
/**
* 获取当前路径所在盘符根目录,例如 D:\
*/
function getDriveRoot(targetPath = '') {
const resolvedPath = path.resolve(targetPath || process.cwd());
return path.parse(resolvedPath).root;
}
/**
* 解析运行期路径策略。
* - 安全路径:继续直接使用应用目录
* - 非 ASCII 路径:切到英文安全路径
*/
function resolveRuntimeStrategy(baseDir) {
const normalizedBaseDir = path.resolve(baseDir);
const driveRoot = getDriveRoot(normalizedBaseDir);
const usesSafePaths = hasNonAscii(normalizedBaseDir);
return {
baseDir: normalizedBaseDir,
driveRoot,
usesSafePaths,
// 与历史客户端隔离,避免继续复用旧品牌运行目录。
safeRuntimeRoot: path.join(driveRoot, 'CN_Tool_Runtime')
};
}
module.exports = {
hasNonAscii,
getDriveRoot,
resolveRuntimeStrategy
};

154
scripts/port-checker.js Normal file
View 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;

126
scripts/startup-manager.js Normal file
View File

@@ -0,0 +1,126 @@
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: 20 },
{ id: 'wait-mysql', label: '确保MySQL服务运行...', progress: 40 },
{ 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() {
const isDev = !process.resourcesPath;
const iconPath = isDev
? path.join(__dirname, '..', 'public', 'images', 'icon.png')
: path.join(process.resourcesPath, 'app.asar.unpacked', 'public', 'images', 'icon.png');
this.loadingWindow = new BrowserWindow({
width: 500,
height: 300,
frame: false,
transparent: true,
resizable: false,
alwaysOnTop: true,
skipTaskbar: true, // 不在任务栏显示
icon: iconPath,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
// 加载 loading 页面
const isDev2 = !process.resourcesPath;
const loadingHtml = isDev2
? path.join(__dirname, '..', 'public', 'html', 'loading.html')
: path.join(process.resourcesPath, 'app.asar', 'public', 'html', 'loading.html');
console.log('[StartupManager] Loading HTML from:', loadingHtml);
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;