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' } from 'electron'
import * as path from 'path' import * as path from 'path'
import * as fs from 'fs' import * as fs from 'fs'
import { spawn } from 'child_process' import { spawn, exec } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService' 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 setDownloadMainWindow } from './services/downloadService'
import { import {
setMainWindow as setPythonMainWindow, setMainWindow as setPythonMainWindow,
@@ -385,7 +414,25 @@ function createWindow() {
updateTrayVisibility(currentConfig) updateTrayVisibility(currentConfig)
log.info('窗口已最小化到托盘,任务栏图标已隐藏') log.info('窗口已最小化到托盘,任务栏图标已隐藏')
} else { } 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) screen.removeListener('display-metrics-changed', recomputeMinSize)
// 置空模块级引用 // 置空模块级引用
mainWindow = null mainWindow = null
// 如果是正在退出,立即执行进程清理
if (isQuitting) {
log.info('窗口关闭,执行最终清理')
setTimeout(async () => {
try {
await forceKillRelatedProcesses()
} catch (e) {
log.error('最终清理失败:', e)
}
process.exit(0)
}, 100)
}
}) })
win.on('minimize', () => { win.on('minimize', () => {
@@ -441,7 +501,7 @@ function createWindow() {
// 保存窗口状态(带防抖) // 保存窗口状态(带防抖)
function saveWindowState() { function saveWindowState() {
if (!mainWindow) return if (!mainWindow || mainWindow.isDestroyed()) return
// 清除之前的定时器 // 清除之前的定时器
if (saveWindowStateTimeout) { if (saveWindowStateTimeout) {
@@ -451,9 +511,15 @@ function saveWindowState() {
// 设置新的定时器500ms后保存 // 设置新的定时器500ms后保存
saveWindowStateTimeout = setTimeout(() => { saveWindowStateTimeout = setTimeout(() => {
try { try {
// 再次检查窗口是否存在且未销毁
if (!mainWindow || mainWindow.isDestroyed()) {
log.warn('窗口已销毁,跳过保存状态')
return
}
const config = loadConfig() const config = loadConfig()
const bounds = mainWindow!.getBounds() const bounds = mainWindow.getBounds()
const isMaximized = mainWindow!.isMaximized() const isMaximized = mainWindow.isMaximized()
// 只有在窗口不是最大化状态时才保存位置和大小 // 只有在窗口不是最大化状态时才保存位置和大小
if (!isMaximized) { if (!isMaximized) {
@@ -496,10 +562,58 @@ ipcMain.handle('window-maximize', () => {
ipcMain.handle('window-close', () => { ipcMain.handle('window-close', () => {
if (mainWindow) { if (mainWindow) {
isQuitting = true
mainWindow.close() 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', () => { ipcMain.handle('window-is-maximized', () => {
return mainWindow ? mainWindow.isMaximized() : false return mainWindow ? mainWindow.isMaximized() : false
}) })
@@ -627,9 +741,10 @@ ipcMain.handle('start-backend', async () => {
return startBackend(appRoot) return startBackend(appRoot)
}) })
// ipcMain.handle('stop-backend', async () => { ipcMain.handle('stop-backend', async () => {
// return stopBackend() const { stopBackend } = await import('./services/pythonService')
// }) return stopBackend()
})
// Git相关 // Git相关
ipcMain.handle('download-git', async () => { ipcMain.handle('download-git', async () => {
@@ -1011,19 +1126,67 @@ app.on('before-quit', async event => {
log.info('应用准备退出') log.info('应用准备退出')
// 清理定时器
if (saveWindowStateTimeout) {
clearTimeout(saveWindowStateTimeout)
saveWindowStateTimeout = null
}
// 清理托盘 // 清理托盘
destroyTray() destroyTray()
// try { // 立即开始强制清理,不等待优雅关闭
// await stopBackend() log.info('开始强制清理所有相关进程')
// log.info('后端服务已停止')
// } catch (e) { try {
// log.error('停止后端时出错:', e) // 并行执行多种清理方法
// console.error('停止后端时出错:', e) const cleanupPromises = [
// } finally { // 方法1: 使用我们的进程管理器
// log.info('应用退出') forceKillRelatedProcesses(),
// app.exit(0)
// } // 方法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', () => { app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit() if (process.platform !== 'darwin') {
isQuitting = true
app.quit()
}
}) })
app.on('activate', () => { app.on('activate', () => {

View File

@@ -16,6 +16,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
windowMaximize: () => ipcRenderer.invoke('window-maximize'), windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'), windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'), 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 // 初始化相关API
checkEnvironment: () => ipcRenderer.invoke('check-environment'), checkEnvironment: () => ipcRenderer.invoke('check-environment'),

View File

@@ -574,38 +574,69 @@ export async function startBackend(appRoot: string, timeoutMs = 30_000) {
} }
/** 停止后端进程(如果没启动就直接返回成功) */ /** 停止后端进程(如果没启动就直接返回成功) */
// export async function stopBackend() { export async function stopBackend() {
// if (!backendProc || backendProc.killed) { if (!backendProc || backendProc.killed) {
// console.log('[Backend] 未运行,无需停止') console.log('[Backend] 未运行,无需停止')
// return { success: true } return { success: true }
// } }
//
// const pid = backendProc.pid const pid = backendProc.pid
// console.log('[Backend] 正在停止后端服务, PID =', pid) console.log('[Backend] 正在停止后端服务, PID =', pid)
//
// return new Promise<{ success: boolean; error?: string }>(resolve => { return new Promise<{ success: boolean; error?: string }>(resolve => {
// // 清监听,避免重复日志 // 设置超时,确保不会无限等待
// backendProc?.stdout?.removeAllListeners('data') const timeout = setTimeout(() => {
// backendProc?.stderr?.removeAllListeners('data') console.warn('[Backend] 停止超时,强制结束进程')
// try {
// backendProc!.once('exit', (code, signal) => { if (backendProc && !backendProc.killed) {
// console.log('[Backend] 已退出', { code, signal }) // 在 Windows 上使用 taskkill 强制结束进程树
// backendProc = null if (process.platform === 'win32') {
// resolve({ success: true }) const { exec } = require('child_process')
// }) exec(`taskkill /f /t /pid ${pid}`, (error: any) => {
// if (error) {
// backendProc!.once('error', err => { console.error('[Backend] taskkill 失败:', error)
// console.error('[Backend] 停止时出错:', err) } else {
// backendProc = null console.log('[Backend] 进程树已强制结束')
// resolve({ success: false, error: err instanceof Error ? err.message : String(err) }) }
// }) })
// } else {
// try { backendProc.kill('SIGKILL')
// backendProc!.kill() // 默认 SIGTERMWindows 下等价于结束进程 }
// } catch (e) { }
// console.error('[Backend] kill 调用失败:', e) } catch (e) {
// backendProc = null console.error('[Backend] 强制结束失败:', e)
// resolve({ success: false, error: e instanceof Error ? e.message : String(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", "name": "frontend",
"private": true, "private": true,
"version": "1.0.2", "version": "5.0.0-alpha.1",
"main": "dist-electron/main.js", "main": "dist-electron/main.js",
"scripts": { "scripts": {
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"", "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()"> <span v-if="updateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
检测到更新 {{ updateInfo.latest_version }} 请尽快更新 检测到更新 {{ updateInfo.latest_version }} 请尽快更新
</span> </span>
<span <span v-if="backendUpdateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
v-if="backendUpdateInfo?.if_need_update"
class="update-hint"
:title="getUpdateTooltip()"
>
检测到更新后端有更新请重启软件即可自动完成更新 检测到更新后端有更新请重启软件即可自动完成更新
</span> </span>
</span> </span>
@@ -32,11 +28,7 @@
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化"> <button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
<MinusOutlined /> <MinusOutlined />
</button> </button>
<button <button class="control-button maximize-button" @click="toggleMaximize" :title="isMaximized ? '还原' : '最大化'">
class="control-button maximize-button"
@click="toggleMaximize"
:title="isMaximized ? '还原' : '最大化'"
>
<BorderOutlined /> <BorderOutlined />
</button> </button>
<button class="control-button close-button" @click="closeWindow" title="关闭"> <button class="control-button close-button" @click="closeWindow" title="关闭">
@@ -125,20 +117,41 @@ const toggleMaximize = async () => {
const closeWindow = async () => { const closeWindow = async () => {
try { try {
// 先调用后端关闭API console.log('开始关闭应用...')
await Service.closeApiCoreClosePost()
console.log('Backend close API called successfully') // 先检查当前进程状态
// 然后关闭窗口 try {
await window.electronAPI?.windowClose() 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) { } catch (error) {
console.error('Failed to close window:', error) console.error('强制退出失败,尝试备用方法:', error)
// 即使API调用失败也尝试关闭窗口
// 备用方法:先尝试正常关闭
try { try {
await window.electronAPI?.windowClose() await window.electronAPI?.windowClose()
} catch (closeError) { setTimeout(async () => {
console.error('Failed to close window after API error:', closeError) await window.electronAPI?.appQuit()
}, 500)
} catch (backupError) {
console.error('备用方法也失败:', backupError)
} }
} }
} catch (error) {
console.error('关闭应用失败:', error)
}
} }
const pollOnce = async () => { const pollOnce = async () => {
@@ -189,7 +202,8 @@ onBeforeUnmount(() => {
user-select: none; user-select: none;
position: relative; position: relative;
z-index: 1000; z-index: 1000;
overflow: hidden; /* 新增:裁剪超出顶栏的发光 */ overflow: hidden;
/* 新增:裁剪超出顶栏的发光 */
} }
.title-bar-dark { .title-bar-dark {
@@ -208,24 +222,29 @@ onBeforeUnmount(() => {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
position: relative; /* 使阴影绝对定位基准 */ position: relative;
/* 使阴影绝对定位基准 */
} }
/* 新增:主题色虚化圆形阴影 */ /* 新增:主题色虚化圆形阴影 */
.logo-glow { .logo-glow {
position: absolute; position: absolute;
left: 55px; /* 调整:更贴近图标 */ left: 55px;
/* 调整:更贴近图标 */
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 200px; /* 缩小尺寸以适配 32px 高度 */ width: 200px;
/* 缩小尺寸以适配 32px 高度 */
height: 100px; height: 100px;
pointer-events: none; pointer-events: none;
border-radius: 50%; border-radius: 50%;
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0, 0, 0, 0) 70%); 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; opacity: 0.4;
z-index: 0; z-index: 0;
} }
.title-bar-dark .logo-glow { .title-bar-dark .logo-glow {
opacity: 0.7; opacity: 0.7;
filter: blur(24px); filter: blur(24px);
@@ -235,7 +254,8 @@ onBeforeUnmount(() => {
width: 20px; width: 20px;
height: 20px; height: 20px;
position: relative; position: relative;
z-index: 1; /* 确保在阴影上方 */ z-index: 1;
/* 确保在阴影上方 */
} }
.title-text { .title-text {
@@ -403,9 +423,11 @@ onBeforeUnmount(() => {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
} }
50% { 50% {
background-position: 100% 50%; background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; background-position: 0% 50%;
} }
@@ -416,14 +438,17 @@ onBeforeUnmount(() => {
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1); filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
transform: scale(1); transform: scale(1);
} }
33% { 33% {
filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08); filter: drop-shadow(0 0 6px rgba(255, 152, 0, 0.5)) brightness(1.08);
transform: scale(1.003); transform: scale(1.003);
} }
66% { 66% {
filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05); filter: drop-shadow(0 0 5px rgba(76, 175, 80, 0.45)) brightness(1.05);
transform: scale(1.002); transform: scale(1.002);
} }
100% { 100% {
filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1); filter: drop-shadow(0 0 4px rgba(255, 64, 129, 0.4)) brightness(1);
transform: scale(1); transform: scale(1);
@@ -435,10 +460,12 @@ onBeforeUnmount(() => {
opacity: 0.08; opacity: 0.08;
transform: scale(0.98); transform: scale(0.98);
} }
50% { 50% {
opacity: 0.04; opacity: 0.04;
transform: scale(1.02); transform: scale(1.02);
} }
100% { 100% {
opacity: 0.08; opacity: 0.08;
transform: scale(0.98); transform: scale(0.98);