pymobiledevice3 4.27.0__py3-none-any.whl → 5.1.2__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 (143) hide show
  1. misc/plist_sniffer.py +15 -15
  2. misc/remotexpc_sniffer.py +29 -28
  3. pymobiledevice3/__main__.py +123 -98
  4. pymobiledevice3/_version.py +2 -2
  5. pymobiledevice3/bonjour.py +351 -117
  6. pymobiledevice3/ca.py +32 -24
  7. pymobiledevice3/cli/activation.py +7 -7
  8. pymobiledevice3/cli/afc.py +19 -19
  9. pymobiledevice3/cli/amfi.py +4 -4
  10. pymobiledevice3/cli/apps.py +51 -39
  11. pymobiledevice3/cli/backup.py +58 -32
  12. pymobiledevice3/cli/bonjour.py +27 -20
  13. pymobiledevice3/cli/cli_common.py +112 -81
  14. pymobiledevice3/cli/companion_proxy.py +4 -4
  15. pymobiledevice3/cli/completions.py +10 -10
  16. pymobiledevice3/cli/crash.py +37 -31
  17. pymobiledevice3/cli/developer.py +601 -519
  18. pymobiledevice3/cli/diagnostics.py +38 -33
  19. pymobiledevice3/cli/lockdown.py +82 -72
  20. pymobiledevice3/cli/mounter.py +84 -67
  21. pymobiledevice3/cli/notification.py +10 -10
  22. pymobiledevice3/cli/pcap.py +19 -14
  23. pymobiledevice3/cli/power_assertion.py +12 -10
  24. pymobiledevice3/cli/processes.py +10 -10
  25. pymobiledevice3/cli/profile.py +88 -77
  26. pymobiledevice3/cli/provision.py +17 -17
  27. pymobiledevice3/cli/remote.py +188 -111
  28. pymobiledevice3/cli/restore.py +43 -40
  29. pymobiledevice3/cli/springboard.py +30 -28
  30. pymobiledevice3/cli/syslog.py +85 -58
  31. pymobiledevice3/cli/usbmux.py +21 -20
  32. pymobiledevice3/cli/version.py +3 -2
  33. pymobiledevice3/cli/webinspector.py +156 -78
  34. pymobiledevice3/common.py +1 -1
  35. pymobiledevice3/exceptions.py +154 -60
  36. pymobiledevice3/irecv.py +49 -53
  37. pymobiledevice3/irecv_devices.py +1489 -492
  38. pymobiledevice3/lockdown.py +400 -251
  39. pymobiledevice3/lockdown_service_provider.py +5 -7
  40. pymobiledevice3/osu/os_utils.py +18 -9
  41. pymobiledevice3/osu/posix_util.py +28 -15
  42. pymobiledevice3/osu/win_util.py +14 -8
  43. pymobiledevice3/pair_records.py +19 -19
  44. pymobiledevice3/remote/common.py +4 -4
  45. pymobiledevice3/remote/core_device/app_service.py +94 -67
  46. pymobiledevice3/remote/core_device/core_device_service.py +17 -14
  47. pymobiledevice3/remote/core_device/device_info.py +5 -5
  48. pymobiledevice3/remote/core_device/diagnostics_service.py +10 -8
  49. pymobiledevice3/remote/core_device/file_service.py +47 -33
  50. pymobiledevice3/remote/remote_service_discovery.py +53 -35
  51. pymobiledevice3/remote/remotexpc.py +64 -42
  52. pymobiledevice3/remote/tunnel_service.py +383 -297
  53. pymobiledevice3/remote/utils.py +14 -13
  54. pymobiledevice3/remote/xpc_message.py +145 -125
  55. pymobiledevice3/resources/dsc_uuid_map.py +19 -19
  56. pymobiledevice3/resources/firmware_notifications.py +16 -16
  57. pymobiledevice3/restore/asr.py +27 -27
  58. pymobiledevice3/restore/base_restore.py +90 -47
  59. pymobiledevice3/restore/consts.py +87 -66
  60. pymobiledevice3/restore/device.py +11 -11
  61. pymobiledevice3/restore/fdr.py +46 -46
  62. pymobiledevice3/restore/ftab.py +19 -19
  63. pymobiledevice3/restore/img4.py +130 -133
  64. pymobiledevice3/restore/mbn.py +587 -0
  65. pymobiledevice3/restore/recovery.py +125 -135
  66. pymobiledevice3/restore/restore.py +535 -523
  67. pymobiledevice3/restore/restore_options.py +122 -115
  68. pymobiledevice3/restore/restored_client.py +25 -22
  69. pymobiledevice3/restore/tss.py +378 -270
  70. pymobiledevice3/service_connection.py +50 -46
  71. pymobiledevice3/services/accessibilityaudit.py +137 -127
  72. pymobiledevice3/services/afc.py +363 -293
  73. pymobiledevice3/services/amfi.py +21 -18
  74. pymobiledevice3/services/companion.py +23 -19
  75. pymobiledevice3/services/crash_reports.py +61 -47
  76. pymobiledevice3/services/debugserver_applist.py +3 -3
  77. pymobiledevice3/services/device_arbitration.py +8 -8
  78. pymobiledevice3/services/device_link.py +56 -48
  79. pymobiledevice3/services/diagnostics.py +971 -968
  80. pymobiledevice3/services/dtfetchsymbols.py +8 -8
  81. pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
  82. pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
  83. pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
  84. pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
  85. pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
  86. pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
  87. pymobiledevice3/services/dvt/instruments/device_info.py +11 -11
  88. pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
  89. pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
  90. pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
  91. pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
  92. pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
  93. pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
  94. pymobiledevice3/services/dvt/instruments/process_control.py +25 -10
  95. pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
  96. pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
  97. pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
  98. pymobiledevice3/services/file_relay.py +10 -10
  99. pymobiledevice3/services/heartbeat.py +8 -7
  100. pymobiledevice3/services/house_arrest.py +12 -15
  101. pymobiledevice3/services/installation_proxy.py +119 -100
  102. pymobiledevice3/services/lockdown_service.py +12 -5
  103. pymobiledevice3/services/misagent.py +22 -19
  104. pymobiledevice3/services/mobile_activation.py +84 -72
  105. pymobiledevice3/services/mobile_config.py +331 -301
  106. pymobiledevice3/services/mobile_image_mounter.py +137 -116
  107. pymobiledevice3/services/mobilebackup2.py +188 -150
  108. pymobiledevice3/services/notification_proxy.py +11 -11
  109. pymobiledevice3/services/os_trace.py +128 -74
  110. pymobiledevice3/services/pcapd.py +306 -306
  111. pymobiledevice3/services/power_assertion.py +10 -9
  112. pymobiledevice3/services/preboard.py +4 -4
  113. pymobiledevice3/services/remote_fetch_symbols.py +16 -14
  114. pymobiledevice3/services/remote_server.py +176 -146
  115. pymobiledevice3/services/restore_service.py +16 -16
  116. pymobiledevice3/services/screenshot.py +13 -10
  117. pymobiledevice3/services/simulate_location.py +7 -7
  118. pymobiledevice3/services/springboard.py +15 -15
  119. pymobiledevice3/services/syslog.py +5 -5
  120. pymobiledevice3/services/web_protocol/alert.py +3 -3
  121. pymobiledevice3/services/web_protocol/automation_session.py +183 -179
  122. pymobiledevice3/services/web_protocol/cdp_screencast.py +44 -36
  123. pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
  124. pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
  125. pymobiledevice3/services/web_protocol/driver.py +47 -45
  126. pymobiledevice3/services/web_protocol/element.py +74 -63
  127. pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
  128. pymobiledevice3/services/web_protocol/selenium_api.py +3 -3
  129. pymobiledevice3/services/web_protocol/session_protocol.py +15 -10
  130. pymobiledevice3/services/web_protocol/switch_to.py +11 -12
  131. pymobiledevice3/services/webinspector.py +142 -116
  132. pymobiledevice3/tcp_forwarder.py +64 -50
  133. pymobiledevice3/tunneld/api.py +20 -15
  134. pymobiledevice3/tunneld/server.py +315 -193
  135. pymobiledevice3/usbmux.py +197 -148
  136. pymobiledevice3/utils.py +14 -11
  137. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/METADATA +2 -6
  138. pymobiledevice3-5.1.2.dist-info/RECORD +173 -0
  139. pymobiledevice3-4.27.0.dist-info/RECORD +0 -172
  140. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/WHEEL +0 -0
  141. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/entry_points.txt +0 -0
  142. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/licenses/LICENSE +0 -0
  143. {pymobiledevice3-4.27.0.dist-info → pymobiledevice3-5.1.2.dist-info}/top_level.txt +0 -0
