From d630425e6b30bf5dcc061e8b1d8abd7e2aa38e43 Mon Sep 17 00:00:00 2001 From: HarcourtC Date: Fri, 29 May 2026 21:37:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(mumu):=20=E5=86=85=E7=BD=AE=E4=B8=80?= =?UTF-8?q?=E5=A5=97=E5=BC=BA=E5=8A=9B=E7=9A=84Kill=E6=A8=A1=E6=8B=9F?= =?UTF-8?q?=E5=99=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/config.py | 4 ++ app/models/schema.py | 3 + app/utils/emulator/mumu.py | 109 ++++++++++++++++++++++++-------- frontend/src/views/Emulator.vue | 76 +++++++++++----------- res/version.json | 3 +- 5 files changed, 129 insertions(+), 66 deletions(-) diff --git a/app/models/config.py b/app/models/config.py index 71a5a490..8a6e21c1 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -184,6 +184,10 @@ def __init__(self) -> None: self.Info_MaxWaitTime = ConfigItem( "Info", "MaxWaitTime", 300, RangeValidator(1, 9999), legacy_group="Data" ) + ## 关闭 MuMu 时强力清理残留进程 + self.Info_ForceKillOnClose = ConfigItem( + "Info", "ForceKillOnClose", True, BoolValidator() + ) super().__init__() diff --git a/app/models/schema.py b/app/models/schema.py index 2863608b..96c0b683 100644 --- a/app/models/schema.py +++ b/app/models/schema.py @@ -107,6 +107,9 @@ class EmulatorConfig_Info(BaseModel): Path: Optional[str] = Field(default=None, description="模拟器路径") BossKey: Optional[str] = Field(default=None, description="老板键快捷键配置") MaxWaitTime: Optional[int] = Field(default=None, description="最大等待时间(秒)") + ForceKillOnClose: Optional[bool] = Field( + default=None, description="关闭 MuMu 时强力清理残留进程" + ) class EmulatorConfig(BaseModel): diff --git a/app/utils/emulator/mumu.py b/app/utils/emulator/mumu.py index 03f6a1b4..a01552cb 100644 --- a/app/utils/emulator/mumu.py +++ b/app/utils/emulator/mumu.py @@ -37,6 +37,12 @@ logger = get_logger("MuMu模拟器管理") +MUMU_FORCE_KILL_KEYWORDS = ( + "mumunxdevice", + "mumunxmain", + "mumuvmmheadless", +) + class MumuManager(DeviceBase): """ @@ -117,38 +123,85 @@ async def open(self, idx: str, package_name: str = "") -> DeviceInfo: raise RuntimeError(f"模拟器 {idx} 启动超时, 当前状态码: {status}") async def close(self, idx: str) -> DeviceStatus: - status = await self.getStatus(idx) - if status not in [DeviceStatus.ONLINE, DeviceStatus.STARTING]: - logger.warning(f"设备{idx}未在线,当前状态: {status}") - return status + try: + status = await self.getStatus(idx) + if status not in [DeviceStatus.ONLINE, DeviceStatus.STARTING]: + logger.warning(f"设备{idx}未在线,当前状态: {status}") + return status + + result = await ProcessRunner.run_process( + self.emulator_path, + "control", + "-v", + idx, + "shutdown", + timeout=self.config.get("Info", "MaxWaitTime"), + if_merge_std=True, + ) + # 参考命令 MuMuManager.exe control -v 2 shutdown + + if result.returncode != 0: + raise RuntimeError(f"命令执行失败: {result.stdout}") + + t = datetime.now() + while datetime.now() - t < timedelta( + seconds=self.config.get("Info", "MaxWaitTime") + ): + status = await self.getStatus(idx) + if status == DeviceStatus.OFFLINE: + return DeviceStatus.OFFLINE + await asyncio.sleep(0.1) + + else: + if status in [DeviceStatus.ERROR, DeviceStatus.UNKNOWN]: + raise RuntimeError(f"模拟器 {idx} 关闭失败, 状态码: {status}") + raise RuntimeError(f"模拟器 {idx} 关闭超时, 当前状态码: {status}") + finally: + if self.config.get("Info", "ForceKillOnClose"): + self._force_kill_mumu_processes() + + def _force_kill_mumu_processes(self) -> None: + """按 MuMu 固定进程白名单清理关闭后的残留进程。""" + + killed_count = 0 + for proc in psutil.process_iter(["pid", "name", "exe"]): + try: + proc_name = proc.info.get("name") or "" + proc_exe = proc.info.get("exe") or "" + if not self._is_mumu_force_kill_target(proc_name, proc_exe): + continue - result = await ProcessRunner.run_process( - self.emulator_path, - "control", - "-v", - idx, - "shutdown", - timeout=self.config.get("Info", "MaxWaitTime"), - if_merge_std=True, - ) - # 参考命令 MuMuManager.exe control -v 2 shutdown + killed_count += self._kill_process_tree(proc) + except (psutil.NoSuchProcess, psutil.AccessDenied, OSError) as e: + logger.warning(f"强力清理 MuMu 残留进程失败: {e}") - if result.returncode != 0: - raise RuntimeError(f"命令执行失败: {result.stdout}") + if killed_count > 0: + logger.info(f"MuMu 残留进程清理完成,共结束 {killed_count} 个进程") + else: + logger.info("未发现需要强力清理的 MuMu 残留进程") - t = datetime.now() - while datetime.now() - t < timedelta( - seconds=self.config.get("Info", "MaxWaitTime") - ): - status = await self.getStatus(idx) - if status == DeviceStatus.OFFLINE: - return DeviceStatus.OFFLINE - await asyncio.sleep(0.1) + def _kill_process_tree(self, proc: psutil.Process) -> int: + killed_count = 0 + for child in proc.children(recursive=True): + killed_count += self._kill_process(child) + killed_count += self._kill_process(proc) + return killed_count - else: - if status in [DeviceStatus.ERROR, DeviceStatus.UNKNOWN]: - raise RuntimeError(f"模拟器 {idx} 关闭失败, 状态码: {status}") - raise RuntimeError(f"模拟器 {idx} 关闭超时, 当前状态码: {status}") + @staticmethod + def _kill_process(proc: psutil.Process) -> int: + try: + proc.kill() + return 1 + except psutil.NoSuchProcess: + return 0 + except (psutil.AccessDenied, OSError) as e: + logger.warning(f"强力清理 MuMu 残留进程失败: {e}") + return 0 + + @staticmethod + def _is_mumu_force_kill_target(proc_name: str, proc_exe: str) -> bool: + target_text = f"{proc_name} {proc_exe}".lower() + return any(keyword in target_text for keyword in MUMU_FORCE_KILL_KEYWORDS) async def getStatus(self, idx: str, data: str | None = None) -> DeviceStatus: if data is None: diff --git a/frontend/src/views/Emulator.vue b/frontend/src/views/Emulator.vue index 63385515..5807381a 100644 --- a/frontend/src/views/Emulator.vue +++ b/frontend/src/views/Emulator.vue @@ -26,6 +26,7 @@ interface EmulatorInfo { path: string max_wait_time: number boss_keys: string[] + force_kill_on_close: boolean } // 安全的 JSON 解析函数 @@ -187,17 +188,20 @@ const canStopDevice = (status: number) => { return status === DeviceStatus.ONLINE || status === DeviceStatus.STARTING } +const buildEditingData = (configData: any): EmulatorInfo => ({ + name: configData?.Info?.Name || '', + type: configData?.Info?.Type || '', + path: configData?.Info?.Path || '', + max_wait_time: configData?.Info?.MaxWaitTime || 300, + boss_keys: safeJsonParse(configData?.Info?.BossKey, []), + force_kill_on_close: configData?.Info?.ForceKillOnClose !== false, +}) + // 获取当前模拟器的编辑数据 const getEditingData = (uuid: string): EmulatorInfo => { if (!editingDataMap.value.has(uuid)) { const configData = emulatorData.value[uuid] - editingDataMap.value.set(uuid, { - name: configData?.Info?.Name || '', - type: configData?.Info?.Type || '', - path: configData?.Info?.Path || '', - max_wait_time: configData?.Info?.MaxWaitTime || 300, - boss_keys: safeJsonParse(configData?.Info?.BossKey, []), - }) + editingDataMap.value.set(uuid, buildEditingData(configData)) } return editingDataMap.value.get(uuid)! } @@ -224,17 +228,11 @@ const loadEmulators = async () => { // 初始化所有模拟器的编辑数据 emulatorIndex.value.forEach(item => { const configData = emulatorData.value[item.uid] - const bossKeys = safeJsonParse(configData?.Info?.BossKey, []) - editingDataMap.value.set(item.uid, { - name: configData?.Info?.Name || '', - type: configData?.Info?.Type || '', - path: configData?.Info?.Path || '', - max_wait_time: configData?.Info?.MaxWaitTime || 300, - boss_keys: bossKeys, - }) + const editData = buildEditingData(configData) + editingDataMap.value.set(item.uid, editData) // 同步 boss_keys 到输入框显示 - if (bossKeys.length > 0) { - bossKeyInputMap.value[item.uid] = bossKeys[0] + if (editData.boss_keys.length > 0) { + bossKeyInputMap.value[item.uid] = editData.boss_keys[0] } }) } else { @@ -293,17 +291,11 @@ const refreshEmulatorConfig = async (uuid?: string) => { // 更新编辑数据 const configData = updatedData[uuid] - const bossKeys = safeJsonParse(configData?.Info?.BossKey, []) - editingDataMap.value.set(uuid, { - name: configData?.Info?.Name || '', - type: configData?.Info?.Type || '', - path: configData?.Info?.Path || '', - max_wait_time: configData?.Info?.MaxWaitTime || 300, - boss_keys: bossKeys, - }) + const editData = buildEditingData(configData) + editingDataMap.value.set(uuid, editData) // 同步 boss_keys 到输入框显示 - if (bossKeys.length > 0) { - bossKeyInputMap.value[uuid] = bossKeys[0] + if (editData.boss_keys.length > 0) { + bossKeyInputMap.value[uuid] = editData.boss_keys[0] } } } else { @@ -314,17 +306,11 @@ const refreshEmulatorConfig = async (uuid?: string) => { // 更新编辑数据 emulatorIndex.value.forEach(item => { const configData = emulatorData.value[item.uid] - const bossKeys = safeJsonParse(configData?.Info?.BossKey, []) - editingDataMap.value.set(item.uid, { - name: configData?.Info?.Name || '', - type: configData?.Info?.Type || '', - path: configData?.Info?.Path || '', - max_wait_time: configData?.Info?.MaxWaitTime || 300, - boss_keys: bossKeys, - }) + const editData = buildEditingData(configData) + editingDataMap.value.set(item.uid, editData) // 同步 boss_keys 到输入框显示 - if (bossKeys.length > 0) { - bossKeyInputMap.value[item.uid] = bossKeys[0] + if (editData.boss_keys.length > 0) { + bossKeyInputMap.value[item.uid] = editData.boss_keys[0] } }) } @@ -355,6 +341,8 @@ const handleSaveChange = async (uuid: string, key: string, value: any) => { configData = { Info: { MaxWaitTime: value } } } else if (key === 'boss_keys') { configData = { Info: { BossKey: JSON.stringify(value) } } + } else if (key === 'force_kill_on_close') { + configData = { Info: { ForceKillOnClose: value } } } const response = await Service.updateEmulatorApiEmulatorUpdatePost({ @@ -946,6 +934,20 @@ const handleBossKeyInputChange = (uuid: string) => { MuMu模拟器无需配置老板键 + + + + diff --git a/res/version.json b/res/version.json index 30abb704..1ffd4026 100644 --- a/res/version.json +++ b/res/version.json @@ -10,7 +10,8 @@ "okww专项 优化用户配置页面的样式", "okww专项 调度台展示游戏配置与启动流程日志", "okww专项 重构配置优化", - "MAAEND专项 配置重构,优化样式" + "MAAEND专项 配置重构,优化样式", + "模拟器管理 添加强力清理MuMu功能(默认开启)" ], "修复BUG": [ "修复静默模式未对M9A本体生效的问题", From 07dc7602ba0d256a0f04792ec89d2f7940d5b75e Mon Sep 17 00:00:00 2001 From: HarcourtC Date: Fri, 29 May 2026 23:23:33 +0800 Subject: [PATCH 2/2] chore(openapi): Update OpenAPI --- frontend/src/api/models/EmulatorConfig_Info.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/api/models/EmulatorConfig_Info.ts b/frontend/src/api/models/EmulatorConfig_Info.ts index 80b8397a..ee7347a1 100644 --- a/frontend/src/api/models/EmulatorConfig_Info.ts +++ b/frontend/src/api/models/EmulatorConfig_Info.ts @@ -23,5 +23,9 @@ export type EmulatorConfig_Info = { * 最大等待时间(秒) */ MaxWaitTime?: (number | null); + /** + * 关闭 MuMu 时强力清理残留进程 + */ + ForceKillOnClose?: (boolean | null); };