import collections import threading import time from typing import Callable, Deque, Optional, Tuple, Union import frida class Reactor: """ Run the given function until return in the main thread (or the thread of the run method) and in a background thread receive and run additional tasks. """ def __init__( self, run_until_return: Callable[["Reactor"], None], on_stop: Optional[Callable[[], None]] = None ) -> None: self._running = False self._run_until_return = run_until_return self._on_stop = on_stop self._pending: Deque[Tuple[Callable[[], None], Union[int, float]]] = collections.deque([]) self._lock = threading.Lock() self._cond = threading.Condition(self._lock) self.io_cancellable = frida.Cancellable() self.ui_cancellable = frida.Cancellable() self._ui_cancellable_fd = self.ui_cancellable.get_pollfd() def __del__(self) -> None: self._ui_cancellable_fd.release() def is_running(self) -> bool: with self._lock: return self._running def run(self) -> None: with self._lock: self._running = True worker = threading.Thread(target=self._run) worker.start() self._run_until_return(self) self.stop() worker.join() def _run(self) -> None: running = True while running: now = time.time() work = None timeout = None previous_pending_length = -1 with self._lock: for item in self._pending: (f, when) = item if now >= when: work = f self._pending.remove(item) break if len(self._pending) > 0: timeout = max([min(map(lambda item: item[1], self._pending)) - now, 0]) previous_pending_length = len(self._pending) if work is not None: with self.io_cancellable: try: work() except frida.OperationCancelledError: pass with self._lock: if self._running and len(self._pending) == previous_pending_length: self._cond.wait(timeout) running = self._running if self._on_stop is not None: self._on_stop() self.ui_cancellable.cancel() def stop(self) -> None: self.schedule(self._stop) def _stop(self) -> None: with self._lock: self._running = False def schedule(self, f: Callable[[], None], delay: Optional[Union[int, float]] = None) -> None: """ append a function to the tasks queue of the reactor, optionally with a delay in seconds """ now = time.time() if delay is not None: when = now + delay else: when = now with self._lock: self._pending.append((f, when)) self._cond.notify() def cancel_io(self) -> None: self.io_cancellable.cancel()