@@ -5,27 +5,50 @@ import logging
5
5
  import os
6
6
  import signal
7
7
  import traceback
8
+ import warnings
8
9
  from contextlib import asynccontextmanager, suppress
10
+ from ssl import SSLEOFError
9
11
  from typing import Optional, Union
10
12
 
11
13
  import construct
12
- import fastapi
14
+
15
+ from pymobiledevice3.bonjour import browse_remoted
16
+
17
+ with warnings.catch_warnings():
18
+ # Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater."
19
+ warnings.simplefilter("ignore", category=UserWarning)
20
+ import fastapi
21
+
13
22
  import uvicorn
14
23
  from construct import StreamError
15
24
  from fastapi import FastAPI
16
25
  from packaging.version import Version
17
26
 
18
27
  from pymobiledevice3 import usbmux
19
- from pymobiledevice3.bonjour import REMOTED_SERVICE_NAMES, browse
20
- from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, DeviceNotFoundError, \
21
- GetProhibitedError, InvalidServiceError, LockdownError, MuxException, PairingError
28
+ from pymobiledevice3.exceptions import (
29
+ ConnectionFailedError,
30
+ ConnectionFailedToUsbmuxdError,
31
+ DeviceNotFoundError,
32
+ GetProhibitedError,
33
+ IncorrectModeError,
34
+ InvalidServiceError,
35
+ LockdownError,
36
+ MuxException,
37
+ PairingError,
38
+ StreamClosedError,
39
+ )
22
40
  from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
23
41
  from pymobiledevice3.osu.os_utils import get_os_utils
24
42
  from pymobiledevice3.remote.common import TunnelProtocol
25
43
  from pymobiledevice3.remote.module_imports import start_tunnel
26
44
  from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService
27
- from pymobiledevice3.remote.tunnel_service import CoreDeviceTunnelProxy, RemotePairingProtocol, TunnelResult, \
28
- create_core_device_tunnel_service_using_rsd, get_remote_pairing_tunnel_services
45
+ from pymobiledevice3.remote.tunnel_service import (
46
+ CoreDeviceTunnelProxy,
47
+ RemotePairingProtocol,
48
+ TunnelResult,
49
+ create_core_device_tunnel_service_using_rsd,
50
+ get_remote_pairing_tunnel_services,
51
+ )
29
52
  from pymobiledevice3.remote.utils import get_rsds, stop_remoted
