Skip to content

Commit

Permalink
[Added] a lite wrapped version of pymobiledevice3
Browse files Browse the repository at this point in the history
  • Loading branch information
ihomway authored and xinzhengzhang committed Jan 26, 2024
1 parent 1b4a925 commit 1bb2ac3
Show file tree
Hide file tree
Showing 11 changed files with 1,677 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugin/pymobiledevicelite/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
common --enable_bzlmod
26 changes: 26 additions & 0 deletions plugin/pymobiledevicelite/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
This python program is a very simplified version of pymobiledevice3(https://github.com/doronz88/pymobiledevice3)
"""

load("@rules_python//python:defs.bzl", "py_binary")
load("@rules_python//python:pip.bzl", "compile_pip_requirements")
load("@my_deps//:requirements.bzl", "requirement")

compile_pip_requirements(
name = "requirements",
extra_args = [
"--allow-unsafe",
"--resolver=backtracking",
],
requirements_in = "requirements.in",
requirements_txt = "requirements_lock.txt",
)

py_binary(
name = "pymobiledevicelite",
srcs = ["pymobiledevicelite.py"],
visibility = ["//visibility:public"],
deps = [
requirement("pymobiledevice3"),WatchLaterWatchLater
],
)
23 changes: 23 additions & 0 deletions plugin/pymobiledevicelite/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""pymobiledeveice3 binary"""
module(
name = "pymobiledevice3",
version = "0.0.1",
)

bazel_dep(name = "rules_python", version = "0.25.0")

python = use_extension("@rules_python//python/extensions:python.bzl", "python")

python.toolchain(
configure_coverage_tool = True,
python_version = "3.10",
is_default = True,
)

pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip")
pip.parse(
hub_name = "my_deps",
python_version = "3.10",
requirements_lock = "//:requirements_lock.txt",
)
use_repo(pip, "my_deps")
Empty file.
85 changes: 85 additions & 0 deletions plugin/pymobiledevicelite/installationProxyService.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from pymobiledevice3.exceptions import AppInstallError
from pymobiledevice3.lockdown import LockdownClient
from pymobiledevice3.services.afc import AfcService
from pymobiledevice3.services.lockdown_service import LockdownService
from typing import Callable, List, Mapping
import click, os, json, posixpath

GET_APPS_ADDITIONAL_INFO = {'ReturnAttributes': ['CFBundleIdentifier', 'StaticDiskUsage', 'DynamicDiskUsage']}

class SimplifiedInstallationProxyService(LockdownService):
SERVICE_NAME = 'com.apple.mobile.installation_proxy'
RSD_SERVICE_NAME = 'com.apple.mobile.installation_proxy.shim.remote'

def __init__(self, lockdown: LockdownClient):
if isinstance(lockdown, LockdownClient):
super().__init__(lockdown, self.SERVICE_NAME)
else:
super().__init__(lockdown, self.RSD_SERVICE_NAME)

def _watch_completion(self, handler: Callable = None, *args) -> None:
while True:
response = self.service.recv_plist()
if not response:
break
error = response.get('Error')
if error:
raise AppInstallError(f'{error}: {response.get("ErrorDescription")}')
completion = response.get('PercentComplete')
if completion:
if handler:
self.logger.debug('calling handler')
handler(completion, *args)
self.logger.info(f'{response.get("PercentComplete")}% Complete')
if response.get('Status') == 'Complete':
return
raise AppInstallError()

def upgrade(self, ipa_path: str, options: Mapping = None, handler: Callable = None, *args) -> None:
""" upgrade given ipa from device path """
self.install_from_local(ipa_path, 'Upgrade', options, handler, args)

def install_from_local(self, ipa_path: str, cmd='Install', options: Mapping = None, handler: Callable = None,
*args) -> None:
""" upload given ipa onto device and install it """
if options is None:
options = {"PackageType": "Developer"}
remote_path = posixpath.join('/', os.path.basename(ipa_path))
with AfcService(self.lockdown) as afc:
afc.set_file_contents(remote_path, open(ipa_path, 'rb').read())
cmd = {'Command': cmd,
'ClientOptions': options,
'PackagePath': remote_path}
self.service.send_plist(cmd)
while True:
response = self.service.recv_plist()
if not response:
break
click.echo(json.dumps(response))
error = response.get('Error')
if error:
raise AppInstallError(f'{error}: {response.get("ErrorDescription")}')
if response.get('Status') == 'Complete':
return
raise AppInstallError()

def lookup(self, options: Mapping = None) -> Mapping:
""" search installation database """
if options is None:
options = {}
cmd = {'Command': 'Lookup', 'ClientOptions': options}
return self.service.send_recv_plist(cmd).get('LookupResult')

def get_apps(self, app_types: List[str] = None) -> Mapping[str, Mapping]:
""" get applications according to given criteria """
result = self.lookup()
# query for additional info
additional_info = self.lookup(GET_APPS_ADDITIONAL_INFO)
for bundle_identifier, app in additional_info.items():
result[bundle_identifier].update(app)
# filter results
filtered_result = {}
for bundle_identifier, app in result.items():
if (app_types is None) or (app['ApplicationType'] in app_types):
filtered_result[bundle_identifier] = app
return filtered_result
43 changes: 43 additions & 0 deletions plugin/pymobiledevicelite/main.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='main',
)
94 changes: 94 additions & 0 deletions plugin/pymobiledevicelite/pymobiledevicelite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import asyncio, click, json
from packaging.version import Version
from pymobiledevice3.exceptions import NoDeviceConnectedError
from pymobiledevice3.cli.cli_common import Command
from pymobiledevice3.cli.remote import get_device_list
from pymobiledevice3.lockdown import create_using_usbmux, LockdownClient
from pymobiledevice3.tcp_forwarder import LockdownTcpForwarder
from pymobiledevice3.usbmux import list_devices
from installationProxyService import SimplifiedInstallationProxyService
from typing import Optional
from untils import tunnel_task

