🛠️ LLDB Extension for iOS and macOS App Development
import lldb
import shlex
import argparse
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    AppCommnad.register_lldb_command(debugger, AppCommnad.__module__)


class AppCommnad(LLDBCommandBase):

    @classmethod
    def cmdname(cls) -> str:
        return 'app'

    @classmethod
    def description(cls) -> str:
        return 'Application debugging. [iLLDB]'

    def create_argparser(self) -> argparse.ArgumentParser:
        parser = argparse.ArgumentParser(description="App debugging",
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")

        subparsers.add_parser("info",
                              help="Show App info",
                              formatter_class=util.HelpFormatter)

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand == "info":
            self.info(args, debugger, result)
        else:
            self.argparser.print_help()

    def info(
        self,
        args: argparse.Namespace,
        debugger: lldb.SBDebugger,
        result: lldb.SBCommandReturnObject
    ) -> None:
        script = util.read_script_file('swift/app.swift')
        script += """
        printAppInfo()
        """

        _ = util.exp_script(
            debugger,
            script
        )

src/app.py
p-x9/iLLDB
import lldb
import shlex
import util


@lldb.command('mirror', doc='Display child elements using Mirror. [iLLDB]')
def mirror(
    debugger: lldb.SBDebugger,
    command: str,
    exe_ctx: lldb.SBExecutionContext,
    result: lldb.SBCommandReturnObject,
    internal_dict: dict
) -> None:
    """
    Display the child elements of an object using the Mirror class in Swift.

    Args:
        debugger (lldb.SBDebugger): The LLDB debugger object.
        command (str): The command string passed to the mirror command.
        exe_ctx (lldb.SBExecutionContext): The execution context of the command.
        result (lldb.SBCommandReturnObject): The object used to return the result of the command.
        internal_dict (dict): The internal dictionary of the command.
    """
    args = shlex.split(command, posix=False)
    if len(args) < 1:
        return
    script = f"""
    import Foundation
    let mirror = Mirror(reflecting: {args[0]})
    mirror.children.enumerated().forEach {{
        print("\\($1.label ?? "[\\($0)]"): \\(String(reflecting: type(of: $1.value))) = \\($1.value)")
    }}
    """
    util.exp_script(
        debugger,
        script,
        lang=lldb.eLanguageTypeSwift
    )

src/commands.py
p-x9/iLLDB
import lldb
import shlex
import argparse
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    CookieCommnad.register_lldb_command(debugger, CookieCommnad.__module__)


class CookieCommnad(LLDBCommandBase):

    @classmethod
    def cmdname(cls) -> str:
        return 'cookie'

    @classmethod
    def description(cls) -> str:
        return 'HTTP Cookie debugging. [iLLDB]'

    def create_argparser(self) -> argparse.ArgumentParser:
        description = "HTTP Cookie debugging"
        parser = argparse.ArgumentParser(description=description,
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")

        # read
        read_command = subparsers.add_parser("read",
                                             help="Read Cookie value",
                                             formatter_class=util.HelpFormatter)
        read_command.add_argument("--group-id", type=str, help="AppGroup identifier for cookie storage")
        read_command.add_argument("--domain", type=str, help="Domain for Cookie")
        read_command.add_argument("--name", type=str, help="Name for Cookie")
        read_command.add_argument("--path", type=str, help="Path for Cookie")

        # delete
        delete_command = subparsers.add_parser("delete",
                                               help="Delete Cookie",
                                               formatter_class=util.HelpFormatter)
        delete_command.add_argument("--group-id", type=str, help="AppGroup identifier for cookie storage")
        delete_command.add_argument("--domain", type=str, help="Domain for Cookie")
        delete_command.add_argument("--name", type=str, help="Name for Cookie")
        delete_command.add_argument("--path", type=str, help="Path for Cookie")

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand == "read":
            self.read(args, debugger, result)
        elif args.subcommand == "delete":
            self.read(args, debugger, result)
            confirm = input('The above cookies will be deleted. Please type "Yes" if OK\n')
            if confirm == "Yes":
                self.delete(args, debugger, result)
            else:
                print("Cancelled")
        else:
            self.argparser.print_help()

    def read(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        script = ""
        if args.group_id is not None:
            script += f'NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"{args.group_id}"];\n'
        else:
            script += "NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n"

        script += util.read_script_file('objc/cookie_read.m')

        # domain
        if args.domain is not None:
            script = script.replace("<DOMAIN>", args.domain)
        else:
            script = script.replace("@\"<DOMAIN>\"", "NULL")

        # name
        if args.name is not None:
            script = script.replace("<NAME>", args.name)
        else:
            script = script.replace("@\"<NAME>\"", "NULL")

        # path
        if args.path is not None:
            script = script.replace("<PATH>", args.path)
        else:
            script = script.replace("@\"<PATH>\"", "NULL")

        _ = util.exp_script(
            debugger,
            script,
            lang=lldb.eLanguageTypeObjC
        )

    def delete(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        script = ""
        if args.group_id is not None:
            script += f'NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedCookieStorageForGroupContainerIdentifier:@"{args.group_id}"];\n'
        else:
            script += "NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage];\n"

        script += util.read_script_file('objc/cookie_delete.m')

        # domain
        if args.domain is not None:
            script = script.replace("<DOMAIN>", args.domain)
        else:
            script = script.replace("@\"<DOMAIN>\"", "NULL")

        # name
        if args.name is not None:
            script = script.replace("<NAME>", args.name)
        else:
            script = script.replace("@\"<NAME>\"", "NULL")

        # path
        if args.path is not None:
            script = script.replace("<PATH>", args.path)
        else:
            script = script.replace("@\"<PATH>\"", "NULL")

        _ = util.exp_script(
            debugger,
            script,
            lang=lldb.eLanguageTypeObjC
        )

src/cookie.py
p-x9/iLLDB
import lldb
import shlex
import argparse
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    DeviceCommnad.register_lldb_command(debugger, DeviceCommnad.__module__)


class DeviceCommnad(LLDBCommandBase):
    @classmethod
    def cmdname(cls) -> str:
        return 'device'

    @classmethod
    def description(cls) -> str:
        return "Device debugging. [iLLDB]"

    def create_argparser(self) -> argparse.ArgumentParser:
        parser = argparse.ArgumentParser(description="Device debugging",
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")

        subparsers.add_parser("info",
                              help="Show device info",
                              formatter_class=util.HelpFormatter)

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand == "info":
            self.info(args, debugger, result)
        else:
            self.argparser.print_help()

    def info(
        self,
        args: argparse.Namespace,
        debugger: lldb.SBDebugger,
        result: lldb.SBCommandReturnObject
    ) -> None:
        swift_file_name = "deviceMacOS" if util.isAppKit(debugger) else "deviceIOS"

        script = util.read_script_file(f'swift/{swift_file_name}.swift')
        script += """
        printDeviceInfo()
        """

        _ = util.exp_script(
            debugger,
            script
        )

src/device.py
p-x9/iLLDB
import lldb
import shlex
import argparse
import subprocess
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    FileCommnad.register_lldb_command(debugger, FileCommnad.__module__)


class FileCommnad(LLDBCommandBase):

    @classmethod
    def cmdname(cls) -> str:
        return 'file'

    @classmethod
    def description(cls) -> str:
        return 'File debugging. [iLLDB]'

    def create_argparser(self) -> argparse.ArgumentParser:
        parser = argparse.ArgumentParser(description="File debugging",
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands",
                                           dest="subcommand")

        tree_command = subparsers.add_parser("tree",
                                             help="Show file hierarchie",
                                             formatter_class=util.HelpFormatter)
        tree_command.add_argument("-b", "--bundle", action="store_true", help="bundle directory")
        tree_command.add_argument("-l", "--library", action="store_true", help="library directory")
        tree_command.add_argument("--documents", action="store_true", help="documents directory")
        tree_command.add_argument("--tmp", action="store_true", help="tmp directory")
        tree_command.add_argument("--depth", type=int, help="Maximum depth to be displayed")

        open_command = subparsers.add_parser("open",
                                             help="Open directory with Finder (Simulator Only)",
                                             formatter_class=util.HelpFormatter)
        open_command.add_argument("-b", "--bundle", action="store_true", help="bundle directory")
        open_command.add_argument("-l", "--library", action="store_true", help="library directory")
        open_command.add_argument("--documents", action="store_true", help="documents directory")
        open_command.add_argument("--tmp", action="store_true", help="tmp directory")

        cat_command = subparsers.add_parser("cat",
                                            help="The content of the specified file is retrieved and displayed",
                                            formatter_class=util.HelpFormatter)
        cat_command.add_argument("path",
                                 type=str,
                                 help="path")
        cat_command.add_argument("--mode", type=str, default='text', help="mode [text, plist]")

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand == "tree":
            self.tree(args, debugger, result)
        elif args.subcommand == "open":
            self.open(args, debugger, result)
        elif args.subcommand == "cat":
            self.cat(args, debugger, result)
        else:
            self.argparser.print_help()

    def tree(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        script = util.read_script_file('swift/file.swift')

        depth = 'nil'
        if args.depth is not None:
            try:
                depth = str(int(args.depth))
            except ValueError:
                pass

        if args.bundle:
            script += f"listFilesInDirectory(Bundle.main.bundleURL, depth: {depth})"
        elif args.library:
            script += f"listFilesInDirectory(FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!, depth: {depth})"
        elif args.documents:
            script += f"listFilesInDirectory(FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!, depth: {depth})"
        elif args.tmp:
            script += f"listFilesInDirectory(FileManager.default.temporaryDirectory, depth: {depth})"
        elif args.path:
            script += f"listFilesInDirectory(URL(fileURLWithPath: {args.path}), depth: {depth})"

        _ = util.exp_script(
            debugger,
            script
        )

    def open(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        if not (util.isIOSSimulator(debugger) or util.isMacOS(debugger)):
            print("Supported only simulator or macOS")
            return

        shell = "open -R "

        script = ""
        if args.bundle:
            script += "Bundle.main.bundlePath"
        elif args.library:
            script += "FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!.path"
        elif args.documents:
            script += "FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path"
        elif args.tmp:
            script += "FileManager.default.temporaryDirectory.path"
        elif args.path:
            shell += f"{args.path}"

        if script != "":
            ret = util.exp_script(debugger, script)
            if ret:
                print(ret.asStr())
                shell += f"{ret.GetObjectDescription()}"

        subprocess.run(shell, shell=True)

    def cat(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        script = util.read_script_file('objc/cat.m')
        script = script.replace("<FILE_PATH>", args.path)
        script = script.replace("<MODE>", args.mode)

        _ = util.exp_script(
            debugger,
            script,
            lang=lldb.eLanguageTypeObjC
        )

src/file.py
p-x9/iLLDB
import lldb
import os
import lldbhelper  # noqa: F401

iLLDB_VERSION = "0.6.1"


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    file_path = os.path.realpath(__file__)
    dir_name = os.path.dirname(file_path)
    load_commands(dir_name, debugger)
    print(f"[iLLDB] loaded: Version {iLLDB_VERSION}")


def load_commands(dir_name: str, debugger: lldb.SBDebugger) -> None:
    ignored_files = {"iLLDB.py"}

    for file in os.listdir(dir_name):
        full_path = dir_name + '/' + file

        if file in ignored_files:
            continue
        elif file.endswith('.py'):
            cmd = 'command script import '
        elif file.endswith('.h'):
            cmd = 'command source -e0 -s1 '
        elif os.path.isdir(full_path):
            load_commands(full_path, debugger)
            continue
        else:
            continue

        lldb.debugger.HandleCommand(cmd + full_path)

src/iLLDB.py
p-x9/iLLDB
import lldb
from typing import Optional


def asInt(self: lldb.SBValue) -> Optional[int]:
    if self.IsValid():
        try:
            value_str = self.GetValue()
            return int(value_str, 0)
        except BaseException:
            return None
    else:
        return None


def asFloat(self: lldb.SBValue) -> Optional[float]:
    if self.IsValid():
        try:
            value_str = self.GetValue()
            return float(value_str)
        except BaseException:
            return None
    else:
        return None


def asBool(self: lldb.SBValue) -> Optional[bool]:
    if self.IsValid():
        value_str = self.GetValue()
        if value_str == 'true' or value_str == 'TRUE' or value_str == 'YES':
            return True
        elif value_str == 'false' or value_str == 'FALSE' or value_str == 'NO':
            return False

        intValue = asInt(self)
        if intValue is not None:
            return intValue > 0
        else:
            return None
    else:
        return None


def asStr(self: lldb.SBValue) -> Optional[str]:
    if self.IsValid():
        summary: str = self.GetSummary()
        if summary is None:
            return None
        if summary.startswith('"') and summary.endswith('"') and len(summary) > 1:
            summary = summary[1:-1]
        elif summary.startswith('@"') and summary.endswith('"') and len(summary) > 2:
            summary = summary[2:-1]
        return summary
    else:
        return None


lldb.SBValue.asInt = asInt
lldb.SBValue.asFloat = asFloat
lldb.SBValue.asBool = asBool
lldb.SBValue.asStr = asStr

src/lldbhelper/SBValue.py
p-x9/iLLDB
__all__ = ['LLDBCommandBase']

from lldbhelper.lldb_command_base import LLDBCommandBase

src/lldbhelper/__init__.py
p-x9/iLLDB
import lldb
from abc import ABC, abstractmethod
import argparse

# ref: https://lldb.llvm.org/use/python-reference.html#create-a-new-lldb-command-using-a-python-function


class LLDBCommandBase(ABC):
    """
    Abstract base class for creating LLDB commands.

    This class provides a framework for creating LLDB commands.
    It defines abstract methods for command name, description, argument parser creation, and command execution.
    It also provides methods for registering the command with LLDB and retrieving help information.
    """

    @classmethod
    @abstractmethod
    def cmdname(cls) -> str:
        """
        Abstract method that returns the name of the command.

        Returns:
            str: The name of the command.
        """
        pass

    @classmethod
    @abstractmethod
    def description(cls) -> str:
        """
        Abstract method that returns the description of the command.

        Returns:
            str: The description of the command.
        """
        pass

    @classmethod
    def register_lldb_command(cls, debugger: lldb.SBDebugger, module_name: str) -> None:
        """
        Registers the command with LLDB.

        This method sets the command's description and adds it to the debugger.

        Args:
            debugger (lldb.SBDebugger): The LLDB debugger.
            module_name (str): The name of the module.

        Returns:
            None
        """
        cls.__doc__ = cls.description()

        command = f"command script add -o -c {module_name}.{cls.__name__} {cls.cmdname()}"
        debugger.HandleCommand(command)

    @abstractmethod
    def create_argparser(self) -> argparse.ArgumentParser:
        """
        Abstract method that creates and returns an argument parser for the command.

        Returns:
            argparse.ArgumentParser: The argument parser for the command.
        """
        pass

    def __init__(self, debugger: lldb.SBDebugger, internal_dict: dict):
        """
        Initializes the LLDBCommandBase instance.

        Args:
            debugger (lldb.SBDebugger): The LLDB debugger.
            internal_dict (dict): The internal dictionary.

        Returns:
            None
        """
        self.argparser = self.create_argparser()

    @abstractmethod
    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        """
        Abstract method that executes the command logic.

        Args:
            debugger (lldb.SBDebugger): The LLDB debugger.
            command (str): The command string.
            exe_ctx (lldb.SBExecutionContext): The execution context.
            result (lldb.SBCommandReturnObject): The command result.

        Returns:
            None
        """
        pass

    def get_short_help(self) -> str:
        """
        Returns the short help message for the command.
        Default implementation is that `description` is returned.

        Returns:
            str: The short help message.
        """
        return self.description()

    def get_long_help(self) -> str:
        """
        Returns the long help message for the command.
        Default implementation is that the argparser help message is returned.

        Returns:
            str: The long help message.
        """
        return self.argparser.format_help()

src/lldbhelper/lldb_command_base.py
p-x9/iLLDB
import lldb
import shlex
import argparse
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    UserDefaultsCommnad.register_lldb_command(debugger, UserDefaultsCommnad.__module__)


class UserDefaultsCommnad(LLDBCommandBase):

    @classmethod
    def cmdname(cls) -> str:
        return 'ud'

    @classmethod
    def description(cls) -> str:
        return 'UserDefault debugging. [iLLDB]'

    def create_argparser(self) -> argparse.ArgumentParser:
        description = "UserDefault debugging"
        parser = argparse.ArgumentParser(description=description,
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")

        # read
        read_command = subparsers.add_parser("read",
                                             help="Read UserDefault value",
                                             formatter_class=util.HelpFormatter)
        read_command.add_argument("--suite", type=str, help="Suite for UserDefault")
        read_command.add_argument("key",
                                  type=str,
                                  help="key")

        # write
        write_command = subparsers.add_parser("write",
                                              help="Write UserDefault value",
                                              formatter_class=util.HelpFormatter)
        write_command.add_argument("--suite", type=str, help="Suite for UserDefault")
        write_command.add_argument("key",
                                   type=str,
                                   help="Key")
        write_command.add_argument("value",
                                   type=str,
                                   help="Value to set")
        write_command.add_argument("--type",
                                   type=str,
                                   default='str',
                                   help="Type of value to set['str', 'number']")

        # delete
        delete_command = subparsers.add_parser("delete",
                                               help="Delete UserDefault value",
                                               formatter_class=util.HelpFormatter)
        delete_command.add_argument("--suite", type=str, help="Suite for UserDefault")
        delete_command.add_argument("key",
                                    type=str,
                                    help="Key")

        read_all = subparsers.add_parser("read-all",
                                         help="Read all UserDefault value",
                                         formatter_class=util.HelpFormatter)
        read_all.add_argument("--suite", type=str, help="Suite for UserDefault")

        delete_all = subparsers.add_parser("delete-all",
                                           help="Delete all UserDefault value",
                                           formatter_class=util.HelpFormatter)
        delete_all.add_argument("--suite", type=str, help="Suite for UserDefault")

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand is None:
            self.argparser.print_help()
            exit(0)

        script = ""
        if args.suite is not None:
            script += """
            @import Foundation;
            NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
            """
        else:
            script += """
            @import Foundation;
            NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
            """

        if args.subcommand == "read":
            key = args.key
            script += f"""({{
            id val = [userDefaults stringForKey:@\"{key}\"];
            if (val != nil) {{
                printf("\\"%s\\"", (char*)[val UTF8String]);
            }} else {{
                val = [userDefaults objectForKey:@\"{key}\"];
            }}
            val;
            }})
            """
        elif args.subcommand == "write":
            key = args.key
            type = args.type
            value = args.value
            if type == "number":
                script += f"[userDefaults setObject:@({value}) forKey:@\"{key}\"];"
            else:
                script += f"[userDefaults setObject:@\"{value}\" forKey:@\"{key}\"];"
        elif args.subcommand == "delete":
            key = args.key
            script += f"[userDefaults removeObjectForKey:@\"{key}\"];"
        elif args.subcommand == "read-all":
            script += "[userDefaults dictionaryRepresentation];"
        elif args.subcommand == "delete-all":
            script += r"""
            NSDictionary *allUserDefaults = [userDefaults dictionaryRepresentation];

            for (NSString *key in [allUserDefaults allKeys]) {
                [userDefaults removeObjectForKey:key];
            }
            """
        else:
            self.argparser.print_help()
            exit(0)

        ret = util.exp_script(
            debugger,
            script,
            lang=lldb.eLanguageTypeObjC_plus_plus
        )
        if ret:
            result.AppendMessage(ret.GetObjectDescription())

src/ud.py
p-x9/iLLDB
import lldb
import shlex
import argparse
from typing import Union, cast
from lldbhelper import LLDBCommandBase
import util


def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict: dict) -> None:
    UICommnad.register_lldb_command(debugger, UICommnad.__module__)


class UICommnad(LLDBCommandBase):

    @classmethod
    def cmdname(cls) -> str:
        return 'ui'

    @classmethod
    def description(cls) -> str:
        return 'UI debugging. [iLLDB]'

    def create_argparser(self) -> argparse.ArgumentParser:
        parser = argparse.ArgumentParser(description="UI debugging",
                                         formatter_class=util.HelpFormatter)
        subparsers = parser.add_subparsers(title="Subcommands", dest="subcommand")

        tree_command = subparsers.add_parser("tree",
                                             help="Show view hierarchie",
                                             formatter_class=util.HelpFormatter)
        tree_command.add_argument("-d", "--detail", action="store_true", help="Enable detailed mode")
        tree_command.add_argument("-s", "--simple", action="store_true", help="Enable simpled mode")
        tree_command.add_argument("--depth", type=int, help="Maximum depth to be displayed")
        tree_command.add_argument("--with-address", action="store_true", help="Print address of ui")

        tree_command.add_argument("--window", type=str, help="Specify the target window")
        tree_command.add_argument("--view", type=str, help="Specify the target view (property or address)")
        tree_command.add_argument("--vc", type=str, help="Specify the target viewController (property or address)")
        tree_command.add_argument("--layer", type=str, help="Specify the target CALayer (property or address)")

        return parser

    def __call__(
        self,
        debugger: lldb.SBDebugger,
        command: str,
        exe_ctx: lldb.SBExecutionContext,
        result: lldb.SBCommandReturnObject
    ) -> None:
        args: Union[list[str], argparse.Namespace] = shlex.split(command, posix=False)
        args = self.argparser.parse_args(cast(list[str], args))

        if args.subcommand == "tree":
            self.tree(args, debugger, result)
        else:
            self.argparser.print_help()

    def tree(self, args: argparse.Namespace, debugger: lldb.SBDebugger, result: lldb.SBCommandReturnObject) -> None:
        mode = 'normal'
        if args.detail:
            mode = 'detail'
        if args.simple:
            mode = 'simple'

        depth = 'nil'
        if args.depth is not None:
            try:
                depth = str(int(args.depth))
            except ValueError:
                pass

        with_address = 'true' if args.with_address else 'false'

        script = ''

        if util.isUIKit(debugger):
            script += """
            import UIKit
            typealias NSUIView = UIView
            typealias NSUIViewController = UIViewController
            typealias NSUIWindow = UIWindow
            typealias NSUIApplication = UIApplication
            """
        elif util.isAppKit(debugger):
            script += """
            import AppKit
            typealias NSUIView = NSView
            typealias NSUIViewController = NSViewController
            typealias NSUIWindow = NSWindow
            typealias NSUIApplication = NSApplication
            """

        self.resolve_adress(args)

        script += util.read_script_file('swift/tree.swift')
        if args.window is not None:
            script += f"\n windowHierarchy({args.window}, mode: \"{mode}\", depth: {depth}, address: {with_address})"
        elif args.view is not None:
            script += f"\n viewHierarchy({args.view}, mode: \"{mode}\", depth: {depth}, address: {with_address})"
        elif args.vc is not None:
            script += f"\n viewControllerHierarchy({args.vc}, mode: \"{mode}\", depth: {depth}, address: {with_address})"
        elif args.layer is not None:
            script += f"\n layerHierarchy({args.layer}, mode: \"{mode}\", depth: {depth}, address: {with_address})"
        else:
            script += f"\n windowHierarchy(NSUIApplication.shared.keyWindow, mode: \"{mode}\", depth: {depth}, address: {with_address})"

        _ = util.exp_script(
            debugger,
            script
        )

    def resolve_adress(self, args: argparse.Namespace) -> None:
        try:
            if args.window is not None and int(args.window, 16):
                args.window = f"Unmanaged<NSUIWindow>.fromOpaque(.init(bitPattern: {args.window})!).takeUnretainedValue()"
            elif args.view is not None and int(args.view, 16):
                args.view = f"Unmanaged<NSUIView>.fromOpaque(.init(bitPattern: {args.view})!).takeUnretainedValue()"
            elif args.vc is not None and int(args.vc, 16):
                args.vc = f"Unmanaged<NSUIViewController>.fromOpaque(.init(bitPattern: {args.vc})!).takeUnretainedValue()"
            elif args.layer is not None and int(args.layer, 16):
                args.layer = f"Unmanaged<CALayer>.fromOpaque(.init(bitPattern: {args.layer})!).takeUnretainedValue()"
        except ValueError:
            pass

src/ui.py
p-x9/iLLDB
import os
import lldb
import argparse
from typing import Optional
from lldbhelper import SBValue  # noqa: F401


def exp_script(
        debugger: lldb.SBDebugger,
        script: str,
        lang: int = lldb.eLanguageTypeSwift) -> Optional[lldb.SBValue]:

    frame: lldb.SBFrame = (
        debugger.GetSelectedTarget()
        .GetProcess()
        .GetSelectedThread()
        .GetSelectedFrame()
    )

    if not frame:
        return None

    options = lldb.SBExpressionOptions()
    options.SetLanguage(lang)
    options.SetIgnoreBreakpoints(True)
    options.SetTrapExceptions(False)
    options.SetTryAllThreads(True)
    options.SetFetchDynamicValue(lldb.eNoDynamicValues)
    options.SetUnwindOnError(True)
    options.SetGenerateDebugInfo(True)

    value: lldb.SBValue = frame.EvaluateExpression(script, options)
    error: lldb.SBError = value.GetError()

    if error.Success() or error.value == 0x1001:  # success or unknown error
        return value
    else:
        print(error)
        return None


def read_script_file(file_name: str) -> str:
    file_path = os.path.realpath(__file__)
    dir_name = os.path.dirname(file_path)

    file = open(f'{dir_name}/{file_name}', mode='r')
    text = file.read()
    file.close()

    return text


def isIOSSimulator(debugger: lldb.SBDebugger) -> bool:
    script = """
    @import Foundation;
    NSString *name = [[[NSProcessInfo processInfo] environment] objectForKey:@"SIMULATOR_DEVICE_NAME"];
    name;
    """
    ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC)
    if ret:
        result: Optional[str] = ret.asStr()
        return result is not None
    else:
        return False


def isAppKit(debugger: lldb.SBDebugger) -> bool:
    script = """
    @import Foundation;
    Class app = NSClassFromString(@"NSApplication");
    BOOL val = (BOOL)(app != nil)
    val
    """
    ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC)

    if ret:
        result: Optional[bool] = ret.asBool()
        return False if result is None else result
    else:
        return False


def isUIKit(debugger: lldb.SBDebugger) -> bool:
    script = """
    @import Foundation;
    Class app = NSClassFromString(@"UIApplication");
    BOOL val = (BOOL)(app != nil)
    val;
    """
    ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC)
    if ret:
        result: Optional[bool] = ret.asBool()
        return False if result is None else result
    else:
        return False


def isMacOS(debugger: lldb.SBDebugger) -> bool:
    model = sysctlbyname(debugger, "hw.model")
    if model:
        return 'Mac' in model and not isIOSSimulator(debugger)
    else:
        return isAppKit(debugger)


def isIOS(debugger: lldb.SBDebugger) -> bool:
    machine = sysctlbyname(debugger, "hw.machine")
    if machine:
        return 'iP' in machine or isIOSSimulator(debugger)
    else:
        return isUIKit(debugger)


def sysctlbyname(debugger: lldb.SBDebugger, key: str) -> Optional[str]:
    script = f"""
    @import Foundation;
    int sysctlbyname(const char *, void *, size_t *, void *, size_t);

    size_t size = 0;
    sysctlbyname("{key}", NULL, &size, NULL, 0);
    char *machine = (char *)malloc(size);
    sysctlbyname("{key}", machine, &size, NULL, 0);

    NSString *result = [NSString stringWithUTF8String:machine];
    free(machine);
    result;
    """

    ret = exp_script(debugger, script, lang=lldb.eLanguageTypeObjC)
    if ret:
        result: Optional[str] = ret.asStr()
        return result
    else:
        return None


class HelpFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
    pass

src/util.py
p-x9/iLLDB