30
53
  from pymobiledevice3.utils import asyncio_print_traceback
31
54
 
@@ -36,7 +59,7 @@ REATTEMPT_INTERVAL = 5
36
59
  REATTEMPT_COUNT = 5
37
60
 
38
61
  REMOTEPAIRING_INTERVAL = 5
39
- MOVDEV2_INTERVAL = 5
62
+ MOBDEV2_INTERVAL = 5
40
63
 
41
64
  USBMUX_INTERVAL = 2
42
65
  OSUTILS = get_os_utils()
@@ -50,8 +73,14 @@ class TunnelTask:
50
73
 
51
74
 
52
75
  class TunneldCore:
53
- def __init__(self, protocol: TunnelProtocol = TunnelProtocol.DEFAULT, wifi_monitor: bool = True,
54
- usb_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True) -> None:
76
+ def __init__(
77
+ self,
78
+ protocol: TunnelProtocol = TunnelProtocol.DEFAULT,
79
+ wifi_monitor: bool = True,
80
+ usb_monitor: bool = True,
81
+ usbmux_monitor: bool = True,
82
+ mobdev2_monitor: bool = True,
83
+ ) -> None:
55
84
  self.protocol = protocol
56
85
  self.tasks: list[asyncio.Task] = []
57
86
  self.tunnel_tasks: dict[str, TunnelTask] = {}
@@ -61,74 +90,103 @@ class TunneldCore:
61
90
  self.mobdev2_monitor = mobdev2_monitor
62
91
 
63
92
  def start(self) -> None:
64
- """ Register all tasks """
93
+ """Register all tasks"""
65
94
  self.tasks = []
66
95
  if self.usb_monitor:
67
- self.tasks.append(asyncio.create_task(self.monitor_usb_task(), name='monitor-usb-task'))
96
+ self.tasks.append(asyncio.create_task(self.monitor_usb_task(), name="monitor-usb-task"))
68
97
  if self.wifi_monitor:
69
- self.tasks.append(asyncio.create_task(self.monitor_wifi_task(), name='monitor-wifi-task'))
98
+ self.tasks.append(asyncio.create_task(self.monitor_wifi_task(), name="monitor-wifi-task"))
70
99
  if self.usbmux_monitor:
71
- self.tasks.append(asyncio.create_task(self.monitor_usbmux_task(), name='monitor-usbmux-task'))
100
+ self.tasks.append(asyncio.create_task(self.monitor_usbmux_task(), name="monitor-usbmux-task"))
72
101
  if self.mobdev2_monitor:
73
- self.tasks.append(asyncio.create_task(self.monitor_mobdev2_task(), name='monitor-mobdev2-task'))
102
+ self.tasks.append(asyncio.create_task(self.monitor_mobdev2_task(), name="monitor-mobdev2-task"))
74
103
 
75
104
  def tunnel_exists_for_udid(self, udid: str) -> bool:
76
105
  for task in self.tunnel_tasks.values():
77
106
  # Linux implementations of `usbmuxd` may report an incorrect value of UDID, dismissing the `-` character.
78
107
  # For such cases, we also check for a UDID without it.
79
108
  # See: <https://github.com/doronz88/pymobiledevice3/issues/1388#issuecomment-2782249770>
80
- task_udid = task.udid or ''
81
- if ((task_udid == udid) or (task_udid.replace('-', '') == udid)) and (task.tunnel is not None):
109
+ task_udid = task.udid or ""
110
+ if ((task_udid == udid) or (task_udid.replace("-", "") == udid)) and (task.tunnel is not None):
82
111
  return True
83
112
 
84
113
  return False
85
114
 
86
115
  @asyncio_print_traceback
87
116
  async def monitor_usb_task(self) -> None:
88
- previous_ips = []
89
- while True:
90
- current_ips = OSUTILS.get_ipv6_ips()
91
- added = [ip for ip in current_ips if ip not in previous_ips]
92
- removed = [ip for ip in previous_ips if ip not in current_ips]
93
-
94
- previous_ips = current_ips
95
-
96
- logger.debug(f'added interfaces: {added}')
97
- logger.debug(f'removed interfaces: {removed}')
98
-
99
- for ip in removed:
100
- if ip in self.tunnel_tasks:
101
- self.tunnel_tasks[ip].task.cancel()
102
- await self.tunnel_tasks[ip].task
103
-
104
- for ip in added:
105
- self.tunnel_tasks[ip] = TunnelTask(
106
- task=asyncio.create_task(self.handle_new_potential_usb_cdc_ncm_interface_task(ip),
107
- name=f'handle-new-potential-usb-cdc-ncm-interface-task-{ip}'))
108
-
109
- # wait before re-iterating
110
- await asyncio.sleep(1)
117
+ try:
118
+ previous_ips = []
119
+ while True:
120
+ current_ips = OSUTILS.get_ipv6_ips()
121
+ added = [ip for ip in current_ips if ip not in previous_ips]
122
+ removed = [ip for ip in previous_ips if ip not in current_ips]
123
+
124
+ previous_ips = current_ips
125
+
126
+ # logger.debug(f'added interfaces: {added}')
127
+ # logger.debug(f'removed interfaces: {removed}')
128
+
129
+ for ip in removed:
130
+ if ip in self.tunnel_tasks:
131
+ self.tunnel_tasks[ip].task.cancel()
132
+ with suppress(asyncio.CancelledError):
133
+ await self.tunnel_tasks[ip].task
134
+
135
+ if added:
136
+ # A new interface was attached
137
+ for answer in await browse_remoted():
138
+ for address in answer.addresses:
139
+ if address.iface.startswith("utun"):
140
+ # Skip already established tunnels
141
+ continue
142
+ if address.full_ip in self.tunnel_tasks:
143
+ # Skip already established tunnels
144
+ continue
145
+ self.tunnel_tasks[address.full_ip] = TunnelTask(
146
+ task=asyncio.create_task(
147
+ self.handle_new_potential_usb_cdc_ncm_interface_task(address.full_ip),
148
+ name=f"handle-new-potential-usb-cdc-ncm-interface-task-{address.full_ip}",
149
+ )
150
+ )
151
+
152
+ # wait before re-iterating
153
+ await asyncio.sleep(1)
154
+ except asyncio.CancelledError:
155
+ pass
111
156
 
