def main() -> None: import argparse import functools import json import math import platform import sys from base64 import b64encode from typing import List, Tuple, Union try: import termios import tty except: pass import _frida from frida_tools.application import ConsoleApplication class PSApplication(ConsoleApplication): def _add_options(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "-a", "--applications", help="list only applications", action="store_true", dest="list_only_applications", default=False, ) parser.add_argument( "-i", "--installed", help="include all installed applications", action="store_true", dest="include_all_applications", default=False, ) parser.add_argument( "-j", "--json", help="output results as JSON", action="store_const", dest="output_format", const="json", default="text", ) def _initialize(self, parser: argparse.ArgumentParser, options: argparse.Namespace, args: List[str]) -> None: if options.include_all_applications and not options.list_only_applications: parser.error("-i cannot be used without -a") self._list_only_applications = options.list_only_applications self._include_all_applications = options.include_all_applications self._output_format = options.output_format self._terminal_type, self._icon_size = self._detect_terminal() def _usage(self) -> str: return "%(prog)s [options]" def _start(self) -> None: if self._list_only_applications: self._list_applications() else: self._list_processes() def _list_processes(self) -> None: if self._output_format == "text" and self._terminal_type == "iterm2": scope = "full" else: scope = "minimal" try: assert self._device is not None processes = self._device.enumerate_processes(scope=scope) except Exception as e: self._update_status(f"Failed to enumerate processes: {e}") self._exit(1) return if self._output_format == "text": if len(processes) > 0: pid_column_width = max(map(lambda p: len(str(p.pid)), processes)) icon_width = max(map(compute_icon_width, processes)) name_column_width = icon_width + max(map(lambda p: len(p.name), processes)) header_format = "%" + str(pid_column_width) + "s %s" self._print(header_format % ("PID", "Name")) self._print(f"{pid_column_width * '-'} {name_column_width * '-'}") line_format = "%" + str(pid_column_width) + "d %s" name_format = "%-" + str(name_column_width - icon_width) + "s" for process in sorted(processes, key=functools.cmp_to_key(compare_processes)): if icon_width != 0: icons = process.parameters.get("icons", None) if icons is not None: icon = self._render_icon(icons[0]) else: icon = " " name = icon + " " + name_format % process.name else: name = name_format % process.name self._print(line_format % (process.pid, name)) else: self._log("error", "No running processes.") elif self._output_format == "json": result = [] for process in sorted(processes, key=functools.cmp_to_key(compare_processes)): result.append({"pid": process.pid, "name": process.name}) self._print(json.dumps(result, sort_keys=False, indent=2)) self._exit(0) def _list_applications(self) -> None: if self._output_format == "text" and self._terminal_type == "iterm2": scope = "full" else: scope = "minimal" try: assert self._device is not None applications = self._device.enumerate_applications(scope=scope) except Exception as e: self._update_status(f"Failed to enumerate applications: {e}") self._exit(1) return if not self._include_all_applications: applications = list(filter(lambda app: app.pid != 0, applications)) if self._output_format == "text": if len(applications) > 0: pid_column_width = max(map(lambda app: len(str(app.pid)), applications)) icon_width = max(map(compute_icon_width, applications)) name_column_width = icon_width + max(map(lambda app: len(app.name), applications)) identifier_column_width = max(map(lambda app: len(app.identifier), applications)) header_format = ( "%" + str(pid_column_width) + "s " + "%-" + str(name_column_width) + "s " + "%-" + str(identifier_column_width) + "s" ) self._print(header_format % ("PID", "Name", "Identifier")) self._print(f"{pid_column_width * '-'} {name_column_width * '-'} {identifier_column_width * '-'}") line_format = "%" + str(pid_column_width) + "s %s %-" + str(identifier_column_width) + "s" name_format = "%-" + str(name_column_width - icon_width) + "s" for app in sorted(applications, key=functools.cmp_to_key(compare_applications)): if icon_width != 0: icons = app.parameters.get("icons", None) if icons is not None: icon = self._render_icon(icons[0]) else: icon = " " name = icon + " " + name_format % app.name else: name = name_format % app.name if app.pid == 0: self._print(line_format % ("-", name, app.identifier)) else: self._print(line_format % (app.pid, name, app.identifier)) elif self._include_all_applications: self._log("error", "No installed applications.") else: self._log("error", "No running applications.") elif self._output_format == "json": result = [] if len(applications) > 0: for app in sorted(applications, key=functools.cmp_to_key(compare_applications)): result.append({"pid": (app.pid or None), "name": app.name, "identifier": app.identifier}) self._print(json.dumps(result, sort_keys=False, indent=2)) self._exit(0) def _render_icon(self, icon) -> str: return "\033]1337;File=inline=1;width={}px;height={}px;:{}\007".format( self._icon_size, self._icon_size, b64encode(icon["image"]).decode("ascii") ) def _detect_terminal(self) -> Tuple[str, int]: icon_size = 0 if not self._have_terminal or self._plain_terminal or platform.system() != "Darwin": return ("simple", icon_size) fd = sys.stdin.fileno() old_attributes = termios.tcgetattr(fd) try: tty.setraw(fd) new_attributes = termios.tcgetattr(fd) new_attributes[3] = new_attributes[3] & ~termios.ICANON & ~termios.ECHO termios.tcsetattr(fd, termios.TCSANOW, new_attributes) sys.stdout.write("\033[1337n") sys.stdout.write("\033[5n") sys.stdout.flush() response = self._read_terminal_response("n") if response not in ("0", "3"): self._read_terminal_response("n") if response.startswith("ITERM2 "): version_tokens = response.split(" ", 1)[1].split(".", 2) if len(version_tokens) >= 2 and int(version_tokens[0]) >= 3: sys.stdout.write("\033[14t") sys.stdout.flush() height_in_pixels = int(self._read_terminal_response("t").split(";")[1]) sys.stdout.write("\033[18t") sys.stdout.flush() height_in_cells = int(self._read_terminal_response("t").split(";")[1]) icon_size = math.ceil((height_in_pixels / height_in_cells) * 1.77) return ("iterm2", icon_size) return ("simple", icon_size) finally: termios.tcsetattr(fd, termios.TCSANOW, old_attributes) def _read_terminal_response(self, terminator: str) -> str: sys.stdin.read(1) sys.stdin.read(1) result = "" while True: ch = sys.stdin.read(1) if ch == terminator: break result += ch return result def compare_applications(a: _frida.Application, b: _frida.Application) -> int: a_is_running = a.pid != 0 b_is_running = b.pid != 0 if a_is_running == b_is_running: if a.name > b.name: return 1 elif a.name < b.name: return -1 else: return 0 elif a_is_running: return -1 else: return 1 def compare_processes(a: _frida.Process, b: _frida.Process) -> int: a_has_icon = "icons" in a.parameters b_has_icon = "icons" in b.parameters if a_has_icon == b_has_icon: if a.name > b.name: return 1 elif a.name < b.name: return -1 else: return 0 elif a_has_icon: return -1 else: return 1 def compute_icon_width(item: Union[_frida.Application, _frida.Process]) -> int: for icon in item.parameters.get("icons", []): if icon["format"] == "png": return 4 return 0 app = PSApplication() app.run() if __name__ == "__main__": try: main() except KeyboardInterrupt: pass