From 280fb9e2c80f11c8ee97951bcdd828dea618bb65 Mon Sep 17 00:00:00 2001 From: tubibibo Date: Fri, 6 Nov 2026 02:46:00 +0800 Subject: [PATCH] ps: Retry without icons when icon loading fails Some devices fail the whole full-scope enumeration when an app icon cannot be loaded, e.g. Android raising Resources$NotFoundException for sym_def_app_icon. Retry with the minimal scope and warn, so the listing still works. Refs frida/frida#3762 --- frida_tools/ps.py | 24 ++++++++++++++++++++++-- tests/test_ps.py | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/test_ps.py diff --git a/frida_tools/ps.py b/frida_tools/ps.py index cbdef69..8a58a30 100644 --- a/frida_tools/ps.py +++ b/frida_tools/ps.py @@ -79,7 +79,11 @@ def main() -> None: try: assert self._device is not None - processes = self._device.enumerate_processes(scope=scope) + processes = _enumerate_with_icon_fallback( + self._device.enumerate_processes, + scope, + lambda e: self._log("warning", f"Unable to load app icons ({e}), listing without them"), + ) except Exception as e: self._update_status(f"Failed to enumerate processes: {e}") self._exit(1) @@ -128,7 +132,11 @@ def main() -> None: try: assert self._device is not None - applications = self._device.enumerate_applications(scope=scope) + applications = _enumerate_with_icon_fallback( + self._device.enumerate_applications, + scope, + lambda e: self._log("warning", f"Unable to load app icons ({e}), listing without them"), + ) except Exception as e: self._update_status(f"Failed to enumerate applications: {e}") self._exit(1) @@ -287,6 +295,18 @@ def main() -> None: app.run() +def _enumerate_with_icon_fallback(enumerate_fn, scope, warn): + if scope != "full": + return enumerate_fn(scope=scope) + + try: + return enumerate_fn(scope="full") + except Exception as e: + result = enumerate_fn(scope="minimal") + warn(e) + return result + + if __name__ == "__main__": try: main() diff --git a/tests/test_ps.py b/tests/test_ps.py new file mode 100644 index 0000000..7da3826 --- /dev/null +++ b/tests/test_ps.py @@ -0,0 +1,48 @@ +import unittest + +from frida_tools.ps import _enumerate_with_icon_fallback + + +class TestPSEnumerationFallback(unittest.TestCase): + def test_full_scope_failure_is_retried_without_icons(self): + device = FakeDevice(fail_scopes={"full"}) + warnings = [] + processes = _enumerate_with_icon_fallback(device.enumerate_processes, "full", warnings.append) + self.assertEqual(processes, ["init"]) + self.assertEqual(device.calls, ["full", "minimal"]) + self.assertEqual(len(warnings), 1) + self.assertIn("could not load icons", str(warnings[0])) + + def test_full_scope_success_is_returned_as_is(self): + device = FakeDevice(fail_scopes=set()) + warnings = [] + processes = _enumerate_with_icon_fallback(device.enumerate_processes, "full", warnings.append) + self.assertEqual(processes, ["init"]) + self.assertEqual(device.calls, ["full"]) + self.assertEqual(warnings, []) + + def test_minimal_scope_failure_is_propagated(self): + device = FakeDevice(fail_scopes={"minimal"}) + with self.assertRaises(RuntimeError): + _enumerate_with_icon_fallback(device.enumerate_processes, "minimal", lambda e: None) + self.assertEqual(device.calls, ["minimal"]) + + def test_failure_that_is_not_about_icons_is_not_reported_as_one(self): + device = FakeDevice(fail_scopes={"full", "minimal"}) + warnings = [] + with self.assertRaises(RuntimeError): + _enumerate_with_icon_fallback(device.enumerate_processes, "full", warnings.append) + self.assertEqual(device.calls, ["full", "minimal"]) + self.assertEqual(warnings, []) + + +class FakeDevice: + def __init__(self, fail_scopes): + self.fail_scopes = fail_scopes + self.calls = [] + + def enumerate_processes(self, scope="minimal"): + self.calls.append(scope) + if scope in self.fail_scopes: + raise RuntimeError(f"could not load icons for {scope}") + return ["init"]