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
@@ -6,69 +6,43 @@ import os
6
6
  import sys
7
7
  import uuid
8
8
  from functools import wraps
9
- from typing import Any, Callable, Optional
9
+ from textwrap import dedent
10
+ from typing import Annotated, Any, Callable, Optional
10
11
 
11
12
  import click
12
13
  import coloredlogs
13
14
  import hexdump
14
15
  import inquirer3
15
- from click import Option, UsageError
16
+ import typer
17
+ from click import UsageError
16
18
  from inquirer3.themes import GreenPassion
17
19
  from pygments import formatters, highlight, lexers
20
+ from typer_injector import Depends
18
21
 
19
22
  from pymobiledevice3.exceptions import AccessDeniedError, DeviceNotFoundError, NoDeviceConnectedError
20
- from pymobiledevice3.lockdown import LockdownClient, create_using_usbmux
23
+ from pymobiledevice3.lockdown import TcpLockdownClient, create_using_usbmux, get_mobdev2_lockdowns
24
+ from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
21
25
  from pymobiledevice3.osu.os_utils import get_os_utils
22
26
  from pymobiledevice3.remote.remote_service_discovery import RemoteServiceDiscoveryService
23
- from pymobiledevice3.tunneld import TUNNELD_DEFAULT_ADDRESS, async_get_tunneld_devices
27
+ from pymobiledevice3.tunneld.api import TUNNELD_DEFAULT_ADDRESS, async_get_tunneld_devices
24
28
  from pymobiledevice3.usbmux import select_devices_by_connection_type
25
29
 
26
- COLORED_OUTPUT = True
27
- UDID_ENV_VAR = 'PYMOBILEDEVICE3_UDID'
28
- TUNNEL_ENV_VAR = 'PYMOBILEDEVICE3_TUNNEL'
29
- USBMUX_ENV_VAR = 'PYMOBILEDEVICE3_USBMUX'
30
+ UDID_ENV_VAR = "PYMOBILEDEVICE3_UDID"
31
+ TUNNEL_ENV_VAR = "PYMOBILEDEVICE3_TUNNEL"
32
+ USBMUX_ENV_VAR = "PYMOBILEDEVICE3_USBMUX"
33
+ USBMUX_OPTION_HELP = (
34
+ "Address of the usbmuxd daemon (unix socket path or HOST:PORT). Defaults to the platform usbmuxd if omitted."
35
+ )
36
+ DEVICE_OPTIONS_PANEL_TITLE = "Device Options"
30
37
  OSUTILS = get_os_utils()
31
38
 
32
- USBMUX_OPTION_HELP = (f'usbmuxd listener address (in the form of either /path/to/unix/socket OR HOST:PORT). '
33
- f'Can be specified via {USBMUX_ENV_VAR} envvar')
34
-
35
-
36
- class RSDOption(Option):
37
- def __init__(self, *args, **kwargs):
38
- self.mutually_exclusive = set(kwargs.pop('mutually_exclusive', []))
39
- help = kwargs.get('help', '')
40
- if self.mutually_exclusive:
41
- ex_str = ', '.join(self.mutually_exclusive)
42
- kwargs['help'] = help + (
43
- '\nNOTE: This argument is mutually exclusive with '
44
- ' arguments: [' + ex_str + '].'
45
- )
46
- super().__init__(*args, **kwargs)
47
-
48
- def handle_parse_result(self, ctx, opts, args):
49
- if (isinstance(ctx.command, RSDCommand) and not (isinstance(ctx.command, Command)) and
50
- ('rsd_service_provider_using_tunneld' not in opts) and ('rsd_service_provider_manually' not in opts)):
51
- # defaulting to `--tunnel ''` if no remote option was specified
52
- opts['rsd_service_provider_using_tunneld'] = ''
53
- if self.mutually_exclusive.intersection(opts) and self.name in opts:
54
- raise UsageError(
55
- 'Illegal usage: `{}` is mutually exclusive with '
56
- 'arguments `{}`.'.format(
57
- self.name,
58
- ', '.join(self.mutually_exclusive)
59
- )
60
- )
61
-
62
- return super().handle_parse_result(
63
- ctx,
64
- opts,
65
- args
66
- )
39
+ # Global options
40
+ COLORED_OUTPUT: bool = True
67
41
 