112
157
  @asyncio_print_traceback
113
158
  async def monitor_wifi_task(self) -> None:
114
159
  try:
115
160
  while True:
116
- for service in await get_remote_pairing_tunnel_services():
117
- if service.hostname in self.tunnel_tasks:
118
- # skip tunnel if already exists for this ip
119
- await service.close()
120
- continue
121
- if self.tunnel_exists_for_udid(service.remote_identifier):
122
- # skip tunnel if already exists for this udid
123
- await service.close()
124
- continue
125
- self.tunnel_tasks[service.hostname] = TunnelTask(
126
- task=asyncio.create_task(self.start_tunnel_task(service.hostname, service),
127
- name=f'start-tunnel-task-wifi-{service.hostname}'),
128
- udid=service.remote_identifier
129
- )
161
+ try:
162
+ remote_pairing_tunnel_services = await get_remote_pairing_tunnel_services()
163
+ for service in remote_pairing_tunnel_services:
164
+ if service.hostname in self.tunnel_tasks:
165
+ # skip tunnel if already exists for this ip
166
+ await service.close()
167
+ continue
168
+ if self.tunnel_exists_for_udid(service.remote_identifier):
169
+ # skip tunnel if already exists for this udid
170
+ await service.close()
171
+ continue
172
+ self.tunnel_tasks[service.hostname] = TunnelTask(
173
+ task=asyncio.create_task(
174
+ self.start_tunnel_task(service.hostname, service),
175
+ name=f"start-tunnel-task-wifi-{service.hostname}",
176
+ ),
177
+ udid=service.remote_identifier,
178
+ )
179
+ except asyncio.exceptions.IncompleteReadError:
180
+ continue
181
+ except asyncio.CancelledError:
182
+ # Raise and cancel gracefully
183
+ raise
184
+ except Exception:
185
+ logger.exception(f"Got exception from {asyncio.current_task().get_name()}")
186
+ continue
130
187
  await asyncio.sleep(REMOTEPAIRING_INTERVAL)
131
188
  except asyncio.CancelledError:
189
+ # Cancel gracefully
132
190
  pass
133
191
 
134
192
  @asyncio_print_traceback
@@ -137,25 +195,42 @@ class TunneldCore:
137
195
  while True:
138
196
  try:
139
197
  for mux_device in usbmux.list_devices():
140
- task_identifier = f'usbmux-{mux_device.serial}-{mux_device.connection_type}'
198
+ task_identifier = f"usbmux-{mux_device.serial}-{mux_device.connection_type}"
141
199
  if self.tunnel_exists_for_udid(mux_device.serial):
200
+ # Skip if already established a tunnel for this udid
201
+ continue
202
+ if task_identifier in self.tunnel_tasks:
203
+ # Skip if already trying to establish a tunnel for this device
142
204
  continue
205
+ service = None
143
206
  try:
144
- service = CoreDeviceTunnelProxy(create_using_usbmux(mux_device.serial))
145
- except (MuxException, InvalidServiceError, GetProhibitedError, construct.core.StreamError,
146
- ConnectionAbortedError, DeviceNotFoundError, LockdownError):
207
+ with create_using_usbmux(mux_device.serial) as lockdown:
208
+ service = await CoreDeviceTunnelProxy.create(lockdown)
209
+ except (
210
+ MuxException,
211
+ InvalidServiceError,
212
+ GetProhibitedError,
213
+ construct.core.StreamError,
214
+ ConnectionAbortedError,
215
+ DeviceNotFoundError,
216
+ LockdownError,
217
+ IncorrectModeError,
218
+ SSLEOFError,
219
+ ):
220
+ if service is not None:
221
+ await service.close()
147
222
  continue
148
223
  self.tunnel_tasks[task_identifier] = TunnelTask(
149
224
  udid=mux_device.serial,
150
225
  task=asyncio.create_task(
151
- self.start_tunnel_task(task_identifier,
152
- service,
153
- protocol=TunnelProtocol.TCP),
154
- name=f'start-tunnel-task-{task_identifier}'))
226
+ self.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP),
227
+ name=f"start-tunnel-task-{task_identifier}",
228
+ ),
229
+ )
155
230
  except ConnectionFailedToUsbmuxdError:
156
231
  # This is exception is expected to occur repeatedly on linux running usbmuxd
157
232
  # as long as there isn't any physical iDevice connected
