autool/DroidBot/input_manager.py
2026-06-17 19:44:18 +08:00

353 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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