pymobiledevice3 4.14.6__py3-none-any.whl → 7.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. misc/plist_sniffer.py +15 -15
  2. misc/remotexpc_sniffer.py +29 -28
  3. misc/understanding_idevice_protocol_layers.md +15 -10
  4. pymobiledevice3/__main__.py +317 -127
  5. pymobiledevice3/_version.py +22 -4
  6. pymobiledevice3/bonjour.py +358 -113
  7. pymobiledevice3/ca.py +253 -16
  8. pymobiledevice3/cli/activation.py +31 -23
  9. pymobiledevice3/cli/afc.py +49 -40
  10. pymobiledevice3/cli/amfi.py +16 -21
  11. pymobiledevice3/cli/apps.py +87 -42
  12. pymobiledevice3/cli/backup.py +160 -90
  13. pymobiledevice3/cli/bonjour.py +44 -40
  14. pymobiledevice3/cli/cli_common.py +204 -198
  15. pymobiledevice3/cli/companion_proxy.py +14 -14
  16. pymobiledevice3/cli/crash.py +105 -56
  17. pymobiledevice3/cli/developer/__init__.py +62 -0
  18. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  19. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  20. pymobiledevice3/cli/developer/arbitration.py +50 -0
  21. pymobiledevice3/cli/developer/condition.py +33 -0
  22. pymobiledevice3/cli/developer/core_device.py +294 -0
  23. pymobiledevice3/cli/developer/debugserver.py +244 -0
  24. pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
  25. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  26. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  27. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  28. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  29. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  30. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  31. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  32. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  33. pymobiledevice3/cli/idam.py +42 -0
  34. pymobiledevice3/cli/lockdown.py +108 -103
  35. pymobiledevice3/cli/mounter.py +158 -99
  36. pymobiledevice3/cli/notification.py +38 -26
  37. pymobiledevice3/cli/pcap.py +45 -24
  38. pymobiledevice3/cli/power_assertion.py +18 -17
  39. pymobiledevice3/cli/processes.py +17 -23
  40. pymobiledevice3/cli/profile.py +165 -109
  41. pymobiledevice3/cli/provision.py +35 -34
  42. pymobiledevice3/cli/remote.py +217 -129
  43. pymobiledevice3/cli/restore.py +159 -143
  44. pymobiledevice3/cli/springboard.py +63 -53
  45. pymobiledevice3/cli/syslog.py +193 -86
  46. pymobiledevice3/cli/usbmux.py +73 -33
  47. pymobiledevice3/cli/version.py +5 -7
  48. pymobiledevice3/cli/webinspector.py +376 -214
  49. pymobiledevice3/common.py +3 -1
  50. pymobiledevice3/exceptions.py +182 -58
  51. pymobiledevice3/irecv.py +52 -53
  52. pymobiledevice3/irecv_devices.py +1489 -464
  53. pymobiledevice3/lockdown.py +473 -275
  54. pymobiledevice3/lockdown_service_provider.py +15 -8
  55. pymobiledevice3/osu/os_utils.py +27 -9
  56. pymobiledevice3/osu/posix_util.py +34 -15
  57. pymobiledevice3/osu/win_util.py +14 -8
  58. pymobiledevice3/pair_records.py +102 -21
  59. pymobiledevice3/remote/common.py +8 -4
  60. pymobiledevice3/remote/core_device/app_service.py +94 -67
  61. pymobiledevice3/remote/core_device/core_device_service.py +17 -14
  62. pymobiledevice3/remote/core_device/device_info.py +5 -5
  63. pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
  64. pymobiledevice3/remote/core_device/file_service.py +53 -23
  65. pymobiledevice3/remote/remote_service_discovery.py +79 -45
  66. pymobiledevice3/remote/remotexpc.py +73 -44
  67. pymobiledevice3/remote/tunnel_service.py +442 -317
  68. pymobiledevice3/remote/utils.py +14 -13
  69. pymobiledevice3/remote/xpc_message.py +145 -125
  70. pymobiledevice3/resources/dsc_uuid_map.py +19 -19
  71. pymobiledevice3/resources/firmware_notifications.py +20 -16
  72. pymobiledevice3/resources/notifications.txt +144 -0
  73. pymobiledevice3/restore/asr.py +27 -27
  74. pymobiledevice3/restore/base_restore.py +110 -21
  75. pymobiledevice3/restore/consts.py +87 -66
  76. pymobiledevice3/restore/device.py +59 -12
  77. pymobiledevice3/restore/fdr.py +46 -48
  78. pymobiledevice3/restore/ftab.py +19 -19
  79. pymobiledevice3/restore/img4.py +163 -0
  80. pymobiledevice3/restore/mbn.py +587 -0
  81. pymobiledevice3/restore/recovery.py +151 -151
  82. pymobiledevice3/restore/restore.py +562 -544
  83. pymobiledevice3/restore/restore_options.py +131 -110
  84. pymobiledevice3/restore/restored_client.py +51 -31
  85. pymobiledevice3/restore/tss.py +385 -267
  86. pymobiledevice3/service_connection.py +252 -59
  87. pymobiledevice3/services/accessibilityaudit.py +202 -120
  88. pymobiledevice3/services/afc.py +962 -365
  89. pymobiledevice3/services/amfi.py +24 -30
  90. pymobiledevice3/services/companion.py +23 -19
  91. pymobiledevice3/services/crash_reports.py +71 -47
  92. pymobiledevice3/services/debugserver_applist.py +3 -3
  93. pymobiledevice3/services/device_arbitration.py +8 -8
  94. pymobiledevice3/services/device_link.py +101 -79
  95. pymobiledevice3/services/diagnostics.py +973 -967
  96. pymobiledevice3/services/dtfetchsymbols.py +8 -8
  97. pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
  98. pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
  99. pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
  100. pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
  101. pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
  102. pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
  103. pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
  104. pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
  105. pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
  106. pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
  107. pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
  108. pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
  109. pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
  110. pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
  111. pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
  112. pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
  113. pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
  114. pymobiledevice3/services/file_relay.py +10 -10
  115. pymobiledevice3/services/heartbeat.py +9 -8
  116. pymobiledevice3/services/house_arrest.py +16 -15
  117. pymobiledevice3/services/idam.py +20 -0
  118. pymobiledevice3/services/installation_proxy.py +173 -81
  119. pymobiledevice3/services/lockdown_service.py +20 -10
  120. pymobiledevice3/services/misagent.py +22 -19
  121. pymobiledevice3/services/mobile_activation.py +147 -64
  122. pymobiledevice3/services/mobile_config.py +331 -294
  123. pymobiledevice3/services/mobile_image_mounter.py +141 -113
  124. pymobiledevice3/services/mobilebackup2.py +203 -145
  125. pymobiledevice3/services/notification_proxy.py +11 -11
  126. pymobiledevice3/services/os_trace.py +134 -74
  127. pymobiledevice3/services/pcapd.py +314 -302
  128. pymobiledevice3/services/power_assertion.py +10 -9
  129. pymobiledevice3/services/preboard.py +4 -4
  130. pymobiledevice3/services/remote_fetch_symbols.py +21 -14
  131. pymobiledevice3/services/remote_server.py +176 -146
  132. pymobiledevice3/services/restore_service.py +16 -16
  133. pymobiledevice3/services/screenshot.py +15 -12
  134. pymobiledevice3/services/simulate_location.py +7 -7
  135. pymobiledevice3/services/springboard.py +15 -15
  136. pymobiledevice3/services/syslog.py +5 -5
  137. pymobiledevice3/services/web_protocol/alert.py +11 -11
  138. pymobiledevice3/services/web_protocol/automation_session.py +251 -239
  139. pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
  140. pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
  141. pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
  142. pymobiledevice3/services/web_protocol/driver.py +114 -111
  143. pymobiledevice3/services/web_protocol/element.py +124 -111
  144. pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
  145. pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
  146. pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
  147. pymobiledevice3/services/web_protocol/switch_to.py +30 -27
  148. pymobiledevice3/services/webinspector.py +189 -155
  149. pymobiledevice3/tcp_forwarder.py +87 -69
  150. pymobiledevice3/tunneld/__init__.py +0 -0
  151. pymobiledevice3/tunneld/api.py +63 -0
  152. pymobiledevice3/tunneld/server.py +603 -0
  153. pymobiledevice3/usbmux.py +198 -147
  154. pymobiledevice3/utils.py +14 -11
  155. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
  156. pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
  157. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
  158. pymobiledevice3/cli/developer.py +0 -1215
  159. pymobiledevice3/cli/diagnostics.py +0 -99
  160. pymobiledevice3/tunneld.py +0 -524
  161. pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
  162. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
  163. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
  164. {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
@@ -1,22 +1,23 @@
1
1
  import asyncio
2
2
  import contextlib
3
3
  import logging
4
- import os
5
4
  import plistlib
6
5
  import tempfile
7
6
  import traceback
8
- from collections.abc import Generator
7
+ from collections.abc import Iterator
9
8
  from pathlib import Path
10
- from typing import IO, Optional, Union
9
+ from typing import IO, Annotated, Optional
11
10
  from zipfile import ZipFile
12
11
 
13
12
  import click
14
13
  import IPython
15
14
  import requests
15
+ import typer
16
16
  from pygments import formatters, highlight, lexers
17
+ from typer_injector import Depends, InjectingTyper
17
18
 
18
19
  from pymobiledevice3 import usbmux
19
- from pymobiledevice3.cli.cli_common import print_json, prompt_selection, set_verbosity
20
+ from pymobiledevice3.cli.cli_common import is_invoked_for_completion, print_json, prompt_selection
20
21
  from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, IncorrectModeError
21
22
  from pymobiledevice3.irecv import IRecv
22
23
  from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
@@ -26,107 +27,132 @@ from pymobiledevice3.restore.restore import Restore
26
27
  from pymobiledevice3.services.diagnostics import DiagnosticsService
27
28
  from pymobiledevice3.utils import file_download
28
29
 
30
+ logger = logging.getLogger(__name__)
31
+
32
+
29
33
  SHELL_USAGE = """
30
34
  # use `irecv` variable to access Restore mode API
31
35
  # for example:
32
36
  print(irecv.getenv('build-version'))
33
37
  """
38
+ IPSWME_API = "https://api.ipsw.me/v4/device/"
39
+
40
+
41
+ cli = InjectingTyper(
42
+ name="restore",
43
+ help="Restore/erase IPSWs, fetch blobs, and manage devices in Recovery/DFU.",
44
+ no_args_is_help=True,
45
+ )
46
+
47
+
48
+ def device_dependency(
49
+ ecid: Annotated[
50
+ Optional[str],
51
+ typer.Option(
52
+ help="Target device ECID; defaults to the first connected USB device or waits for Recovery/DFU.",
53
+ ),
54
+ ] = None,
55
+ ) -> Optional[Device]:
56
+ if is_invoked_for_completion():
57
+ # prevent lockdown connection establishment when in autocomplete mode
58
+ return None
59
+
60
+ logger.debug("searching among connected devices via lockdownd")
61
+ devices = [dev for dev in usbmux.list_devices() if dev.connection_type == "USB"]
62
+ if len(devices) > 1:
63
+ raise click.ClickException("Multiple device detected")
64
+ try:
65
+ for device in devices:
66
+ try:
67
+ lockdown = create_using_usbmux(serial=device.serial, connection_type="USB")
68
+ except (ConnectionFailedError, IncorrectModeError):
69
+ continue
70
+ if (ecid is None) or (lockdown.ecid == ecid):
71
+ logger.debug("found device")
72
+ return Device(lockdown=lockdown)
73
+ else:
74
+ continue
75
+ except ConnectionFailedToUsbmuxdError:
76
+ pass
34
77
 
35
- logger = logging.getLogger(__name__)
36
- IPSWME_API = 'https://api.ipsw.me/v4/device/'
37
-
38
-
39
- class Command(click.Command):
40
- def __init__(self, *args, **kwargs):
41
- super().__init__(*args, **kwargs)
42
- self.params[:0] = [
43
- click.Option(('device', '--ecid'), type=click.INT, callback=self.device),
44
- click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False),
45
- ]
46
-
47
- @staticmethod
48
- def device(ctx, param, value) -> Optional[Union[LockdownClient, IRecv]]:
49
- if '_PYMOBILEDEVICE3_COMPLETE' in os.environ:
50
- # prevent lockdown connection establishment when in autocomplete mode
51
- return
52
-
53
- ecid = value
54
- logger.debug('searching among connected devices via lockdownd')
55
- devices = [dev for dev in usbmux.list_devices() if dev.connection_type == 'USB']
56
- if len(devices) > 1:
57
- raise click.ClickException('Multiple device detected')
58
- try:
59
- for device in devices:
60
- try:
61
- lockdown = create_using_usbmux(serial=device.serial, connection_type='USB')
62
- except (ConnectionFailedError, IncorrectModeError):
63
- continue
64
- if (ecid is None) or (lockdown.ecid == value):
65
- logger.debug('found device')
66
- return lockdown
67
- else:
68
- continue
69
- except ConnectionFailedToUsbmuxdError:
70
- pass
71
-
72
- logger.debug('waiting for device to be available in Recovery mode')
73
- return IRecv(ecid=ecid)
78
+ logger.debug("waiting for device to be available in Recovery mode")
79
+ return Device(irecv=IRecv(ecid=ecid))
80
+
81
+
82
+ DeviceDep = Annotated[
83
+ Device,
84
+ Depends(device_dependency),
85
+ ]
74
86
 
