pymobiledevice3 5.0.4__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.
- misc/understanding_idevice_protocol_layers.md +10 -5
- pymobiledevice3/__main__.py +171 -46
- pymobiledevice3/_version.py +2 -2
- pymobiledevice3/bonjour.py +22 -21
- 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 +175 -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 +438 -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 +42 -0
- 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 +109 -100
- pymobiledevice3/cli/restore.py +134 -129
- pymobiledevice3/cli/springboard.py +50 -50
- pymobiledevice3/cli/syslog.py +145 -65
- pymobiledevice3/cli/usbmux.py +66 -27
- pymobiledevice3/cli/version.py +2 -5
- pymobiledevice3/cli/webinspector.py +232 -156
- pymobiledevice3/exceptions.py +6 -2
- pymobiledevice3/lockdown.py +5 -1
- pymobiledevice3/lockdown_service_provider.py +5 -0
- pymobiledevice3/remote/remote_service_discovery.py +18 -10
- pymobiledevice3/restore/device.py +28 -4
- pymobiledevice3/restore/restore.py +2 -2
- pymobiledevice3/service_connection.py +15 -12
- pymobiledevice3/services/afc.py +731 -220
- pymobiledevice3/services/device_link.py +45 -31
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/lockdown_service.py +12 -9
- pymobiledevice3/services/mobile_config.py +1 -0
- pymobiledevice3/services/mobilebackup2.py +6 -3
- pymobiledevice3/services/os_trace.py +97 -55
- pymobiledevice3/services/remote_fetch_symbols.py +13 -8
- pymobiledevice3/services/screenshot.py +2 -2
- pymobiledevice3/services/web_protocol/alert.py +8 -8
- pymobiledevice3/services/web_protocol/automation_session.py +87 -79
- pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
- pymobiledevice3/services/web_protocol/driver.py +71 -70
- pymobiledevice3/services/web_protocol/element.py +58 -56
- pymobiledevice3/services/web_protocol/selenium_api.py +47 -47
- pymobiledevice3/services/web_protocol/session_protocol.py +3 -2
- pymobiledevice3/services/web_protocol/switch_to.py +23 -19
- pymobiledevice3/services/webinspector.py +42 -67
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +5 -3
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/RECORD +76 -61
- pymobiledevice3/cli/completions.py +0 -50
- pymobiledevice3/cli/developer.py +0 -1539
- pymobiledevice3/cli/diagnostics.py +0 -110
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/licenses/LICENSE +0 -0
- {pymobiledevice3-5.0.4.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
|
@@ -287,14 +287,14 @@ pymobiledevice3 developer dvt launch com.apple.mobilesafari
|
|
|
287
287
|
|
|
288
288
|
## DVT
|
|
289
289
|
|
|
290
|
-
One of the more interesting developer services
|
|
290
|
+
One of the more interesting developer services is the one exposed by `DTServiceHub`. It is using DTX protocol messages,
|
|
291
291
|
but since it mainly wraps and allows access to stuff in `DVTFoundation.framework` we called it DVT in our
|
|
292
292
|
implementation (probably standing for DeveloperTools).
|
|
293
293
|
|
|
294
294
|
We don't delve too much into this protocol, but we'll say in general it allows us to invoke a whitelist of ObjC methods
|
|
295
295
|
in different ObjC objects. The terminology used by DVT to each such ObjC object is called "channels".
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
To access this different object use the following APIs:
|
|
298
298
|
|
|
299
299
|
```python
|
|
300
300
|
from pymobiledevice3.lockdown import create_using_usbmux
|
|
@@ -313,17 +313,22 @@ dvt_channel = Screenshot(dvt)
|
|
|
313
313
|
open('/tmp/screen.png', 'wb').write(dvt_channel.get_screenshot())
|
|
314
314
|
```
|
|
315
315
|
|
|
316
|
-
Looking for an unimplemented feature/channel? Feel free to play with it (and submit a PR
|
|
316
|
+
Looking for an unimplemented feature/channel? Feel free to play with it (and submit a PR afterward 🙏) using the
|
|
317
317
|
following shell:
|
|
318
318
|
|
|
319
319
|
```shell
|
|
320
320
|
pymobiledevice3 developer dvt shell
|
|
321
321
|
```
|
|
322
322
|
|
|
323
|
+
> **NOTE:** The full list of the methods that can be invoked on a DVT channel can be found by looking at all ObjC
|
|
324
|
+
> classes in `DVTInstrumentsFoundation.framework` implementing the `DTXAllowedRPC` protocol.
|
|
325
|
+
> There is an existing [Anubis rule](https://github.com/netanelc305/anubis/blob/9da337178ebd7e9f168e9df2d82b192eba4f1b30/example_rules.yaml#L14-L17)
|
|
326
|
+
> I use to diff new methods against the existing ones.
|
|
327
|
+
|
|
323
328
|
## RemoteXPC
|
|
324
329
|
|
|
325
330
|
Starting at iOS 17.0, Apple made a large refactor in the manner we all interact with the developer services. There can
|
|
326
|
-
be multiple reasons for that decision, but in general
|
|
331
|
+
be multiple reasons for that decision, but in general these refactor main key points are:
|
|
327
332
|
|
|
328
333
|
- Create a single standard for interacting with the new lockdown services (XPC Messages, Apple's proprietary IPC)
|
|
329
334
|
- Optimize the protocol for large file transfers (such as the dyld_shared_cache)
|
|
@@ -347,7 +352,7 @@ Since all this communication is IP-based, but without any additional exported TC
|
|
|
347
352
|
help us here. Instead, starting at iOS 16.0, when connecting an iDevice, it exports another non-standard USB-Ethernet
|
|
348
353
|
adapter (with IPv6 link-local address), placing us in a subnet with the device's `remoted`.
|
|
349
354
|
|
|
350
|
-
As we've said this communication is non-standard
|
|
355
|
+
As we've said, this communication is non-standard and requires either:
|
|
351
356
|
|
|
352
357
|
- macOS Monterey or higher
|
|
353
358
|
- Special driver on your linux/Windows machine
|
pymobiledevice3/__main__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import difflib
|
|
3
|
+
import importlib
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
@@ -7,12 +8,18 @@ import sys
|
|
|
7
8
|
import textwrap
|
|
8
9
|
import traceback
|
|
9
10
|
import warnings
|
|
10
|
-
from
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import Annotated, Optional, Union
|
|
11
13
|
|
|
12
14
|
import click
|
|
13
15
|
import coloredlogs
|
|
16
|
+
import typer
|
|
17
|
+
import typer.core
|
|
18
|
+
from packaging.version import Version
|
|
19
|
+
from typer.core import TyperGroup
|
|
20
|
+
from typer_injector import InjectingTyper
|
|
14
21
|
|
|
15
|
-
from pymobiledevice3.cli.cli_common import TUNNEL_ENV_VAR, isatty
|
|
22
|
+
from pymobiledevice3.cli.cli_common import TUNNEL_ENV_VAR, isatty, set_color_flag, set_verbosity
|
|
16
23
|
from pymobiledevice3.exceptions import (
|
|
17
24
|
AccessDeniedError,
|
|
18
25
|
CloudConfigurationAlreadyPresentError,
|
|
@@ -37,10 +44,11 @@ from pymobiledevice3.exceptions import (
|
|
|
37
44
|
QuicProtocolNotSupportedError,
|
|
38
45
|
RSDRequiredError,
|
|
39
46
|
SetProhibitedError,
|
|
47
|
+
StartServiceError,
|
|
40
48
|
TunneldConnectionError,
|
|
41
49
|
UserDeniedPairingError,
|
|
42
50
|
)
|
|
43
|
-
from pymobiledevice3.lockdown import retry_create_using_usbmux
|
|
51
|
+
from pymobiledevice3.lockdown import create_using_usbmux, retry_create_using_usbmux
|
|
44
52
|
from pymobiledevice3.osu.os_utils import get_os_utils
|
|
45
53
|
|
|
46
54
|
coloredlogs.install(level=logging.INFO)
|
|
@@ -109,26 +117,28 @@ CLI_GROUPS = {
|
|
|
109
117
|
"syslog": "syslog",
|
|
110
118
|
"usbmux": "usbmux",
|
|
111
119
|
"webinspector": "webinspector",
|
|
120
|
+
"idam": "idam",
|
|
112
121
|
"version": "version",
|
|
113
|
-
"install-completions": "completions",
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
# Set if used the `--reconnect` option
|
|
117
125
|
RECONNECT = False
|
|
118
126
|
|
|
119
127
|
|
|
120
|
-
class
|
|
121
|
-
def list_commands(self, ctx):
|
|
122
|
-
|
|
128
|
+
class Pmd3TyperGroup(TyperGroup):
|
|
129
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
130
|
+
# Order is preserved by dict insertion; adjust if you want alphabetical
|
|
131
|
+
return list(CLI_GROUPS.keys())
|
|
123
132
|
|
|
124
|
-
def get_command(self, ctx: click.Context,
|
|
125
|
-
if
|
|
126
|
-
self.handle_invalid_command(ctx,
|
|
127
|
-
return self.import_and_get_command(ctx,
|
|
133
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command:
|
|
134
|
+
if cmd_name not in CLI_GROUPS:
|
|
135
|
+
self.handle_invalid_command(ctx, cmd_name)
|
|
136
|
+
return self.import_and_get_command(ctx, cmd_name)
|
|
128
137
|
|
|
129
|
-
def handle_invalid_command(self, ctx
|
|
138
|
+
def handle_invalid_command(self, ctx, name: str) -> None:
|
|
130
139
|
suggested_commands = self.search_commands(name)
|
|
131
140
|
suggestion = self.format_suggestions(suggested_commands)
|
|
141
|
+
# ctx.fail raises a ClickException underneath, which Typer displays nicely
|
|
132
142
|
ctx.fail(f"No such command {name!r}{suggestion}")
|
|
133
143
|
|
|
134
144
|
@staticmethod
|
|
@@ -136,70 +146,162 @@ class Pmd3Cli(click.Group):
|
|
|
136
146
|
if not suggestions:
|
|
137
147
|
return ""
|
|
138
148
|
cmds = textwrap.indent("\n".join(suggestions), " " * 4)
|
|
139
|
-
return f"\nDid you mean
|
|
149
|
+
return f"\nDid you mean:\n{cmds}"
|
|
140
150
|
|
|
141
151
|
@staticmethod
|
|
142
152
|
def import_and_get_command(ctx: click.Context, name: str) -> click.Command:
|
|
143
153
|
module_name = f"pymobiledevice3.cli.{CLI_GROUPS[name]}"
|
|
144
|
-
mod =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
command = mod.cli.get_command(ctx, command_name)
|
|
149
|
-
return command
|
|
154
|
+
mod = importlib.import_module(module_name)
|
|
155
|
+
# submodules expose a Typer Group named "cli"
|
|
156
|
+
cli: typer.Typer = mod.cli
|
|
157
|
+
return typer.main.get_command(cli)
|
|
150
158
|
|
|
151
159
|
@staticmethod
|
|
152
160
|
def highlight_keyword(text: str, keyword: str) -> str:
|
|
153
|
-
return re.sub(f"({keyword})",
|
|
161
|
+
return re.sub(f"({keyword})", typer.style("\\1", bold=True), text, flags=re.IGNORECASE)
|
|
154
162
|
|
|
155
163
|
@staticmethod
|
|
156
|
-
def collect_commands(command: click.Command) -> Union[str, list[str]]:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for
|
|
160
|
-
|
|
161
|
-
if isinstance(
|
|
162
|
-
|
|
164
|
+
def collect_commands(command: Union[TyperGroup, click.Command]) -> Union[str, list[str]]:
|
|
165
|
+
if isinstance(command, TyperGroup): # group
|
|
166
|
+
cmds = []
|
|
167
|
+
for v in command.commands.values():
|
|
168
|
+
child = Pmd3TyperGroup.collect_commands(v)
|
|
169
|
+
if isinstance(child, list):
|
|
170
|
+
cmds.extend([f"{command.name} {c}" for c in child])
|
|
163
171
|
else:
|
|
164
|
-
|
|
165
|
-
return
|
|
166
|
-
return
|
|
172
|
+
cmds.append(f"{command.name} {child}")
|
|
173
|
+
return cmds
|
|
174
|
+
return command.name or ""
|
|
167
175
|
|
|
168
176
|
@staticmethod
|
|
169
177
|
def search_commands(pattern: str) -> list[str]:
|
|
170
|
-
all_commands =
|
|
178
|
+
all_commands = Pmd3TyperGroup.load_all_commands()
|
|
171
179
|
matched = sorted(filter(lambda cmd: re.search(pattern, cmd), all_commands))
|
|
172
180
|
if not matched:
|
|
173
181
|
matched = difflib.get_close_matches(pattern, all_commands, n=20, cutoff=0.4)
|
|
174
182
|
if isatty():
|
|
175
|
-
matched = [
|
|
183
|
+
matched = [Pmd3TyperGroup.highlight_keyword(cmd, pattern) for cmd in matched]
|
|
176
184
|
return matched
|
|
177
185
|
|
|
178
186
|
@staticmethod
|
|
179
187
|
def load_all_commands() -> list[str]:
|
|
180
|
-
all_commands = []
|
|
188
|
+
all_commands: list[str] = []
|
|
181
189
|
for key in CLI_GROUPS:
|
|
182
190
|
module_name = f"pymobiledevice3.cli.{CLI_GROUPS[key]}"
|
|
183
|
-
mod =
|
|
184
|
-
|
|
191
|
+
mod = importlib.import_module(module_name)
|
|
192
|
+
if isinstance(mod.cli, typer.Typer):
|
|
193
|
+
cmd = Pmd3TyperGroup.collect_commands(typer.main.get_group(mod.cli))
|
|
194
|
+
else:
|
|
195
|
+
cmd = Pmd3TyperGroup.collect_commands(mod.cli.commands[key])
|
|
185
196
|
if isinstance(cmd, list):
|
|
186
197
|
all_commands.extend(cmd)
|
|
187
198
|
else:
|
|
188
199
|
all_commands.append(cmd)
|
|
189
200
|
return all_commands
|
|
190
201
|
|
|
202
|
+
def resolve_command(
|
|
203
|
+
self, ctx: click.Context, args: list[str]
|
|
204
|
+
) -> tuple[Optional[str], Optional[click.Command], list[str]]:
|
|
205
|
+
return super().resolve_command(ctx, args)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
app = InjectingTyper(
|
|
209
|
+
cls=Pmd3TyperGroup,
|
|
210
|
+
context_settings=CONTEXT_SETTINGS,
|
|
211
|
+
no_args_is_help=True,
|
|
212
|
+
# add_completion=False,
|
|
213
|
+
rich_markup_mode="markdown",
|
|
214
|
+
help=(
|
|
215
|
+
"Swiss-army CLI for pairing, inspecting, backing up, and automating iOS devices.\n\n"
|
|
216
|
+
"Docs and examples: https://github.com/doronz88/pymobiledevice3"
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
|
|
191
220
|
|
|
192
|
-
@
|
|
193
|
-
|
|
194
|
-
|
|
221
|
+
@app.callback()
|
|
222
|
+
def _root(
|
|
223
|
+
reconnect: Annotated[
|
|
224
|
+
bool,
|
|
225
|
+
typer.Option(
|
|
226
|
+
"--reconnect",
|
|
227
|
+
help="Automatically reconnect if the device disconnects mid-command.",
|
|
228
|
+
show_default=False,
|
|
229
|
+
),
|
|
230
|
+
] = False,
|
|
231
|
+
verbosity: Annotated[
|
|
232
|
+
int,
|
|
233
|
+
typer.Option(
|
|
234
|
+
"--verbose",
|
|
235
|
+
"-v",
|
|
236
|
+
count=True,
|
|
237
|
+
help="Increase logging verbosity (repeat for more detail).",
|
|
238
|
+
),
|
|
239
|
+
] = 0,
|
|
240
|
+
color: Annotated[
|
|
241
|
+
bool,
|
|
242
|
+
typer.Option(help="Colorize output; disable with --no-color for plain logs."),
|
|
243
|
+
] = True,
|
|
244
|
+
) -> None:
|
|
195
245
|
"""
|
|
196
|
-
|
|
197
|
-
Interact with a connected iDevice (iPhone, iPad, ...)
|
|
198
|
-
For more information please look at:
|
|
199
|
-
https://github.com/doronz88/pymobiledevice3
|
|
246
|
+
Top-level options for pymobiledevice3.
|
|
200
247
|
"""
|
|
201
248
|
global RECONNECT
|
|
202
249
|
RECONNECT = reconnect
|
|
250
|
+
set_verbosity(verbosity)
|
|
251
|
+
set_color_flag(color)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def device_might_need_tunneld(identifier: str) -> bool:
|
|
255
|
+
"""
|
|
256
|
+
Determines if the device might require tunneling based on its product version.
|
|
257
|
+
|
|
258
|
+
This function uses the `create_using_usbmux` context manager to establish a lockdown
|
|
259
|
+
session with the specified identifier. It retrieves the device's product version,
|
|
260
|
+
and checks if it is greater than or equal to version "17.0". If so, the function
|
|
261
|
+
returns True, indicating that the device might require tunneling. Otherwise, it
|
|
262
|
+
returns False.
|
|
263
|
+
|
|
264
|
+
:param identifier: A string representing the device identifier.
|
|
265
|
+
:return: A boolean indicating whether the device might require tunneling.
|
|
266
|
+
"""
|
|
267
|
+
with create_using_usbmux(identifier) as lockdown:
|
|
268
|
+
return Version(lockdown.product_version) >= Version("17.0")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class PossiblyMisplacedOption(click.NoSuchOption):
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
option_name: str,
|
|
275
|
+
message: Optional[str] = None,
|
|
276
|
+
possibilities: Optional[Sequence[str]] = None,
|
|
277
|
+
ctx: Optional[click.Context] = None,
|
|
278
|
+
suggested_ctx: Optional[click.Context] = None,
|
|
279
|
+
) -> None:
|
|
280
|
+
super().__init__(option_name, message, possibilities, ctx)
|
|
281
|
+
if suggested_ctx is not None:
|
|
282
|
+
if ctx is not None:
|
|
283
|
+
self.message += f" for subcommand: {ctx.command_path}"
|
|
284
|
+
|
|
285
|
+
suggestion = f"{suggested_ctx.command_path} {option_name}"
|
|
286
|
+
suggestion += ctx.command_path.removeprefix(suggested_ctx.command_path) if ctx is not None else " ..."
|
|
287
|
+
|
|
288
|
+
self.message += f"\nDid you mean: {suggestion}?"
|
|
289
|
+
|
|
290
|
+
@staticmethod
|
|
291
|
+
def from_no_such_option(e: click.NoSuchOption) -> "PossiblyMisplacedOption":
|
|
292
|
+
ctx = e.ctx
|
|
293
|
+
while ctx:
|
|
294
|
+
for param in ctx.command.params:
|
|
295
|
+
if isinstance(param, typer.core.TyperOption) and (
|
|
296
|
+
e.option_name in param.opts or e.option_name in param.secondary_opts
|
|
297
|
+
):
|
|
298
|
+
break
|
|
299
|
+
else:
|
|
300
|
+
ctx = ctx.parent
|
|
301
|
+
continue
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
return PossiblyMisplacedOption(e.option_name, e.message, e.possibilities, e.ctx, ctx)
|
|
203
305
|
|
|
204
306
|
|
|
205
307
|
def invoke_cli_with_error_handling() -> bool:
|
|
@@ -208,7 +310,11 @@ def invoke_cli_with_error_handling() -> bool:
|
|
|
208
310
|
disconnected.
|
|
209
311
|
"""
|
|
210
312
|
try:
|
|
211
|
-
|
|
313
|
+
# Typer apps are callable; this executes the CLI with current sys.argv
|
|
314
|
+
try:
|
|
315
|
+
app(standalone_mode=False)
|
|
316
|
+
except click.NoSuchOption as e:
|
|
317
|
+
raise PossiblyMisplacedOption.from_no_such_option(e) from e
|
|
212
318
|
except NoDeviceConnectedError:
|
|
213
319
|
logger.error("Device is not connected")
|
|
214
320
|
return True
|
|
@@ -249,13 +355,19 @@ def invoke_cli_with_error_handling() -> bool:
|
|
|
249
355
|
if isinstance(e, RSDRequiredError):
|
|
250
356
|
logger.warning("Trying again over tunneld since RSD is required for this command")
|
|
251
357
|
should_retry_over_tunneld = True
|
|
252
|
-
elif (
|
|
358
|
+
elif (
|
|
359
|
+
(e.identifier is not None)
|
|
360
|
+
and ("developer" in sys.argv)
|
|
361
|
+
and ("--tunnel" not in sys.argv)
|
|
362
|
+
and device_might_need_tunneld(e.identifier)
|
|
363
|
+
):
|
|
253
364
|
logger.warning("Got an InvalidServiceError. Trying again over tunneld since it is a developer command")
|
|
254
365
|
should_retry_over_tunneld = True
|
|
255
366
|
if should_retry_over_tunneld:
|
|
256
|
-
# use a single space because
|
|
367
|
+
# use a single space because Typer/Click will ignore envvars of empty strings
|
|
257
368
|
os.environ[TUNNEL_ENV_VAR] = e.identifier or " "
|
|
258
|
-
|
|
369
|
+
main()
|
|
370
|
+
return False
|
|
259
371
|
logger.error(INVALID_SERVICE_MESSAGE)
|
|
260
372
|
except PasswordRequiredError:
|
|
261
373
|
logger.error("Device is password protected. Please unlock and retry")
|
|
@@ -291,6 +403,19 @@ def invoke_cli_with_error_handling() -> bool:
|
|
|
291
403
|
)
|
|
292
404
|
except QuicProtocolNotSupportedError:
|
|
293
405
|
logger.error("Encountered a QUIC protocol error.")
|
|
406
|
+
except StartServiceError as e:
|
|
407
|
+
if e.message == "ServiceProhibited" and e.service_name == "com.apple.pcapd.shim.remote":
|
|
408
|
+
logger.error(
|
|
409
|
+
f"The {e.service_name} service is USB only (at least for some iOS versions).\n"
|
|
410
|
+
"Full discussion is available in: https://github.com/doronz88/pymobiledevice3/issues/1515"
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
logger.error(f"Failed to start: {e.service_name} with. Received error: {e.message}.")
|
|
414
|
+
except click.ClickException as e:
|
|
415
|
+
from typer import rich_utils
|
|
416
|
+
|
|
417
|
+
rich_utils.rich_format_error(e)
|
|
418
|
+
sys.exit(e.exit_code)
|
|
294
419
|
|
|
295
420
|
return False
|
|
296
421
|
|
pymobiledevice3/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '
|
|
32
|
-
__version_tuple__ = version_tuple = (
|
|
31
|
+
__version__ = version = '7.0.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (7, 0, 6)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
pymobiledevice3/bonjour.py
CHANGED
|
@@ -50,7 +50,7 @@ def dataclass_compat(*d_args, **d_kwargs):
|
|
|
50
50
|
@dataclass_compat(slots=True)
|
|
51
51
|
class Address:
|
|
52
52
|
ip: str
|
|
53
|
-
iface:
|
|
53
|
+
iface: str # local interface name (e.g., "en0"), or None if unknown
|
|
54
54
|
|
|
55
55
|
@property
|
|
56
56
|
def full_ip(self) -> str:
|
|
@@ -300,10 +300,9 @@ async def browse_service(service_type: str, timeout: float = 4.0) -> list[Servic
|
|
|
300
300
|
adapters = _Adapters()
|
|
301
301
|
|
|
302
302
|
ptr_targets: set[str] = set()
|
|
303
|
-
srv_map: dict[str, dict] = {}
|
|
303
|
+
srv_map: dict[str, list[dict]] = defaultdict(list) # instance_name -> list of {"target", "port"}
|
|
304
304
|
txt_map: dict[str, dict] = {}
|
|
305
|
-
# host -> list[(ip, iface)]
|
|
306
|
-
host_addrs: dict[str, list[Address]] = defaultdict(list)
|
|
305
|
+
host_addrs: dict[str, list[Address]] = defaultdict(list) # host -> list[(ip, iface)]
|
|
307
306
|
|
|
308
307
|
def _record_addr(rr_name: str, ip_str: str, pkt_addr):
|
|
309
308
|
# Determine family and possible scopeid from the packet that delivered this RR
|
|
@@ -312,7 +311,9 @@ async def browse_service(service_type: str, timeout: float = 4.0) -> list[Servic
|
|
|
312
311
|
if isinstance(pkt_addr, tuple) and len(pkt_addr) == 4: # IPv6 remote tuple
|
|
313
312
|
scopeid = pkt_addr[3]
|
|
314
313
|
iface = adapters.pick_iface_for_ip(ip_str, family, scopeid)
|
|
315
|
-
|
|
314
|
+
if iface is None:
|
|
315
|
+
return
|
|
316
|
+
# Avoid duplicates for the same host/ip
|
|
316
317
|
existing = host_addrs[rr_name]
|
|
317
318
|
if not any(a.ip == ip_str for a in existing):
|
|
318
319
|
existing.append(Address(ip=ip_str, iface=iface))
|
|
@@ -331,11 +332,10 @@ async def browse_service(service_type: str, timeout: float = 4.0) -> list[Servic
|
|
|
331
332
|
if t == QTYPE_PTR and rr.get("name") == service_type:
|
|
332
333
|
ptr_targets.add(rr.get("ptrdname"))
|
|
333
334
|
elif t == QTYPE_SRV:
|
|
334
|
-
srv_map[rr["name"]]
|
|
335
|
-
"target": rr.get("target"),
|
|
336
|
-
"port": rr.get("port"),
|
|
337
|
-
}
|
|
335
|
+
srv_map[rr["name"]].append({"target": rr.get("target"), "port": rr.get("port")})
|
|
338
336
|
elif t == QTYPE_TXT:
|
|
337
|
+
# TODO: This could possibly mix the properties of multiple TXT records for the same instance.
|
|
338
|
+
# However, it's currently unused.
|
|
339
339
|
txt_map[rr["name"]] = rr.get("txt", {})
|
|
340
340
|
elif (t == QTYPE_A and rr.get("address")) or (t == QTYPE_AAAA and rr.get("address")):
|
|
341
341
|
_record_addr(rr["name"], rr["address"], pkt_addr)
|
|
@@ -346,20 +346,21 @@ async def browse_service(service_type: str, timeout: float = 4.0) -> list[Servic
|
|
|
346
346
|
# Assemble dataclasses
|
|
347
347
|
results: list[ServiceInstance] = []
|
|
348
348
|
for inst in sorted(ptr_targets):
|
|
349
|
-
|
|
350
|
-
target = srv.get("target")
|
|
351
|
-
host = (target[:-1] if target and target.endswith(".") else target) or None
|
|
352
|
-
addrs = host_addrs.get(target, []) if target else []
|
|
349
|
+
srv_entries = srv_map.get(inst, [])
|
|
353
350
|
props = txt_map.get(inst, {})
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
351
|
+
for srv in srv_entries:
|
|
352
|
+
target = srv.get("target")
|
|
353
|
+
host = (target[:-1] if target and target.endswith(".") else target) or None
|
|
354
|
+
addrs = host_addrs.get(target, []) if target else []
|
|
355
|
+
results.append(
|
|
356
|
+
ServiceInstance(
|
|
357
|
+
instance=inst,
|
|
358
|
+
host=host,
|
|
359
|
+
port=srv.get("port"),
|
|
360
|
+
addresses=addrs,
|
|
361
|
+
properties=props,
|
|
362
|
+
)
|
|
361
363
|
)
|
|
362
|
-
)
|
|
363
364
|
return results
|
|
364
365
|
|
|
365
366
|
|
|
@@ -1,30 +1,32 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from pymobiledevice3.cli.cli_common import Command
|
|
4
|
-
from pymobiledevice3.lockdown import LockdownClient
|
|
5
|
-
from pymobiledevice3.services.mobile_activation import MobileActivationService
|
|
1
|
+
from typing import Annotated
|
|
6
2
|
|
|
3
|
+
import typer
|
|
4
|
+
from typer_injector import InjectingTyper
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
pass
|
|
11
|
-
|
|
6
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep
|
|
7
|
+
from pymobiledevice3.services.mobile_activation import MobileActivationService
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
9
|
+
cli = InjectingTyper(
|
|
10
|
+
name="activation",
|
|
11
|
+
help="Perform iCloud activation/deactivation or query the current state",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
)
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
@
|
|
20
|
-
def state(service_provider:
|
|
16
|
+
@cli.command()
|
|
17
|
+
def state(service_provider: ServiceProviderDep) -> None:
|
|
21
18
|
"""Get current activation state"""
|
|
22
19
|
print(MobileActivationService(service_provider).state)
|
|
23
20
|
|
|
24
21
|
|
|
25
|
-
@
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
@cli.command()
|
|
23
|
+
def activate(
|
|
24
|
+
service_provider: ServiceProviderDep,
|
|
25
|
+
now: Annotated[
|
|
26
|
+
bool,
|
|
27
|
+
typer.Option(help="do not wait for next nonce cycle"),
|
|
28
|
+
] = False,
|
|
29
|
+
) -> None:
|
|
28
30
|
"""Activate device"""
|
|
29
31
|
activation_service = MobileActivationService(service_provider)
|
|
30
32
|
if not now:
|
|
@@ -32,13 +34,13 @@ def activate(service_provider: LockdownClient, now):
|
|
|
32
34
|
activation_service.activate()
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
@
|
|
36
|
-
def deactivate(service_provider:
|
|
37
|
+
@cli.command()
|
|
38
|
+
def deactivate(service_provider: ServiceProviderDep) -> None:
|
|
37
39
|
"""Deactivate device"""
|
|
38
40
|
MobileActivationService(service_provider).deactivate()
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
@
|
|
42
|
-
def itunes(service_provider:
|
|
43
|
+
@cli.command()
|
|
44
|
+
def itunes(service_provider: ServiceProviderDep) -> None:
|
|
43
45
|
"""Tell the device that it has been connected to iTunes (useful for < iOS 4)"""
|
|
44
46
|
service_provider.set_value(True, key="iTunesHasConnected")
|
pymobiledevice3/cli/afc.py
CHANGED
|
@@ -1,56 +1,64 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
from pymobiledevice3.cli.cli_common import Command
|
|
4
|
-
from pymobiledevice3.lockdown import LockdownClient
|
|
5
|
-
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
|
|
6
|
-
from pymobiledevice3.services.afc import AfcService, AfcShell
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
7
3
|
|
|
4
|
+
import typer
|
|
5
|
+
from typer_injector import InjectingTyper
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
pass
|
|
12
|
-
|
|
7
|
+
from pymobiledevice3.cli.cli_common import ServiceProviderDep
|
|
8
|
+
from pymobiledevice3.services.afc import AfcService, AfcShell
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
10
|
+
cli = InjectingTyper(
|
|
11
|
+
name="afc",
|
|
12
|
+
help="Browse, push, and pull files via the AFC service (/var/mobile/Media).",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
18
15
|
|
|
19
16
|
|
|
20
|
-
@
|
|
21
|
-
def afc_shell(service_provider:
|
|
22
|
-
"""
|
|
17
|
+
@cli.command("shell")
|
|
18
|
+
def afc_shell(service_provider: ServiceProviderDep) -> None:
|
|
19
|
+
"""Open an interactive AFC shell rooted at /var/mobile/Media."""
|
|
23
20
|
AfcShell.create(service_provider)
|
|
24
21
|
|
|
25
22
|
|
|
26
|
-
@
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
@cli.command("pull")
|
|
24
|
+
def afc_pull(
|
|
25
|
+
service_provider: ServiceProviderDep,
|
|
26
|
+
remote_file: Path,
|
|
27
|
+
local_file: Path,
|
|
28
|
+
ignore_errors: Annotated[
|
|
29
|
+
bool,
|
|
30
|
+
typer.Option(
|
|
31
|
+
"--ignore-errors",
|
|
32
|
+
"-i",
|
|
33
|
+
help="Continue downloading even if some files error (best-effort pull).",
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Download a remote path under /var/mobile/Media to the local filesystem."""
|
|
38
|
+
AfcService(lockdown=service_provider).pull(str(remote_file), str(local_file), ignore_errors=ignore_errors)
|
|
33
39
|
|
|
34
40
|
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"""push local file into /var/mobile/Media"""
|
|
40
|
-
AfcService(lockdown=service_provider).push(local_file, remote_file)
|
|
41
|
+
@cli.command("push")
|
|
42
|
+
def afc_push(service_provider: ServiceProviderDep, local_file: Path, remote_file: Path) -> None:
|
|
43
|
+
"""Upload a local file into /var/mobile/Media."""
|
|
44
|
+
AfcService(lockdown=service_provider).push(str(local_file), str(remote_file))
|
|
41
45
|
|
|
42
46
|
|
|
43
|
-
@
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
@cli.command("ls")
|
|
48
|
+
def afc_ls(
|
|
49
|
+
service_provider: ServiceProviderDep,
|
|
50
|
+
remote_file: Path,
|
|
51
|
+
recursive: Annotated[
|
|
52
|
+
bool,
|
|
53
|
+
typer.Option("--recursive", "-r", help="Recurse into subdirectories when listing."),
|
|
54
|
+
] = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""List files under /var/mobile/Media (optionally recursively)."""
|
|
57
|
+
for path in AfcService(lockdown=service_provider).dirlist(str(remote_file), -1 if recursive else 1):
|
|
49
58
|
print(path)
|
|
50
59
|
|
|
51
60
|
|
|
52
|
-
@
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
AfcService(lockdown=service_provider).rm(remote_file)
|
|
61
|
+
@cli.command("rm")
|
|
62
|
+
def afc_rm(service_provider: ServiceProviderDep, remote_file: Path) -> None:
|
|
63
|
+
"""Delete a file under /var/mobile/Media."""
|
|
64
|
+
AfcService(lockdown=service_provider).rm(str(remote_file))
|