@click.group()
def cli():
pass

@click.command()
def list_device():
""" list connected devices """
connected_devices = []
for device in list_devices():
udid = device.serial

lockdown = create_using_usbmux(udid, autopair=False, connection_type=device.connection_type)
connected_devices.append(lockdown.short_info)
json_str = json.dumps(connected_devices)
click.echo(json_str)

@click.command(cls=Command)
@click.argument('app_path', type=click.Path(exists=True))
def install_app(service_provider: LockdownClient, app_path: str):
""" install application from the specific path """
proxyService = SimplifiedInstallationProxyService(lockdown=service_provider)
proxyService.install_from_local(app_path)

@click.command(cls=Command)
@click.argument('local_port', type=click.INT, required=False)
def debug_server(service_provider: LockdownClient, local_port: Optional[int] = None):
"""
if local_port is provided, start a debugserver at remote listening on a given port locally.
if local_port is not provided and iOS version >= 17.0 then just print the connect string
Please note the connection must be done soon afterwards using your own lldb client.
This can be done using the following commands within lldb shell:
(lldb) platform select remote-ios
(lldb) platform connect connect://localhost:<local_port>
"""

if Version(service_provider.product_version) < Version('17.0'):
service_name = 'com.apple.debugserver.DVTSecureSocketProxy'
else:
service_name = 'com.apple.internal.dt.remote.debugproxy'

if local_port is not None:
click.echo(json.dumps({"host": '127.0.0.1', "port": local_port}))
LockdownTcpForwarder(service_provider, local_port, service_name).start()
elif Version(service_provider.product_version) >= Version('17.0'):
debugserver_port = service_provider.get_service_port(service_name)
click.echo(json.dumps({"host": service_provider.service.address[0], "port": debugserver_port}))
else:
click.BadOptionUsage('--local_port', 'local_port is required for iOS < 17.0')

@click.command(name='start-quic-tunnel')
@click.option('--udid', help='UDID for a specific device to look for')
def cli_start_quic_tunnel(udid: str):
""" start quic tunnel """
devices = get_device_list()
rsd = [device for device in devices if device.udid == udid]
if len(rsd) > 0:
rsd = rsd[0]
else:
raise NoDeviceConnectedError()

if udid is not None and rsd.udid != udid:
raise NoDeviceConnectedError()

asyncio.run(tunnel_task(rsd), debug=True)

@click.command(cls=Command)
def installed_app_path(service_provider: LockdownClient):
""" get installed app path """
result = SimplifiedInstallationProxyService(service_provider).get_apps(['User'])
click.echo(json.dumps(result))

cli.add_command(list_device)
cli.add_command(install_app)
cli.add_command(debug_server)
cli.add_command(cli_start_quic_tunnel)
cli.add_command(installed_app_path)

if __name__ == '__main__':
cli()
37 changes: 37 additions & 0 deletions plugin/pymobiledevicelite/pymobiledevicelite.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
['pymobiledevicelite.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='pymobiledevicelite',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch='universal2',
codesign_identity=None,
entitlements_file=None,
)
3 changes: 3 additions & 0 deletions plugin/pymobiledevicelite/requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--extra-index-url https://pypi.python.org/simple/

pymobiledevice3==2.17.2
Loading

0 comments on commit 1bb2ac3

Please sign in to comment.