Skip to content

Commit

Permalink
remotexpc: refactor to use asyncio
Browse files Browse the repository at this point in the history
Close #739
  • Loading branch information
doronz88 committed Apr 15, 2024
1 parent 6d2275c commit 251a261
Show file tree
Hide file tree
Showing 20 changed files with 463 additions and 310 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,14 @@ for line in SyslogService(service_provider=rsds[0]).watch():
# Or you could connect manually to a specific tunnel created by `start-tunnel`
host = 'fded:c26b:3d2f::1'
port = 65177
with RemoteServiceDiscoveryService((host, port)) as rsd:
async with RemoteServiceDiscoveryService((host, port)) as rsd:
# you can now use this connection as any other LockdownClient connection
pass
# Alternatively, you can use this API not in a context-manager
rsd = RemoteServiceDiscoveryService((host, port))
await rsd.connect()
await rsd.close()
```
## Example
Expand Down
3 changes: 2 additions & 1 deletion pymobiledevice3/bonjour.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ def query_bonjour(service_names: List[str], ip: str) -> BonjourQuery:
return BonjourQuery(aiozc, service_browser, listener)


async def browse(service_names: List[str], ips: List[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> List[BonjourAnswer]:
async def browse(service_names: List[str], ips: List[str], timeout: float = DEFAULT_BONJOUR_TIMEOUT) \
-> List[BonjourAnswer]:
bonjour_queries = [query_bonjour(service_names, adapter) for adapter in ips]
answers = []
await asyncio.sleep(timeout)
Expand Down
3 changes: 2 additions & 1 deletion pymobiledevice3/cli/cli_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
from pymobiledevice3.tunneld import get_tunneld_devices
from pymobiledevice3.usbmux import select_devices_by_connection_type
from pymobiledevice3.utils import get_asyncio_loop

USBMUX_OPTION_HELP = 'usbmuxd listener address (in the form of either /path/to/unix/socket OR HOST:PORT'
COLORED_OUTPUT = True
Expand Down Expand Up @@ -219,7 +220,7 @@ def __init__(self, *args, **kwargs):
def rsd(self, ctx, param: str, value: Optional[Tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
if value is not None:
rsd = RemoteServiceDiscoveryService(value)
rsd.connect()
get_asyncio_loop().run_until_complete(rsd.connect())
self.service_provider = rsd
return self.service_provider

Expand Down
127 changes: 88 additions & 39 deletions pymobiledevice3/cli/developer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# flake8: noqa: C901
import asyncio
import json
import logging
import os
Expand All @@ -10,7 +11,7 @@
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from typing import List, Optional, Tuple

import click
from click.exceptions import MissingParameter, UsageError
Expand All @@ -20,8 +21,8 @@
import pymobiledevice3
from pymobiledevice3.cli.cli_common import BASED_INT, Command, RSDCommand, default_json_encoder, print_json, \
user_requested_colored_output
from pymobiledevice3.exceptions import DeviceAlreadyInUseError, DvtDirListError, ExtractingStackshotError, \
RSDRequiredError, UnrecognizedSelectorError
from pymobiledevice3.exceptions import ArgumentError, DeviceAlreadyInUseError, DvtDirListError, \
ExtractingStackshotError, RSDRequiredError, UnrecognizedSelectorError
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
from pymobiledevice3.osu.os_utils import get_os_utils
Expand Down Expand Up @@ -687,20 +688,26 @@ def fetch_symbols():
pass


@fetch_symbols.command('list', cls=Command)
def fetch_symbols_list(service_provider: LockdownServiceProvider):
""" list of files to be downloaded """
async def fetch_symbols_list_task(service_provider: LockdownServiceProvider) -> None:
if Version(service_provider.product_version) < Version('17.0'):
print_json(DtFetchSymbols(service_provider).list_files())
else:
with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
print_json([f.file_path for f in fetch_symbols.get_dsc_file_list()])
if not isinstance(service_provider, RemoteServiceDiscoveryService):
raise ArgumentError('service_provider must be a RemoteServiceDiscoveryService for iOS 17+ devices')

async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
print_json([f.file_path for f in await fetch_symbols.get_dsc_file_list()])

@fetch_symbols.command('download', cls=Command)
@click.argument('out', type=click.Path(dir_okay=True, file_okay=False))
def fetch_symbols_download(service_provider: LockdownServiceProvider, out):
""" download the linker and dyld cache to a specified directory """

@fetch_symbols.command('list', cls=Command)
def fetch_symbols_list(service_provider: LockdownServiceProvider) -> None:
""" list of files to be downloaded """
asyncio.run(fetch_symbols_list_task(service_provider), debug=True)


async def fetch_symbols_download_task(service_provider: LockdownServiceProvider, out: str) -> None:
if not isinstance(service_provider, RemoteServiceDiscoveryService):
raise ArgumentError('service_provider must be a RemoteServiceDiscoveryService for iOS 17+ devices')

out = Path(out)
out.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -728,8 +735,15 @@ def fetch_symbols_download(service_provider: LockdownServiceProvider, out):
logger.info(f'writing to: {file}')
fetch_symbols.get_file(i, f)
else:
with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
fetch_symbols.download(out)
async with RemoteFetchSymbolsService(service_provider) as fetch_symbols:
await fetch_symbols.download(out)


@fetch_symbols.command('download', cls=Command)
@click.argument('out', type=click.Path(dir_okay=True, file_okay=False))
def fetch_symbols_download(service_provider: LockdownServiceProvider, out: str) -> None:
""" download the linker and dyld cache to a specified directory """
asyncio.run(fetch_symbols_download_task(service_provider, out), debug=True)


@developer.group('simulate-location')
Expand Down Expand Up @@ -1025,66 +1039,101 @@ def dvt_simulate_location_play(service_provider: LockdownClient, filename: str,


@developer.group()
def core_device():
def core_device() -> None:
""" core-device options """
pass


async def core_device_list_processes_task(service_provider: RemoteServiceDiscoveryService) -> None:
async with AppServiceService(service_provider) as app_service:
print_json(await app_service.list_processes())


@core_device.command('list-processes', cls=RSDCommand)
def core_device_list_processes(service_provider: RemoteServiceDiscoveryService):
def core_device_list_processes(service_provider: RemoteServiceDiscoveryService) -> None:
""" Get process list """
with AppServiceService(service_provider) as app_service:
print_json(app_service.list_processes())
asyncio.run(core_device_list_processes_task(service_provider))


async def core_device_uninstall_app_task(service_provider: RemoteServiceDiscoveryService,
bundle_identifier: str) -> None:
async with AppServiceService(service_provider) as app_service:
await app_service.uninstall_app(bundle_identifier)


@core_device.command('uninstall', cls=RSDCommand)
@click.argument('bundle_identifier')
def core_device_uninstall_app(service_provider: RemoteServiceDiscoveryService, bundle_identifier: str):
def core_device_uninstall_app(service_provider: RemoteServiceDiscoveryService, bundle_identifier: str) -> None:
""" Uninstall application """
with AppServiceService(service_provider) as app_service:
app_service.uninstall_app(bundle_identifier)
asyncio.run(core_device_uninstall_app_task(service_provider, bundle_identifier))


async def core_device_send_signal_to_process_task(
service_provider: RemoteServiceDiscoveryService, pid: int, signal: int) -> None:
async with AppServiceService(service_provider) as app_service:
print_json(await app_service.send_signal_to_process(pid, signal))


@core_device.command('send-signal-to-process', cls=RSDCommand)
@click.argument('pid', type=click.INT)
@click.argument('signal', type=click.INT)
def core_device_send_signal_to_process(service_provider: RemoteServiceDiscoveryService, pid: int, signal: int):
def core_device_send_signal_to_process(service_provider: RemoteServiceDiscoveryService, pid: int, signal: int) -> None:
""" Send signal to process """
with AppServiceService(service_provider) as app_service:
print_json(app_service.send_signal_to_process(pid, signal))
asyncio.run(core_device_send_signal_to_process_task(service_provider, pid, signal))


async def core_device_get_device_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
async with DeviceInfoService(service_provider) as app_service:
print_json(await app_service.get_device_info())


@core_device.command('get-device-info', cls=RSDCommand)
def core_device_get_device_info(service_provider: RemoteServiceDiscoveryService):
def core_device_get_device_info(service_provider: RemoteServiceDiscoveryService) -> None:
""" Get device information """
with DeviceInfoService(service_provider) as app_service:
print_json(app_service.get_device_info())
asyncio.run(core_device_get_device_info_task(service_provider))


async def core_device_get_display_info_task(service_provider: RemoteServiceDiscoveryService) -> None:
async with DeviceInfoService(service_provider) as app_service:
print_json(await app_service.get_display_info())


@core_device.command('get-display-info', cls=RSDCommand)
def core_device_get_display_info(service_provider: RemoteServiceDiscoveryService):
def core_device_get_display_info(service_provider: RemoteServiceDiscoveryService) -> None:
""" Get display information """
with DeviceInfoService(service_provider) as app_service:
print_json(app_service.get_display_info())
asyncio.run(core_device_get_display_info_task(service_provider))


async def core_device_query_mobilegestalt_task(service_provider: RemoteServiceDiscoveryService, key: List[str]) -> None:
""" Query MobileGestalt """
async with DeviceInfoService(service_provider) as app_service:
print_json(await app_service.query_mobilegestalt(key))


@core_device.command('query-mobilegestalt', cls=RSDCommand)
@click.argument('key', nargs=-1, type=click.STRING)
def core_device_query_mobilegestalt(service_provider: RemoteServiceDiscoveryService, key: List[str]):
def core_device_query_mobilegestalt(service_provider: RemoteServiceDiscoveryService, key: Tuple[str]) -> None:
""" Query MobileGestalt """
with DeviceInfoService(service_provider) as app_service:
print_json(app_service.query_mobilegestalt(list(key)))
asyncio.run(core_device_query_mobilegestalt_task(service_provider, list(key)))


async def core_device_get_lockstate_task(service_provider: RemoteServiceDiscoveryService) -> None:
async with DeviceInfoService(service_provider) as app_service:
print_json(await app_service.get_lockstate())


@core_device.command('get-lockstate', cls=RSDCommand)
def core_device_get_lockstate(service_provider: RemoteServiceDiscoveryService):
def core_device_get_lockstate(service_provider: RemoteServiceDiscoveryService) -> None:
""" Get lockstate """
with DeviceInfoService(service_provider) as app_service:
print_json(app_service.get_lockstate())
asyncio.run(core_device_get_lockstate_task(service_provider))


async def core_device_list_apps_task(service_provider: RemoteServiceDiscoveryService) -> None:
async with AppServiceService(service_provider) as app_service:
print_json(await app_service.list_apps())


@core_device.command('list-apps', cls=RSDCommand)
def core_device_list_apps(service_provider: RemoteServiceDiscoveryService):
def core_device_list_apps(service_provider: RemoteServiceDiscoveryService) -> None:
""" Get application list """
with AppServiceService(service_provider) as app_service:
print_json(app_service.list_apps())
asyncio.run(core_device_list_apps_task(service_provider))
38 changes: 19 additions & 19 deletions pymobiledevice3/remote/core_device/app_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,37 @@ class AppServiceService(CoreDeviceService):
def __init__(self, rsd: RemoteServiceDiscoveryService):
super().__init__(rsd, self.SERVICE_NAME)

def list_apps(self, include_app_clips: bool = True, include_removable_apps: bool = True,
include_hidden_apps: bool = True, include_internal_apps: bool = True,
include_default_apps: bool = True) -> List[Mapping]:
async def list_apps(self, include_app_clips: bool = True, include_removable_apps: bool = True,
include_hidden_apps: bool = True, include_internal_apps: bool = True,
include_default_apps: bool = True) -> List[Mapping]:
""" List applications """
return self.invoke('com.apple.coredevice.feature.listapps', {
return await self.invoke('com.apple.coredevice.feature.listapps', {
'includeAppClips': include_app_clips, 'includeRemovableApps': include_removable_apps,
'includeHiddenApps': include_hidden_apps, 'includeInternalApps': include_internal_apps,
'includeDefaultApps': include_default_apps})

def list_processes(self) -> List[Mapping]:
async def list_processes(self) -> List[Mapping]:
""" List processes """
return self.invoke('com.apple.coredevice.feature.listprocesses')['processTokens']
return (await self.invoke('com.apple.coredevice.feature.listprocesses'))['processTokens']

def list_roots(self) -> Mapping:
async def list_roots(self) -> Mapping:
"""
List roots.
Can only be performed on certain devices
"""
return self.invoke('com.apple.coredevice.feature.listroots', {
return await self.invoke('com.apple.coredevice.feature.listroots', {
'rootPoint': {
'relative': '/'
}})

def spawn_executable(self, executable: str, arguments: List[str]) -> Mapping:
async def spawn_executable(self, executable: str, arguments: List[str]) -> Mapping:
"""
Spawn given executable.
Can only be performed on certain devices
"""
return self.invoke('com.apple.coredevice.feature.spawnexecutable', {
return await self.invoke('com.apple.coredevice.feature.spawnexecutable', {
'executableItem': {
'url': {
'_0': {
Expand All @@ -67,36 +67,36 @@ def spawn_executable(self, executable: str, arguments: List[str]) -> Mapping:
},
})

def monitor_process_termination(self, pid: int) -> Mapping:
async def monitor_process_termination(self, pid: int) -> Mapping:
"""
Monitor process termination.
Can only be performed on certain devices
"""
return self.invoke('com.apple.coredevice.feature.monitorprocesstermination', {
return await self.invoke('com.apple.coredevice.feature.monitorprocesstermination', {
'processToken': {'processIdentifier': XpcInt64Type(pid)}})

def uninstall_app(self, bundle_identifier: str) -> None:
async def uninstall_app(self, bundle_identifier: str) -> None:
"""
Uninstall given application by its bundle identifier
"""
self.invoke('com.apple.coredevice.feature.uninstallapp', {'bundleIdentifier': bundle_identifier})
await self.invoke('com.apple.coredevice.feature.uninstallapp', {'bundleIdentifier': bundle_identifier})

def send_signal_to_process(self, pid: int, signal: int) -> Mapping:
async def send_signal_to_process(self, pid: int, signal: int) -> Mapping:
"""
Send signal to given process by its pid
"""
return self.invoke('com.apple.coredevice.feature.sendsignaltoprocess', {
return await self.invoke('com.apple.coredevice.feature.sendsignaltoprocess', {
'process': {'processIdentifier': XpcInt64Type(pid)},
'signal': XpcInt64Type(signal),
})

def fetch_icons(self, bundle_identifier: str, width: float, height: float, scale: float,
allow_placeholder: bool) -> Mapping:
async def fetch_icons(self, bundle_identifier: str, width: float, height: float, scale: float,
allow_placeholder: bool) -> Mapping:
"""
Fetch given application's icons
"""
return self.invoke('com.apple.coredevice.feature.fetchappicons', {
return await self.invoke('com.apple.coredevice.feature.fetchappicons', {
'width': width,
'height': height,
'scale': scale,
Expand Down
4 changes: 2 additions & 2 deletions pymobiledevice3/remote/core_device/core_device_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ def _generate_core_device_version_dict(version: str) -> Mapping:


class CoreDeviceService(RemoteService):
def invoke(self, feature_identifier: str, input_: Mapping = None) -> Any:
async def invoke(self, feature_identifier: str, input_: Mapping = None) -> Any:
if input_ is None:
input_ = {}
response = self.service.send_receive_request({
response = await self.service.send_receive_request({
'CoreDevice.CoreDeviceDDIProtocolVersion': XpcInt64Type(0),
'CoreDevice.action': {},
'CoreDevice.coreDeviceVersion': CORE_DEVICE_VERSION,
Expand Down
16 changes: 8 additions & 8 deletions pymobiledevice3/remote/core_device/device_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,28 @@ class DeviceInfoService(CoreDeviceService):
def __init__(self, rsd: RemoteServiceDiscoveryService):
super().__init__(rsd, self.SERVICE_NAME)

def get_device_info(self) -> Mapping:
async def get_device_info(self) -> Mapping:
"""
Get device information
"""
return self.invoke('com.apple.coredevice.feature.getdeviceinfo', {})
return await self.invoke('com.apple.coredevice.feature.getdeviceinfo', {})

def get_display_info(self) -> Mapping:
async def get_display_info(self) -> Mapping:
"""
Get display information
"""
return self.invoke('com.apple.coredevice.feature.getdisplayinfo', {})
return await self.invoke('com.apple.coredevice.feature.getdisplayinfo', {})

def query_mobilegestalt(self, keys: List[str]) -> Mapping:
async def query_mobilegestalt(self, keys: List[str]) -> Mapping:
"""
Query MobileGestalt.
Can only be performed to specific devices
"""
return self.invoke('com.apple.coredevice.feature.querymobilegestalt', {'keys': keys})
return await self.invoke('com.apple.coredevice.feature.querymobilegestalt', {'keys': keys})

def get_lockstate(self) -> Mapping:
async def get_lockstate(self) -> Mapping:
"""
Get lockstate
"""
return self.invoke('com.apple.coredevice.feature.getlockstate', {})
return await self.invoke('com.apple.coredevice.feature.getlockstate', {})
Loading

0 comments on commit 251a261

Please sign in to comment.