158
- logger.debug('failed to connect to usbmux. waiting for it to restart')
233
+ logger.debug("failed to connect to usbmux. waiting for it to restart")
159
234
  finally:
160
235
  await asyncio.sleep(USBMUX_INTERVAL)
161
236
  except asyncio.CancelledError:
@@ -166,29 +241,39 @@ class TunneldCore:
166
241
  try:
167
242
  while True:
168
243
  async for ip, lockdown in get_mobdev2_lockdowns(only_paired=True):
169
- if self.tunnel_exists_for_udid(lockdown.udid):
170
- # skip tunnel if already exists for this udid
171
- continue
172
- task_identifier = f'mobdev2-{lockdown.udid}-{ip}'
173
- try:
174
- tunnel_service = CoreDeviceTunnelProxy(lockdown)
175
- except InvalidServiceError:
176
- logger.warning(f'[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping')
177
- lockdown.close()
178
- continue
244
+ with lockdown:
245
+ udid = lockdown.udid
246
+ task_identifier = f"mobdev2-{udid}-{ip}"
247
+ if self.tunnel_exists_for_udid(udid):
248
+ # Skip tunnel if already exists for this udid
249
+ continue
250
+ if task_identifier in self.tunnel_tasks:
251
+ # Skip if already trying to establish a tunnel for this device
252
+ continue
253
+ try:
254
+ tunnel_service = await CoreDeviceTunnelProxy.create(lockdown)
255
+ except InvalidServiceError:
256
+ logger.warning(f"[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping")
257
+ continue
179
258
  self.tunnel_tasks[task_identifier] = TunnelTask(
180
- task=asyncio.create_task(self.start_tunnel_task(task_identifier, tunnel_service),
181
- name=f'start-tunnel-task-{task_identifier}'),
182
- udid=lockdown.udid
259
+ task=asyncio.create_task(
260
+ self.start_tunnel_task(task_identifier, tunnel_service),
261
+ name=f"start-tunnel-task-{task_identifier}",
262
+ ),
263
+ udid=udid,
183
264
  )
184
- await asyncio.sleep(MOVDEV2_INTERVAL)
265
+ await asyncio.sleep(MOBDEV2_INTERVAL)
185
266
  except asyncio.CancelledError:
186
267
  pass
187
268
 
188
269
  @asyncio_print_traceback
189
270
  async def start_tunnel_task(
190
- self, task_identifier: str, protocol_handler: Union[RemotePairingProtocol, CoreDeviceTunnelProxy],
191
- queue: Optional[asyncio.Queue] = None, protocol: Optional[TunnelProtocol] = None) -> None:
271
+ self,
272
+ task_identifier: str,
273
+ protocol_handler: Union[RemotePairingProtocol, CoreDeviceTunnelProxy],
274
+ queue: Optional[asyncio.Queue] = None,
275
+ protocol: Optional[TunnelProtocol] = None,
276
+ ) -> None:
192
277
  if protocol is None:
193
278
  protocol = self.protocol
194
279
  if isinstance(protocol_handler, CoreDeviceTunnelProxy):
@@ -208,37 +293,42 @@ class TunneldCore:
208
293
  queue.put_nowait(tun)
209
294
  # avoid sending another message if succeeded
210
295
  queue = None
211
- logger.info(f'[{asyncio.current_task().get_name()}] Created tunnel --rsd {tun.address} {tun.port}')
296
+ logger.info(f"[{asyncio.current_task().get_name()}] Created tunnel --rsd {tun.address} {tun.port}")
212
297
  await tun.client.wait_closed()
213
298
  else:
214
299
  bailed_out = True
215
300
  logger.debug(
216
- f'not establishing tunnel from {asyncio.current_task().get_name()} '
217
- f'since there is already an active one for same udid')
301
+ f"[{asyncio.current_task().get_name()}] Not establishing tunnel since there is already an "
302
+ f"active one for same udid"
303
+ )
218
304
  except asyncio.CancelledError:
219
305
  pass
220
- except (asyncio.exceptions.IncompleteReadError, TimeoutError, OSError, ConnectionResetError, StreamError,
221
- InvalidServiceError) as e:
306
+ except (
307
+ asyncio.exceptions.IncompleteReadError,
308
+ TimeoutError,
309
+ OSError,
310
+ ConnectionResetError,
311
+ StreamError,
312
+ InvalidServiceError,
313
+ ) as e:
222
314
  if tun is None:
223
- logger.debug(f'got {e.__class__.__name__} from {asyncio.current_task().get_name()}')
315
+ logger.debug(f"Got {e.__class__.__name__} from {asyncio.current_task().get_name()}")
224
316
  else:
225
- logger.debug(f'got {e.__class__.__name__} from tunnel --rsd {tun.address} {tun.port}')
317
+ logger.debug(f"Got {e.__class__.__name__} from tunnel --rsd {tun.address} {tun.port}")
226
318
  except Exception:
227
- logger.error(f'got exception from {asyncio.current_task().get_name()}: {traceback.format_exc()}')
319
+ logger.exception(f"Got exception from {asyncio.current_task().get_name()}")
228
320
  finally:
229
321
  if queue is not None:
230
322
  # notify something went wrong
231
323
  queue.put_nowait(None)
232
324
 
233
325
  if tun is not None and not bailed_out:
234
- logger.info(f'disconnected from tunnel --rsd {tun.address} {tun.port}')
326
+ logger.info(f"Disconnected from tunnel --rsd {tun.address} {tun.port}")
235
327
  await tun.client.stop_tunnel()
236
328
 