68
42
 
69
43
  def default_json_encoder(obj):
70
44
  if isinstance(obj, bytes):
71
- return f'<{obj.hex()}>'
45
+ return f"<{obj.hex()}>"
72
46
  if isinstance(obj, datetime.datetime):
73
47
  return str(obj)
74
48
  if isinstance(obj, uuid.UUID):
@@ -76,13 +50,14 @@ def default_json_encoder(obj):
76
50
  raise TypeError()
77
51
 
78
52
 
79
- def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder):
53
+ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder) -> str:
80
54
  if colored is None:
81
55
  colored = user_requested_colored_output()
82
56
  formatted_json = json.dumps(buf, sort_keys=True, indent=4, default=default)
83
57
  if colored and os.isatty(sys.stdout.fileno()):
84
- colorful_json = highlight(formatted_json, lexers.JsonLexer(),
85
- formatters.Terminal256Formatter(style='stata-dark'))
58
+ colorful_json = highlight(
59
+ formatted_json, lexers.JsonLexer(), formatters.Terminal256Formatter(style="stata-dark")
60
+ )
86
61
  print(colorful_json)
87
62
  return colorful_json
88
63
  else:
@@ -90,19 +65,19 @@ def print_json(buf, colored: Optional[bool] = None, default=default_json_encoder
90
65
  return formatted_json
91
66
 
92
67
 
93
- def print_hex(data, colored=True):
94
- hex_dump = hexdump.hexdump(data, result='return')
68
+ def print_hex(data, colored=True) -> None:
69
+ hex_dump = hexdump.hexdump(data, result="return")
95
70
  if colored:
96
- print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.Terminal256Formatter(style='native')))
71
+ print(highlight(hex_dump, lexers.HexdumpLexer(), formatters.Terminal256Formatter(style="native")))
97
72
  else:
98
- print(hex_dump, end='\n\n')
73
+ print(hex_dump, end="\n\n")
99
74
 
100
75
 
101
- def set_verbosity(ctx, param, value):
102
- coloredlogs.set_level(logging.INFO - (value * 10))
76
+ def set_verbosity(level: int) -> None:
77
+ coloredlogs.set_level(logging.INFO - (level * 10))
103
78
 
104
79
 
105
- def set_color_flag(ctx, param, value) -> None:
80
+ def set_color_flag(value: bool) -> None:
106
81
  global COLORED_OUTPUT
107
82
  COLORED_OUTPUT = value
108
83
 
@@ -116,7 +91,7 @@ def user_requested_colored_output() -> bool:
116
91
 
117
92
 
118
93
  def get_last_used_terminal_formatting(buf: str) -> str:
119
- return '\x1b' + buf.rsplit('\x1b', 1)[1].split('m')[0] + 'm'
94
+ return "\x1b" + buf.rsplit("\x1b", 1)[1].split("m")[0] + "m"
120
95
 
121
96
 
122
97
  def sudo_required(func):
@@ -131,173 +106,204 @@ def sudo_required(func):
131
106
 
132
107
 
133
108
  def prompt_selection(choices: list[Any], message: str, idx: bool = False) -> Any:
134
- question = [inquirer3.List('selection', message=message, choices=choices, carousel=True)]
109
+ question = [inquirer3.List("selection", message=message, choices=choices, carousel=True)]
135
110
  try:
136
111
  result = inquirer3.prompt(question, theme=GreenPassion(), raise_keyboard_interrupt=True)
137
112
  except KeyboardInterrupt:
