async def _start_companion_command( self, arguments: List[str] ) -> AsyncGenerator[asyncio.subprocess.Process, None]: companion_path = self._companion_path if companion_path is None: if platform == "darwin": raise IdbException("Companion path not provided") else: raise IdbException( "Companion interactions do not work on non-macOS platforms" ) cmd: List[str] = [companion_path] device_set_path = self._device_set_path if device_set_path is not None: cmd.extend(["--device-set-path", device_set_path]) cmd.extend(arguments) process = await asyncio.create_subprocess_exec(*cmd, stdout=subprocess.PIPE, stderr=None) logger = self._logger.getChild(f"{process.pid}:{' '.join(arguments)}") logger.info("Launched process") try: yield process finally: await _terminate_process(process=process, timeout=self._companion_teardown_timeout, logger=logger)
async def func_wrapper(*args: Any, **kwargs: Any) -> Any: # pyre-ignore try: return await func(*args, **kwargs) except GRPCError as e: raise IdbException(e.message) from e # noqa B306 except (ProtocolError, StreamTerminatedError) as e: raise IdbException(e.args) from e
async def get_companion_info(self, target_udid: Optional[str]) -> CompanionInfo: async with self._use_stored_companions() as companions: # If we get a target by udid we expect only one value. if target_udid is not None: matching = [ companion for companion in companions if companion.udid == target_udid ] if len(matching) == 1: return matching[0] elif len(matching) > 1: raise IdbException( f"More than one companion matching udid {target_udid}: {matching}" ) else: raise IdbException( f"No companion for {target_udid}, existing {companions}" ) # With no udid provided make sure there is only a single match elif len(companions) == 1: companion = companions[0] self.logger.info( f"Using sole default companion with udid {companion.udid}") return companion elif len(companions) > 1: raise IdbException( f"No UDID provided and there's multiple companions: {companions}" ) else: raise IdbException("No UDID provided and no companions exist")
async def from_udid(self, udid: Optional[str]) -> AsyncGenerator[Client, None]: companions = { companion.udid: companion for companion in await self._companion_set.get_companions() } if udid is not None and udid in companions: companion = companions[udid] self._logger.debug( f"Got existing companion {companion} for udid {udid}") elif udid is not None: self._logger.debug( f"No running companion for {udid}, spawning one") companion = await self._spawn_companion_server(udid=udid) elif len(companions) == 1: self._logger.debug( "No udid provided, and there is a sole companion, using it") companion = list(companions.values())[0] elif len(companions) == 0: raise IdbException( "No udid provided and there no companions, unclear which target to run against. Please specify a UDID" ) else: raise IdbException( f"No udid provided and there are multiple companions to run against {companions.keys()}. Please specify a UDID unclear which target to run against" ) async with Client.build( address=companion.address, logger=self._logger, ) as client: self._logger.debug(f"Constructed client for companion {companion}") yield client
async def _tramp(*args: Any, **kwargs: Any) -> Any: # pyre-ignore try: client = await _make_client() return await call(client, *args, **kwargs) except GRPCError as e: raise IdbException(e.message) from e # noqa B306 except (ProtocolError, StreamTerminatedError) as e: raise IdbException(e.args) from e
async def func_wrapper_gen(*args: Any, **kwargs: Any) -> Any: try: async for item in func(*args, **kwargs): yield item except GRPCError as e: raise IdbException(e.message) from e # noqa B306 except (ProtocolError, StreamTerminatedError) as e: raise IdbException(e.args) from e
async def target_description( self, udid: str, only: Optional[TargetType] = None) -> TargetDescription: all_details = await self.list_targets(only=only) details = [target for target in all_details if target.udid == udid] if len(details) > 1: raise IdbException(f"More than one device info found {details}") if len(details) == 0: raise IdbException(f"No device info found, got {all_details}") return details[0]
async def _run_companion_command(self, arguments: List[str]) -> str: timeout = self._companion_command_timeout async with self._start_companion_command( arguments=arguments) as process: try: (output, _) = await asyncio.wait_for(process.communicate(), timeout=timeout) if process.returncode != 0: raise IdbException(f"Failed to run {arguments}") self.logger.info(f"Ran {arguments} successfully.") return output.decode() except asyncio.TimeoutError: raise IdbException( f"Timed out after {timeout} secs on command {' '.join(arguments)}" )
async def target_description( self, udid: Optional[str] = None, only: Optional[OnlyFilter] = None, timeout: Optional[timedelta] = None, ) -> TargetDescription: all_details = await self.list_targets(only=only, timeout=timeout) details = all_details if udid is not None: details = [target for target in all_details if target.udid == udid] if len(details) > 1: raise IdbException(f"More than one device info found {details}") if len(details) == 0: raise IdbException(f"No device info found, got {all_details}") return details[0]
async def connect( self, destination: ConnectionDestination, metadata: Optional[Dict[str, str]] = None, ) -> CompanionInfo: self._logger.debug( f"Connecting directly to {destination} with meta {metadata}") if isinstance(destination, TCPAddress) or isinstance( destination, DomainSocketAddress): async with IdbClient.build(address=destination, is_local=False, logger=self._logger) as client: with tempfile.NamedTemporaryFile(mode="w+b") as f: response = await client.stub.connect( ConnectRequest(metadata=metadata, local_file_path=f.name)) companion = CompanionInfo( address=destination, udid=response.companion.udid, is_local=response.companion.is_local, ) self._logger.debug(f"Connected directly to {companion}") await self._direct_companion_manager.add_companion(companion) return companion else: companion = await self._spawn_companion(target_udid=destination) if companion: return companion else: raise IdbException(f"can't find target for udid {destination}")
def target_type_from_string(output: str) -> TargetType: normalized = output.lower() if "sim" in normalized: return TargetType.SIMULATOR if "dev" in normalized: return TargetType.DEVICE raise IdbException(f"Could not interpret target type from {output}")
async def start( stub: CompanionServiceStub, logger: logging.Logger, pkg_id: str ) -> AsyncGenerator["RemoteDapServer", None]: """ Created a RemoteDapServer starting a new grpc stream and sending a start dap server request to companion """ logger.info("Starting dap connection") async with stub.dap.open() as stream: await stream.send_message( DapRequest(start=DapRequest.Start(debugger_pkg_id=pkg_id)) ) response = await stream.recv_message() logger.debug(f"Dap response after start request: {response}") if response and response.started: logger.info("Dap stream ready to receive messages") dap_server = RemoteDapServer( stream=stream, logger=logger, ) try: yield dap_server finally: await dap_server.__stop() else: logger.error(f"Starting dap server failed! {response}") raise IdbException("Failed to spawn dap server.") logger.info("Dap grpc stream is closed.")
async def connect( self, destination: ConnectionDestination, metadata: Optional[Dict[str, str]] = None, ) -> CompanionInfo: self.logger.debug(f"Connecting directly to {destination} with meta {metadata}") if isinstance(destination, Address): channel = Channel( destination.host, destination.port, loop=asyncio.get_event_loop() ) stub = CompanionServiceStub(channel=channel) with tempfile.NamedTemporaryFile(mode="w+b") as f: response = await stub.connect( ConnectRequest( destination=destination_to_grpc(destination), metadata=metadata, local_file_path=f.name, ) ) companion = CompanionInfo( udid=response.companion.udid, host=destination.host, port=destination.port, is_local=response.companion.is_local, ) self.logger.debug(f"Connected directly to {companion}") self.direct_companion_manager.add_companion(companion) channel.close() return companion else: companion = await self.spawn_companion(target_udid=destination) if companion: return companion else: raise IdbException(f"can't find target for udid {destination}")
async def build( cls, address: Address, logger: logging.Logger, is_local: Optional[bool] = None, exchange_metadata: bool = True, extra_metadata: Optional[Dict[str, str]] = None, use_tls: bool = False, ) -> AsyncGenerator["Client", None]: metadata_to_companion = ( { **{ key: value for (key, value) in plugin.resolve_metadata(logger=logger).items() if isinstance(value, str) }, **(extra_metadata or {}), } if exchange_metadata else {} ) ssl_context = plugin.channel_ssl_context() if use_tls else None if use_tls: assert ssl_context is not None async with ( Channel( host=address.host, port=address.port, loop=asyncio.get_event_loop(), ssl=ssl_context, ) if isinstance(address, TCPAddress) else Channel(path=address.path, loop=asyncio.get_event_loop()) ) as channel: stub = CompanionServiceStub(channel=channel) with tempfile.NamedTemporaryFile(mode="w+b") as f: try: response = await stub.connect( ConnectRequest( metadata=metadata_to_companion, local_file_path=f.name ) ) except Exception as ex: raise IdbException( f"Failed to connect to companion at address {address}: {ex}" ) companion = companion_to_py( companion=response.companion, address=address, is_local=is_local ) if exchange_metadata: metadata_from_companion = { key: value for (key, value) in companion.metadata.items() if isinstance(value, str) } plugin.append_companion_metadata( logger=logger, metadata=metadata_from_companion ) yield Client(stub=stub, companion=companion, logger=logger)
async def _run_companion_command(self, arguments: List[str]) -> str: async with self._start_companion_command( arguments=arguments) as process: (output, _) = await process.communicate() if process.returncode != 0: raise IdbException(f"Failed to run {arguments}") self.logger.info(f"Ran {arguments} successfully.") return output.decode()
async def _run_companion_command(self, arguments: List[str], timeout: Optional[timedelta]) -> str: timeout = timeout if timeout is not None else DEFAULT_COMPANION_COMMAND_TIMEOUT async with self._start_companion_command( arguments=arguments) as process: try: (output, _) = await asyncio.wait_for(process.communicate(), timeout=timeout.total_seconds()) if process.returncode != 0: raise IdbException(f"Failed to run {arguments}") self._logger.info(f"Ran {arguments} successfully.") return output.decode() except asyncio.TimeoutError: raise IdbException( f"Timed out after {timeout} secs on command {' '.join(arguments)}" )
async def boot(self) -> None: if self.target_udid: cmd: List[str] = ["idb_companion", "--boot", none_throws(self.target_udid)] process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) await process.communicate() else: raise IdbException("boot needs --udid to work")
def func_wrapper(client, *args, **kwargs): # pyre-ignore try: client.companion_info: CompanionInfo = client.direct_companion_manager.get_companion_info( target_udid=client.target_udid) client.logger.info(f"using companion {client.companion_info}") client.channel = Channel( client.companion_info.host, client.companion_info.port, loop=asyncio.get_event_loop(), ) client.stub: Optional[CompanionServiceStub] = CompanionServiceStub( channel=client.channel) return func(client, *args, **kwargs) except GRPCError as e: raise IdbException(e.message) from e # noqa B306 except (ProtocolError, StreamTerminatedError) as e: raise IdbException(e.args) from e
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), )
async def unix_domain_server(self, udid: str, path: str) -> AsyncContextManager[str]: async with self._start_companion_command( ["--udid", udid, "--grpc-domain-sock", path]) as process: line = await none_throws(process.stdout).readline() output = parse_json_line(line) grpc_path = output.get("grpc_path") if grpc_path is None: raise IdbException(f"No grpc_path in {line}") self._logger.info(f"Started domain sock server on {grpc_path}") yield grpc_path
def get_companion_info(self, target_udid: Optional[str]) -> CompanionInfo: self.load_companions() if target_udid: companions = [ companion for companion in self.companions if companion.udid == target_udid ] if len(companions) > 0: return companions[0] else: raise IdbException( f"Couldn't find companion for target with udid {target_udid}" ) elif len(self.companions) >= 1: companion = self.companions[0] self.logger.info(f"using default companion with udid {companion.udid}") return companion else: raise IdbException("No UDID provided and couldn't find a default companion")
async def _local_target_type(companion: Companion, udid: str) -> TargetType: if udid == "mac": return TargetType.MAC targets = { target.udid: target for target in await companion.list_targets(only=None) } target = targets.get(udid) if target is None: raise IdbException( f"Cannot spawn companion for {udid}, no matching target in available udids {targets.keys()}" ) return target.target_type
async def unix_domain_server( self, udid: str, path: str, only: Optional[OnlyFilter] = None ) -> AsyncGenerator[str, None]: async with self._start_companion_command( ["--udid", udid, "--grpc-domain-sock", path] + _only_arg_from_filter(only=only) ) as process: line = await none_throws(process.stdout).readline() output = parse_json_line(line) grpc_path = output.get("grpc_path") if grpc_path is None: raise IdbException(f"No grpc_path in {line}") self._logger.info(f"Started domain sock server on {grpc_path}") yield grpc_path
async def _start_companion_command( self, arguments: List[str] ) -> AsyncContextManager[asyncio.subprocess.Process]: companion_path = self.companion_path if companion_path is None: if platform == "darwin": raise IdbException("Companion path not provided") else: raise IdbException( "Companion interactions do not work on non-macOS platforms" ) cmd: List[str] = [companion_path] device_set_path = self.device_set_path if device_set_path is not None: cmd.extend(["--device-set-path", device_set_path]) cmd.extend(arguments) process = await asyncio.create_subprocess_exec(*cmd, stdout=subprocess.PIPE, stderr=None) try: yield process finally: await _terminate_process(process=process, logger=self.logger)
async def _open_lockfile(filename: str) -> AsyncGenerator[None, None]: timeout = 3 retry_time = 0.05 deadline = datetime.now() + timedelta(seconds=timeout) lock_path = filename + ".lock" lock = None try: while lock is None: try: lock = os.open(lock_path, os.O_CREAT | os.O_EXCL) yield None except FileExistsError: if datetime.now() >= deadline: raise IdbException("Failed to open the lockfile {lock_path}") await asyncio.sleep(retry_time) finally: if lock is not None: os.close(lock) os.unlink(lock_path)
async def connect( self, destination: ConnectionDestination, metadata: Optional[Dict[str, str]] = None, ) -> CompanionInfo: self._logger.debug( f"Connecting directly to {destination} with meta {metadata}") if isinstance(destination, TCPAddress) or isinstance( destination, DomainSocketAddress): async with Client.build(address=destination, logger=self._logger) as client: companion = client.companion self._logger.debug(f"Connected directly to {companion}") await self._companion_set.add_companion(companion) return companion else: companion = await self._spawn_companion_server(udid=destination) if companion: return companion else: raise IdbException(f"can't find target for udid {destination}")
async def install_dsym_test_bundle( self, args: Namespace, client: Client, test_bundle_location: str) -> Optional[str]: dsym_name = None dsym_path_location = args.install_dsym_test_bundle if args.install_dsym_test_bundle == NO_SPECIFIED_PATH: dsym_path_location = test_bundle_location + ".dSYM" if not os.path.exists(dsym_path_location): raise IdbException( "XCTest run failed! Error: --install-dsym flag was used but there is no file at location {}." .format(dsym_path_location)) async for install_response in client.install_dsym( dsym=dsym_path_location, bundle_id=args.test_bundle_id, bundle_type=FileContainerType.XCTEST, compression=None, ): if install_response.progress == 0.0: dsym_name = install_response.name return dsym_name
async def _spawn_companion_server(self, udid: str) -> CompanionInfo: companion = self._companion if companion is None: raise IdbException( f"Cannot spawn companion for {udid}, no companion executable") target_type = await _local_target_type(companion=companion, udid=udid) path = os.path.join(BASE_IDB_FILE_PATH, f"{udid}_companion.sock") address = DomainSocketAddress(path=path) self._logger.info( f"Checking whether domain sock {path} is bound for {udid}") is_bound = await _check_domain_socket_is_bound(path=path) if is_bound: self._logger.info( f"Domain socket {path} is bound for {udid}, connecting to it.") companion_info = await self.connect(destination=address) else: self._logger.info( f"No existing companion at {path}, spawning one...") process = await companion.spawn_domain_sock_server( config=CompanionServerConfig( udid=udid, only=target_type, log_file_path=None, cwd=None, tmp_path=None, reparent=True, ), path=path, ) self._logger.info(f"Companion at {path} spawned for {udid}") companion_info = CompanionInfo( address=address, udid=udid, is_local=True, pid=process.pid, ) await self._companion_set.add_companion(companion_info) return companion_info