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} 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} args - Java 程序参数 * @param {Object} options - spawn 选项 * @returns {Promise} 退出代码 */ 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} 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; // 将Java端口记录到文件,供手动清理脚本使用 if (options.javaPort) { this.recordJavaPort(options.javaPort); } // 进程退出时清理端口记录 javaProcess.on('close', () => { this.cleanupJavaPortFile(); }); return javaProcess; } /** * 停止 Spring Boot 应用 */ stopSpringBoot() { return new Promise(async (resolve) => { const { exec } = require('child_process'); const killedPids = new Set(); let killAttempts = 0; // 方法1: 如果有进程引用,通过PID杀死 if (this.springBootProcess && !this.springBootProcess.killed) { const pid = this.springBootProcess.pid; console.log('[Java] Method 1: Stopping Spring Boot by PID:', pid); // 使用 /F 强制终止,/T 终止子进程树 const killCommand = `taskkill /F /T /PID ${pid}`; console.log('[Java] Executing:', killCommand); exec(killCommand, (error, stdout, stderr) => { if (error) { console.error('[Java] taskkill by PID failed:', error); } else { console.log('[Java] taskkill by PID success:', stdout); killedPids.add(pid); } killAttempts++; checkComplete(); }); } else { killAttempts++; } // 方法2: 通过端口杀死占用进程(精确定位,不会误杀其他Java进程) const recordedPort = this.currentJavaPort || this.getRecordedJavaPort(); if (recordedPort) { console.log(`[Java] Method 2: Killing process on port ${recordedPort} (precise targeting)`); // 查找占用端口的进程 const findCommand = `netstat -ano | findstr :${recordedPort}`; exec(findCommand, (error, stdout) => { if (!error && stdout) { // 提取PID(最后一列) const lines = stdout.trim().split('\n'); const pids = new Set(); lines.forEach(line => { const parts = line.trim().split(/\s+/); const pid = parts[parts.length - 1]; if (pid && pid !== '0' && !killedPids.has(pid)) { pids.add(pid); } }); console.log(`[Java] Found PIDs on port ${recordedPort}:`, Array.from(pids)); if (pids.size > 0) { // 杀死所有找到的进程 let portsKilled = 0; pids.forEach(pid => { exec(`taskkill /F /T /PID ${pid}`, (err, out) => { portsKilled++; if (!err) { console.log(`[Java] Killed process ${pid} on port ${recordedPort}`); } else { console.warn(`[Java] Failed to kill process ${pid}:`, err); } if (portsKilled === pids.size) { killAttempts++; checkComplete(); } }); }); } else { console.log(`[Java] No process found on port ${recordedPort} (already cleaned)`); killAttempts++; checkComplete(); } } else { console.log(`[Java] No process found on port ${recordedPort}`); killAttempts++; checkComplete(); } }); } else { console.log('[Java] No port recorded, skipping port-based kill'); killAttempts++; } // 检查是否所有清理方法都已完成 function checkComplete() { const expectedAttempts = recordedPort ? 2 : 1; if (killAttempts >= expectedAttempts) { // 清理端口记录文件 this.cleanupJavaPortFile(); // 等待500ms确保进程完全终止 setTimeout(() => { console.log('[Java] Spring Boot stop process completed'); console.log('[Java] Note: Other Java processes (like IDEA) are NOT affected'); resolve(); }, 500); } } // 绑定this上下文 checkComplete = checkComplete.bind(this); }); } /** * 记录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;