# AUTO-MAS: A Multi-Script, Multi-Config Management and Automation Software # Copyright © 2024-2025 DLmaster361 # This file is part of AUTO-MAS. # AUTO-MAS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published # by the Free Software Foundation, either version 3 of the License, # or (at your option) any later version. # AUTO-MAS is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty # of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See # the GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with AUTO-MAS. If not, see . # Contact: DLmaster_361@163.com import asyncio import psutil import subprocess from datetime import datetime, timedelta from pathlib import Path from typing import Dict, Any from app.utils.device_manager.utils import BaseDevice, DeviceStatus from app.utils.logger import get_logger class ProcessManager: """进程监视器类, 用于跟踪主进程及其所有子进程的状态""" def __init__(self): super().__init__() self.main_pid = None self.tracked_pids = set() self.check_task = None self.track_end_time = datetime.now() async def open_process( self, path: Path, args: list = [], tracking_time: int = 60 ) -> None: """ 启动一个新进程并返回其pid, 并开始监视该进程 Parameters ---------- path: 可执行文件的路径 args: 启动参数列表 tracking_time: 子进程追踪持续时间(秒) """ process = subprocess.Popen( [path, *args], cwd=path.parent, creationflags=subprocess.CREATE_NO_WINDOW, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) await self.start_monitoring(process.pid, tracking_time) async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None: """ 启动进程监视器, 跟踪指定的主进程及其子进程 :param pid: 被监视进程的PID :param tracking_time: 子进程追踪持续时间(秒) """ await self.clear() self.main_pid = pid self.tracking_time = tracking_time # 扫描并记录所有相关进程 try: # 获取主进程 main_proc = psutil.Process(self.main_pid) self.tracked_pids.add(self.main_pid) # 递归获取所有子进程 if tracking_time: for child in main_proc.children(recursive=True): self.tracked_pids.add(child.pid) except psutil.NoSuchProcess: pass # 启动持续追踪任务 if tracking_time > 0: self.track_end_time = datetime.now() + timedelta(seconds=tracking_time) self.check_task = asyncio.create_task(self.track_processes()) async def track_processes(self) -> None: """更新子进程列表""" while datetime.now() < self.track_end_time: current_pids = set(self.tracked_pids) for pid in current_pids: try: proc = psutil.Process(pid) for child in proc.children(): if child.pid not in self.tracked_pids: # 新发现的子进程 self.tracked_pids.add(child.pid) except psutil.NoSuchProcess: continue await asyncio.sleep(0.1) async def is_running(self) -> bool: """检查所有跟踪的进程是否还在运行""" for pid in self.tracked_pids: try: proc = psutil.Process(pid) if proc.is_running(): return True except psutil.NoSuchProcess: continue return False async def kill(self, if_force: bool = False) -> None: """停止监视器并中止所有跟踪的进程""" for pid in self.tracked_pids: try: proc = psutil.Process(pid) if if_force: kill_process = subprocess.Popen( ["taskkill", "/F", "/T", "/PID", str(pid)], creationflags=subprocess.CREATE_NO_WINDOW, ) kill_process.wait() proc.terminate() except psutil.NoSuchProcess: continue await self.clear() async def clear(self) -> None: """清空跟踪的进程列表""" if self.check_task is not None and not self.check_task.done(): self.check_task.cancel() try: await self.check_task except asyncio.CancelledError: pass self.main_pid = None self.tracked_pids.clear() class GeneralDeviceManager(BaseDevice): """ 通用设备管理器,基于BaseDevice和ProcessManager实现 用于管理一般应用程序进程 """ def __init__(self, executable_path: str, name: str = "通用设备"): """ 初始化通用设备管理器 Args: executable_path (str): 可执行文件的绝对路径 name (str): 设备管理器名称 """ self.executable_path = Path(executable_path) self.name = name self.logger = get_logger(f"{name}管理器") # 进程管理实例字典,以idx为键 self.process_managers: Dict[str, ProcessManager] = {} # 设备信息存储 self.device_info: Dict[str, Dict[str, Any]] = {} # 默认等待时间 self.wait_time = 60 if not self.executable_path.exists(): raise FileNotFoundError(f"可执行文件不存在: {executable_path}") async def start(self, idx: str, package_name: str = "") -> tuple[bool, int, dict]: """ 启动设备 Args: idx: 设备ID package_name: 包名(可选) Returns: tuple[bool, int, dict]: (是否成功, 状态码, 启动信息) """ try: # 检查是否已经在运行 current_status = await self.get_status(idx) if current_status in [DeviceStatus.ONLINE, DeviceStatus.STARTING]: self.logger.warning(f"设备{idx}已经在运行,状态: {current_status}") return False, current_status, {} # 创建进程管理器 if idx not in self.process_managers: self.process_managers[idx] = ProcessManager() # 准备启动参数 args = [] if package_name: args.extend(["-pkg", package_name]) # 启动进程 await self.process_managers[idx].open_process( self.executable_path, args, tracking_time=self.wait_time ) # 等待进程启动 start_time = datetime.now() timeout = timedelta(seconds=self.wait_time) while datetime.now() - start_time < timeout: if await self.process_managers[idx].is_running(): self.device_info[idx] = { "title": f"{self.name}_{idx}", "status": str(DeviceStatus.ONLINE), "pid": self.process_managers[idx].main_pid, "start_time": start_time.isoformat(), } self.logger.info(f"设备{idx}启动成功") return True, DeviceStatus.ONLINE, self.device_info[idx] await asyncio.sleep(0.1) self.logger.error(f"设备{idx}启动超时") return False, DeviceStatus.ERROR, {} except Exception as e: self.logger.error(f"启动设备{idx}失败: {str(e)}") return False, DeviceStatus.ERROR, {} async def close(self, idx: str) -> tuple[bool, int]: """ 关闭设备或服务 Args: idx: 设备ID Returns: tuple[bool, int]: (是否成功, 状态码) """ try: if idx not in self.process_managers: self.logger.warning(f"设备{idx}的进程管理器不存在") return False, DeviceStatus.NOT_FOUND # 检查进程是否在运行 if not await self.process_managers[idx].is_running(): self.logger.info(f"设备{idx}进程已经停止") return True, DeviceStatus.OFFLINE # 终止进程 await self.process_managers[idx].kill(if_force=False) # 等待进程完全停止 stop_time = datetime.now() timeout = timedelta(seconds=10) # 10秒超时 while datetime.now() - stop_time < timeout: if not await self.process_managers[idx].is_running(): # 清理设备信息 if idx in self.device_info: del self.device_info[idx] self.logger.info(f"设备{idx}已成功关闭") return True, DeviceStatus.OFFLINE await asyncio.sleep(0.1) # 强制终止 self.logger.warning(f"设备{idx}未能正常关闭,尝试强制终止") await self.process_managers[idx].kill(if_force=True) if idx in self.device_info: del self.device_info[idx] return True, DeviceStatus.OFFLINE except Exception as e: self.logger.error(f"关闭设备{idx}失败: {str(e)}") return False, DeviceStatus.ERROR async def get_status(self, idx: str) -> int: """ 获取指定设备当前状态 Args: idx: 设备ID Returns: int: 状态码 """ try: if idx not in self.process_managers: return DeviceStatus.OFFLINE if await self.process_managers[idx].is_running(): return DeviceStatus.ONLINE else: return DeviceStatus.OFFLINE except Exception as e: self.logger.error(f"获取设备{idx}状态失败: {str(e)}") return DeviceStatus.ERROR async def hide_device(self, idx: str) -> tuple[bool, int]: """ 隐藏设备窗口 Args: idx: 设备ID Returns: tuple[bool, int]: (是否成功, 状态码) """ try: status = await self.get_status(idx) if status != DeviceStatus.ONLINE: return False, status if ( idx not in self.process_managers or not self.process_managers[idx].main_pid ): return False, DeviceStatus.NOT_FOUND # 窗口隐藏功能(简化实现) # 注意:完整的窗口隐藏功能需要更复杂的Windows API调用 self.logger.info(f"设备{idx}窗口隐藏请求已处理(简化实现)") return True, DeviceStatus.ONLINE self.logger.info(f"设备{idx}窗口已隐藏") return True, DeviceStatus.ONLINE except ImportError: self.logger.warning("隐藏窗口功能需要pywin32库") return False, DeviceStatus.ERROR except Exception as e: self.logger.error(f"隐藏设备{idx}窗口失败: {str(e)}") return False, DeviceStatus.ERROR async def show_device(self, idx: str) -> tuple[bool, int]: """ 显示设备窗口 Args: idx: 设备ID Returns: tuple[bool, int]: (是否成功, 状态码) """ try: status = await self.get_status(idx) if status != DeviceStatus.ONLINE: return False, status if ( idx not in self.process_managers or not self.process_managers[idx].main_pid ): return False, DeviceStatus.NOT_FOUND # 窗口显示功能(简化实现) # 注意:完整的窗口显示功能需要更复杂的Windows API调用 self.logger.info(f"设备{idx}窗口显示请求已处理(简化实现)") return True, DeviceStatus.ONLINE self.logger.info(f"设备{idx}窗口已显示") return True, DeviceStatus.ONLINE except ImportError: self.logger.warning("显示窗口功能需要pywin32库") return False, DeviceStatus.ERROR except Exception as e: self.logger.error(f"显示设备{idx}窗口失败: {str(e)}") return False, DeviceStatus.ERROR async def get_all_info(self) -> dict[str, dict[str, str]]: """ 获取所有设备信息 Returns: dict[str, dict[str, str]]: 设备信息字典 结构示例: { "0": { "title": "设备名称", "status": "1" } } """ result = {} for idx in list(self.process_managers.keys()): try: status = await self.get_status(idx) if idx in self.device_info: title = self.device_info[idx].get("title", f"{self.name}_{idx}") else: title = f"{self.name}_{idx}" result[idx] = {"title": title, "status": str(status)} except Exception as e: self.logger.error(f"获取设备{idx}信息失败: {str(e)}") result[idx] = { "title": f"{self.name}_{idx}", "status": str(DeviceStatus.ERROR), } return result async def cleanup(self) -> None: """ 清理所有资源 """ self.logger.info("开始清理设备管理器资源") for idx, pm in list(self.process_managers.items()): try: if await pm.is_running(): await pm.kill(if_force=True) await pm.clear() except Exception as e: self.logger.error(f"清理设备{idx}资源失败: {str(e)}") self.process_managers.clear() self.device_info.clear() self.logger.info("设备管理器资源清理完成") def __del__(self): """析构函数,确保资源被正确释放""" try: # 注意:析构函数中不能使用async/await # 这里只是标记,实际清理需要显式调用cleanup() if hasattr(self, "process_managers") and self.process_managers: self.logger.warning("设备管理器未正确清理,请显式调用cleanup()方法") except: # noqa: E722 pass # 使用示例 if __name__ == "__main__": async def main(): # 创建通用设备管理器 manager = GeneralDeviceManager( executable_path=r"C:\Windows\System32\notepad.exe", name="记事本" ) try: # 启动设备 success, status, info = await manager.start("0") print(f"启动结果: {success}, 状态: {status}, 信息: {info}") if success: # 获取所有设备信息 all_info = await manager.get_all_info() print(f"所有设备信息: {all_info}") # 等待5秒 await asyncio.sleep(5) # 关闭设备 close_success, close_status = await manager.close("0") print(f"关闭结果: {close_success}, 状态: {close_status}") finally: # 清理资源 await manager.cleanup() # 运行示例 asyncio.run(main())