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