75
87
 
76
88
  @contextlib.contextmanager
77
- def tempzip_download_ctx(url: str) -> Generator[ZipFile, None, None]:
89
+ def tempzip_download_ctx(url: str) -> Iterator[ZipFile]:
78
90
  with tempfile.TemporaryDirectory() as tmpdir:
79
- tmpzip = Path(tmpdir) / url.split('/')[-1]
91
+ tmpzip = Path(tmpdir) / url.split("/")[-1]
80
92
  file_download(url, tmpzip)
81
93
  yield ZipFile(tmpzip)
82
94
 
83
95
 
84
96
  @contextlib.contextmanager
85
- def zipfile_ctx(path: str) -> Generator[ZipFile, None, None]:
97
+ def zipfile_ctx(path: str) -> Iterator[ZipFile]:
86
98
  yield ZipFile(path)
87
99
 
88
100
 
89
- class IPSWCommand(Command):
90
- def __init__(self, *args, **kwargs):
91
- super().__init__(*args, **kwargs)
92
- self.params.extend([click.Option(('ipsw_ctx', '-i', '--ipsw'), required=False,
93
- callback=self.ipsw_ctx, help='local IPSW file'),
94
- click.Option(('tss', '--tss'), type=click.File('rb'), callback=self.tss)])
101
+ def ipsw_ctx_dependency(
102
+ device: DeviceDep,
103
+ ipsw: Annotated[
104
+ Optional[str],
105
+ typer.Option(
106
+ "--ipsw",
107
+ "-i",
108
+ help="Path or URL to an IPSW. If omitted, choose a signed build interactively.",
109
+ ),
110
+ ] = None,
111
+ ) -> contextlib.AbstractContextManager[ZipFile]:
112
+ if ipsw and not ipsw.startswith(("http://", "https://")):
113
+ return zipfile_ctx(ipsw)
95
114
 
