486 lines
14 KiB
JavaScript
486 lines
14 KiB
JavaScript
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;
|
||
|