refactor: 添加进程管理功能,支持强制清理相关进程和获取进程信息

This commit is contained in:
2025-09-24 23:33:45 +08:00
parent 34df37c040
commit af28869d2a
6 changed files with 438 additions and 83 deletions

View File

@@ -11,8 +11,37 @@ import {
} from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
import { spawn, exec } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService'
// 强制清理相关进程的函数
async function forceKillRelatedProcesses(): Promise<void> {
try {
const { killAllRelatedProcesses } = await import('./utils/processManager')
await killAllRelatedProcesses()
log.info('所有相关进程已清理')
} catch (error) {
log.error('清理进程时出错:', error)
// 备用清理方法
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
return new Promise((resolve) => {
// 使用更简单的命令强制结束相关进程
exec(`taskkill /f /im python.exe`, (error) => {
if (error) {
log.warn('备用清理方法失败:', error.message)
} else {
log.info('备用清理方法执行成功')
}
resolve()
})
})
}
}
}
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import {
setMainWindow as setPythonMainWindow,
@@ -385,7 +414,25 @@ function createWindow() {
updateTrayVisibility(currentConfig)
log.info('窗口已最小化到托盘,任务栏图标已隐藏')
} else {
saveWindowState()
// 立即保存窗口状态,不使用防抖
if (!win.isDestroyed()) {
try {
const config = loadConfig()
const bounds = win.getBounds()
const isMaximized = win.isMaximized()
if (!isMaximized) {
config.UI.size = `${bounds.width},${bounds.height}`
config.UI.location = `${bounds.x},${bounds.y}`
}
config.UI.maximized = isMaximized
saveConfig(config)
log.info('窗口状态已保存')
} catch (error) {
log.error('保存窗口状态失败:', error)
}
}
}
})
@@ -395,6 +442,19 @@ function createWindow() {
screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用
mainWindow = null
// 如果是正在退出,立即执行进程清理
if (isQuitting) {
log.info('窗口关闭,执行最终清理')
setTimeout(async () => {
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('最终清理失败:', e)
}
process.exit(0)
}, 100)
}
})
win.on('minimize', () => {
@@ -441,7 +501,7 @@ function createWindow() {
// 保存窗口状态(带防抖)
function saveWindowState() {
if (!mainWindow) return
if (!mainWindow || mainWindow.isDestroyed()) return
// 清除之前的定时器
if (saveWindowStateTimeout) {
@@ -451,9 +511,15 @@ function saveWindowState() {
// 设置新的定时器500ms后保存
saveWindowStateTimeout = setTimeout(() => {
try {
// 再次检查窗口是否存在且未销毁
if (!mainWindow || mainWindow.isDestroyed()) {
log.warn('窗口已销毁,跳过保存状态')
return
}
const config = loadConfig()
const bounds = mainWindow!.getBounds()
const isMaximized = mainWindow!.isMaximized()
const bounds = mainWindow.getBounds()
const isMaximized = mainWindow.isMaximized()
// 只有在窗口不是最大化状态时才保存位置和大小
if (!isMaximized) {
@@ -496,10 +562,58 @@ ipcMain.handle('window-maximize', () => {
ipcMain.handle('window-close', () => {
if (mainWindow) {
isQuitting = true
mainWindow.close()
}
})
// 添加强制退出处理器
ipcMain.handle('app-quit', () => {
isQuitting = true
app.quit()
})
// 添加进程管理相关的 IPC 处理器
ipcMain.handle('get-related-processes', async () => {
try {
const { getRelatedProcesses } = await import('./utils/processManager')
return await getRelatedProcesses()
} catch (error) {
log.error('获取进程信息失败:', error)
return []
}
})
ipcMain.handle('kill-all-processes', async () => {
try {
await forceKillRelatedProcesses()
return { success: true }
} catch (error) {
log.error('强制清理进程失败:', error)
return { success: false, error: error instanceof Error ? error.message : String(error) }
}
})
// 添加一个测试用的强制退出命令
ipcMain.handle('force-exit', async () => {
log.info('收到强制退出命令')
isQuitting = true
// 立即清理进程
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('强制清理失败:', e)
}
// 强制退出
setTimeout(() => {
process.exit(0)
}, 500)
return { success: true }
})
ipcMain.handle('window-is-maximized', () => {
return mainWindow ? mainWindow.isMaximized() : false
})
@@ -627,9 +741,10 @@ ipcMain.handle('start-backend', async () => {
return startBackend(appRoot)
})
// ipcMain.handle('stop-backend', async () => {
// return stopBackend()
// })
ipcMain.handle('stop-backend', async () => {
const { stopBackend } = await import('./services/pythonService')
return stopBackend()
})
// Git相关
ipcMain.handle('download-git', async () => {
@@ -1011,19 +1126,67 @@ app.on('before-quit', async event => {
log.info('应用准备退出')
// 清理定时器
if (saveWindowStateTimeout) {
clearTimeout(saveWindowStateTimeout)
saveWindowStateTimeout = null
}
// 清理托盘
destroyTray()
// try {
// await stopBackend()
// log.info('后端服务已停止')
// } catch (e) {
// log.error('停止后端时出错:', e)
// console.error('停止后端时出错:', e)
// } finally {
// log.info('应用退出')
// app.exit(0)
// }
// 立即开始强制清理,不等待优雅关闭
log.info('开始强制清理所有相关进程')
try {
// 并行执行多种清理方法
const cleanupPromises = [
// 方法1: 使用我们的进程管理器
forceKillRelatedProcesses(),
// 方法2: 直接使用 taskkill 命令
new Promise<void>((resolve) => {
if (process.platform === 'win32') {
const appRoot = getAppRoot()
const commands = [
`taskkill /f /im python.exe`,
`wmic process where "CommandLine like '%main.py%'" delete`,
`wmic process where "CommandLine like '%${appRoot.replace(/\\/g, '\\\\')}%'" delete`
]
let completed = 0
commands.forEach(cmd => {
exec(cmd, () => {
completed++
if (completed === commands.length) {
resolve()
}
})
})
// 2秒超时
setTimeout(resolve, 2000)
} else {
resolve()
}
})
]
// 最多等待3秒
const timeoutPromise = new Promise(resolve => setTimeout(resolve, 3000))
await Promise.race([Promise.all(cleanupPromises), timeoutPromise])
log.info('进程清理完成')
} catch (e) {
log.error('进程清理时出错:', e)
}
log.info('应用强制退出')
// 使用 process.exit 而不是 app.exit更加强制
setTimeout(() => {
process.exit(0)
}, 500)
}
})
@@ -1054,7 +1217,10 @@ app.whenReady().then(() => {
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
if (process.platform !== 'darwin') {
isQuitting = true
app.quit()
}
})
app.on('activate', () => {

View File

@@ -16,6 +16,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
appQuit: () => ipcRenderer.invoke('app-quit'),
// 进程管理
getRelatedProcesses: () => ipcRenderer.invoke('get-related-processes'),
killAllProcesses: () => ipcRenderer.invoke('kill-all-processes'),
forceExit: () => ipcRenderer.invoke('force-exit'),
// 初始化相关API
checkEnvironment: () => ipcRenderer.invoke('check-environment'),

View File

@@ -574,38 +574,69 @@ export async function startBackend(appRoot: string, timeoutMs = 30_000) {
}
/** 停止后端进程(如果没启动就直接返回成功) */
// 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) })
// }
// })
// }
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 => {
// 设置超时,确保不会无限等待
const timeout = setTimeout(() => {
console.warn('[Backend] 停止超时,强制结束进程')
try {
if (backendProc && !backendProc.killed) {
// 在 Windows 上使用 taskkill 强制结束进程树
if (process.platform === 'win32') {
const { exec } = require('child_process')
exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
if (error) {
console.error('[Backend] taskkill 失败:', error)
} else {
console.log('[Backend] 进程树已强制结束')
}
})
} else {
backendProc.kill('SIGKILL')
}
}
} catch (e) {
console.error('[Backend] 强制结束失败:', e)
}
backendProc = null
resolve({ success: true })
}, 2000) // 2秒超时
// 清监听,避免重复日志
backendProc?.stdout?.removeAllListeners('data')
backendProc?.stderr?.removeAllListeners('data')
backendProc!.once('exit', (code, signal) => {
clearTimeout(timeout)
console.log('[Backend] 已退出', { code, signal })
backendProc = null
resolve({ success: true })
})
backendProc!.once('error', err => {
clearTimeout(timeout)
console.error('[Backend] 停止时出错:', err)
backendProc = null
resolve({ success: false, error: err instanceof Error ? err.message : String(err) })
})
try {
// 首先尝试优雅关闭
backendProc!.kill('SIGTERM')
console.log('[Backend] 已发送 SIGTERM 信号')
} catch (e) {
clearTimeout(timeout)
console.error('[Backend] kill 调用失败:', e)
backendProc = null
resolve({ success: false, error: e instanceof Error ? e.message : String(e) })
}
})
}

View File

@@ -0,0 +1,125 @@
import { exec } from 'child_process'
import * as path from 'path'
import { getAppRoot } from '../services/environmentService'
export interface ProcessInfo {
pid: number
name: string
commandLine: string
}
/**
* 获取所有相关的进程信息
*/
export async function getRelatedProcesses(): Promise<ProcessInfo[]> {
return new Promise((resolve) => {
if (process.platform !== 'win32') {
resolve([])
return
}
const appRoot = getAppRoot()
const pythonExePath = path.join(appRoot, 'environment', 'python', 'python.exe')
// 使用 wmic 获取详细的进程信息
const cmd = `wmic process where "Name='python.exe' or Name='AUTO-MAS.exe' or CommandLine like '%main.py%'" get ProcessId,Name,CommandLine /format:csv`
exec(cmd, (error, stdout, stderr) => {
if (error) {
console.error('获取进程信息失败:', error)
resolve([])
return
}
const processes: ProcessInfo[] = []
const lines = stdout.split('\n').filter(line => line.trim() && !line.startsWith('Node'))
for (const line of lines) {
const parts = line.split(',')
if (parts.length >= 4) {
const commandLine = parts[1] || ''
const name = parts[2] || ''
const pid = parseInt(parts[3]) || 0
if (pid > 0 && (
commandLine.includes(pythonExePath) ||
commandLine.includes('main.py') ||
name === 'AUTO-MAS.exe'
)) {
processes.push({ pid, name, commandLine })
}
}
}
resolve(processes)
})
})
}
/**
* 强制结束指定的进程
*/
export async function killProcess(pid: number): Promise<boolean> {
return new Promise((resolve) => {
if (process.platform !== 'win32') {
resolve(false)
return
}
exec(`taskkill /f /t /pid ${pid}`, (error) => {
if (error) {
console.error(`结束进程 ${pid} 失败:`, error.message)
resolve(false)
} else {
console.log(`进程 ${pid} 已结束`)
resolve(true)
}
})
})
}
/**
* 强制结束所有相关进程
*/
export async function killAllRelatedProcesses(): Promise<void> {
console.log('开始清理所有相关进程...')
const processes = await getRelatedProcesses()
console.log(`找到 ${processes.length} 个相关进程:`)
for (const proc of processes) {
console.log(`- PID: ${proc.pid}, Name: ${proc.name}, CMD: ${proc.commandLine.substring(0, 100)}...`)
}
// 并行结束所有进程
const killPromises = processes.map(proc => killProcess(proc.pid))
await Promise.all(killPromises)
console.log('进程清理完成')
}
/**
* 等待进程结束
*/
export async function waitForProcessExit(pid: number, timeoutMs: number = 5000): Promise<boolean> {
return new Promise((resolve) => {
const startTime = Date.now()
const checkProcess = () => {
if (Date.now() - startTime > timeoutMs) {
resolve(false)
return
}
exec(`tasklist /fi "PID eq ${pid}"`, (error, stdout) => {
if (error || !stdout.includes(pid.toString())) {
resolve(true)
} else {
setTimeout(checkProcess, 100)
}
})
}
checkProcess()
})
}

View File

@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "1.0.2",
"version": "5.0.0-alpha.1",
"main": "dist-electron/main.js",
"scripts": {
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",

View File

@@ -12,11 +12,7 @@
<span v-if="updateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
检测到更新 {{ updateInfo.latest_version }} 请尽快更新
</span>
<span
v-if="backendUpdateInfo?.if_need_update"
class="update-hint"
:title="getUpdateTooltip()"
>
<span v-if="backendUpdateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
检测到更新后端有更新请重启软件即可自动完成更新
</span>
</span>
@@ -32,11 +28,7 @@
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
<MinusOutlined />
</button>
<button
class="control-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? '还原' : '最大化'"
>
<button class="control-button maximize-button" @click="toggleMaximize" :title="isMaximized ? '还原' : '最大化'">
<BorderOutlined />
</button>
<button class="control-button close-button" @click="closeWindow" title="关闭">
@@ -125,19 +117,40 @@ const toggleMaximize = async () => {
const closeWindow = async () => {
try {
// 先调用后端关闭API
await Service.closeApiCoreClosePost()
console.log('Backend close API called successfully')
// 然后关闭窗口
await window.electronAPI?.windowClose()
} catch (error) {
console.error('Failed to close window:', error)
// 即使API调用失败也尝试关闭窗口
console.log('开始关闭应用...')
// 先检查当前进程状态
try {
await window.electronAPI?.windowClose()
} catch (closeError) {
console.error('Failed to close window after API error:', closeError)
const processes = await window.electronAPI?.getRelatedProcesses()
console.log('关闭前的进程状态:', processes)
} catch (e) {
console.warn('无法获取进程状态:', e)
}
// 异步调用后端关闭API不等待响应
Service.closeApiCoreClosePost().catch(error => {
console.warn('Backend close API failed (this is expected):', error)
})
// 使用更激进的强制退出方法
try {
console.log('执行强制退出...')
await window.electronAPI?.forceExit()
} catch (error) {
console.error('强制退出失败,尝试备用方法:', error)
// 备用方法:先尝试正常关闭
try {
await window.electronAPI?.windowClose()
setTimeout(async () => {
await window.electronAPI?.appQuit()
}, 500)
} catch (backupError) {
console.error('备用方法也失败:', backupError)
}
}
} catch (error) {
console.error('关闭应用失败:', error)
}
}
@@ -189,7 +202,8 @@ onBeforeUnmount(() => {
user-select: none;
position: relative;
z-index: 1000;
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */
overflow: hidden;
/* 新增:裁剪超出顶栏的发光 */
}
.title-bar-dark {
@@ -208,24 +222,29 @@ onBeforeUnmount(() => {
display: flex;
align-items: center;
gap: 8px;
position: relative; /* 使阴影绝对定位基准 */
position: relative;
/* 使阴影绝对定位基准 */
}
/* 新增:主题色虚化圆形阴影 */
.logo-glow {
position: absolute;
left: 55px; /* 调整:更贴近图标 */
left: 55px;
/* 调整:更贴近图标 */
top: 50%;
transform: translate(-50%, -50%);
width: 200px; /* 缩小尺寸以适配 32px 高度 */
width: 200px;
/* 缩小尺寸以适配 32px 高度 */
height: 100px;
pointer-events: none;
border-radius: 50%;
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0, 0, 0, 0) 70%);
filter: blur(24px); /* 降低模糊避免越界过多 */
filter: blur(24px);
/* 降低模糊避免越界过多 */
opacity: 0.4;
z-index: 0;
}
.title-bar-dark .logo-glow {
opacity: 0.7;
filter: blur(24px);
@@ -235,7 +254,8 @@ onBeforeUnmount(() => {
width: 20px;
height: 20px;
position: relative;
z-index: 1; /* 确保在阴影上方 */
z-index: 1;
/* 确保在阴影上方 */
}
.title-text {
@@ -382,7 +402,7 @@ onBeforeUnmount(() => {
}
/* 为相邻的更新提示添加间距 */
.update-hint + .update-hint {
.update-hint+.update-hint {
margin-left: 12px;
}
@@ -403,9 +423,11 @@ onBeforeUnmount(() => {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
@@ -416,14 +438,17 @@ onBeforeUnmount(() => {
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
transform: scale(1);
}
33% {
filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08);
transform: scale(1.003);
}
66% {
filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05);
transform: scale(1.002);
}
100% {
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
transform: scale(1);
@@ -435,10 +460,12 @@ onBeforeUnmount(() => {
opacity: 0.08;
transform: scale(0.98);
}
50% {
opacity: 0.04;
transform: scale(1.02);
}
100% {
opacity: 0.08;
transform: scale(0.98);