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,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
- from typing import Union
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 AccessDeniedError, CloudConfigurationAlreadyPresentError, \
16
- ConnectionFailedToUsbmuxdError, DeprecationError, DeveloperModeError, DeveloperModeIsNotEnabledError, \
17
- DeviceHasPasscodeSetError, DeviceNotFoundError, FeatureNotSupportedError, InternalError, InvalidServiceError, \
18
- MessageNotSupportedError, MissingValueError, NoDeviceConnectedError, NotEnoughDiskSpaceError, NotPairedError, \
19
- OSNotSupportedError, PairingDialogResponsePendingError, PasswordRequiredError, RSDRequiredError, \
20
- SetProhibitedError, TunneldConnectionError, UserDeniedPairingError
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('quic').disabled = True
26
- logging.getLogger('asyncio').disabled = True
27
- logging.getLogger('zeroconf').disabled = True
28
- logging.getLogger('parso.cache').disabled = True
29
- logging.getLogger('parso.cache.pickle').disabled = True
30
- logging.getLogger('parso.python.diff').disabled = True
31
- logging.getLogger('humanfriendly.prompts').disabled = True
32
- logging.getLogger('blib2to3.pgen2.driver').disabled = True
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 == 'win32':
39
- asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
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 = dict(help_option_names=['-h', '--help'], max_content_width=400)
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
- 'activation': 'activation',
65
- 'afc': 'afc',
66
- 'amfi': 'amfi',
67
- 'apps': 'apps',
68
- 'backup2': 'backup',
69
- 'bonjour': 'bonjour',
70
- 'companion': 'companion_proxy',
71
- 'crash': 'crash',
72
- 'developer': 'developer',
73
- 'diagnostics': 'diagnostics',
74
- 'lockdown': 'lockdown',
75
- 'mounter': 'mounter',
76
- 'notification': 'notification',
77
- 'pcap': 'pcap',
78
- 'power-assertion': 'power_assertion',
79
- 'processes': 'processes',
80
- 'profile': 'profile',
81
- 'provision': 'provision',
82
- 'remote': 'remote',
83
- 'restore': 'restore',
84
- 'springboard': 'springboard',
85
- 'syslog': 'syslog',
86
- 'usbmux': 'usbmux',
87
- 'webinspector': 'webinspector',
88
- 'version': 'version',
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
- def get_command(self, ctx: click.Context, name: str) -> click.Command:
97
- if name not in CLI_GROUPS.keys():
98
- self.handle_invalid_command(ctx, name)
99
- return self.import_and_get_command(ctx, name)
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 handle_invalid_command(self, ctx: click.Context, name: str) -> None:
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(f'No such command {name!r}{suggestion}')
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('\n'.join(suggestions), ' ' * 4)
111
- return f'\nDid you mean this?\n{cmds}'
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'pymobiledevice3.cli.{CLI_GROUPS[name]}'
116
- mod = __import__(module_name, None, None, ['cli'])
117
- command = mod.cli.get_command(ctx, name)
118
- if not command:
119
- command_name = mod.cli.list_commands(ctx)[0]
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'({keyword})', click.style('\\1', bold=True), text, flags=re.IGNORECASE)
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
- commands = []
130
- if isinstance(command, click.Group):
131
- for k, v in command.commands.items():
132
- cmd = Pmd3Cli.collect_commands(v)
133
- if isinstance(cmd, list):
134
- commands.extend([f'{command.name} {c}' for c in cmd])
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
- commands.append(f'{command.name} {cmd}')
137
- return commands
138
- return f'{command.name}'
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 = Pmd3Cli.load_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 = [Pmd3Cli.highlight_keyword(cmd, pattern) for cmd in 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.keys():
154
- module_name = f'pymobiledevice3.cli.{CLI_GROUPS[key]}'
155
- mod = __import__(module_name, None, None, ['cli'])
156
- cmd = Pmd3Cli.collect_commands(mod.cli.commands[key])
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
- @click.command(cls=Pmd3Cli, context_settings=CONTEXT_SETTINGS)
165
- def cli():
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
- \b
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
- pass
248
+ global RECONNECT
249
+ RECONNECT = reconnect
250
+ set_verbosity(verbosity)
251
+ set_color_flag(color)
173
252
 
174
253
 
175
- def main() -> None:
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
- cli()
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('Device is not connected')
319
+ logger.error("Device is not connected")
320
+ return True
180
321
  except ConnectionAbortedError:
181
- logger.error('Device was disconnected')
322
+ logger.error("Device was disconnected")
323
+ return True
182
324
  except NotPairedError:
183
- logger.error('Device is not paired')
325
+ logger.error("Device is not paired")
184
326
  except UserDeniedPairingError:
185
- logger.error('User refused to trust this computer')
327
+ logger.error("User refused to trust this computer")
186
328
  except PairingDialogResponsePendingError:
187
- logger.error('Waiting for user dialog approval')
329
+ logger.error("Waiting for user dialog approval")
188
330
  except SetProhibitedError:
189
- logger.error('lockdownd denied the access')
331
+ logger.error("lockdownd denied the access")
190
332
  except MissingValueError:
191
- logger.error('No such value')
333
+ logger.error("No such value")
192
334
  except DeviceHasPasscodeSetError:
193
- logger.error('Cannot enable developer-mode when passcode is set')
194
- except DeveloperModeError as e:
195
- logger.error(f'Failed to enable developer-mode. Error: {e}')
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('Failed to connect to usbmuxd socket. Make sure it\'s running.')
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('Message not supported for this iOS version')
344
+ logger.error("Message not supported for this iOS version")
200
345
  traceback.print_exc()
201
346
  except InternalError:
202
- logger.error('Internal Error')
347
+ logger.error("Internal Error")
203
348
  except DeveloperModeIsNotEnabledError:
204
- logger.error('Developer Mode is disabled. You can try to enable it using: '
205
- 'python3 -m pymobiledevice3 amfi enable-developer-mode')
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('Trying again over tunneld since RSD is required for this command')
356
+ logger.warning("Trying again over tunneld since RSD is required for this command")
210
357
  should_retry_over_tunneld = True
211
- elif (e.identifier is not None) and ('developer' in sys.argv) and ('--tunnel' not in sys.argv):
212
- logger.warning('Got an InvalidServiceError. Trying again over tunneld since it is a developer command')
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 click will ignore envvars of empty strings
216
- os.environ[TUNNEL_ENV_VAR] = ' '
217
- return main()
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('Device is password protected. Please unlock and retry')
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
- 'Unable to connect to Tunneld. You can start one using:\n'
228
- 'sudo python3 -m pymobiledevice3 remote tunneld')
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'Device not found: {e.udid}')
383
+ logger.error(f"Device not found: {e.udid}")
231
384
  except NotEnoughDiskSpaceError:
232
- logger.error('Not enough disk space')
385
+ logger.error("Not enough disk space")
233
386
  except DeprecationError:
234
- logger.error('failed to query MobileGestalt, MobileGestalt deprecated (iOS >= 17.4).')
387
+ logger.error("failed to query MobileGestalt, MobileGestalt deprecated (iOS >= 17.4).")
235
388
  except OSNotSupportedError as e:
236
389
  logger.error(
237
- f'Unsupported OS - {e.os_name}. To add support, consider contributing at '
238
- f'https://github.com/doronz88/pymobiledevice3.')
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('A cloud configuration is already present on device. You must first erase the device in order '
241
- 'to install new one:\n'
242
- '> pymobiledevice3 profile erase-device')
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'Missing implementation of `{e.feature}` on `{e.os_name}`. To add support, consider contributing at '
246
- f'https://github.com/doronz88/pymobiledevice3.')
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__ == '__main__':
439
+ if __name__ == "__main__":
250
440
  main()
@@ -1,16 +1,34 @@
1
- # file generated by setuptools_scm
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, Union
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
- __version__ = version = '4.14.6'
16
- __version_tuple__ = version_tuple = (4, 14, 6)
34
+ __commit_id__ = commit_id = None