2025-10-16 20:18:20 +08:00
|
|
|
|
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() {
|
2025-11-26 08:50:22 +08:00
|
|
|
|
return new Promise(async (resolve) => {
|
|
|
|
|
|
const { exec } = require('child_process');
|
|
|
|
|
|
const killedPids = new Set();
|
|
|
|
|
|
let killAttempts = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 方法1: 如果有进程引用,通过PID杀死
|
2025-10-16 20:18:20 +08:00
|
|
|
|
if (this.springBootProcess && !this.springBootProcess.killed) {
|
2025-11-26 08:50:22 +08:00
|
|
|
|
const pid = this.springBootProcess.pid;
|
|
|
|
|
|
console.log('[Java] Method 1: Stopping Spring Boot by PID:', pid);
|
2025-10-16 20:18:20 +08:00
|
|
|
|
|
2025-11-26 08:50:22 +08:00
|
|
|
|
// 使用 /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();
|
2025-10-16 20:18:20 +08:00
|
|
|
|
});
|
2025-11-26 08:50:22 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
killAttempts++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 方法2: 通过端口杀死占用进程(精确定位,不会误杀其他Java进程)
|
|
|
|
|
|
const recordedPort = this.currentJavaPort || this.getRecordedJavaPort();
|
|
|
|
|
|
if (recordedPort) {
|
|
|
|
|
|
console.log(`[Java] Method 2: Killing process on port ${recordedPort} (precise targeting)`);
|
2025-10-16 20:18:20 +08:00
|
|
|
|
|
2025-11-26 08:50:22 +08:00
|
|
|
|
// 查找占用端口的进程
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-10-16 20:18:20 +08:00
|
|
|
|
} else {
|
2025-11-26 08:50:22 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-10-16 20:18:20 +08:00
|
|
|
|
}
|
2025-11-26 08:50:22 +08:00
|
|
|
|
|
|
|
|
|
|
// 绑定this上下文
|
|
|
|
|
|
checkComplete = checkComplete.bind(this);
|
2025-10-16 20:18:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 记录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;
|
|
|
|
|
|
|