refactor: 添加后端停止功能,优化后端启动逻辑

This commit is contained in:
2025-09-02 17:17:37 +08:00
parent 17cc1fa8a3
commit f4fe4ec019
2 changed files with 129 additions and 49 deletions

View File

@@ -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()) {

View File

@@ -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<void>((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() // 默认 SIGTERMWindows 下等价于结束进程
} catch (e) {
console.error('[Backend] kill 调用失败:', e)
backendProc = null
resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
}
})
}