Files
CN_Tool_client/scripts/java-runner.js
2026-04-13 17:32:58 +08:00

486 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;