import json import logging import time from .core import EventLog from .core.abstract_input_event import EventType from .input_policy import POLICY_NONE, POLICY_MEMORY_GUIDED, POLICY_MANUAL, MemoryGuidedPolicy, NoneInputPolicy, ManualPolicy from .traffic_monitor import TrafficMonitor from .exceptions import FATAL_EXCEPTIONS, InputInterruptedException, AppCrashException, AppNeedUpdateException, AppLaunchErrorException DEFAULT_POLICY = POLICY_MEMORY_GUIDED DEFAULT_EVENT_INTERVAL = 1 DEFAULT_EVENT_COUNT = 100000000 DEFAULT_TIMEOUT = -1 WARMUP_STEPS = 10 STALL_STEPS_LIMIT = 33 STOP_STALL_STEPS_LIMIT = 3 * STALL_STEPS_LIMIT MIN_EXPLORATION_STEPS = 300 MAX_EXPLORATION_STEPS = 750 BLOCK_MIN_EXPLORATION_STEPS = 100 BLOCK_MAX_EXPLORATION_STEPS = 300 APP_CRASH_CYCLE_LIMIT = 3 # 闪退检测: 连续关闭->拉起失败的次数阈值 GOOGLE_PLAY_RECOVER_LIMIT = 3 GOOGLE_PLAY_STABLE_STEP_LIMIT = 3 REDIRECT_PULL_BACK_LIMIT = 3 class InputManager(object): """ This class manages all events to send during app running """ def __init__(self, device, policy_name, random_input, event_count, event_interval, profiling_method=None, replay_output=None, enable_guiagent=False, app_name=None, pcap_callback=None, enable_app_block=False): """ manage input event sent to the target device :param device: instance of Device (device internally manages app) :param policy_name: policy of ting events, string :param pcap_callback: callback to push pcap file :param enable_app_block: whether traffic blocking is enabled for this task :return: """ self.logger = logging.getLogger('InputEventManager') self.enabled = True self.device = device self.policy_name = policy_name self.random_input = random_input self.events = [] self.policy = None self.event_count = event_count self.event_interval = event_interval self.replay_output = replay_output self.enable_guiagent = enable_guiagent self.app_name = app_name self.profiling_method = profiling_method self.pcap_callback = pcap_callback self.enable_app_block = enable_app_block # Initialize TrafficMonitor and step counters (must be before get_input_policy) self.traffic_monitor = TrafficMonitor(device, device.app_identifier, pcap_callback=self.pcap_callback) self.total_exploring_steps = 0 self.current_stall_steps = 0 self.steps_since_last_stall_check = 0 self._force_stop = False # 用户手动中断标志(KeyboardInterrupt) self.user_interrupted = False # GuiAgent execution flag - skip foreground check and step counting during agent execution self.is_guiagent_executing = False # 闪退检测: 追踪连续关闭->拉起失败的循环次数 self._crash_cycle_count = 0 self._awaiting_restart_result = False # 标记是否正在等待重启结果 # Google Play 拉回恢复状态 self._google_play_recover_attempts = 0 self._google_play_stable_steps = 0 # 跳转目标包名记录 self._last_redirect_package = None self._redirect_pull_back_count = 0 # Initialize policy (uses traffic_monitor) self.policy = self.get_input_policy(device) def _reset_google_play_recovery(self): self._google_play_recover_attempts = 0 self._google_play_stable_steps = 0 def _handle_google_play_redirect(self): self._google_play_recover_attempts += 1 self._google_play_stable_steps = 0 self.logger.warning( f"检测到 Google Play Store,尝试拉回 " f"({self._google_play_recover_attempts}/{GOOGLE_PLAY_RECOVER_LIMIT})" ) if self.device.pull_back_to_app(): self.logger.info("Google Play Store 拉回成功,进入稳定观察") else: self.logger.warning("Google Play Store 拉回失败") time.sleep(1) if self._google_play_recover_attempts >= GOOGLE_PLAY_RECOVER_LIMIT: self.logger.error( f"连续 {GOOGLE_PLAY_RECOVER_LIMIT} 次从 Google Play Store 拉回仍未稳定,判定为需更新" ) raise AppNeedUpdateException("应用反复跳转 Google Play Store,需更新") def _mark_google_play_recovered(self): if self._google_play_recover_attempts == 0: return self._google_play_stable_steps += 1 if self._google_play_stable_steps < GOOGLE_PLAY_STABLE_STEP_LIMIT: self.logger.info( f"Google Play Store 拉回后稳定观察 " f"({self._google_play_stable_steps}/{GOOGLE_PLAY_STABLE_STEP_LIMIT})" ) return self.logger.info("Google Play Store 拉回后已稳定,清空恢复状态") self._reset_google_play_recovery() def get_input_policy(self, device): if self.policy_name == POLICY_NONE: input_policy = NoneInputPolicy(device, enable_guiagent=self.enable_guiagent) elif self.policy_name == POLICY_MEMORY_GUIDED: input_policy = MemoryGuidedPolicy(device, self.random_input, enable_guiagent=self.enable_guiagent, app_name=self.app_name, traffic_monitor=self.traffic_monitor) elif self.policy_name == POLICY_MANUAL: input_policy = ManualPolicy(device, enable_guiagent=self.enable_guiagent) else: self.logger.warning("No valid input policy specified. Using policy \"none\".") input_policy = None return input_policy def add_event(self, event): """ add one event to the event list :param event: the event to be added, should be subclass of AppEvent :return: """ if event is None: return # 如果GuiAgent正在执行,跳过前台检查和步数累计,只执行事件 if self.is_guiagent_executing: self.logger.debug(f"[GuiAgent执行中] 直接执行事件: {event.event_type}") self.events.append(event) event_log = EventLog(self.device, self.device._app, event, self.profiling_method) event_log.start() event_log.stop() return # 1. 基础限制检查 max_steps = BLOCK_MAX_EXPLORATION_STEPS if self.enable_app_block else MAX_EXPLORATION_STEPS min_steps = BLOCK_MIN_EXPLORATION_STEPS if self.enable_app_block else MIN_EXPLORATION_STEPS if self.total_exploring_steps >= max_steps: self.logger.info(f"Reached max exploration steps ({max_steps}), stopping collection.") raise InputInterruptedException() # 2. 前台状态与采集终止检查 is_kill_app = hasattr(event, 'event_type') and event.event_type == EventType.KILL_APP if not is_kill_app: curr_in_foreground = self.device.is_foreground() if not curr_in_foreground: # C10: Web平台简化处理,无app_store/launcher概念 if self.device.get_platform_name() == "web": self.logger.info("Web平台: 检测到离站,尝试拉回") self.device.pull_back_to_app() return redirect_info = self.device.get_redirect_target_info() redirect_type = redirect_info.get("type") if redirect_info else "unknown" redirect_target = redirect_info.get("target") if redirect_info else None # 情况1: 跳转到 Google Play Store -> 需连续检测确认 if redirect_type == "app_store": self._handle_google_play_redirect() return # 情况2: 跳转到桌面 -> 闪退逻辑 if redirect_type in {"launcher", "unknown"}: self._last_redirect_package = None self._redirect_pull_back_count = 0 if self.total_exploring_steps >= min_steps and self._force_stop: self.logger.info(f"App out of foreground and total steps ({self.total_exploring_steps}) >= min exploration steps ({min_steps}) with _force_stop. Ending.") raise InputInterruptedException() else: self.logger.info(f"App out of foreground (launcher) but not reached min exploration steps ({self.total_exploring_steps}/{min_steps}). Pulling back.") pull_back_success = self.device.pull_back_to_app() if not pull_back_success: if self._awaiting_restart_result: self._crash_cycle_count += 1 self.logger.warning(f"应用重启后再次失败,闪退循环计数: {self._crash_cycle_count}/{APP_CRASH_CYCLE_LIMIT}") if self._crash_cycle_count >= APP_CRASH_CYCLE_LIMIT: if self.enable_app_block: self.logger.warning(f"Block任务检测到连续闪退 {self._crash_cycle_count} 次,重置计数并继续尝试(流量阻塞可能导致正常闪退)") self._crash_cycle_count = 0 self._awaiting_restart_result = False else: self.logger.error(f"检测到应用闪退!连续 {self._crash_cycle_count} 次关闭->拉起失败循环") raise AppCrashException(f"应用闪退:连续 {self._crash_cycle_count} 次关闭并重启后仍无法运行") self.logger.warning("前台拉回失败,将关闭应用并重启") from .core import PlatformFactory CloseAppEvent = PlatformFactory.get_event_class(self.device.get_platform_name(), 'kill_app') if CloseAppEvent: event = CloseAppEvent(app=self.device.app_identifier) self._awaiting_restart_result = True time.sleep(10) # 情况3: 跳转到其他应用 -> 记录目标并尝试拉回 else: self._last_redirect_package = redirect_target self.logger.warning(f"应用跳转到其他应用: {redirect_target},尝试拉回") pull_back_success = self.device.pull_back_to_app() if pull_back_success: self._redirect_pull_back_count = 0 self._last_redirect_package = None self.logger.info(f"成功拉回到目标应用") else: self._redirect_pull_back_count += 1 self.logger.warning(f"拉回失败 ({self._redirect_pull_back_count}/{REDIRECT_PULL_BACK_LIMIT}),跳转目标: {self._last_redirect_package}") if self._redirect_pull_back_count >= REDIRECT_PULL_BACK_LIMIT: self.logger.error(f"连续 {REDIRECT_PULL_BACK_LIMIT} 次拉回失败,判定为启动异常,跳转目标: {self._last_redirect_package}") raise AppLaunchErrorException(f"启动异常:跳转至 {self._last_redirect_package}") else: if self._force_stop: self.logger.info("App is in foreground, resetting _force_stop.") self._force_stop = False if self._awaiting_restart_result: self.logger.info("应用重启成功,重置闪退检测计数") self._crash_cycle_count = 0 self._awaiting_restart_result = False self._mark_google_play_recovered() self._last_redirect_package = None self._redirect_pull_back_count = 0 # 3. 统计当前步 self.total_exploring_steps += 1 self.current_stall_steps += 1 self.steps_since_last_stall_check += 1 # 4. 流量监控与 停滞处理 has_new = False if self.total_exploring_steps >= WARMUP_STEPS: self.traffic_monitor.update() has_new, new_count = self.traffic_monitor.has_new_features() if self.steps_since_last_stall_check >= STALL_STEPS_LIMIT: # 监控环境健康检查 self.traffic_monitor.check_monitor_health() self.logger.info(f"Steps reached {STALL_STEPS_LIMIT}, checking for new features...") if not has_new: from .core import PlatformFactory if self.current_stall_steps >= STOP_STALL_STEPS_LIMIT: if self.device.check_network(): self.logger.info("Network check success, force stop") self._force_stop = True else: self.logger.warning("Network check failed, retry") self._force_stop = False self.logger.info(f"No new features for {self.current_stall_steps} steps. Generating KillAppEvent.") KillAppEvent = PlatformFactory.get_event_class(self.device.get_platform_name(), 'kill_app') if KillAppEvent: event = KillAppEvent(app=self.device.app_identifier) self.current_stall_steps = 0 else: # 总步数达标,标志停止 if self.total_exploring_steps >= min_steps: if self.device.check_network(): self.logger.info("Network check success, force stop") self._force_stop = True else: self.logger.warning("Network check failed, retry") self._force_stop = False self.logger.info(f"No new features detected. Overriding with BACK event.") KeyEvent = PlatformFactory.get_event_class(self.device.get_platform_name(), 'key') if KeyEvent: event = KeyEvent(key_name="BACK") # 重置单次 check 计数,以便下一次 loop 继续 check self.steps_since_last_stall_check = 0 else: self.logger.info(f"New features found ({new_count}). Resetting stall counters.") self.current_stall_steps = 0 self.steps_since_last_stall_check = 0 self._force_stop = False # 5. 执行事件 self.logger.info(f"Step {self.total_exploring_steps}: sending {event.event_type} (stall: {self.current_stall_steps}/{STOP_STALL_STEPS_LIMIT}, check: {self.steps_since_last_stall_check}/{STALL_STEPS_LIMIT})") self.events.append(event) event_log = EventLog(self.device, self.device._app, event, self.profiling_method) event_log.start() event_log.stop() def start(self): """ start sending event """ self.logger.info("start sending events, policy is %s" % self.policy_name) try: if self.policy is not None: self.policy.start(self) elif self.policy_name == POLICY_NONE: self.device.start_app() if self.event_count == 0: return while self.enabled: time.sleep(1) elif self.policy_name == POLICY_MANUAL: self.device.start_app() while self.enabled: keyboard_input = input("press ENTER to save current state, type q to exit...") if keyboard_input.startswith('q'): break state = self.device.get_current_state() if state is not None: state.save2dir() except KeyboardInterrupt: # 记录用户手动中断标志,并向上传播 self.user_interrupted = True raise except FATAL_EXCEPTIONS: raise except Exception as e: self.logger.error(f'Non-fatal exception in input_manager: {e}') import traceback traceback.print_exc() finally: # 探索结束后,一次性完成所有文件写入(无论正常结束还是异常退出) if self.policy and hasattr(self.policy, 'utg'): self.policy.utg.finalize() self.stop() self.logger.info("Finish sending events") def stop(self): """ stop sending event """ self.enabled = False