idb/cli/__init__.py (141 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
import json
import logging
import os
from abc import ABCMeta, abstractmethod
from argparse import ArgumentParser, Namespace
from typing import AsyncGenerator, Optional
from idb.common import plugin
from idb.common.command import Command
from idb.common.companion import Companion as LocalCompanion
from idb.common.logging import log_call
from idb.common.types import (
Address,
Client,
ClientManager,
Companion,
DomainSocketAddress,
IdbConnectionException,
IdbException,
LoggingMetadata,
TCPAddress,
)
from idb.grpc.client import Client as GrpcClient
from idb.grpc.management import ClientManager as GrpcClientManager
from idb.utils.contextlib import asynccontextmanager
def _parse_address(value: str) -> Address:
values = value.rsplit(":", 1)
if len(values) == 1:
return DomainSocketAddress(path=value)
(host, port) = values
return TCPAddress(host=host, port=int(port))
def _get_management_client(logger: logging.Logger, args: Namespace) -> ClientManager:
return GrpcClientManager(
companion_path=args.companion_path,
logger=logger,
prune_dead_companion=args.prune_dead_companion,
)
@asynccontextmanager
async def _get_client(
args: Namespace, logger: logging.Logger
) -> AsyncGenerator[GrpcClient, None]:
companion = vars(args).get("companion")
if companion is not None:
async with GrpcClient.build(
address=_parse_address(companion),
logger=logger,
use_tls=args.companion_tls,
) as client:
yield client
else:
async with GrpcClientManager(
logger=logger, companion_path=args.companion_path
).from_udid(udid=vars(args).get("udid")) as client:
yield client
class BaseCommand(Command, metaclass=ABCMeta):
def __init__(self) -> None:
super().__init__()
# Will inherit log levels when the log level is set on the base logger in run()
self.logger: logging.Logger = logging.getLogger(self.name)
def add_parser_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
"--log",
dest="log_level_deprecated",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default=None,
help="Set the logging level. Deprecated: Please place --log before the command name",
)
parser.add_argument(
"--json",
action="store_true",
default=False,
help="Create json structured output",
)
async def run(self, args: Namespace) -> None:
# In order to keep the argparse compatible with old invocations
# We should use the --log after command if set, otherwise use the pre-command --log
logging.getLogger().setLevel(args.log_level_deprecated or args.log_level)
name = self.__class__.__name__
self.logger.debug(f"{name} command run with: {args}")
if args.log_level_deprecated is not None:
self.logger.warning(
"Setting --log after the command is deprecated, please place it at the start of the invocation"
)
metadata: LoggingMetadata = plugin.resolve_metadata(logger=self.logger)
metadata["arguments"] = json.dumps(args.__dict__, default=lambda v: str(v))
async with log_call(
name=name,
metadata=metadata,
):
await self._run_impl(args)
@abstractmethod
async def _run_impl(self, args: Namespace) -> None:
raise Exception("subclass")
# A command that vends the IdbClient interface.
class ClientCommand(BaseCommand):
def add_parser_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
"--udid",
help="Udid of target, can also be set with the IDB_UDID env var",
default=os.environ.get("IDB_UDID"),
)
super().add_parser_arguments(parser)
async def _run_impl(self, args: Namespace) -> None:
address: Optional[Address] = None
try:
async with _get_client(args=args, logger=self.logger) as client:
address = client.address
await self.run_with_client(args=args, client=client)
except IdbConnectionException as ex:
if not args.prune_dead_companion:
raise ex
if address is None:
raise ex
try:
await _get_management_client(logger=self.logger, args=args).disconnect(
destination=address
)
finally:
raise ex
@abstractmethod
async def run_with_client(self, args: Namespace, client: Client) -> None:
pass
# A command that vends the ClientManagerface
class ManagementCommand(BaseCommand):
async def _run_impl(self, args: Namespace) -> None:
await self.run_with_manager(
args=args, manager=_get_management_client(logger=self.logger, args=args)
)
@abstractmethod
async def run_with_manager(self, args: Namespace, manager: ClientManager) -> None:
pass
# A command that vends the Companion interface
class CompanionCommand(BaseCommand):
async def _run_impl(self, args: Namespace) -> None:
companion_path = args.companion_path
if companion_path is None:
raise IdbException(
"Companion interactions do not work on non-macOS platforms"
)
await self.run_with_companion(
args=args,
companion=LocalCompanion(
companion_path=companion_path, device_set_path=None, logger=self.logger
),
)
@abstractmethod
async def run_with_companion(self, args: Namespace, companion: Companion) -> None:
pass