96
- @staticmethod
97
- def ipsw_ctx(ctx, param, value) -> Generator[ZipFile, None, None]:
98
- if value and not value.startswith(('http://', 'https://')):
99
- return zipfile_ctx(value)
115
+ url = ipsw
116
+ if url is None:
117
+ url = query_ipswme(device.product_type)
118
+ return tempzip_download_ctx(url)
100
119
 
101
- url = value
102
- if url is None:
103
- url = query_ipswme(ctx.params['device'].product_type)
104
- return tempzip_download_ctx(url)
105
120
 
106
- @staticmethod
107
- def tss(ctx, param, value) -> Optional[IO]:
108
- if value is None:
109
- return
110
- return plistlib.load(value)
121
+ IPSWCtxDep = Annotated[
122
+ contextlib.AbstractContextManager[ZipFile],
123
+ Depends(ipsw_ctx_dependency),
124
+ ]
111
125
 
112
126
 
113
- def query_ipswme(identifier: str) -> str:
114
- resp = requests.get(IPSWME_API + identifier, headers={'Accept': 'application/json'})
115
- firmwares = resp.json()['firmwares']
116
- display_list = [f'{entry["version"]}: {entry["buildid"]}' for entry in firmwares if entry['signed']]
117
- idx = prompt_selection(display_list, 'Choose version', idx=True)
118
- return firmwares[idx]['url']
127
+ def tss_dependency(
128
+ tss: Annotated[
129
+ Optional[Path],
130
+ typer.Option(help="Path to SHSH blob plist to use for signing requests."),
131
+ ] = None,
132
+ ) -> None:
133
+ if tss is None:
134
+ return
135
+ with tss.open("rb") as tss_file:
136
+ return plistlib.load(tss_file)
119
137
 