138
- raise click.ClickException('No selection was made')
139
- return result['selection'] if not idx else choices.index(result['selection'])
113
+ raise click.ClickException("No selection was made") from None
114
+ return result["selection"] if not idx else choices.index(result["selection"])
140
115
 
141
116
 
142
117
  def prompt_device_list(device_list: list):
143
- return prompt_selection(device_list, 'Choose device')
144
-
145
-
146
- def choose_service_provider(callback: Callable):
147
- def wrap_callback_calling(**kwargs: dict) -> None:
148
- service_provider = None
149
- lockdown_service_provider = kwargs.pop('lockdown_service_provider', None)
150
- rsd_service_provider_manually = kwargs.pop('rsd_service_provider_manually', None)
151
- rsd_service_provider_using_tunneld = kwargs.pop('rsd_service_provider_using_tunneld', None)
152
- if lockdown_service_provider is not None:
153
- service_provider = lockdown_service_provider
154
- if rsd_service_provider_manually is not None:
155
- service_provider = rsd_service_provider_manually
156
- if rsd_service_provider_using_tunneld is not None:
157
- service_provider = rsd_service_provider_using_tunneld
158
- callback(service_provider=service_provider, **kwargs)
159
-
160
- return wrap_callback_calling
161
-
162
-
163
- class BaseCommand(click.Command):
164
- def __init__(self, *args, **kwargs):
165
- super().__init__(*args, **kwargs)
166
- self.params[:0] = [
167
- click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False),
168
- click.Option(('color', '--color/--no-color'), default=True, callback=set_color_flag, is_flag=True,
169
- expose_value=False, help='colorize output'),
170
- ]
171
-
172
-
173
- class BaseServiceProviderCommand(BaseCommand):
174
- def __init__(self, *args, **kwargs):
175
- super().__init__(*args, **kwargs)
176
- self.params[:0] = [
177
- click.Option(('verbosity', '-v', '--verbose'), count=True, callback=set_verbosity, expose_value=False),
178
- ]
179
- self.service_provider = None
180
- self.callback = choose_service_provider(self.callback)
181
-
182
-
183
- class LockdownCommand(BaseServiceProviderCommand):
184
- def __init__(self, *args, **kwargs):
185
- super().__init__(*args, **kwargs)
186
- self.usbmux_address = None
187
- self.params[:0] = [
188
- click.Option(('usbmux', '--usbmux'), callback=self.usbmux, expose_value=False,
189
- envvar=USBMUX_ENV_VAR, help=USBMUX_OPTION_HELP),
190
- click.Option(('lockdown_service_provider', '--udid'), envvar=UDID_ENV_VAR, callback=self.udid,
191
- help=f'Device unique identifier. You may pass {UDID_ENV_VAR} environment variable to pass this'
192
- f' option as well'),
193
- ]
194
-
195
- def usbmux(self, ctx, param: str, value: Optional[str] = None) -> None:
196
- if value is None:
197
- return
198
- self.usbmux_address = value
199
-
200
- def udid(self, ctx, param: str, value: str) -> Optional[LockdownClient]:
201
- if '_PYMOBILEDEVICE3_COMPLETE' in os.environ:
118
+ return prompt_selection(device_list, "Choose device")
119
+
120
+
121
+ def is_invoked_for_completion() -> bool:
122
+ """Returns True if the command is invoked for autocompletion."""
123
+ return any(env.startswith("_") and env.endswith("_COMPLETE") for env in os.environ)
124
+
125
+
126
+ async def get_mobdev2_devices(udid: Optional[str] = None) -> list[TcpLockdownClient]:
127
+ return [lockdown async for _, lockdown in get_mobdev2_lockdowns(udid=udid)]
128
+
129
+
130
+ async def _tunneld(udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
131
+ if udid is None:
132
+ return
133
+
134
+ udid = udid.strip()
135
+ port = TUNNELD_DEFAULT_ADDRESS[1]
136
+ if ":" in udid:
137
+ udid, port = udid.split(":")
138
+
139
+ rsds = await async_get_tunneld_devices((TUNNELD_DEFAULT_ADDRESS[0], int(port)))
140
+ if len(rsds) == 0:
141
+ raise NoDeviceConnectedError()
142
+
143
+ if udid != "":
144
+ service_provider = next((rsd for rsd in rsds if rsd.udid == udid), None)
145
+ if service_provider is None:
146
+ raise DeviceNotFoundError(udid) from None
147
+ else:
148
+ service_provider = rsds[0] if len(rsds) == 1 else prompt_device_list(rsds)
149
+
150
+ for rsd in rsds:
151
+ if rsd == service_provider:
152
+ continue
153
+ await rsd.close()
154
+
155
+ return service_provider
156
+
157
+
158
+ def make_rsd_dependency(*, allow_none: bool) -> Callable[..., Optional[RemoteServiceDiscoveryService]]:
159
+ def rsd_dependency(
160
+ rsd: Annotated[
161
+ Optional[tuple[str, int]],
162
+ typer.Option(
163
+ metavar="HOST PORT",
164
+ help=dedent("""\
165
+ Hostname and port of a RemoteServiceDiscovery (from any of the `start-tunnel` subcommands).
166
+ Mutually exclusive with --tunnel.
167
+ """),
168
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
169
+ ),
170
+ ] = None,
171
+ tunnel: Annotated[
172
+ Optional[str],
173
+ typer.Option(
174
+ envvar=TUNNEL_ENV_VAR,
175
+ help=dedent("""\
176
+ Use a device discovered via tunneld. Provide a UDID (optionally with :PORT) or leave empty to pick
177
+ interactively. Mutually exclusive with --rsd.
178
+ """),
179
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
180
+ ),
181
+ ] = None,
182
+ ) -> Optional[RemoteServiceDiscoveryService]:
183
+ if is_invoked_for_completion():
202
184
  # prevent lockdown connection establishment when in autocomplete mode
203
- return
204
-
205
- if self.service_provider is not None:
206
- return self.service_provider
207
-
208
- if value is not None:
209
- return create_using_usbmux(serial=value)
210
-
211
- devices = select_devices_by_connection_type(connection_type='USB', usbmux_address=self.usbmux_address)
212
- if len(devices) <= 1:
213
- return create_using_usbmux(usbmux_address=self.usbmux_address)
214
-
215
- return prompt_device_list(
216
- [create_using_usbmux(serial=device.serial, usbmux_address=self.usbmux_address) for device in devices])
217
-
218
-
219
- class RSDCommand(BaseServiceProviderCommand):
220
- def __init__(self, *args, **kwargs):
221
- super().__init__(*args, **kwargs)
222
- self.params[:0] = [
223
- RSDOption(('rsd_service_provider_manually', '--rsd'), type=(str, int), callback=self.rsd,
224
- mutually_exclusive=['rsd_service_provider_using_tunneld'],
225
- help='\b\n'
226
- 'RSD hostname and port number (as provided by a `start-tunnel` subcommand).'),
227
- RSDOption(('rsd_service_provider_using_tunneld', '--tunnel'), callback=self.tunneld,
228
- mutually_exclusive=['rsd_service_provider_manually'], envvar=TUNNEL_ENV_VAR,
229
- help='\b\n'
230
- 'Either an empty string to force tunneld device selection, or a UDID of a tunneld '
231
- 'discovered device.\n'
232
- 'The string may be suffixed with :PORT in case tunneld is not serving at the default port.\n'
233
- f'This option may also be transferred as an environment variable: {TUNNEL_ENV_VAR}')
234
- ]
235
-
236
- def rsd(self, ctx, param: str, value: Optional[tuple[str, int]]) -> Optional[RemoteServiceDiscoveryService]:
237
- if value is not None:
238
- rsd = RemoteServiceDiscoveryService(value)
239
- asyncio.run(rsd.connect(), debug=True)
240
- self.service_provider = rsd
241
- return self.service_provider
242
-
243
- async def _tunneld(self, udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
244
- if udid is None:
245
- return
246
-
247
- udid = udid.strip()
248
- port = TUNNELD_DEFAULT_ADDRESS[1]
249
- if ':' in udid:
250
- udid, port = udid.split(':')
251
-
252
- rsds = await async_get_tunneld_devices((TUNNELD_DEFAULT_ADDRESS[0], port))
253
- if len(rsds) == 0:
185
+ return None
186
+
187
+ if rsd is not None and tunnel is not None:
188
+ raise UsageError("Illegal usage: --rsd is mutually exclusive with --tunnel.")
189
+
190
+ if rsd is not None:
191
+ rsd_service = RemoteServiceDiscoveryService(rsd)
192
+ asyncio.run(rsd_service.connect(), debug=True)
193
+ return rsd_service
194
+
195
+ if tunnel is not None or not allow_none:
196
+ return asyncio.run(_tunneld(tunnel or ""), debug=True)
197
+
198
+ return rsd_dependency
199
+
200
+
201
+ def any_service_provider_dependency(
202
+ rsd_service_provider: Annotated[
203
+ Optional[RemoteServiceDiscoveryService],
204
+ Depends(make_rsd_dependency(allow_none=True)),
205
+ ] = None,
206
+ mobdev2: Annotated[
207
+ bool,
208
+ typer.Option(
209
+ help="Discover devices over bonjour/mobdev2 instead of usbmux.",
210
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
211
+ ),
212
+ ] = False,
213
+ usbmux: Annotated[
214
+ Optional[str],
215
+ typer.Option(
216
+ envvar=USBMUX_ENV_VAR,
217
+ help=USBMUX_OPTION_HELP,
218
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
219
+ ),
220
+ ] = None,
221
+ udid: Annotated[
222
+ Optional[str],
223
+ typer.Option(
224
+ envvar=UDID_ENV_VAR,
225
+ help="Target device UDID (defaults to the first USB device).",
226
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
227
+ ),
228
+ ] = None,
229
+ ) -> LockdownServiceProvider:
230
+ if is_invoked_for_completion():
231
+ # prevent lockdown connection establishment when in autocomplete mode
232
+ return # type: ignore[return-value]
233
+
234
+ if rsd_service_provider is not None:
235
+ return rsd_service_provider
236
+
237
+ if mobdev2:
238
+ devices = asyncio.run(get_mobdev2_devices(udid=udid))
239
+ if not devices:
254
240
  raise NoDeviceConnectedError()
255
241
 
256
- if udid != '':
257
- try:
258
- # Connect to the specified device
259
- self.service_provider = [rsd for rsd in rsds if rsd.udid == udid][0]
260
- except IndexError:
261
- raise DeviceNotFoundError(udid)
262
- else:
263
- if len(rsds) == 1:
264
- self.service_provider = rsds[0]
265
- else:
266
- self.service_provider = prompt_device_list(rsds)
242
+ if len(devices) == 1:
243
+ return devices[0]
267
244
 
268
- for rsd in rsds:
269
- if rsd == self.service_provider:
270
- continue
271
- await rsd.close()
245
+ return prompt_device_list(devices)
272
246
 
273
- return self.service_provider
247
+ if udid is not None:
248
+ return create_using_usbmux(serial=udid, usbmux_address=usbmux)
274
249
 
275
- def tunneld(self, ctx, param: str, udid: Optional[str] = None) -> Optional[RemoteServiceDiscoveryService]:
276
- return asyncio.run(self._tunneld(udid), debug=True)
250
+ devices = select_devices_by_connection_type(connection_type="USB", usbmux_address=usbmux)
251
+ if len(devices) <= 1:
252
+ return create_using_usbmux(usbmux_address=usbmux)
277
253
 
254
+ return prompt_device_list([create_using_usbmux(serial=device.serial, usbmux_address=usbmux) for device in devices])
278
255
 
279
- class Command(RSDCommand, LockdownCommand):
280
- def __init__(self, *args, **kwargs):
281
- super().__init__(*args, **kwargs)
282
256
 
257
+ def no_autopair_service_provider_dependency(
258
+ rsd_service_provider: Annotated[
259
+ Optional[RemoteServiceDiscoveryService],
260
+ Depends(make_rsd_dependency(allow_none=True)),
261
+ ] = None,
262
+ udid: Annotated[
263
+ Optional[str],
264
+ typer.Option(
265
+ envvar=UDID_ENV_VAR,
266
+ help="Target device UDID (defaults to the first USB device).",
267
+ rich_help_panel=DEVICE_OPTIONS_PANEL_TITLE,
268
+ ),
269
+ ] = None,
270
+ ) -> LockdownServiceProvider:
271
+ if is_invoked_for_completion():
272
+ # prevent lockdown connection establishment when in autocomplete mode
273
+ return # type: ignore[return-value]
283
274
 
284
- class CommandWithoutAutopair(Command):
285
- @staticmethod
286
- def udid(ctx, param, value):
287
- if '_PYMOBILEDEVICE3_COMPLETE' in os.environ:
288
- # prevent lockdown connection establishment when in autocomplete mode
289
- return
290
- return create_using_usbmux(serial=value, autopair=False)
275
+ if rsd_service_provider is not None:
276
+ return rsd_service_provider
277
+
278
+ return create_using_usbmux(serial=udid, autopair=False)
279
+
280
+
281
+ RSDServiceProviderDep = Annotated[
282
+ RemoteServiceDiscoveryService,
283
+ Depends(make_rsd_dependency(allow_none=False)),
284
+ ]
285
+
286
+
287
+ ServiceProviderDep = Annotated[
288
+ LockdownServiceProvider,
289
+ Depends(any_service_provider_dependency),
290
+ ]
291
+
292
+
293
+ NoAutoPairServiceProviderDep = Annotated[
294
+ LockdownServiceProvider,
295
+ Depends(no_autopair_service_provider_dependency),
296
+ ]
291
297
 
292
298
 
293
299
  class BasedIntParamType(click.ParamType):
294
- name = 'based int'
300
+ name = "based int"
295
301
 
296
302
  def convert(self, value, param, ctx):
297
303
  try:
298
304
  return int(value, 0)
299
305
  except ValueError:
300
- self.fail(f'{value!r} is not a valid int.', param, ctx)
306
+ self.fail(f"{value!r} is not a valid int.", param, ctx)
301
307
 
302
308
 
303
309
  BASED_INT = BasedIntParamType()
@@ -1,22 +1,22 @@
1
- import click
1
+ from typer_injector import InjectingTyper
2
2
 
3
- from pymobiledevice3.cli.cli_common import Command, print_json
4
- from pymobiledevice3.lockdown import LockdownClient
3
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
5
4
  from pymobiledevice3.services.companion import CompanionProxyService
6
5
 
7
-
8
- @click.group()
9
- def cli() -> None:
10
- pass
6
+ cli = InjectingTyper(
7
+ name="companion",
8
+ help='List paired "companion" devices',
9
+ no_args_is_help=True,
10
+ )
11
11
 
12
12
 
13
- @cli.group()
14
- def companion() -> None:
15
- """ List paired "companion" devices """
13
+ @cli.callback()
14
+ def callback() -> None:
15
+ # Force subgroup
16
16
  pass
17
17
 
18
18
 
19
- @companion.command('list', cls=Command)
20
- def companion_list(service_provider: LockdownClient):
21
- """ list all paired companion devices """
22
- print_json(CompanionProxyService(service_provider).list(), default=lambda x: '<non-serializable>')
19
+ @cli.command("list")
20
+ def companion_list(service_provider: ServiceProviderDep) -> None:
21
+ """list all paired companion devices"""
22
+ print_json(CompanionProxyService(service_provider).list(), default=lambda x: "<non-serializable>")