237
329
  if protocol_handler is not None:
238
- try:
330
+ with suppress(OSError):
239
331
  await protocol_handler.close()
240
- except OSError:
241
- pass
242
332
 
243
333
  if task_identifier in self.tunnel_tasks:
244
334
  # in case the tunnel was removed just now
@@ -248,63 +338,60 @@ class TunneldCore:
248
338
  async def handle_new_potential_usb_cdc_ncm_interface_task(self, ip: str) -> None:
249
339
  rsd = None
250
340
  try:
251
- answers = None
252
- for i in range(REATTEMPT_COUNT):
253
- answers = await browse(REMOTED_SERVICE_NAMES, [ip])
254
- if answers:
255
- break
256
- logger.debug(f'No addresses found for: {ip}')
257
- await asyncio.sleep(REATTEMPT_INTERVAL)
258
-
259
- if not answers:
260
- raise asyncio.CancelledError()
261
-
262
- peer_address = answers[0].ips[0]
263
-
264
341
  # establish an untrusted RSD handshake
265
- rsd = RemoteServiceDiscoveryService((peer_address, RSD_PORT))
342
+ rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
266
343
 
267
344
  with stop_remoted():
268
- try:
269
- await rsd.connect()
270
- except (ConnectionRefusedError, TimeoutError):
271
- raise asyncio.CancelledError()
272
-
273
- if (self.protocol == TunnelProtocol.QUIC) and (Version(rsd.product_version) < Version('17.0.0')):
345
+ first_time = True
346
+ retry = False
347
+ while retry or first_time:
348
+ retry = False
349
+ try:
350
+ await rsd.connect()
351
+ except StreamClosedError:
352
+ # Could be on first try because of remoted race
353
+ if first_time:
354
+ retry = True
355
+ except (ConnectionRefusedError, TimeoutError, OSError) as e:
356
+ raise asyncio.CancelledError() from e
357
+ finally:
358
+ first_time = False
359
+
360
+ if (self.protocol == TunnelProtocol.QUIC) and (Version(rsd.product_version) < Version("17.0.0")):
274
361
  await rsd.close()
362
+ rsd = None
275
363
  raise asyncio.CancelledError()
276
364
 
277
365
  await asyncio.create_task(
278
366
  self.start_tunnel_task(ip, await create_core_device_tunnel_service_using_rsd(rsd)),
279
- name=f'start-tunnel-task-usb-{ip}')
367
+ name=f"start-tunnel-task-usb-{ip}",
368
+ )
280
369
  except asyncio.CancelledError:
281
370
  pass
282
- except PairingError as e:
283
- logger.error(f'Failed to pair with {ip} with error: {e}')
371
+ except PairingError:
372
+ logger.exception(f"Failed to pair with {ip}")
284
373
  except RuntimeError:
285
- logger.debug(f'Got RuntimeError from: {asyncio.current_task().get_name()}')
374
+ logger.debug(f"Got RuntimeError from: {asyncio.current_task().get_name()}")
286
375
  except Exception:
287
- logger.error(f'Error raised from: {asyncio.current_task().get_name()}: {traceback.format_exc()}')
376
+ logger.exception(f"Error raised from: {asyncio.current_task().get_name()}: {traceback.format_exc()}")
288
377
  finally:
289
378
  if rsd is not None:
290
- try:
379
+ with suppress(OSError):
291
380
  await rsd.close()
292
- except OSError:
293
- pass
294
381
 
295
382
  if ip in self.tunnel_tasks:
296
- # in case the tunnel was removed just now
383
+ # In case the tunnel was removed just now
297
384
  self.tunnel_tasks.pop(ip)
298
385
 
299
386
  async def close(self) -> None:
300
- """ close all tasks """
387
+ """close all tasks"""
301
388
  for task in self.tasks + [tunnel_task.task for tunnel_task in self.tunnel_tasks.values()]:
302
389
  task.cancel()
303
390
  with suppress(asyncio.CancelledError):
304
391
  await task
305
392
 
306
393
  def get_tunnels_ips(self) -> dict:
307
- """ Retrieve the available tunnel tasks and format them as {UDID: [IP]} """
394
+ """Retrieve the available tunnel tasks and format them as {UDID: [IP]}"""
308
395
  tunnels_ips = {}
309
396
  for ip, active_tunnel in self.tunnel_tasks.items():
310
397
  if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
@@ -316,48 +403,75 @@ class TunneldCore:
316
403
  return tunnels_ips
317
404
 
318
405
  def cancel(self, udid: str) -> None:
319
- """ Cancel active tunnels """
406
+ """Cancel active tunnels"""
320
407
  for tunnel_ip in self.get_tunnels_ips().get(udid, []):
321
408
  self.tunnel_tasks.pop(tunnel_ip).task.cancel()
322
- logger.info(f'canceling tunnel {tunnel_ip}')
409
+ logger.info(f"Canceling tunnel {tunnel_ip}")
323
410
 
324
411
  def clear(self) -> None:
325
- """ Clear active tunnels """
326
- for udid, tunnel in self.tunnel_tasks.items():
327
- logger.info(f'Removing tunnel {tunnel}')
412
+ """Clear active tunnels"""
413
+ for _udid, tunnel in self.tunnel_tasks.items():
414
+ logger.info(f"Removing tunnel {tunnel}")
328
415
  tunnel.task.cancel()
329
416
  self.tunnel_tasks = {}
330
417
 
331
418
 
332
419
  class TunneldRunner:
