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.
- misc/plist_sniffer.py +15 -15
- misc/remotexpc_sniffer.py +29 -28
- misc/understanding_idevice_protocol_layers.md +15 -10
- pymobiledevice3/__main__.py +317 -127
- pymobiledevice3/_version.py +22 -4
- pymobiledevice3/bonjour.py +358 -113
- pymobiledevice3/ca.py +253 -16
- pymobiledevice3/cli/activation.py +31 -23
- pymobiledevice3/cli/afc.py +49 -40
- pymobiledevice3/cli/amfi.py +16 -21
- pymobiledevice3/cli/apps.py +87 -42
- pymobiledevice3/cli/backup.py +160 -90
- pymobiledevice3/cli/bonjour.py +44 -40
- pymobiledevice3/cli/cli_common.py +204 -198
- pymobiledevice3/cli/companion_proxy.py +14 -14
- pymobiledevice3/cli/crash.py +105 -56
- 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 +108 -103
- pymobiledevice3/cli/mounter.py +158 -99
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +45 -24
- pymobiledevice3/cli/power_assertion.py +18 -17
- pymobiledevice3/cli/processes.py +17 -23
- pymobiledevice3/cli/profile.py +165 -109
- pymobiledevice3/cli/provision.py +35 -34
- pymobiledevice3/cli/remote.py +217 -129
- pymobiledevice3/cli/restore.py +159 -143
- pymobiledevice3/cli/springboard.py +63 -53
- pymobiledevice3/cli/syslog.py +193 -86
- pymobiledevice3/cli/usbmux.py +73 -33
- pymobiledevice3/cli/version.py +5 -7
- pymobiledevice3/cli/webinspector.py +376 -214
- pymobiledevice3/common.py +3 -1
- pymobiledevice3/exceptions.py +182 -58
- pymobiledevice3/irecv.py +52 -53
- pymobiledevice3/irecv_devices.py +1489 -464
- pymobiledevice3/lockdown.py +473 -275
- pymobiledevice3/lockdown_service_provider.py +15 -8
- pymobiledevice3/osu/os_utils.py +27 -9
- pymobiledevice3/osu/posix_util.py +34 -15
- pymobiledevice3/osu/win_util.py +14 -8
- pymobiledevice3/pair_records.py +102 -21
- pymobiledevice3/remote/common.py +8 -4
- pymobiledevice3/remote/core_device/app_service.py +94 -67
- pymobiledevice3/remote/core_device/core_device_service.py +17 -14
- pymobiledevice3/remote/core_device/device_info.py +5 -5
- pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
- pymobiledevice3/remote/core_device/file_service.py +53 -23
- pymobiledevice3/remote/remote_service_discovery.py +79 -45
- pymobiledevice3/remote/remotexpc.py +73 -44
- pymobiledevice3/remote/tunnel_service.py +442 -317
- pymobiledevice3/remote/utils.py +14 -13
- pymobiledevice3/remote/xpc_message.py +145 -125
- pymobiledevice3/resources/dsc_uuid_map.py +19 -19
- pymobiledevice3/resources/firmware_notifications.py +20 -16
- pymobiledevice3/resources/notifications.txt +144 -0
- pymobiledevice3/restore/asr.py +27 -27
- pymobiledevice3/restore/base_restore.py +110 -21
- pymobiledevice3/restore/consts.py +87 -66
- pymobiledevice3/restore/device.py +59 -12
- pymobiledevice3/restore/fdr.py +46 -48
- pymobiledevice3/restore/ftab.py +19 -19
- pymobiledevice3/restore/img4.py +163 -0
- pymobiledevice3/restore/mbn.py +587 -0
- pymobiledevice3/restore/recovery.py +151 -151
- pymobiledevice3/restore/restore.py +562 -544
- pymobiledevice3/restore/restore_options.py +131 -110
- pymobiledevice3/restore/restored_client.py +51 -31
- pymobiledevice3/restore/tss.py +385 -267
- pymobiledevice3/service_connection.py +252 -59
- pymobiledevice3/services/accessibilityaudit.py +202 -120
- pymobiledevice3/services/afc.py +962 -365
- pymobiledevice3/services/amfi.py +24 -30
- pymobiledevice3/services/companion.py +23 -19
- pymobiledevice3/services/crash_reports.py +71 -47
- pymobiledevice3/services/debugserver_applist.py +3 -3
- pymobiledevice3/services/device_arbitration.py +8 -8
- pymobiledevice3/services/device_link.py +101 -79
- pymobiledevice3/services/diagnostics.py +973 -967
- pymobiledevice3/services/dtfetchsymbols.py +8 -8
- pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
- pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
- pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
- pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
- pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
- pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
- pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
- pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
- pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
- pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
- pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
- pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
- pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
- pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
- pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
- pymobiledevice3/services/file_relay.py +10 -10
- pymobiledevice3/services/heartbeat.py +9 -8
- pymobiledevice3/services/house_arrest.py +16 -15
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/installation_proxy.py +173 -81
- pymobiledevice3/services/lockdown_service.py +20 -10
- pymobiledevice3/services/misagent.py +22 -19
- pymobiledevice3/services/mobile_activation.py +147 -64
- pymobiledevice3/services/mobile_config.py +331 -294
- pymobiledevice3/services/mobile_image_mounter.py +141 -113
- pymobiledevice3/services/mobilebackup2.py +203 -145
- pymobiledevice3/services/notification_proxy.py +11 -11
- pymobiledevice3/services/os_trace.py +134 -74
- pymobiledevice3/services/pcapd.py +314 -302
- pymobiledevice3/services/power_assertion.py +10 -9
- pymobiledevice3/services/preboard.py +4 -4
- pymobiledevice3/services/remote_fetch_symbols.py +21 -14
- pymobiledevice3/services/remote_server.py +176 -146
- pymobiledevice3/services/restore_service.py +16 -16
- pymobiledevice3/services/screenshot.py +15 -12
- pymobiledevice3/services/simulate_location.py +7 -7
- pymobiledevice3/services/springboard.py +15 -15
- pymobiledevice3/services/syslog.py +5 -5
- pymobiledevice3/services/web_protocol/alert.py +11 -11
- pymobiledevice3/services/web_protocol/automation_session.py +251 -239
- pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
- pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
- pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
- pymobiledevice3/services/web_protocol/driver.py +114 -111
- pymobiledevice3/services/web_protocol/element.py +124 -111
- pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
- pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
- pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
- pymobiledevice3/services/web_protocol/switch_to.py +30 -27
- pymobiledevice3/services/webinspector.py +189 -155
- pymobiledevice3/tcp_forwarder.py +87 -69
- pymobiledevice3/tunneld/__init__.py +0 -0
- pymobiledevice3/tunneld/api.py +63 -0
- pymobiledevice3/tunneld/server.py +603 -0
- pymobiledevice3/usbmux.py +198 -147
- pymobiledevice3/utils.py +14 -11
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
- pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
- pymobiledevice3/cli/developer.py +0 -1215
- pymobiledevice3/cli/diagnostics.py +0 -99
- pymobiledevice3/tunneld.py +0 -524
- pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
pymobiledevice3/__main__.py
CHANGED
|
@@ -1,42 +1,74 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import difflib
|
|
3
|
+
import importlib
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
6
7
|
import sys
|
|
7
8
|
import textwrap
|
|
8
9
|
import traceback
|
|
9
|
-
|
|
10
|
+
import warnings
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import Annotated, Optional, Union
|
|
10
13
|
|
|
11
14
|
import click
|
|
12
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
|
|
13
21
|
|
|
14
|
-
from pymobiledevice3.cli.cli_common import TUNNEL_ENV_VAR, isatty
|
|
15
|
-
from pymobiledevice3.exceptions import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
from pymobiledevice3.cli.cli_common import TUNNEL_ENV_VAR, isatty, set_color_flag, set_verbosity
|
|
23
|
+
from pymobiledevice3.exceptions import (
|
|
24
|
+
AccessDeniedError,
|
|
25
|
+
CloudConfigurationAlreadyPresentError,
|
|
26
|
+
ConnectionFailedError,
|
|
27
|
+
ConnectionFailedToUsbmuxdError,
|
|
28
|
+
DeprecationError,
|
|
29
|
+
DeveloperModeError,
|
|
30
|
+
DeveloperModeIsNotEnabledError,
|
|
31
|
+
DeviceHasPasscodeSetError,
|
|
32
|
+
DeviceNotFoundError,
|
|
33
|
+
FeatureNotSupportedError,
|
|
34
|
+
InternalError,
|
|
35
|
+
InvalidServiceError,
|
|
36
|
+
MessageNotSupportedError,
|
|
37
|
+
MissingValueError,
|
|
38
|
+
NoDeviceConnectedError,
|
|
39
|
+
NotEnoughDiskSpaceError,
|
|
40
|
+
NotPairedError,
|
|
41
|
+
OSNotSupportedError,
|
|
42
|
+
PairingDialogResponsePendingError,
|
|
43
|
+
PasswordRequiredError,
|
|
44
|
+
QuicProtocolNotSupportedError,
|
|
45
|
+
RSDRequiredError,
|
|
46
|
+
SetProhibitedError,
|
|
47
|
+
StartServiceError,
|
|
48
|
+
TunneldConnectionError,
|
|
49
|
+
UserDeniedPairingError,
|
|
50
|
+
)
|
|
51
|
+
from pymobiledevice3.lockdown import create_using_usbmux, retry_create_using_usbmux
|
|
21
52
|
from pymobiledevice3.osu.os_utils import get_os_utils
|
|
22
53
|
|
|
23
54
|
coloredlogs.install(level=logging.INFO)
|
|
24
55
|
|
|
25
|
-
logging.getLogger(
|
|
26
|
-
logging.getLogger(
|
|
27
|
-
logging.getLogger(
|
|
28
|
-
logging.getLogger(
|
|
29
|
-
logging.getLogger(
|
|
30
|
-
logging.getLogger(
|
|
31
|
-
logging.getLogger(
|
|
32
|
-
logging.getLogger(
|
|
33
|
-
logging.getLogger('urllib3.connectionpool').disabled = True
|
|
56
|
+
logging.getLogger("quic").disabled = True
|
|
57
|
+
logging.getLogger("asyncio").disabled = True
|
|
58
|
+
logging.getLogger("parso.cache").disabled = True
|
|
59
|
+
logging.getLogger("parso.cache.pickle").disabled = True
|
|
60
|
+
logging.getLogger("parso.python.diff").disabled = True
|
|
61
|
+
logging.getLogger("humanfriendly.prompts").disabled = True
|
|
62
|
+
logging.getLogger("blib2to3.pgen2.driver").disabled = True
|
|
63
|
+
logging.getLogger("urllib3.connectionpool").disabled = True
|
|
34
64
|
|
|
35
65
|
logger = logging.getLogger(__name__)
|
|
36
66
|
|
|
37
67
|
# For issue https://github.com/doronz88/pymobiledevice3/issues/1217, details: https://bugs.python.org/issue37373
|
|
38
|
-
if sys.platform ==
|
|
39
|
-
|
|
68
|
+
if sys.platform == "win32":
|
|
69
|
+
with warnings.catch_warnings():
|
|
70
|
+
warnings.simplefilter("ignore", category=DeprecationWarning)
|
|
71
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
40
72
|
|
|
41
73
|
INVALID_SERVICE_MESSAGE = """Failed to start service. Possible reasons are:
|
|
42
74
|
- If you were trying to access a developer service (developer subcommand):
|
|
@@ -51,200 +83,358 @@ INVALID_SERVICE_MESSAGE = """Failed to start service. Possible reasons are:
|
|
|
51
83
|
- Make sure you passed the --rsd option to the subcommand
|
|
52
84
|
https://github.com/doronz88/pymobiledevice3#working-with-developer-tools-ios--170
|
|
53
85
|
|
|
54
|
-
- Apple removed this service
|
|
86
|
+
- Apple removed this service, or your iOS version does not support it.
|
|
55
87
|
|
|
56
88
|
- A bug. Please file a bug report:
|
|
57
89
|
https://github.com/doronz88/pymobiledevice3/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=
|
|
58
90
|
"""
|
|
59
91
|
|
|
60
|
-
CONTEXT_SETTINGS =
|
|
92
|
+
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"], "max_content_width": 400}
|
|
61
93
|
|
|
62
94
|
# Mapping of index options to import file names
|
|
63
95
|
CLI_GROUPS = {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
"activation": "activation",
|
|
97
|
+
"afc": "afc",
|
|
98
|
+
"amfi": "amfi",
|
|
99
|
+
"apps": "apps",
|
|
100
|
+
"backup2": "backup",
|
|
101
|
+
"bonjour": "bonjour",
|
|
102
|
+
"companion": "companion_proxy",
|
|
103
|
+
"crash": "crash",
|
|
104
|
+
"developer": "developer",
|
|
105
|
+
"diagnostics": "diagnostics",
|
|
106
|
+
"lockdown": "lockdown",
|
|
107
|
+
"mounter": "mounter",
|
|
108
|
+
"notification": "notification",
|
|
109
|
+
"pcap": "pcap",
|
|
110
|
+
"power-assertion": "power_assertion",
|
|
111
|
+
"processes": "processes",
|
|
112
|
+
"profile": "profile",
|
|
113
|
+
"provision": "provision",
|
|
114
|
+
"remote": "remote",
|
|
115
|
+
"restore": "restore",
|
|
116
|
+
"springboard": "springboard",
|
|
117
|
+
"syslog": "syslog",
|
|
118
|
+
"usbmux": "usbmux",
|
|
119
|
+
"webinspector": "webinspector",
|
|
120
|
+
"idam": "idam",
|
|
121
|
+
"version": "version",
|
|
89
122
|
}
|
|
90
123
|
|
|
124
|
+
# Set if used the `--reconnect` option
|
|
125
|
+
RECONNECT = False
|
|
91
126
|
|
|
92
|
-
class Pmd3Cli(click.Group):
|
|
93
|
-
def list_commands(self, ctx):
|
|
94
|
-
return CLI_GROUPS.keys()
|
|
95
127
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
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())
|
|
100
132
|
|
|
101
|
-
def
|
|
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)
|
|
137
|
+
|
|
138
|
+
def handle_invalid_command(self, ctx, name: str) -> None:
|
|
102
139
|
suggested_commands = self.search_commands(name)
|
|
103
140
|
suggestion = self.format_suggestions(suggested_commands)
|
|
104
|
-
ctx.fail
|
|
141
|
+
# ctx.fail raises a ClickException underneath, which Typer displays nicely
|
|
142
|
+
ctx.fail(f"No such command {name!r}{suggestion}")
|
|
105
143
|
|
|
106
144
|
@staticmethod
|
|
107
145
|
def format_suggestions(suggestions: list[str]) -> str:
|
|
108
146
|
if not suggestions:
|
|
109
|
-
return
|
|
110
|
-
cmds = textwrap.indent(
|
|
111
|
-
return f
|
|
147
|
+
return ""
|
|
148
|
+
cmds = textwrap.indent("\n".join(suggestions), " " * 4)
|
|
149
|
+
return f"\nDid you mean:\n{cmds}"
|
|
112
150
|
|
|
113
151
|
@staticmethod
|
|
114
152
|
def import_and_get_command(ctx: click.Context, name: str) -> click.Command:
|
|
115
|
-
module_name = f
|
|
116
|
-
mod =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
command = mod.cli.get_command(ctx, command_name)
|
|
121
|
-
return command
|
|
153
|
+
module_name = f"pymobiledevice3.cli.{CLI_GROUPS[name]}"
|
|
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)
|
|
122
158
|
|
|
123
159
|
@staticmethod
|
|
124
160
|
def highlight_keyword(text: str, keyword: str) -> str:
|
|
125
|
-
return re.sub(f
|
|
161
|
+
return re.sub(f"({keyword})", typer.style("\\1", bold=True), text, flags=re.IGNORECASE)
|
|
126
162
|
|
|
127
163
|
@staticmethod
|
|
128
|
-
def collect_commands(command: click.Command) -> Union[str, list[str]]:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
for
|
|
132
|
-
|
|
133
|
-
if isinstance(
|
|
134
|
-
|
|
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])
|
|
135
171
|
else:
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
return
|
|
172
|
+
cmds.append(f"{command.name} {child}")
|
|
173
|
+
return cmds
|
|
174
|
+
return command.name or ""
|
|
139
175
|
|
|
140
176
|
@staticmethod
|
|
141
177
|
def search_commands(pattern: str) -> list[str]:
|
|
142
|
-
all_commands =
|
|
178
|
+
all_commands = Pmd3TyperGroup.load_all_commands()
|
|
143
179
|
matched = sorted(filter(lambda cmd: re.search(pattern, cmd), all_commands))
|
|
144
180
|
if not matched:
|
|
145
181
|
matched = difflib.get_close_matches(pattern, all_commands, n=20, cutoff=0.4)
|
|
146
182
|
if isatty():
|
|
147
|
-
matched = [
|
|
183
|
+
matched = [Pmd3TyperGroup.highlight_keyword(cmd, pattern) for cmd in matched]
|
|
148
184
|
return matched
|
|
149
185
|
|
|
150
186
|
@staticmethod
|
|
151
187
|
def load_all_commands() -> list[str]:
|
|
152
|
-
all_commands = []
|
|
153
|
-
for key in CLI_GROUPS
|
|
154
|
-
module_name = f
|
|
155
|
-
mod =
|
|
156
|
-
|
|
188
|
+
all_commands: list[str] = []
|
|
189
|
+
for key in CLI_GROUPS:
|
|
190
|
+
module_name = f"pymobiledevice3.cli.{CLI_GROUPS[key]}"
|
|
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])
|
|
157
196
|
if isinstance(cmd, list):
|
|
158
197
|
all_commands.extend(cmd)
|
|
159
198
|
else:
|
|
160
199
|
all_commands.append(cmd)
|
|
161
200
|
return all_commands
|
|
162
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
|
+
)
|
|
163
219
|
|
|
164
|
-
|
|
165
|
-
|
|
220
|
+
|
|
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:
|
|
166
245
|
"""
|
|
167
|
-
|
|
168
|
-
Interact with a connected iDevice (iPhone, iPad, ...)
|
|
169
|
-
For more information please look at:
|
|
170
|
-
https://github.com/doronz88/pymobiledevice3
|
|
246
|
+
Top-level options for pymobiledevice3.
|
|
171
247
|
"""
|
|
172
|
-
|
|
248
|
+
global RECONNECT
|
|
249
|
+
RECONNECT = reconnect
|
|
250
|
+
set_verbosity(verbosity)
|
|
251
|
+
set_color_flag(color)
|
|
173
252
|
|
|
174
253
|
|
|
175
|
-
def
|
|
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)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def invoke_cli_with_error_handling() -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Invoke the command line interface and return `True` if the failure reason of the command was that the device was
|
|
310
|
+
disconnected.
|
|
311
|
+
"""
|
|
176
312
|
try:
|
|
177
|
-
|
|
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
|
|
178
318
|
except NoDeviceConnectedError:
|
|
179
|
-
logger.error(
|
|
319
|
+
logger.error("Device is not connected")
|
|
320
|
+
return True
|
|
180
321
|
except ConnectionAbortedError:
|
|
181
|
-
logger.error(
|
|
322
|
+
logger.error("Device was disconnected")
|
|
323
|
+
return True
|
|
182
324
|
except NotPairedError:
|
|
183
|
-
logger.error(
|
|
325
|
+
logger.error("Device is not paired")
|
|
184
326
|
except UserDeniedPairingError:
|
|
185
|
-
logger.error(
|
|
327
|
+
logger.error("User refused to trust this computer")
|
|
186
328
|
except PairingDialogResponsePendingError:
|
|
187
|
-
logger.error(
|
|
329
|
+
logger.error("Waiting for user dialog approval")
|
|
188
330
|
except SetProhibitedError:
|
|
189
|
-
logger.error(
|
|
331
|
+
logger.error("lockdownd denied the access")
|
|
190
332
|
except MissingValueError:
|
|
191
|
-
logger.error(
|
|
333
|
+
logger.error("No such value")
|
|
192
334
|
except DeviceHasPasscodeSetError:
|
|
193
|
-
logger.error(
|
|
194
|
-
except DeveloperModeError
|
|
195
|
-
logger.error(
|
|
335
|
+
logger.error("Cannot enable developer-mode when passcode is set")
|
|
336
|
+
except DeveloperModeError:
|
|
337
|
+
logger.error("Failed to enable developer-mode.")
|
|
196
338
|
except ConnectionFailedToUsbmuxdError:
|
|
197
|
-
logger.error(
|
|
339
|
+
logger.error("Failed to connect to usbmuxd socket. Make sure it's running.")
|
|
340
|
+
except ConnectionFailedError:
|
|
341
|
+
logger.error("Failed to connect to service port.")
|
|
342
|
+
return True
|
|
198
343
|
except MessageNotSupportedError:
|
|
199
|
-
logger.error(
|
|
344
|
+
logger.error("Message not supported for this iOS version")
|
|
200
345
|
traceback.print_exc()
|
|
201
346
|
except InternalError:
|
|
202
|
-
logger.error(
|
|
347
|
+
logger.error("Internal Error")
|
|
203
348
|
except DeveloperModeIsNotEnabledError:
|
|
204
|
-
logger.error(
|
|
205
|
-
|
|
349
|
+
logger.error(
|
|
350
|
+
"Developer Mode is disabled. You can try to enable it using: "
|
|
351
|
+
"python3 -m pymobiledevice3 amfi enable-developer-mode"
|
|
352
|
+
)
|
|
206
353
|
except (InvalidServiceError, RSDRequiredError) as e:
|
|
207
354
|
should_retry_over_tunneld = False
|
|
208
355
|
if isinstance(e, RSDRequiredError):
|
|
209
|
-
logger.warning(
|
|
356
|
+
logger.warning("Trying again over tunneld since RSD is required for this command")
|
|
210
357
|
should_retry_over_tunneld = True
|
|
211
|
-
elif (
|
|
212
|
-
|
|
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
|
+
):
|
|
364
|
+
logger.warning("Got an InvalidServiceError. Trying again over tunneld since it is a developer command")
|
|
213
365
|
should_retry_over_tunneld = True
|
|
214
366
|
if should_retry_over_tunneld:
|
|
215
|
-
# use a single space because
|
|
216
|
-
os.environ[TUNNEL_ENV_VAR] =
|
|
217
|
-
|
|
367
|
+
# use a single space because Typer/Click will ignore envvars of empty strings
|
|
368
|
+
os.environ[TUNNEL_ENV_VAR] = e.identifier or " "
|
|
369
|
+
main()
|
|
370
|
+
return False
|
|
218
371
|
logger.error(INVALID_SERVICE_MESSAGE)
|
|
219
372
|
except PasswordRequiredError:
|
|
220
|
-
logger.error(
|
|
373
|
+
logger.error("Device is password protected. Please unlock and retry")
|
|
221
374
|
except AccessDeniedError:
|
|
222
375
|
logger.error(get_os_utils().access_denied_error)
|
|
223
376
|
except BrokenPipeError:
|
|
224
377
|
traceback.print_exc()
|
|
225
378
|
except TunneldConnectionError:
|
|
226
379
|
logger.error(
|
|
227
|
-
|
|
228
|
-
|
|
380
|
+
"Unable to connect to Tunneld. You can start one using:\nsudo python3 -m pymobiledevice3 remote tunneld"
|
|
381
|
+
)
|
|
229
382
|
except DeviceNotFoundError as e:
|
|
230
|
-
logger.error(f
|
|
383
|
+
logger.error(f"Device not found: {e.udid}")
|
|
231
384
|
except NotEnoughDiskSpaceError:
|
|
232
|
-
logger.error(
|
|
385
|
+
logger.error("Not enough disk space")
|
|
233
386
|
except DeprecationError:
|
|
234
|
-
logger.error(
|
|
387
|
+
logger.error("failed to query MobileGestalt, MobileGestalt deprecated (iOS >= 17.4).")
|
|
235
388
|
except OSNotSupportedError as e:
|
|
236
389
|
logger.error(
|
|
237
|
-
f
|
|
238
|
-
f
|
|
390
|
+
f"Unsupported OS - {e.os_name}. To add support, consider contributing at "
|
|
391
|
+
f"https://github.com/doronz88/pymobiledevice3."
|
|
392
|
+
)
|
|
239
393
|
except CloudConfigurationAlreadyPresentError:
|
|
240
|
-
logger.error(
|
|
241
|
-
|
|
242
|
-
|
|
394
|
+
logger.error(
|
|
395
|
+
"A cloud configuration is already present on device. You must first erase the device in order "
|
|
396
|
+
"to install new one:\n"
|
|
397
|
+
"> pymobiledevice3 profile erase-device"
|
|
398
|
+
)
|
|
243
399
|
except FeatureNotSupportedError as e:
|
|
244
400
|
logger.error(
|
|
245
|
-
f
|
|
246
|
-
f
|
|
401
|
+
f"Missing implementation of `{e.feature}` on `{e.os_name}`. To add support, consider contributing at "
|
|
402
|
+
f"https://github.com/doronz88/pymobiledevice3."
|
|
403
|
+
)
|
|
404
|
+
except QuicProtocolNotSupportedError:
|
|
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)
|
|
419
|
+
|
|
420
|
+
return False
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def main() -> None:
|
|
424
|
+
# Retry to invoke the CLI
|
|
425
|
+
while invoke_cli_with_error_handling():
|
|
426
|
+
# If reached here, this means the failure reason was that the device is disconnected
|
|
427
|
+
if not RECONNECT:
|
|
428
|
+
# If not invoked with the `--reconnect` option, break here
|
|
429
|
+
break
|
|
430
|
+
try:
|
|
431
|
+
# Wait for the device to be available again
|
|
432
|
+
lockdown = retry_create_using_usbmux(None)
|
|
433
|
+
lockdown.close()
|
|
434
|
+
except KeyboardInterrupt:
|
|
435
|
+
print("Aborted.")
|
|
436
|
+
break
|
|
247
437
|
|
|
248
438
|
|
|
249
|
-
if __name__ ==
|
|
439
|
+
if __name__ == "__main__":
|
|
250
440
|
main()
|
pymobiledevice3/_version.py
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
3
13
|
TYPE_CHECKING = False
|
|
4
14
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
6
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
7
20
|
else:
|
|
8
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
9
23
|
|
|
10
24
|
version: str
|
|
11
25
|
__version__: str
|
|
12
26
|
__version_tuple__: VERSION_TUPLE
|
|
13
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '7.0.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (7, 0, 6)
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
__version_tuple__ = version_tuple = (4, 14, 6)
|
|
34
|
+
__commit_id__ = commit_id = None
|