From 1c267a2a175c4612e9b5e310c80747c24fac68be Mon Sep 17 00:00:00 2001 From: huiyuhang1 <42746033+huiyuhang1@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:58:45 +0800 Subject: [PATCH] =?UTF-8?q?fix(AutoCheckinBigGod):=20=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E9=80=80=E5=87=BA=E6=97=B6=E6=B8=85=E7=90=86=E6=89=80=E5=B1=9E?= =?UTF-8?q?=20Frida=20=E8=BF=9B=E7=A8=8B=E5=8F=8A=E5=AD=90=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tasks/AutoCheckinBigGod/script_task.py | 93 ++++++++++++++----- tests/test_biggod_frida_cleanup.py | 121 +++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 25 deletions(-) create mode 100644 tests/test_biggod_frida_cleanup.py diff --git a/tasks/AutoCheckinBigGod/script_task.py b/tasks/AutoCheckinBigGod/script_task.py index 3ec519a1dc..e1bbd5600b 100644 --- a/tasks/AutoCheckinBigGod/script_task.py +++ b/tasks/AutoCheckinBigGod/script_task.py @@ -47,6 +47,17 @@ class ScriptTask(BaseTask): def run(self): + self._adb_connected = False + self._restore_game_after_checkin = False + try: + self._run_checkin() + finally: + self._cleanup(restore_game=self._restore_game_after_checkin) + session = getattr(self, 'session', None) + if session is not None: + session.close() + + def _run_checkin(self): self.gl_uid = "" self.gl_token = "" self.gl_deviceid = "" @@ -71,6 +82,7 @@ def run(self): self.set_next_run('AutoCheckinBigGod', success=False, finish=True) raise TaskEnd('AutoCheckinBigGod') logger.info('ADB已连接') + self._adb_connected = True if not self._ensure_frida_server_running(): logger.error('Frida Server启动失败,请检查模拟器环境') @@ -80,6 +92,7 @@ def run(self): # [2/5] 启动大神APP logger.info('[2/5] 启动大神APP...') + self._restore_game_after_checkin = True pid = self._launch_app() if not pid: logger.error('无法启动大神APP,请确保模拟器中已安装大神APP') @@ -141,7 +154,6 @@ def run(self): if not rewards: logger.info('没有可领取的礼包') - self._cleanup(restore_game=True) self.set_next_run('AutoCheckinBigGod', success=True, finish=True) raise TaskEnd('AutoCheckinBigGod') @@ -154,21 +166,56 @@ def run(self): time.sleep(0.5) logger.info(f'完成! 成功领取 {success_count}/{len(rewards)} 个礼包') - self._cleanup(restore_game=True) self.set_next_run('AutoCheckinBigGod', success=True, finish=True) raise TaskEnd('AutoCheckinBigGod') # ======================== 清理 ======================== - def _cleanup(self, restore_game=False): - logger.info('清理:关闭大神APP和Frida Server...') + @staticmethod + def _stop_frida_process(proc): + if proc is None: + return try: - if self._frida_session is not None: - self._frida_session.kill() - except Exception: - pass + if proc.poll() is None: + if sys.platform == 'win32' and isinstance(proc.pid, int): + # The Windows CLI launcher may have a Python child process. + # Target only this task's Popen PID and its descendants. + subprocess.run( + ['taskkill', '/PID', str(proc.pid), '/T', '/F'], + capture_output=True, timeout=5, creationflags=_NO_WINDOW) + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=3) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait(timeout=3) + except Exception as e: + logger.warning(f'关闭Frida进程失败: {e}') + try: + proc.kill() + proc.wait(timeout=3) + except Exception: + pass + finally: + for pipe in (proc.stdin, proc.stdout, proc.stderr): + if pipe is not None: + try: + pipe.close() + except Exception: + pass + + def _close_frida_repl(self): + self._stop_frida_process(getattr(self, '_frida_session', None)) self._frida_session = None self._frida_attached_pid = None + + def _cleanup(self, restore_game=False): + logger.info('清理:关闭大神APP和Frida Server...') + self._close_frida_repl() + self.frida_pid = None + if not getattr(self, '_adb_connected', False): + return try: self._adb_shell(['am', 'force-stop', GL_PACKAGE]) except Exception: @@ -444,14 +491,9 @@ def _ensure_frida_repl(self, pid): return self._frida_session # PID 变了,需要重新 attach logger.info(f'目标PID变更 ({self._frida_attached_pid} -> {pid}),重新attach...') - try: - self._frida_session.kill() - except Exception: - pass else: logger.warning('Frida REPL 进程已退出,重新启动...') - self._frida_session = None - self._frida_attached_pid = None + self._close_frida_repl() frida_exe = self._get_frida_exe() if hasattr(self, '_adb_serial') and self._adb_serial: @@ -476,7 +518,7 @@ def _ensure_frida_repl(self, pid): if proc.poll() is not None: out = proc.stdout.read().decode('utf-8', errors='ignore') logger.warning(f'Frida REPL 进程提前退出: {out[:300]}') - self._frida_session = None + self._close_frida_repl() return None # 直接向 stdin 写 ping,不走 _run_frida_script 避免副作用 @@ -488,15 +530,11 @@ def _ensure_frida_repl(self, pid): # 所有重试都失败 logger.warning('Frida REPL 启动超时') - try: - proc.kill() - except Exception: - pass - self._frida_session = None + self._close_frida_repl() return None except Exception as e: logger.warning(f'启动Frida REPL失败: {e}') - self._frida_session = None + self._close_frida_repl() return None def _ping_frida_repl(self, proc, timeout=8): @@ -560,7 +598,7 @@ def _run_frida_script(self, pid, script_content, timeout=10): proc.stdin.flush() except (BrokenPipeError, OSError) as e: logger.warning(f'Frida REPL stdin写入失败: {e}') - self._frida_session = None + self._close_frida_repl() return self._run_frida_script_oneshot(pid, script_content, timeout) # 从 stdout 读取直到看到 __DONE__ 标记或超时 @@ -593,6 +631,8 @@ def _read_until_done(): if reader.is_alive(): timed_out.set() logger.warning(f'Frida REPL 读取超时 ({timeout}s)') + self._close_frida_repl() + reader.join(timeout=1) return '\n'.join(output_lines) if output_lines else '' @@ -601,6 +641,7 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): with tempfile.NamedTemporaryFile(mode='w', suffix='.js', delete=False, encoding='utf-8') as f: f.write(script_content) script_path = f.name + process = None try: frida_exe = self._get_frida_exe() @@ -619,11 +660,12 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): time.sleep(2) process.stdin.close() + process.stdin = None try: stdout_bytes, stderr_bytes = process.communicate(timeout=timeout) except subprocess.TimeoutExpired: - process.kill() - stdout_bytes, stderr_bytes = process.communicate() + logger.warning(f'Frida单次执行超时 ({timeout}s)') + return None # finally closes the launcher and its Python child together. stdout = stdout_bytes.decode('utf-8', errors='ignore') if stdout_bytes else '' stderr = stderr_bytes.decode('utf-8', errors='ignore') if stderr_bytes else '' @@ -632,6 +674,7 @@ def _run_frida_script_oneshot(self, pid, script_content, timeout=10): logger.warning(f'Frida脚本执行失败: {e}') return None finally: + self._stop_frida_process(process) try: os.unlink(script_path) except: @@ -1144,4 +1187,4 @@ def _claim_reward(self, bag_id, title): config = Config('oas1') device = Device(config) t = ScriptTask(config, device) - t.run() \ No newline at end of file + t.run() diff --git a/tests/test_biggod_frida_cleanup.py b/tests/test_biggod_frida_cleanup.py new file mode 100644 index 0000000000..613a65d7ae --- /dev/null +++ b/tests/test_biggod_frida_cleanup.py @@ -0,0 +1,121 @@ +import subprocess +import sys +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from module.exception import TaskEnd +from tasks.AutoCheckinBigGod.script_task import ScriptTask + + +def task_with_process(): + task = object.__new__(ScriptTask) + task._frida_session = subprocess.Popen( + [sys.executable, '-c', 'import time; time.sleep(60)'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)) + task._frida_attached_pid = 123 + task.frida_pid = 123 + return task + + +@pytest.mark.parametrize('exit_kind', ['return', 'task_end', 'error', 'interrupt']) +def test_run_reaps_owned_process_on_every_exit(exit_kind): + task = task_with_process() + proc = task._frida_session + commands = [] + task._adb_shell = lambda command: commands.append(command) + task.device = SimpleNamespace(app_start=Mock()) + task.session = SimpleNamespace(close=Mock()) + + def checkin(): + assert proc.poll() is None # The process remains usable during the task. + task._adb_connected = True + task._restore_game_after_checkin = True + if exit_kind == 'task_end': + raise TaskEnd('AutoCheckinBigGod') + if exit_kind == 'error': + raise RuntimeError('failed') + if exit_kind == 'interrupt': + raise KeyboardInterrupt() + + task._run_checkin = checkin + try: + if exit_kind == 'return': + task.run() + else: + expected = {'task_end': TaskEnd, 'error': RuntimeError, 'interrupt': KeyboardInterrupt}[exit_kind] + with pytest.raises(expected): + task.run() + assert proc.poll() is not None + assert all(pipe.closed for pipe in (proc.stdin, proc.stdout, proc.stderr)) + assert task._frida_session is None and task._frida_attached_pid is None + assert task.frida_pid is None + assert ['su -c "killall frida-server"'] in commands + task.device.app_start.assert_called_once() + task.session.close.assert_called_once() + finally: + if proc.poll() is None: + proc.kill() + proc.wait(timeout=3) + + +def test_failed_adb_check_does_not_touch_unspecified_device(): + task = object.__new__(ScriptTask) + task._run_checkin = Mock(side_effect=TaskEnd('AutoCheckinBigGod')) + task._adb_shell = Mock() + with pytest.raises(TaskEnd): + task.run() + task._adb_shell.assert_not_called() + + +def test_stdin_failure_reaps_repl_before_fallback(): + task = object.__new__(ScriptTask) + proc = Mock() + proc.poll.return_value = None + proc.stdin.write.side_effect = BrokenPipeError() + task._frida_session = proc + task._frida_attached_pid = 123 + def fallback(*args): + proc.wait.assert_called_once() + assert task._frida_session is None + return 'fallback' + task._run_frida_script_oneshot = fallback + assert task._run_frida_script(123, 'test') == 'fallback' + proc.terminate.assert_called_once() + + +@pytest.mark.parametrize('error', [OSError('failed'), subprocess.TimeoutExpired('frida', 10)]) +def test_oneshot_exception_reaps_process(monkeypatch, error): + task = object.__new__(ScriptTask) + task._get_frida_exe = lambda: 'frida' + proc = Mock() + proc.poll.return_value = None + proc.communicate.side_effect = error + monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.subprocess.Popen', lambda *a, **kw: proc) + monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.time.sleep', lambda seconds: None) + assert task._run_frida_script_oneshot(123, 'test') is None + proc.terminate.assert_called_once() + proc.wait.assert_called_once() + + +def test_unresponsive_process_is_killed_and_reaped(): + proc = Mock() + proc.poll.return_value = None + proc.wait.side_effect = [subprocess.TimeoutExpired('frida', 3), 0] + ScriptTask._stop_frida_process(proc) + proc.terminate.assert_called_once() + proc.kill.assert_called_once() + assert proc.wait.call_count == 2 + + +@pytest.mark.skipif(sys.platform != 'win32', reason='Windows launcher process tree') +def test_windows_cleanup_targets_only_owned_process_tree(monkeypatch): + proc = Mock(pid=12345) + proc.poll.return_value = None + taskkill = Mock() + monkeypatch.setattr('tasks.AutoCheckinBigGod.script_task.subprocess.run', taskkill) + ScriptTask._stop_frida_process(proc) + assert taskkill.call_args.args[0] == ['taskkill', '/PID', '12345', '/T', '/F'] + proc.wait.assert_called_once()