diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index e50c348..a346956 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -10,6 +10,7 @@ import { installPipPackage, installDependencies, startBackend, + stopBackend, } from './services/pythonService' import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService' @@ -301,6 +302,18 @@ app.on('second-instance', () => { } }) +app.on('before-quit', async event => { + // 只处理一次,避免多重触发 + event.preventDefault() + try { + await stopBackend() + } catch (e) { + console.error('停止后端时出错:', e) + } finally { + app.exit(0) + } +}) + app.whenReady().then(() => { // 检查管理员权限 if (!isRunningAsAdmin()) { diff --git a/frontend/electron/services/pythonService.ts b/frontend/electron/services/pythonService.ts index 121c2de..b533230 100644 --- a/frontend/electron/services/pythonService.ts +++ b/frontend/electron/services/pythonService.ts @@ -4,6 +4,7 @@ import { spawn } from 'child_process' import { BrowserWindow } from 'electron' import AdmZip from 'adm-zip' import { downloadFile } from './downloadService' +import { ChildProcessWithoutNullStreams } from 'node:child_process' let mainWindow: BrowserWindow | null = null @@ -470,73 +471,139 @@ export async function installPipPackage( } // 启动后端 -export async function startBackend(appRoot: string): Promise<{ success: boolean; error?: string }> { +let backendProc: ChildProcessWithoutNullStreams | null = null + +/** + * 启动后端 + * @param appRoot 项目根目录 + * @param timeoutMs 等待启动超时(默认 30 秒) + */ +export async function startBackend(appRoot: string, timeoutMs = 30_000) { try { - const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe') - const backendPath = path.join(appRoot) - const mainPyPath = path.join(backendPath, 'main.py') - - // 检查文件是否存在 - if (!fs.existsSync(pythonPath)) { - throw new Error('Python可执行文件不存在') - } - if (!fs.existsSync(mainPyPath)) { - throw new Error('后端主文件不存在') + // 如果已经在运行,直接返回 + if (backendProc && !backendProc.killed && backendProc.exitCode == null) { + console.log('[Backend] 已在运行, PID =', backendProc.pid) + return { success: true } } - console.log(`启动后端指令: "${pythonPath}" "${mainPyPath}"(cwd: ${appRoot})`) + const pythonExe = path.join(appRoot, 'environment', 'python', 'python.exe') + const mainPy = path.join(appRoot, 'main.py') - // 启动后端进程 - const backendProcess = spawn(pythonPath, [mainPyPath], { + if (!fs.existsSync(pythonExe)) { + throw new Error(`Python可执行文件不存在: ${pythonExe}`) + } + if (!fs.existsSync(mainPy)) { + throw new Error(`后端主文件不存在: ${mainPy}`) + } + + console.log(`[Backend] spawn "${pythonExe}" "${mainPy}" (cwd=${appRoot})`) + + backendProc = spawn(pythonExe, [mainPy], { cwd: appRoot, - stdio: 'pipe', - env: { - ...process.env, - PYTHONIOENCODING: 'utf-8', // 设置Python输出编码为UTF-8 - }, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, PYTHONIOENCODING: 'utf-8' }, }) - // 等待后端启动 + backendProc.stdout.setEncoding('utf8') + backendProc.stderr.setEncoding('utf8') + + backendProc.stdout.on('data', d => { + const line = d.toString().trim() + if (line) console.log('[Backend]', line) + }) + backendProc.stderr.on('data', d => { + const line = d.toString().trim() + if (line) console.log('[Backend]', line) + }) + + backendProc.once('exit', (code, signal) => { + console.log('[Backend] 退出', { code, signal }) + backendProc = null + }) + backendProc.once('error', e => { + console.error('[Backend] 进程错误:', e) + }) + + // 等待启动成功(匹配 Uvicorn 的输出) await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('后端启动超时')) - }, 30000) // 30秒超时 + let settled = false + const timer = setTimeout(() => { + if (!settled) { + settled = true + reject(new Error('后端启动超时')) + } + }, timeoutMs) - backendProcess.stdout?.on('data', data => { - const output = data.toString() - console.log('Backend output:', output) - - // 检查是否包含启动成功的标志 - if (output.includes('Uvicorn running') || output.includes('36163')) { - clearTimeout(timeout) + const checkReady = (buf: Buffer | string) => { + if (settled) return + const s = buf.toString() + if (/Uvicorn running|http:\/\/0\.0\.0\.0:\d+/.test(s)) { + settled = true + clearTimeout(timer) resolve() } - }) + } - // ✅ 重要:也要监听 stderr - backendProcess.stderr?.on('data', data => { - const output = data.toString() - console.log('Backend output:', output) // 保留原有日志 + backendProc!.stdout.on('data', checkReady) + backendProc!.stderr.on('data', checkReady) - // ✅ 在 stderr 中也检查启动标志 - if (output.includes('Uvicorn running') || output.includes('36163')) { - clearTimeout(timeout) - resolve() + backendProc!.once('exit', (code, sig) => { + if (!settled) { + settled = true + clearTimeout(timer) + reject(new Error(`后端提前退出: code=${code}, signal=${sig ?? ''}`)) } }) - - backendProcess.stderr?.on('data', data => { - console.log('Backend output:', data.toString()) - }) - - backendProcess.on('error', error => { - clearTimeout(timeout) - reject(error) + backendProc!.once('error', err => { + if (!settled) { + settled = true + clearTimeout(timer) + reject(err) + } }) }) + console.log('[Backend] 启动成功, PID =', backendProc.pid) return { success: true } - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : String(error) } + } catch (e) { + console.error('[Backend] 启动失败:', e) + return { success: false, error: e instanceof Error ? e.message : String(e) } } } + +/** 停止后端进程(如果没启动就直接返回成功) */ +export async function stopBackend() { + if (!backendProc || backendProc.killed) { + console.log('[Backend] 未运行,无需停止') + return { success: true } + } + + const pid = backendProc.pid + console.log('[Backend] 正在停止后端服务, PID =', pid) + + return new Promise<{ success: boolean; error?: string }>(resolve => { + // 清监听,避免重复日志 + backendProc?.stdout?.removeAllListeners('data') + backendProc?.stderr?.removeAllListeners('data') + + backendProc!.once('exit', (code, signal) => { + console.log('[Backend] 已退出', { code, signal }) + backendProc = null + resolve({ success: true }) + }) + + backendProc!.once('error', err => { + console.error('[Backend] 停止时出错:', err) + backendProc = null + resolve({ success: false, error: err instanceof Error ? err.message : String(err) }) + }) + + try { + backendProc!.kill() // 默认 SIGTERM,Windows 下等价于结束进程 + } catch (e) { + console.error('[Backend] kill 调用失败:', e) + backendProc = null + resolve({ success: false, error: e instanceof Error ? e.message : String(e) }) + } + }) +}