diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index fa0a30d..edd8709 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -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 { + 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((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', () => { diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index c7331ed..1497313 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -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'), diff --git a/frontend/electron/services/pythonService.ts b/frontend/electron/services/pythonService.ts index 9d67d07..d44ebac 100644 --- a/frontend/electron/services/pythonService.ts +++ b/frontend/electron/services/pythonService.ts @@ -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() // 默认 SIGTERM,Windows 下等价于结束进程 -// } 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) }) + } + }) +} diff --git a/frontend/electron/utils/processManager.ts b/frontend/electron/utils/processManager.ts new file mode 100644 index 0000000..22d7fd7 --- /dev/null +++ b/frontend/electron/utils/processManager.ts @@ -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 { + 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 { + 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 { + 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 { + 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() + }) +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 76eb09e..b0302d6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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\"", diff --git a/frontend/src/components/TitleBar.vue b/frontend/src/components/TitleBar.vue index 8f1c33d..e0e98cc 100644 --- a/frontend/src/components/TitleBar.vue +++ b/frontend/src/components/TitleBar.vue @@ -12,11 +12,7 @@ 检测到更新 {{ updateInfo.latest_version }} 请尽快更新 - + 检测到更新后端有更新。请重启软件即可自动完成更新 @@ -32,11 +28,7 @@ -