333
- """ TunneldRunner orchestrate between the webserver and TunneldCore """
420
+ """TunneldRunner orchestrate between the webserver and TunneldCore"""
334
421
 
335
422
  @classmethod
336
- def create(cls, host: str, port: int, protocol: TunnelProtocol = TunnelProtocol.QUIC, usb_monitor: bool = True,
337
- wifi_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True) -> None:
338
- cls(host, port, protocol=protocol, usb_monitor=usb_monitor, wifi_monitor=wifi_monitor,
339
- usbmux_monitor=usbmux_monitor, mobdev2_monitor=mobdev2_monitor)._run_app()
340
-
341
- def __init__(self, host: str, port: int, protocol: TunnelProtocol = TunnelProtocol.QUIC, usb_monitor: bool = True,
342
- wifi_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True):
423
+ def create(
424
+ cls,
425
+ host: str,
426
+ port: int,
427
+ protocol: TunnelProtocol = TunnelProtocol.QUIC,
428
+ usb_monitor: bool = True,
429
+ wifi_monitor: bool = True,
430
+ usbmux_monitor: bool = True,
431
+ mobdev2_monitor: bool = True,
432
+ ) -> None:
433
+ cls(
434
+ host,
435
+ port,
436
+ protocol=protocol,
437
+ usb_monitor=usb_monitor,
438
+ wifi_monitor=wifi_monitor,
439
+ usbmux_monitor=usbmux_monitor,
440
+ mobdev2_monitor=mobdev2_monitor,
441
+ )._run_app()
442
+
443
+ def __init__(
444
+ self,
445
+ host: str,
446
+ port: int,
447
+ protocol: TunnelProtocol = TunnelProtocol.QUIC,
448
+ usb_monitor: bool = True,
449
+ wifi_monitor: bool = True,
450
+ usbmux_monitor: bool = True,
451
+ mobdev2_monitor: bool = True,
452
+ ):
343
453
  @asynccontextmanager
344
454
  async def lifespan(app: FastAPI):
345
- logging.getLogger('zeroconf').disabled = True
346
455
  self._tunneld_core.start()
347
456
  yield
348
- logger.info('Closing tunneld tasks...')
457
+ logger.info("Closing tunneld tasks...")
349
458
  await self._tunneld_core.close()
350
459
 
351
460
  self.host = host
352
461
  self.port = port
353
462
  self.protocol = protocol
354
463
  self._app = FastAPI(lifespan=lifespan)
355
- self._tunneld_core = TunneldCore(protocol=protocol, wifi_monitor=wifi_monitor, usb_monitor=usb_monitor,
356
- usbmux_monitor=usbmux_monitor, mobdev2_monitor=mobdev2_monitor)
357
-
358
- @self._app.get('/')
464
+ self._tunneld_core = TunneldCore(
465
+ protocol=protocol,
466
+ wifi_monitor=wifi_monitor,
467
+ usb_monitor=usb_monitor,
468
+ usbmux_monitor=usbmux_monitor,
469
+ mobdev2_monitor=mobdev2_monitor,
470
+ )
471
+
472
+ @self._app.get("/")
359
473
  async def list_tunnels() -> dict[str, list[dict]]:
