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