pymobiledevice3 6.2.0__py3-none-any.whl → 7.0.0__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.
- pymobiledevice3/__main__.py +136 -44
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +19 -20
- pymobiledevice3/cli/activation.py +24 -22
- pymobiledevice3/cli/afc.py +49 -41
- pymobiledevice3/cli/amfi.py +13 -18
- pymobiledevice3/cli/apps.py +71 -65
- pymobiledevice3/cli/backup.py +134 -93
- pymobiledevice3/cli/bonjour.py +31 -29
- pymobiledevice3/cli/cli_common.py +179 -232
- pymobiledevice3/cli/companion_proxy.py +12 -12
- pymobiledevice3/cli/crash.py +95 -52
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +18 -22
- pymobiledevice3/cli/lockdown.py +70 -75
- pymobiledevice3/cli/mounter.py +99 -57
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +36 -20
- pymobiledevice3/cli/power_assertion.py +15 -16
- pymobiledevice3/cli/processes.py +11 -17
- pymobiledevice3/cli/profile.py +120 -75
- pymobiledevice3/cli/provision.py +27 -26
- pymobiledevice3/cli/remote.py +108 -99
- pymobiledevice3/cli/restore.py +134 -129
- pymobiledevice3/cli/springboard.py +50 -50
- pymobiledevice3/cli/syslog.py +138 -74
- pymobiledevice3/cli/usbmux.py +66 -27
- pymobiledevice3/cli/version.py +2 -5
- pymobiledevice3/cli/webinspector.py +149 -103
- pymobiledevice3/remote/remote_service_discovery.py +11 -10
- pymobiledevice3/restore/device.py +28 -4
- pymobiledevice3/service_connection.py +1 -1
- pymobiledevice3/services/mobilebackup2.py +4 -1
- pymobiledevice3/services/screenshot.py +2 -2
- pymobiledevice3/services/web_protocol/automation_session.py +4 -2
- pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
- pymobiledevice3/services/web_protocol/element.py +3 -3
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
- pymobiledevice3/cli/completions.py +0 -50
- pymobiledevice3/cli/developer.py +0 -1645
- pymobiledevice3/cli/diagnostics.py +0 -110
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
- {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
pymobiledevice3/cli/remote.py
CHANGED
|
@@ -4,14 +4,15 @@ import logging
|
|
|
4
4
|
import sys
|
|
5
5
|
import tempfile
|
|
6
6
|
from functools import partial
|
|
7
|
-
from
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional, TextIO
|
|
8
9
|
|
|
9
|
-
import
|
|
10
|
+
import typer
|
|
11
|
+
from typer_injector import InjectingTyper
|
|
10
12
|
|
|
11
13
|
from pymobiledevice3.bonjour import DEFAULT_BONJOUR_TIMEOUT, browse_remotepairing_manual_pairing
|
|
12
14
|
from pymobiledevice3.cli.cli_common import (
|
|
13
|
-
|
|
14
|
-
RSDCommand,
|
|
15
|
+
RSDServiceProviderDep,
|
|
15
16
|
print_json,
|
|
16
17
|
prompt_device_list,
|
|
17
18
|
sudo_required,
|
|
@@ -38,6 +39,7 @@ logger = logging.getLogger(__name__)
|
|
|
38
39
|
async def browse_rsd(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> list[dict]:
|
|
39
40
|
devices = []
|
|
40
41
|
for rsd in await get_rsds(timeout):
|
|
42
|
+
assert rsd.peer_info is not None
|
|
41
43
|
devices.append({
|
|
42
44
|
"address": rsd.service.address[0],
|
|
43
45
|
"port": RSD_PORT,
|
|
@@ -66,40 +68,36 @@ async def cli_browse(timeout: float = DEFAULT_BONJOUR_TIMEOUT) -> None:
|
|
|
66
68
|
})
|
|
67
69
|
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@cli.group("remote")
|
|
75
|
-
def remote_cli() -> None:
|
|
76
|
-
"""Create RemoteXPC tunnels"""
|
|
77
|
-
pass
|
|
71
|
+
cli = InjectingTyper(
|
|
72
|
+
name="remote",
|
|
73
|
+
help="Create and browse RemoteXPC tunnels (RSD/tunneld) for developer services.",
|
|
74
|
+
no_args_is_help=True,
|
|
75
|
+
)
|
|
78
76
|
|
|
79
77
|
|
|
80
|
-
@
|
|
81
|
-
@click.option("--host", default=TUNNELD_DEFAULT_ADDRESS[0])
|
|
82
|
-
@click.option("--port", type=click.INT, default=TUNNELD_DEFAULT_ADDRESS[1])
|
|
83
|
-
@click.option("-d", "--daemonize", is_flag=True)
|
|
84
|
-
@click.option(
|
|
85
|
-
"-p",
|
|
86
|
-
"--protocol",
|
|
87
|
-
type=click.Choice([e.value for e in TunnelProtocol]),
|
|
88
|
-
help="Transport protocol. If python version >= 3.13 will default to TCP. Otherwise will default to QUIC",
|
|
89
|
-
default=TunnelProtocol.DEFAULT.value,
|
|
90
|
-
)
|
|
91
|
-
@click.option("--usb/--no-usb", default=True, help="Enable usb monitoring")
|
|
92
|
-
@click.option("--wifi/--no-wifi", default=True, help="Enable wifi monitoring")
|
|
93
|
-
@click.option("--usbmux/--no-usbmux", default=True, help="Enable usbmux monitoring")
|
|
94
|
-
@click.option("--mobdev2/--no-mobdev2", default=True, help="Enable mobdev2 monitoring")
|
|
78
|
+
@cli.command("tunneld")
|
|
95
79
|
@sudo_required
|
|
96
80
|
def cli_tunneld(
|
|
97
|
-
host: str,
|
|
81
|
+
host: Annotated[str, typer.Option(help="Address to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[0],
|
|
82
|
+
port: Annotated[int, typer.Option(help="Port to bind the tunneld server to.")] = TUNNELD_DEFAULT_ADDRESS[1],
|
|
83
|
+
daemonize: Annotated[bool, typer.Option("--daemonize", "-d", help="Run tunneld in the background.")] = False,
|
|
84
|
+
protocol: Annotated[
|
|
85
|
+
TunnelProtocol,
|
|
86
|
+
typer.Option(
|
|
87
|
+
"--protocol",
|
|
88
|
+
"-p",
|
|
89
|
+
case_sensitive=False,
|
|
90
|
+
help="Transport protocol for tunneld (default: TCP on Python >=3.13, otherwise QUIC).",
|
|
91
|
+
),
|
|
92
|
+
] = TunnelProtocol.DEFAULT,
|
|
93
|
+
usb: Annotated[bool, typer.Option(help="Enable USB monitoring")] = True,
|
|
94
|
+
wifi: Annotated[bool, typer.Option(help="Enable WiFi monitoring")] = True,
|
|
95
|
+
usbmux: Annotated[bool, typer.Option(help="Enable usbmux monitoring")] = True,
|
|
96
|
+
mobdev2: Annotated[bool, typer.Option(help="Enable mobdev2 monitoring")] = True,
|
|
98
97
|
) -> None:
|
|
99
98
|
"""Start Tunneld service for remote tunneling"""
|
|
100
99
|
if not verify_tunnel_imports():
|
|
101
100
|
return
|
|
102
|
-
protocol = TunnelProtocol(protocol)
|
|
103
101
|
tunneld_runner = partial(
|
|
104
102
|
TunneldRunner.create,
|
|
105
103
|
host,
|
|
@@ -123,15 +121,16 @@ def cli_tunneld(
|
|
|
123
121
|
tunneld_runner()
|
|
124
122
|
|
|
125
123
|
|
|
126
|
-
@
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
@cli.command("browse")
|
|
125
|
+
def browse(
|
|
126
|
+
timeout: Annotated[float, typer.Option(help="Bonjour timeout (in seconds)")] = DEFAULT_BONJOUR_TIMEOUT,
|
|
127
|
+
) -> None:
|
|
129
128
|
"""browse RemoteXPC devices using bonjour"""
|
|
130
129
|
asyncio.run(cli_browse(timeout), debug=True)
|
|
131
130
|
|
|
132
131
|
|
|
133
|
-
@
|
|
134
|
-
def rsd_info(service_provider:
|
|
132
|
+
@cli.command("rsd-info")
|
|
133
|
+
def rsd_info(service_provider: RSDServiceProviderDep) -> None:
|
|
135
134
|
"""show info extracted from RSD peer"""
|
|
136
135
|
print_json(service_provider.peer_info)
|
|
137
136
|
|
|
@@ -153,32 +152,32 @@ async def tunnel_task(
|
|
|
153
152
|
if user_requested_colored_output():
|
|
154
153
|
if secrets is not None:
|
|
155
154
|
print(
|
|
156
|
-
|
|
157
|
-
+
|
|
155
|
+
typer.style("Secrets: ", bold=True, fg="magenta")
|
|
156
|
+
+ typer.style(secrets.name, bold=True, fg="white")
|
|
158
157
|
)
|
|
159
158
|
print(
|
|
160
|
-
|
|
161
|
-
+
|
|
159
|
+
typer.style("Identifier: ", bold=True, fg="yellow")
|
|
160
|
+
+ typer.style(service.remote_identifier, bold=True, fg="white")
|
|
162
161
|
)
|
|
163
162
|
print(
|
|
164
|
-
|
|
165
|
-
+
|
|
163
|
+
typer.style("Interface: ", bold=True, fg="yellow")
|
|
164
|
+
+ typer.style(tunnel_result.interface, bold=True, fg="white")
|
|
166
165
|
)
|
|
167
166
|
print(
|
|
168
|
-
|
|
169
|
-
+
|
|
167
|
+
typer.style("Protocol: ", bold=True, fg="yellow")
|
|
168
|
+
+ typer.style(tunnel_result.protocol, bold=True, fg="white")
|
|
170
169
|
)
|
|
171
170
|
print(
|
|
172
|
-
|
|
173
|
-
+
|
|
171
|
+
typer.style("RSD Address: ", bold=True, fg="yellow")
|
|
172
|
+
+ typer.style(tunnel_result.address, bold=True, fg="white")
|
|
174
173
|
)
|
|
175
174
|
print(
|
|
176
|
-
|
|
177
|
-
+
|
|
175
|
+
typer.style("RSD Port: ", bold=True, fg="yellow")
|
|
176
|
+
+ typer.style(tunnel_result.port, bold=True, fg="white")
|
|
178
177
|
)
|
|
179
178
|
print(
|
|
180
|
-
|
|
181
|
-
+
|
|
179
|
+
typer.style("Use the follow connection option:\n", bold=True, fg="yellow")
|
|
180
|
+
+ typer.style(f"--rsd {tunnel_result.address} {tunnel_result.port}", bold=True, fg="cyan")
|
|
182
181
|
)
|
|
183
182
|
else:
|
|
184
183
|
if secrets is not None:
|
|
@@ -224,52 +223,60 @@ async def start_tunnel_task(
|
|
|
224
223
|
)
|
|
225
224
|
|
|
226
225
|
|
|
227
|
-
@
|
|
228
|
-
@click.option(
|
|
229
|
-
"-t",
|
|
230
|
-
"--connection-type",
|
|
231
|
-
type=click.Choice([e.value for e in ConnectionType], case_sensitive=False),
|
|
232
|
-
default=ConnectionType.USB.value,
|
|
233
|
-
)
|
|
234
|
-
@click.option("--udid", help="UDID for a specific device to look for")
|
|
235
|
-
@click.option("--secrets", type=click.File("wt"), help="TLS keyfile for decrypting with Wireshark")
|
|
236
|
-
@click.option(
|
|
237
|
-
"--script-mode",
|
|
238
|
-
is_flag=True,
|
|
239
|
-
help="Show only HOST and port number to allow easy parsing from external shell scripts",
|
|
240
|
-
)
|
|
241
|
-
@click.option(
|
|
242
|
-
"--max-idle-timeout", type=click.FLOAT, default=MAX_IDLE_TIMEOUT, help="Maximum QUIC idle time (ping interval)"
|
|
243
|
-
)
|
|
244
|
-
@click.option(
|
|
245
|
-
"-p",
|
|
246
|
-
"--protocol",
|
|
247
|
-
type=click.Choice([e.value for e in TunnelProtocol], case_sensitive=False),
|
|
248
|
-
default=TunnelProtocol.DEFAULT.value,
|
|
249
|
-
)
|
|
226
|
+
@cli.command("start-tunnel")
|
|
250
227
|
@sudo_required
|
|
251
228
|
def cli_start_tunnel(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
229
|
+
*,
|
|
230
|
+
connection_type: Annotated[
|
|
231
|
+
ConnectionType,
|
|
232
|
+
typer.Option(
|
|
233
|
+
"--connection-type",
|
|
234
|
+
"-t",
|
|
235
|
+
case_sensitive=False,
|
|
236
|
+
help="Connection interface to tunnel (USB, WiFi, etc.).",
|
|
237
|
+
),
|
|
238
|
+
] = ConnectionType.USB,
|
|
239
|
+
udid: Annotated[
|
|
240
|
+
Optional[str],
|
|
241
|
+
typer.Option(help="UDID for a specific device to look for"),
|
|
242
|
+
] = None,
|
|
243
|
+
secrets: Annotated[
|
|
244
|
+
Path,
|
|
245
|
+
typer.Option(help="File to write TLS secrets for Wireshark decryption."),
|
|
246
|
+
],
|
|
247
|
+
script_mode: Annotated[
|
|
248
|
+
bool,
|
|
249
|
+
typer.Option(help="Print only HOST and port for scripts instead of formatted output."),
|
|
250
|
+
] = False,
|
|
251
|
+
max_idle_timeout: Annotated[
|
|
252
|
+
float,
|
|
253
|
+
typer.Option(help="Maximum idle time before QUIC keepalive pings are sent."),
|
|
254
|
+
] = MAX_IDLE_TIMEOUT,
|
|
255
|
+
protocol: Annotated[
|
|
256
|
+
TunnelProtocol,
|
|
257
|
+
typer.Option(
|
|
258
|
+
"--protocol",
|
|
259
|
+
"-p",
|
|
260
|
+
case_sensitive=False,
|
|
261
|
+
help="Transport protocol for the tunnel (default: TCP on Python >=3.13, otherwise QUIC).",
|
|
262
|
+
),
|
|
263
|
+
] = TunnelProtocol.DEFAULT,
|
|
258
264
|
) -> None:
|
|
259
265
|
"""start tunnel"""
|
|
260
266
|
if not verify_tunnel_imports():
|
|
261
267
|
return
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
with secrets.open("wt") as secrets_file:
|
|
269
|
+
asyncio.run(
|
|
270
|
+
start_tunnel_task(
|
|
271
|
+
connection_type,
|
|
272
|
+
secrets_file,
|
|
273
|
+
udid,
|
|
274
|
+
script_mode,
|
|
275
|
+
max_idle_timeout=max_idle_timeout,
|
|
276
|
+
protocol=protocol,
|
|
277
|
+
),
|
|
278
|
+
debug=True,
|
|
279
|
+
)
|
|
273
280
|
|
|
274
281
|
|
|
275
282
|
@dataclasses.dataclass
|
|
@@ -280,7 +287,7 @@ class RemotePairingManualPairingDevice:
|
|
|
280
287
|
identifier: str
|
|
281
288
|
|
|
282
289
|
|
|
283
|
-
async def start_remote_pair_task(device_name: str) -> None:
|
|
290
|
+
async def start_remote_pair_task(device_name: Optional[str]) -> None:
|
|
284
291
|
if start_tunnel is None:
|
|
285
292
|
raise NotImplementedError("failed to start the tunnel on your platform")
|
|
286
293
|
|
|
@@ -311,17 +318,20 @@ async def start_remote_pair_task(device_name: str) -> None:
|
|
|
311
318
|
await service.connect(autopair=True)
|
|
312
319
|
|
|
313
320
|
|
|
314
|
-
@
|
|
315
|
-
|
|
316
|
-
|
|
321
|
+
@cli.command("pair")
|
|
322
|
+
def cli_pair(
|
|
323
|
+
name: Annotated[
|
|
324
|
+
Optional[str],
|
|
325
|
+
typer.Option(help="Device name for a specific device to look for"),
|
|
326
|
+
] = None,
|
|
327
|
+
) -> None:
|
|
317
328
|
"""start remote pairing for devices which allow"""
|
|
318
329
|
asyncio.run(start_remote_pair_task(name), debug=True)
|
|
319
330
|
|
|
320
331
|
|
|
321
|
-
@
|
|
322
|
-
@click.argument("udid")
|
|
332
|
+
@cli.command("delete-pair")
|
|
323
333
|
@sudo_required
|
|
324
|
-
def cli_delete_pair(udid: str):
|
|
334
|
+
def cli_delete_pair(udid: str) -> None:
|
|
325
335
|
"""delete a pairing record"""
|
|
326
336
|
pair_record_path = get_home_folder() / f"{get_remote_pairing_record_filename(udid)}.{PAIRING_RECORD_EXT}"
|
|
327
337
|
pair_record_path.unlink()
|
|
@@ -332,8 +342,7 @@ async def cli_service_task(service_provider: RemoteServiceDiscoveryService, serv
|
|
|
332
342
|
service.shell()
|
|
333
343
|
|
|
334
344
|
|
|
335
|
-
@
|
|
336
|
-
|
|
337
|
-
def cli_service(service_provider: RemoteServiceDiscoveryService, service_name: str) -> None:
|
|
345
|
+
@cli.command("service")
|
|
346
|
+
def cli_service(service_provider: RSDServiceProviderDep, service_name: str) -> None:
|
|
338
347
|
"""start an ipython shell for interacting with given service"""
|
|
339
348
|
asyncio.run(cli_service_task(service_provider, service_name), debug=True)
|
pymobiledevice3/cli/restore.py
CHANGED
|
@@ -4,18 +4,20 @@ import logging
|
|
|
4
4
|
import plistlib
|
|
5
5
|
import tempfile
|
|
6
6
|
import traceback
|
|
7
|
-
from collections.abc import
|
|
7
|
+
from collections.abc import Iterator
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import IO,
|
|
9
|
+
from typing import IO, Annotated, Optional
|
|
10
10
|
from zipfile import ZipFile
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
import IPython
|
|
14
14
|
import requests
|
|
15
|
+
import typer
|
|
15
16
|
from pygments import formatters, highlight, lexers
|
|
17
|
+
from typer_injector import Depends, InjectingTyper
|
|
16
18
|
|
|
17
19
|
from pymobiledevice3 import usbmux
|
|
18
|
-
from pymobiledevice3.cli.cli_common import is_invoked_for_completion, print_json, prompt_selection
|
|
20
|
+
from pymobiledevice3.cli.cli_common import is_invoked_for_completion, print_json, prompt_selection
|
|
19
21
|
from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, IncorrectModeError
|
|
20
22
|
from pymobiledevice3.irecv import IRecv
|
|
21
23
|
from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
|
|
@@ -25,55 +27,66 @@ from pymobiledevice3.restore.restore import Restore
|
|
|
25
27
|
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
26
28
|
from pymobiledevice3.utils import file_download
|
|
27
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
SHELL_USAGE = """
|
|
29
34
|
# use `irecv` variable to access Restore mode API
|
|
30
35
|
# for example:
|
|
31
36
|
print(irecv.getenv('build-version'))
|
|
32
37
|
"""
|
|
33
|
-
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
38
|
IPSWME_API = "https://api.ipsw.me/v4/device/"
|
|
36
39
|
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
77
|
+
|
|
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
|
+
]
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
@contextlib.contextmanager
|
|
76
|
-
def tempzip_download_ctx(url: str) ->
|
|
89
|
+
def tempzip_download_ctx(url: str) -> Iterator[ZipFile]:
|
|
77
90
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
78
91
|
tmpzip = Path(tmpdir) / url.split("/")[-1]
|
|
79
92
|
file_download(url, tmpzip)
|
|
@@ -81,33 +94,52 @@ def tempzip_download_ctx(url: str) -> Generator[ZipFile, None, None]:
|
|
|
81
94
|
|
|
82
95
|
|
|
83
96
|
@contextlib.contextmanager
|
|
84
|
-
def zipfile_ctx(path: str) ->
|
|
97
|
+
def zipfile_ctx(path: str) -> Iterator[ZipFile]:
|
|
85
98
|
yield ZipFile(path)
|
|
86
99
|
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
114
|
+
|
|
115
|
+
url = ipsw
|
|
116
|
+
if url is None:
|
|
117
|
+
url = query_ipswme(device.product_type)
|
|
118
|
+
return tempzip_download_ctx(url)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
IPSWCtxDep = Annotated[
|
|
122
|
+
contextlib.AbstractContextManager[ZipFile],
|
|
123
|
+
Depends(ipsw_ctx_dependency),
|
|
124
|
+
]
|
|
95
125
|
|
|
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)
|
|
100
126
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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)
|
|
105
137
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
|
|
139
|
+
TSSDep = Annotated[
|
|
140
|
+
Optional[dict],
|
|
141
|
+
Depends(tss_dependency),
|
|
142
|
+
]
|
|
111
143
|
|
|
112
144
|
|
|
113
145
|
def query_ipswme(identifier: str) -> str:
|
|
@@ -118,15 +150,9 @@ def query_ipswme(identifier: str) -> str:
|
|
|
118
150
|
return firmwares[idx]["url"]
|
|
119
151
|
|
|
120
152
|
|
|
121
|
-
async def restore_update_task(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if isinstance(device, LockdownClient):
|
|
125
|
-
lockdown = device
|
|
126
|
-
elif isinstance(device, IRecv):
|
|
127
|
-
irecv = device
|
|
128
|
-
device = Device(lockdown=lockdown, irecv=irecv)
|
|
129
|
-
|
|
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,19 +165,8 @@ async def restore_update_task(device: Device, ipsw: ZipFile, tss: Optional[IO],
|
|
|
139
165
|
raise
|
|
140
166
|
|
|
141
167
|
|
|
142
|
-
@
|
|
143
|
-
def
|
|
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):
|
|
168
|
+
@cli.command("shell")
|
|
169
|
+
def restore_shell(device: DeviceDep) -> None:
|
|
155
170
|
"""create an IPython shell for interacting with iBoot"""
|
|
156
171
|
IPython.embed(
|
|
157
172
|
header=highlight(SHELL_USAGE, lexers.PythonLexer(), formatters.Terminal256Formatter(style="native")),
|
|
@@ -161,40 +176,34 @@ def restore_shell(device):
|
|
|
161
176
|
)
|
|
162
177
|
|
|
163
178
|
|
|
164
|
-
@
|
|
165
|
-
def restore_enter(device):
|
|
179
|
+
@cli.command("enter")
|
|
180
|
+
def restore_enter(device: DeviceDep) -> None:
|
|
166
181
|
"""enter Recovery mode"""
|
|
167
182
|
if isinstance(device, LockdownClient):
|
|
168
183
|
device.enter_recovery()
|
|
169
184
|
|
|
170
185
|
|
|
171
|
-
@
|
|
172
|
-
def restore_exit():
|
|
186
|
+
@cli.command("exit")
|
|
187
|
+
def restore_exit() -> None:
|
|
173
188
|
"""exit Recovery mode"""
|
|
174
189
|
irecv = IRecv()
|
|
175
190
|
irecv.set_autoboot(True)
|
|
176
191
|
irecv.reboot()
|
|
177
192
|
|
|
178
193
|
|
|
179
|
-
@
|
|
180
|
-
def restore_restart(device):
|
|
194
|
+
@cli.command("restart")
|
|
195
|
+
def restore_restart(device: DeviceDep) -> None:
|
|
181
196
|
"""restarts device"""
|
|
182
|
-
if
|
|
183
|
-
with DiagnosticsService(device) as diagnostics:
|
|
197
|
+
if device.is_lockdown:
|
|
198
|
+
with DiagnosticsService(device.lockdown) as diagnostics:
|
|
184
199
|
diagnostics.restart()
|
|
185
200
|
else:
|
|
186
|
-
device.reboot()
|
|
201
|
+
device.irecv.reboot()
|
|
187
202
|
|
|
188
203
|
|
|
189
|
-
async def restore_tss_task(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if isinstance(device, LockdownClient):
|
|
193
|
-
lockdown = device
|
|
194
|
-
elif isinstance(device, IRecv):
|
|
195
|
-
irecv = device
|
|
196
|
-
|
|
197
|
-
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:
|
|
198
207
|
with ipsw_ctx as ipsw:
|
|
199
208
|
tss = await Recovery(ipsw, device).fetch_tss_record()
|
|
200
209
|
if out:
|
|
@@ -202,46 +211,42 @@ async def restore_tss_task(device: Device, ipsw_ctx: Generator, tss: IO, out: Op
|
|
|
202
211
|
print_json(tss)
|
|
203
212
|
|
|
204
213
|
|
|
205
|
-
@
|
|
206
|
-
|
|
207
|
-
def restore_tss(device: Device, ipsw_ctx: Generator, tss: IO, out: Optional[IO]) -> None:
|
|
214
|
+
@cli.command("tss")
|
|
215
|
+
def restore_tss(device: DeviceDep, ipsw_ctx: IPSWCtxDep, out: Optional[Path] = None) -> None:
|
|
208
216
|
"""query SHSH blobs"""
|
|
209
|
-
|
|
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)
|
|
210
219
|
|
|
211
220
|
|
|
212
|
-
async def restore_ramdisk_task(device: Device, ipsw_ctx:
|
|
213
|
-
lockdown = None
|
|
214
|
-
irecv = None
|
|
215
|
-
if isinstance(device, LockdownClient):
|
|
216
|
-
lockdown = device
|
|
217
|
-
elif isinstance(device, IRecv):
|
|
218
|
-
irecv = device
|
|
219
|
-
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
223
|
await Recovery(ipsw, device).boot_ramdisk()
|
|
223
224
|
|
|
224
225
|
|
|
225
|
-
@
|
|
226
|
-
def restore_ramdisk(device:
|
|
226
|
+
@cli.command("ramdisk")
|
|
227
|
+
def restore_ramdisk(device: DeviceDep, ipsw_ctx: IPSWCtxDep) -> None:
|
|
227
228
|
"""
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
ipsw can be either a filename or an url
|
|
229
|
+
Boot only the update ramdisk without performing a restore (IPSW path or URL accepted).
|
|
231
230
|
"""
|
|
232
231
|
asyncio.run(restore_ramdisk_task(device, ipsw_ctx), debug=True)
|
|
233
232
|
|
|
234
233
|
|
|
235
|
-
@
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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:
|
|
241
248
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
ipsw can be either a filename or an url
|
|
249
|
+
Update or restore the device using an IPSW (local path or URL).
|
|
245
250
|
"""
|
|
246
251
|
with ipsw_ctx as ipsw:
|
|
247
252
|
asyncio.run(restore_update_task(device, ipsw, tss, erase, ignore_fdr), debug=True)
|