360
- """ Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS} """
474
+ """Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
361
475
  tunnels = {}
362
476
  for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
363
477
  if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
@@ -365,52 +479,53 @@ class TunneldRunner:
365
479
  if active_tunnel.udid not in tunnels:
366
480
  tunnels[active_tunnel.udid] = []
367
481
  tunnels[active_tunnel.udid].append({
368
- 'tunnel-address': active_tunnel.tunnel.address,
369
- 'tunnel-port': active_tunnel.tunnel.port,
370
- 'interface': ip})
482
+ "tunnel-address": active_tunnel.tunnel.address,
483
+ "tunnel-port": active_tunnel.tunnel.port,
484
+ "interface": ip,
485
+ })
371
486
  return tunnels
372
487
 
373
- @self._app.get('/shutdown')
488
+ @self._app.get("/shutdown")
374
489
  async def shutdown() -> fastapi.Response:
375
- """ Shutdown Tunneld """
490
+ """Shutdown Tunneld"""
376
491
  os.kill(os.getpid(), signal.SIGINT)
377
- data = {'operation': 'shutdown', 'data': True, 'message': 'Server shutting down...'}
492
+ data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
378
493
  return generate_http_response(data)
379
494
 
380
- @self._app.get('/clear_tunnels')
495
+ @self._app.get("/clear_tunnels")
381
496
  async def clear_tunnels() -> fastapi.Response:
382
497
  self._tunneld_core.clear()
383
- data = {'operation': 'clear_tunnels', 'data': True, 'message': 'Cleared tunnels...'}
498
+ data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."}
384
499
  return generate_http_response(data)
385
500
 
386
- @self._app.get('/cancel')
501
+ @self._app.get("/cancel")
387
502
  async def cancel_tunnel(udid: str) -> fastapi.Response:
388
503
  self._tunneld_core.cancel(udid=udid)
389
- data = {'operation': 'cancel', 'udid': udid, 'data': True, 'message': f'tunnel {udid} Canceled ...'}
504
+ data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."}
390
505
  return generate_http_response(data)
391
506
 
392
- @self._app.get('/hello')
507
+ @self._app.get("/hello")
393
508
  async def hello() -> fastapi.Response:
394
- data = {'message': 'Hello, I\'m alive'}
509
+ data = {"message": "Hello, I'm alive"}
395
510
  return generate_http_response(data)
396
511
 
397
512
  def generate_http_response(
398
- data: dict, status_code: int = 200, media_type: str = "application/json") -> fastapi.Response:
399
- return fastapi.Response(
400
- status_code=status_code,
401
- media_type=media_type,
402
- content=json.dumps(data))
513
+ data: dict, status_code: int = 200, media_type: str = "application/json"
514
+ ) -> fastapi.Response:
515
+ return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data))
403
516
 
404
- @self._app.get('/start-tunnel')
517
+ @self._app.get("/start-tunnel")
405
518
  async def start_tunnel(
406
- udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None) -> fastapi.Response:
407
- udid_tunnels = [t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if
408
- t.udid == udid and t.tunnel is not None]
519
+ udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None
520
+ ) -> fastapi.Response:
521
+ udid_tunnels = [
522
+ t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None
523
+ ]
409
524
  if len(udid_tunnels) > 0:
410
525
  data = {
411
- 'interface': udid_tunnels[0].interface,
412
- 'port': udid_tunnels[0].port,
413
- 'address': udid_tunnels[0].address
526
+ "interface": udid_tunnels[0].interface,
527
+ "port": udid_tunnels[0].port,
528
+ "address": udid_tunnels[0].address,
414
529
  }
415
530
  return generate_http_response(data)
416
531
 
@@ -418,32 +533,36 @@ class TunneldRunner:
418
533
  created_task = False
419
534
 
420
535
  try:
421
- if not created_task and connection_type in ('usbmux', None):
422
- task_identifier = f'usbmux-{udid}'
536
+ if not created_task and connection_type in ("usbmux", None):
537
+ task_identifier = f"usbmux-{udid}"
423
538
  try:
424
- service = CoreDeviceTunnelProxy(create_using_usbmux(udid))
539
+ with create_using_usbmux(udid) as lockdown:
540
+ service = await CoreDeviceTunnelProxy.create(lockdown)
425
541
  task = asyncio.create_task(
426
- self._tunneld_core.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP,
427
- queue=queue),
428
- name=f'start-tunnel-task-{task_identifier}')
542
+ self._tunneld_core.start_tunnel_task(
543
+ task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue
544
+ ),
545
+ name=f"start-tunnel-task-{task_identifier}",
546
+ )
429
547
  self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
430
548
  created_task = True
431
549
  except (ConnectionFailedError, InvalidServiceError, MuxException):
432
550
  pass
433
- if connection_type in ('usb', None):
551
+ if connection_type in ("usb", None):
434
552
  for rsd in await get_rsds(udid=udid):
435
553
  rsd_ip = rsd.service.address[0]
436
554
  if ip is not None and rsd_ip != ip:
437
555
  await rsd.close()
438
556
  continue
439
557
  task = asyncio.create_task(
440
- self._tunneld_core.start_tunnel_task(rsd_ip,
441
- await create_core_device_tunnel_service_using_rsd(rsd),
442
- queue=queue),
443
- name=f'start-tunnel-usb-{rsd_ip}')
558
+ self._tunneld_core.start_tunnel_task(
559
+ rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue
560
+ ),
561
+ name=f"start-tunnel-usb-{rsd_ip}",
562
+ )
444
563
  self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
445
564
  created_task = True
446
- if not created_task and connection_type in ('wifi', None):
565
+ if not created_task and connection_type in ("wifi", None):
447
566
  for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
448
567
  remotepairing_ip = remotepairing.hostname
449
568
  if ip is not None and remotepairing_ip != ip:
@@ -451,31 +570,34 @@ class TunneldRunner:
451
570
  continue
452
571
  task = asyncio.create_task(
453
572
  self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
454
- name=f'start-tunnel-wifi-{remotepairing_ip}')
573
+ name=f"start-tunnel-wifi-{remotepairing_ip}",
574
+ )
455
575
  self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
456
- task=task, udid=remotepairing.remote_identifier)
576
+ task=task, udid=remotepairing.remote_identifier
577
+ )
457
578
  created_task = True
458
579
  except Exception as e:
459
- return fastapi.Response(status_code=501,
460
- content=json.dumps({'error': {
461
- 'exception': e.__class__.__name__,
462
- 'traceback': traceback.format_exc(),
463
- }}))
580
+ return fastapi.Response(
581
+ status_code=501,
582
+ content=json.dumps({
583
+ "error": {
584
+ "exception": e.__class__.__name__,
585
+ "traceback": traceback.format_exc(),
586
+ }
587
+ }),
588
+ )
464
589
 
465
590
  if not created_task:
466
- return fastapi.Response(status_code=501, content=json.dumps({'error': 'task not not created'}))
591
+ return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"}))
467
592
 
468
593
  tunnel: Optional[TunnelResult] = await queue.get()
469
594
  if tunnel is not None:
470
- data = {
471
- 'interface': tunnel.interface,
472
- 'port': tunnel.port,
473
- 'address': tunnel.address
474
- }
595
+ data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address}
475
596
  return generate_http_response(data)
476
597
  else:
477
- return fastapi.Response(status_code=404,
478
- content=json.dumps({'error': 'something went wrong during tunnel creation'}))
598
+ return fastapi.Response(
599
+ status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"})
600
+ )
479
601
 
480
602
  def _run_app(self) -> None:
481
- uvicorn.run(self._app, host=self.host, port=self.port, loop='asyncio')
603
+ uvicorn.run(self._app, host=self.host, port=self.port, loop="asyncio")