353 lines
17 KiB
Python
353 lines
17 KiB
Python
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
|
||
|