Files
lyroc/electron-app/modules/backend-service.js
2025-05-27 14:16:48 +08:00

229 lines
7.3 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.
/**
* 后端服务管理模块
* 负责启动、管理和监控Python后端服务
*/
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
const findProcess = require('find-process');
const getPort = require('get-port');
// 存储Python进程引用
let pythonProcess = null;
let backendPort = 5000;
let frontendPort = 5173;
/**
* 获取可用的后端端口
* @returns {Promise<number>} 可用端口号
*/
async function checkPort() {
try {
// 获取可用端口首选5000
backendPort = await getPort({ port: 5000 });
console.log(`后端将使用端口: ${backendPort}`);
return backendPort;
} catch (err) {
console.error('获取可用端口失败:', err);
throw err;
}
}
/**
* 获取可用的前端端口
* @returns {Promise<number>} 可用端口号
*/
async function getFrontendPort() {
try {
// 获取完全随机的可用端口,不指定首选端口
frontendPort = await getPort();
console.log(`前端将使用随机端口: ${frontendPort}`);
return frontendPort;
} catch (err) {
console.error('获取前端端口失败:', err);
throw err;
}
}
/**
* 清理已存在的Python进程
* @returns {Promise<void>}
*/
async function cleanupExistingProcesses() {
try {
const processList = await findProcess('port', 5000);
for (const proc of processList) {
if (proc.name.includes('python')) {
console.log(`杀死已存在的Python进程: PID ${proc.pid}`);
process.kill(proc.pid, 'SIGKILL');
}
}
} catch (err) {
console.error('清理进程失败:', err);
}
}
/**
* 启动Python后端服务
* @param {boolean} isDev 是否为开发模式
* @returns {Promise<void>}
*/
async function startPythonBackend(isDev) {
try {
// 在开发模式和生产模式下使用不同的路径
let pythonPath;
let scriptPath;
if (isDev) {
// 开发模式:使用项目中的虚拟环境
const venvPath = path.join(__dirname, '..', '..', 'backend', 'venv');
pythonPath = process.platform === 'win32'
? path.join(venvPath, 'Scripts', 'python.exe')
: path.join(venvPath, 'bin', 'python3');
scriptPath = path.join(__dirname, '..', '..', 'backend', 'main.py');
} else {
// 生产模式:使用打包的虚拟环境
const venvPath = path.join(process.resourcesPath, 'backend', 'venv');
pythonPath = process.platform === 'win32'
? path.join(venvPath, 'Scripts', 'python.exe')
: path.join(venvPath, 'bin', 'python3');
scriptPath = path.join(process.resourcesPath, 'backend', 'main.py');
}
console.log(`启动Python后端: ${scriptPath} 在端口 ${backendPort}`);
console.log(`后端脚本路径存在: ${fs.existsSync(scriptPath)}`);
console.log(`Python解释器路径: ${pythonPath}`);
// 检查Python解释器是否存在
if (!fs.existsSync(pythonPath)) {
throw new Error(`Python解释器不存在: ${pythonPath}`);
}
// 打印Python脚本目录内容
try {
const scriptDir = path.dirname(scriptPath);
console.log(`后端目录内容: ${fs.readdirSync(scriptDir).join(', ')}`);
} catch (err) {
console.error(`无法读取后端目录: ${err}`);
}
// 启动子进程
pythonProcess = spawn(pythonPath, [scriptPath], {
env: { ...process.env, PORT: backendPort.toString() },
stdio: 'pipe' // 确保可以读取标准输出和错误
});
// 输出Python进程的日志
pythonProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
console.log(`Python后端输出: ${output}`);
});
pythonProcess.stderr.on('data', (data) => {
const message = data.toString().trim();
// 判断是否是错误日志还是普通日志
if (message.includes('ERROR') || message.includes('CRITICAL') || message.includes('WARN')) {
console.error(`Python后端错误: ${message}`);
} else {
console.log(`Python后端日志: ${message}`);
}
});
pythonProcess.on('close', (code) => {
console.log(`Python后端退出退出码: ${code}`);
// 如果应用仍在运行,尝试重启后端
if (global.appReady && code !== 0) {
console.log('尝试重启Python后端...');
setTimeout(() => startPythonBackend(isDev), 1000);
}
});
// 等待后端启动 - 改为检测后端是否真正启动完成而非固定等待时间
return new Promise((resolve, reject) => {
// 后端启动超时时间,开发模式较短,生产模式较长
const maxTimeout = isDev ? 15000 : 30000;
const startTime = Date.now();
let backendReady = false;
// 添加额外的日志解析以检测后端就绪状态
pythonProcess.stdout.on('data', (data) => {
const output = data.toString().trim();
console.log(`Python后端输出: ${output}`);
// 检测后端就绪信号uvicorn输出"Application startup complete"表示应用已启动)
if (output.includes('Application startup complete') || output.includes('INFO: Application startup complete')) {
console.log('检测到后端应用启动完成信号');
backendReady = true;
// 再等待短暂时间确保所有服务都初始化完成
setTimeout(() => {
console.log('后端已完全启动,准备创建前端窗口');
resolve();
}, 500);
}
});
// 实现HTTP测试以检测后端是否正常响应
const testBackendConnection = () => {
const http = require('http');
const testUrl = `http://127.0.0.1:${backendPort}/`;
// 如果已经检测到后端就绪,不再继续测试
if (backendReady) return;
// 检查是否超时
if (Date.now() - startTime > maxTimeout) {
console.warn(`后端启动超时(${maxTimeout}ms),将继续尝试启动前端`);
resolve();
return;
}
http.get(testUrl, (res) => {
if (res.statusCode === 200 || res.statusCode === 404) {
// 404也表示服务器在运行只是路径不存在
console.log('通过HTTP检测确认后端已启动');
backendReady = true;
resolve();
} else {
console.log(`后端响应状态码: ${res.statusCode},继续等待...`);
// 短时间后再次测试
setTimeout(testBackendConnection, 1000);
}
}).on('error', (err) => {
console.log(`后端连接测试失败: ${err.message}, 继续等待...`);
// 短时间后再次测试
setTimeout(testBackendConnection, 1000);
});
};
// 启动后端连接测试
setTimeout(testBackendConnection, 1000);
});
} catch (err) {
console.error('启动Python后端失败:', err);
throw err;
}
}
/**
* 停止Python后端
*/
function stopPythonBackend() {
if (pythonProcess) {
console.log('终止Python后端进程...');
pythonProcess.kill();
pythonProcess = null;
}
}
module.exports = {
checkPort,
getFrontendPort,
cleanupExistingProcesses,
startPythonBackend,
stopPythonBackend,
getBackendPort: () => backendPort,
getFrontendPortNumber: () => frontendPort,
getPythonProcess: () => pythonProcess
};