120
138
 
121
- async def restore_update_task(device: Device, ipsw: ZipFile, tss: Optional[IO], erase: bool, ignore_fdr: bool) -> None:
122
- lockdown = None
123
- irecv = None
124
- if isinstance(device, LockdownClient):
125
- lockdown = device
126
- elif isinstance(device, IRecv):
127
- irecv = device
128
- device = Device(lockdown=lockdown, irecv=irecv)
139
+ TSSDep = Annotated[
140
+ Optional[dict],
141
+ Depends(tss_dependency),
142
+ ]
129
143
 
144
+
145
+ def query_ipswme(identifier: str) -> str:
146
+ resp = requests.get(IPSWME_API + identifier, headers={"Accept": "application/json"})
147
+ firmwares = resp.json()["firmwares"]
148
+ display_list = [f"{entry['version']}: {entry['buildid']}" for entry in firmwares if entry["signed"]]
149
+ idx = prompt_selection(display_list, "Choose version", idx=True)
150
+ return firmwares[idx]["url"]
151
+
152
+
153
+ async def restore_update_task(
154
+ device: Device, ipsw: ZipFile, tss: Optional[dict], erase: bool, ignore_fdr: bool
155
+ ) -> None:
130
156
  behavior = Behavior.Update
131
157
  if erase:
132
158
  behavior = Behavior.Erase
@@ -139,98 +165,88 @@ async def restore_update_task(device: Device, ipsw: ZipFile, tss: Optional[IO],
139
165
  raise
140
166
 
141
167
 
142
- @click.group()
143
- def cli() -> None:
144
- pass
145
-
146
-
147
- @cli.group()
148
- def restore() -> None:
149
- """ Restore an IPSW or access device in recovery mode """
150
- pass
151
-
152
-
153
- @restore.command('shell', cls=Command)
154
- def restore_shell(device):
155
- """ create an IPython shell for interacting with iBoot """
168
+ @cli.command("shell")
169
+ def restore_shell(device: DeviceDep) -> None:
170
+ """create an IPython shell for interacting with iBoot"""
156
171
  IPython.embed(
157
- header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style='native')),
172
+ header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style="native")),
158
173
  user_ns={
159
- 'irecv': device,
160
- })
174
+ "irecv": device,
175
+ },
176
+ )
161
177
 
162
178
 
163
- @restore.command('enter', cls=Command)
164
- def restore_enter(device):
165
- """ enter Recovery mode """
179
+ @cli.command("enter")
180
+ def restore_enter(device: DeviceDep) -> None:
181
+ """enter Recovery mode"""
166
182
  if isinstance(device, LockdownClient):
167
183
  device.enter_recovery()
168
184
 
169
185
 
170
- @restore.command('exit')
171
- def restore_exit():
172
- """ exit Recovery mode """
186
+ @cli.command("exit")
187
+ def restore_exit() -> None:
188
+ """exit Recovery mode"""
173
189
  irecv = IRecv()
174
190
  irecv.set_autoboot(True)
175
191
  irecv.reboot()
176
192
 
177
193
 
178
- @restore.command('restart', cls=Command)
179
- def restore_restart(device):
180
- """ restarts device """
181
- if isinstance(device, LockdownClient):
182
- with DiagnosticsService(device) as diagnostics:
194
+ @cli.command("restart")
195
+ def restore_restart(device: DeviceDep) -> None:
196
+ """restarts device"""
197
+ if device.is_lockdown:
198
+ with DiagnosticsService(device.lockdown) as diagnostics:
183
199
  diagnostics.restart()
184
200
  else:
185
- device.reboot()
201
+ device.irecv.reboot()
186
202
 
187
203
 
188
- @restore.command('tss', cls=IPSWCommand)
189
- @click.argument('out', type=click.File('wb'), required=False)
190
- def restore_tss(device: Device, ipsw_ctx: Generator, out):
191
- """ query SHSH blobs """
192
- lockdown = None
193
- irecv = None
194
- if isinstance(device, LockdownClient):
195
- lockdown = device
196
- elif isinstance(device, IRecv):
197
- irecv = device
198
-
199
- device = Device(lockdown=lockdown, irecv=irecv)
204
+ async def restore_tss_task(
205
+ device: Device, ipsw_ctx: contextlib.AbstractContextManager[ZipFile], out: Optional[IO]
206
+ ) -> None:
200
207
  with ipsw_ctx as ipsw:
201
- tss = Recovery(ipsw, device).fetch_tss_record()
208
+ tss = await Recovery(ipsw, device).fetch_tss_record()
202
209
  if out:
203
210
  plistlib.dump(tss, out)
204
211
  print_json(tss)
205
212
 
