refactor: 添加后端停止功能,优化后端启动逻辑
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
installPipPackage,
|
installPipPackage,
|
||||||
installDependencies,
|
installDependencies,
|
||||||
startBackend,
|
startBackend,
|
||||||
|
stopBackend,
|
||||||
} from './services/pythonService'
|
} from './services/pythonService'
|
||||||
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
// 检查管理员权限
|
// 检查管理员权限
|
||||||
if (!isRunningAsAdmin()) {
|
if (!isRunningAsAdmin()) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { spawn } from 'child_process'
|
|||||||
import { BrowserWindow } from 'electron'
|
import { BrowserWindow } from 'electron'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { downloadFile } from './downloadService'
|
import { downloadFile } from './downloadService'
|
||||||
|
import { ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
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 {
|
try {
|
||||||
const pythonPath = path.join(appRoot, 'environment', 'python', 'python.exe')
|
// 如果已经在运行,直接返回
|
||||||
const backendPath = path.join(appRoot)
|
if (backendProc && !backendProc.killed && backendProc.exitCode == null) {
|
||||||
const mainPyPath = path.join(backendPath, 'main.py')
|
console.log('[Backend] 已在运行, PID =', backendProc.pid)
|
||||||
|
return { success: true }
|
||||||
// 检查文件是否存在
|
|
||||||
if (!fs.existsSync(pythonPath)) {
|
|
||||||
throw new Error('Python可执行文件不存在')
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(mainPyPath)) {
|
|
||||||
throw new Error('后端主文件不存在')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`启动后端指令: "${pythonPath}" "${mainPyPath}"(cwd: ${appRoot})`)
|
const pythonExe = path.join(appRoot, 'environment', 'python', 'python.exe')
|
||||||
|
const mainPy = path.join(appRoot, 'main.py')
|
||||||
|
|
||||||
// 启动后端进程
|
if (!fs.existsSync(pythonExe)) {
|
||||||
const backendProcess = spawn(pythonPath, [mainPyPath], {
|
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,
|
cwd: appRoot,
|
||||||
stdio: 'pipe',
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
env: {
|
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
||||||
...process.env,
|
|
||||||
PYTHONIOENCODING: 'utf-8', // 设置Python输出编码为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) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
let settled = false
|
||||||
reject(new Error('后端启动超时'))
|
const timer = setTimeout(() => {
|
||||||
}, 30000) // 30秒超时
|
if (!settled) {
|
||||||
|
settled = true
|
||||||
|
reject(new Error('后端启动超时'))
|
||||||
|
}
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
backendProcess.stdout?.on('data', data => {
|
const checkReady = (buf: Buffer | string) => {
|
||||||
const output = data.toString()
|
if (settled) return
|
||||||
console.log('Backend output:', output)
|
const s = buf.toString()
|
||||||
|
if (/Uvicorn running|http:\/\/0\.0\.0\.0:\d+/.test(s)) {
|
||||||
// 检查是否包含启动成功的标志
|
settled = true
|
||||||
if (output.includes('Uvicorn running') || output.includes('36163')) {
|
clearTimeout(timer)
|
||||||
clearTimeout(timeout)
|
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// ✅ 重要:也要监听 stderr
|
backendProc!.stdout.on('data', checkReady)
|
||||||
backendProcess.stderr?.on('data', data => {
|
backendProc!.stderr.on('data', checkReady)
|
||||||
const output = data.toString()
|
|
||||||
console.log('Backend output:', output) // 保留原有日志
|
|
||||||
|
|
||||||
// ✅ 在 stderr 中也检查启动标志
|
backendProc!.once('exit', (code, sig) => {
|
||||||
if (output.includes('Uvicorn running') || output.includes('36163')) {
|
if (!settled) {
|
||||||
clearTimeout(timeout)
|
settled = true
|
||||||
resolve()
|
clearTimeout(timer)
|
||||||
|
reject(new Error(`后端提前退出: code=${code}, signal=${sig ?? ''}`))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
backendProc!.once('error', err => {
|
||||||
backendProcess.stderr?.on('data', data => {
|
if (!settled) {
|
||||||
console.log('Backend output:', data.toString())
|
settled = true
|
||||||
})
|
clearTimeout(timer)
|
||||||
|
reject(err)
|
||||||
backendProcess.on('error', error => {
|
}
|
||||||
clearTimeout(timeout)
|
|
||||||
reject(error)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('[Backend] 启动成功, PID =', backendProc.pid)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
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) })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user