206
213
 
207
- @restore.command('ramdisk', cls=IPSWCommand)
208
- def restore_ramdisk(device: Device, ipsw_ctx: Generator, tss: IO):
209
- """
210
- don't perform an actual restore. just enter the update ramdisk
214
+ @cli.command("tss")
215
+ def restore_tss(device: DeviceDep, ipsw_ctx: IPSWCtxDep, out: Optional[Path] = None) -> None:
216
+ """query SHSH blobs"""
217
+ with out.open("wb") if out else contextlib.nullcontext() as out_file:
218
+ asyncio.run(restore_tss_task(device, ipsw_ctx, out_file), debug=True)
211
219
 
212
- ipsw can be either a filename or an url
213
- """
214
- lockdown = None
215
- irecv = None
216
- if isinstance(device, LockdownClient):
217
- lockdown = device
218
- elif isinstance(device, IRecv):
219
- irecv = device
220
- device = Device(lockdown=lockdown, irecv=irecv)
220
+
221
+ async def restore_ramdisk_task(device: Device, ipsw_ctx: contextlib.AbstractContextManager[ZipFile]) -> None:
221
222
  with ipsw_ctx as ipsw:
222
- Recovery(ipsw, device, tss=tss).boot_ramdisk()
223
+ await Recovery(ipsw, device).boot_ramdisk()
223
224
 
224
225
 
225
- @restore.command('update', cls=IPSWCommand)
226
- @click.option('--erase', is_flag=True, help='use the Erase BuildIdentity (full factory-reset)')
227
- @click.option('--ignore-fdr', is_flag=True, help='only establish an FDR service connection, but don\'t proxy any '
228
- 'traffic')
229
- def restore_update(device: Device, ipsw_ctx: Generator, tss: IO, erase: bool, ignore_fdr: bool) -> None:
226
+ @cli.command("ramdisk")
227
+ def restore_ramdisk(device: DeviceDep, ipsw_ctx: IPSWCtxDep) -> None:
230
228
  """
231
- perform an update
232
-
233
- ipsw can be either a filename or an url
229
+ Boot only the update ramdisk without performing a restore (IPSW path or URL accepted).
230
+ """
231
+ asyncio.run(restore_ramdisk_task(device, ipsw_ctx), debug=True)
232
+
233
+
234
+ @cli.command("update")
235
+ def restore_update(
236
+ device: DeviceDep,
237
+ ipsw_ctx: IPSWCtxDep,
238
+ tss: TSSDep,
239
+ erase: Annotated[
240
+ bool,
241
+ typer.Option(help="Erase and restore (factory reset) instead of updating in place."),
242
+ ] = False,
243
+ ignore_fdr: Annotated[
244
+ bool,
245
+ typer.Option(help="Connect to the FDR service only (debug mode; no traffic proxying)."),
246
+ ] = False,
247
+ ) -> None:
248
+ """
249
+ Update or restore the device using an IPSW (local path or URL).
234
250
  """
235
251
  with ipsw_ctx as ipsw:
236
252
  asyncio.run(restore_update_task(device, ipsw, tss, erase, ignore_fdr), debug=True)
@@ -1,80 +1,90 @@
1
- from typing import IO
1
+ from pathlib import Path
2
+ from typing import Annotated, Literal
2
3
 
3
- import click
4
4
  import IPython
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
- from pymobiledevice3.cli.cli_common import Command, print_json
7
- from pymobiledevice3.lockdown import LockdownClient
8
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
8
9
  from pymobiledevice3.services.springboard import SpringBoardServicesService
9
10
 
10
- SHELL_USAGE = '''
11
+ SHELL_USAGE = """
11
12
  Use `service` to access the service features
12
- '''
13
+ """
13
14
 
14
15
 
15
- @click.group()
16
- def cli():
17
- pass
16
+ cli = InjectingTyper(
17
+ name="springboard",
18
+ help="Interact with SpringBoard UI (icons, wallpapers, orientation, shell).",
19
+ no_args_is_help=True,
20
+ )
21
+ state_cli = InjectingTyper(
22
+ name="state",
23
+ help="Icon state operations.",
24
+ no_args_is_help=True,
25
+ )
26
+ cli.add_typer(state_cli)
18
27
 
19
28
 
20
- @cli.group()
21
- def springboard():
22
- """ Access device UI """
23
- pass
24
-
25
-
26
- @springboard.group()
27
- def state():
28
- """ icons state options """
29
- pass
30
-
31
-
32
- @state.command('get', cls=Command)
33
- def state_get(service_provider: LockdownClient):
34
- """ get icon state """
29
+ @state_cli.command("get")
30
+ def state_get(service_provider: ServiceProviderDep) -> None:
31
+ """Fetch the current icon layout/state."""
35
32
  print_json(SpringBoardServicesService(lockdown=service_provider).get_icon_state())
36
33
 
37
34
 
38
- @springboard.command('shell', cls=Command)
39
- def springboard_shell(service_provider: LockdownClient):
40
- """ open a shell to communicate with SpringBoardServicesService """
35
+ @cli.command("shell")
36
+ def springboard_shell(service_provider: ServiceProviderDep) -> None:
37
+ """Open an IPython shell bound to SpringBoardServicesService."""
41
38
  service = SpringBoardServicesService(lockdown=service_provider)
42
39
  IPython.embed(
43
40
  header=SHELL_USAGE,
44
41
  user_ns={
45
- 'service': service,
46
- })
42
+ "service": service,
43
+ },
44
+ )
47
45
 
48
46
 
49
- @springboard.command('icon', cls=Command)
50
- @click.argument('bundle_id')
51
- @click.argument('out', type=click.File('wb'))
52
- def springboard_icon(service_provider: LockdownClient, bundle_id, out):
53
- """ get application's icon """
54
- out.write(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
47
+ @cli.command("icon")
48
+ def springboard_icon(service_provider: ServiceProviderDep, bundle_id: str, out: Path) -> None:
49
+ """Save an app's icon PNG to the given path."""
50
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
55
51
 
56
52
 
57
- @springboard.command('orientation', cls=Command)
58
- def springboard_orientation(service_provider: LockdownClient):
59
- """ get screen orientation """
53
+ @cli.command("orientation")
54
+ def springboard_orientation(service_provider: ServiceProviderDep) -> None:
55
+ """Print current screen orientation."""
60
56
  print(SpringBoardServicesService(lockdown=service_provider).get_interface_orientation())
61
57
 
62
58
 
63
- @springboard.command('wallpaper-home-screen', cls=Command)
64
- @click.argument('out', type=click.File('wb'))
65
- def springboard_wallpaper_home_screen(service_provider: LockdownClient, out: IO) -> None:
66
- """ get homescreen wallpaper """
67
- out.write(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
68
-
69
-
70
- @springboard.command('wallpaper-preview-image', cls=Command)
71
- @click.argument('wallpaper-name', type=click.Choice(['homescreen', 'lockscreen']))
72
- @click.argument('out', type=click.File('wb'))
73
- @click.option('-r', '--reload', is_flag=True, help='reload icon state before fetching image')
74
- def springboard_wallpaper_preview_image(service_provider: LockdownClient, wallpaper_name: str, out: IO,
75
- reload: bool) -> None:
76
- """ get the preview image of either the homescreen or the lockscreen """
59
+ @cli.command("wallpaper-home-screen")
60
+ def springboard_wallpaper_home_screen(service_provider: ServiceProviderDep, out: Path) -> None:
61
+ """Save the homescreen wallpaper PNG to the given path."""
62
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
63
+
64
+
65
+ @cli.command("wallpaper-preview-image")
66
+ def springboard_wallpaper_preview_image(
67
+ service_provider: ServiceProviderDep,
68
+ wallpaper_name: Literal["homescreen", "lockscreen"],
69
+ out: Path,
70
+ reload: Annotated[
71
+ bool,
72
+ typer.Option(
73
+ "--reload",
74
+ "-r",
75
+ help="reload icon state before fetching image",
76
+ ),
77
+ ] = False,
78
+ ) -> None:
79
+ """Save the preview image for the homescreen or lockscreen wallpaper (optionally reload state first)."""
77
80
  with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
78
81
  if reload:
79
82
  springboard_service.reload_icon_state()
80
- out.write(springboard_service.get_wallpaper_preview_image(wallpaper_name))
83
+ out.write_bytes(springboard_service.get_wallpaper_preview_image(wallpaper_name))
84
+
85
+
86
+ @cli.command("homescreen-icon-metrics")
87
+ def springboard_homescreen_icon_metrics(service_provider: ServiceProviderDep) -> None:
88
+ """Print homescreen icon spacing/metrics."""
89
+ with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
90
+ print_json(springboard_service.get_homescreen_icon_metrics())