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
@@ -0,0 +1,603 @@
1
+ import asyncio
2
+ import dataclasses
3
+ import json
4
+ import logging
5
+ import os
6
+ import signal
7
+ import traceback
8
+ import warnings
9
+ from contextlib import asynccontextmanager, suppress
10
+ from ssl import SSLEOFError
11
+ from typing import Optional, Union
12
+
13
+ import construct
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
+
22
+ import uvicorn
23
+ from construct import StreamError
24
+ from fastapi import FastAPI
25
+ from packaging.version import Version
26
+
27
+ from pymobiledevice3 import usbmux
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
+ )
40
+ from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
41
+ from pymobiledevice3.osu.os_utils import get_os_utils
42
+ from pymobiledevice3.remote.common import TunnelProtocol
43
+ from pymobiledevice3.remote.module_imports import start_tunnel
44
+ from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService
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
+ )
52
+ from pymobiledevice3.remote.utils import get_rsds, stop_remoted
53
+ from pymobiledevice3.utils import asyncio_print_traceback
54
+
55
+ logger = logging.getLogger(__name__)
56
+
57
+ # bugfix: after the device reboots, it might take some time for remoted to start answering the bonjour queries
58
+ REATTEMPT_INTERVAL = 5
59
+ REATTEMPT_COUNT = 5
60
+
61
+ REMOTEPAIRING_INTERVAL = 5
62
+ MOBDEV2_INTERVAL = 5
63
+
64
+ USBMUX_INTERVAL = 2
65
+ OSUTILS = get_os_utils()
66
+
67
+
68
+ @dataclasses.dataclass
69
+ class TunnelTask:
70
+ task: asyncio.Task
71
+ udid: Optional[str] = None
72
+ tunnel: Optional[TunnelResult] = None
73
+
74
+
75
+ class TunneldCore:
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:
84
+ self.protocol = protocol
85
+ self.tasks: list[asyncio.Task] = []
86
+ self.tunnel_tasks: dict[str, TunnelTask] = {}
87
+ self.usb_monitor = usb_monitor
88
+ self.wifi_monitor = wifi_monitor
89
+ self.usbmux_monitor = usbmux_monitor
90
+ self.mobdev2_monitor = mobdev2_monitor
91
+
92
+ def start(self) -> None:
93
+ """Register all tasks"""
94
+ self.tasks = []
95
+ if self.usb_monitor:
96
+ self.tasks.append(asyncio.create_task(self.monitor_usb_task(), name="monitor-usb-task"))
97
+ if self.wifi_monitor:
98
+ self.tasks.append(asyncio.create_task(self.monitor_wifi_task(), name="monitor-wifi-task"))
99
+ if self.usbmux_monitor:
100
+ self.tasks.append(asyncio.create_task(self.monitor_usbmux_task(), name="monitor-usbmux-task"))
101
+ if self.mobdev2_monitor:
102
+ self.tasks.append(asyncio.create_task(self.monitor_mobdev2_task(), name="monitor-mobdev2-task"))
103
+
104
+ def tunnel_exists_for_udid(self, udid: str) -> bool:
105
+ for task in self.tunnel_tasks.values():
106
+ # Linux implementations of `usbmuxd` may report an incorrect value of UDID, dismissing the `-` character.
107
+ # For such cases, we also check for a UDID without it.
108
+ # See: <https://github.com/doronz88/pymobiledevice3/issues/1388#issuecomment-2782249770>
109
+ task_udid = task.udid or ""
110
+ if ((task_udid == udid) or (task_udid.replace("-", "") == udid)) and (task.tunnel is not None):
111
+ return True
112
+
113
+ return False
114
+
115
+ @asyncio_print_traceback
116
+ async def monitor_usb_task(self) -> None:
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
156
+
157
+ @asyncio_print_traceback
158
+ async def monitor_wifi_task(self) -> None:
159
+ try:
160
+ while True:
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
187
+ await asyncio.sleep(REMOTEPAIRING_INTERVAL)
188
+ except asyncio.CancelledError:
189
+ # Cancel gracefully
190
+ pass
191
+
192
+ @asyncio_print_traceback
193
+ async def monitor_usbmux_task(self) -> None:
194
+ try:
195
+ while True:
196
+ try:
197
+ for mux_device in usbmux.list_devices():
198
+ task_identifier = f"usbmux-{mux_device.serial}-{mux_device.connection_type}"
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
204
+ continue
205
+ service = None
206
+ try:
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()
222
+ continue
223
+ self.tunnel_tasks[task_identifier] = TunnelTask(
224
+ udid=mux_device.serial,
225
+ task=asyncio.create_task(
226
+ self.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP),
227
+ name=f"start-tunnel-task-{task_identifier}",
228
+ ),
229
+ )
230
+ except ConnectionFailedToUsbmuxdError:
231
+ # This is exception is expected to occur repeatedly on linux running usbmuxd
232
+ # as long as there isn't any physical iDevice connected
233
+ logger.debug("failed to connect to usbmux. waiting for it to restart")
234
+ finally:
235
+ await asyncio.sleep(USBMUX_INTERVAL)
236
+ except asyncio.CancelledError:
237
+ pass
238
+
239
+ @asyncio_print_traceback
240
+ async def monitor_mobdev2_task(self) -> None:
241
+ try:
242
+ while True:
243
+ async for ip, lockdown in get_mobdev2_lockdowns(only_paired=True):
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
258
+ self.tunnel_tasks[task_identifier] = TunnelTask(
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,
264
+ )
265
+ await asyncio.sleep(MOBDEV2_INTERVAL)
266
+ except asyncio.CancelledError:
267
+ pass
268
+
269
+ @asyncio_print_traceback
270
+ async def start_tunnel_task(
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:
277
+ if protocol is None:
278
+ protocol = self.protocol
279
+ if isinstance(protocol_handler, CoreDeviceTunnelProxy):
280
+ protocol = TunnelProtocol.TCP
281
+ tun = None
282
+ bailed_out = False
283
+ try:
284
+ if self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
285
+ # cancel current tunnel creation
286
+ raise asyncio.CancelledError()
287
+
288
+ async with start_tunnel(protocol_handler, protocol=protocol) as tun:
289
+ if not self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
290
+ self.tunnel_tasks[task_identifier].tunnel = tun
291
+ self.tunnel_tasks[task_identifier].udid = protocol_handler.remote_identifier
292
+ if queue is not None:
293
+ queue.put_nowait(tun)
294
+ # avoid sending another message if succeeded
295
+ queue = None
296
+ logger.info(f"[{asyncio.current_task().get_name()}] Created tunnel --rsd {tun.address} {tun.port}")
297
+ await tun.client.wait_closed()
298
+ else:
299
+ bailed_out = True
300
+ logger.debug(
301
+ f"[{asyncio.current_task().get_name()}] Not establishing tunnel since there is already an "
302
+ f"active one for same udid"
303
+ )
304
+ except asyncio.CancelledError:
305
+ pass
306
+ except (
307
+ asyncio.exceptions.IncompleteReadError,
308
+ TimeoutError,
309
+ OSError,
310
+ ConnectionResetError,
311
+ StreamError,
312
+ InvalidServiceError,
313
+ ) as e:
314
+ if tun is None:
315
+ logger.debug(f"Got {e.__class__.__name__} from {asyncio.current_task().get_name()}")
316
+ else:
317
+ logger.debug(f"Got {e.__class__.__name__} from tunnel --rsd {tun.address} {tun.port}")
318
+ except Exception:
319
+ logger.exception(f"Got exception from {asyncio.current_task().get_name()}")
320
+ finally:
321
+ if queue is not None:
322
+ # notify something went wrong
323
+ queue.put_nowait(None)
324
+
325
+ if tun is not None and not bailed_out:
326
+ logger.info(f"Disconnected from tunnel --rsd {tun.address} {tun.port}")
327
+ await tun.client.stop_tunnel()
328
+
329
+ if protocol_handler is not None:
330
+ with suppress(OSError):
331
+ await protocol_handler.close()
332
+
333
+ if task_identifier in self.tunnel_tasks:
334
+ # in case the tunnel was removed just now
335
+ self.tunnel_tasks.pop(task_identifier)
336
+
337
+ @asyncio_print_traceback
338
+ async def handle_new_potential_usb_cdc_ncm_interface_task(self, ip: str) -> None:
339
+ rsd = None
340
+ try:
341
+ # establish an untrusted RSD handshake
342
+ rsd = RemoteServiceDiscoveryService((ip, RSD_PORT))
343
+
344
+ with stop_remoted():
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")):
361
+ await rsd.close()
362
+ rsd = None
363
+ raise asyncio.CancelledError()
364
+
365
+ await asyncio.create_task(
366
+ self.start_tunnel_task(ip, await create_core_device_tunnel_service_using_rsd(rsd)),
367
+ name=f"start-tunnel-task-usb-{ip}",
368
+ )
369
+ except asyncio.CancelledError:
370
+ pass
371
+ except PairingError:
372
+ logger.exception(f"Failed to pair with {ip}")
373
+ except RuntimeError:
374
+ logger.debug(f"Got RuntimeError from: {asyncio.current_task().get_name()}")
375
+ except Exception:
376
+ logger.exception(f"Error raised from: {asyncio.current_task().get_name()}: {traceback.format_exc()}")
377
+ finally:
378
+ if rsd is not None:
379
+ with suppress(OSError):
380
+ await rsd.close()
381
+
382
+ if ip in self.tunnel_tasks:
383
+ # In case the tunnel was removed just now
384
+ self.tunnel_tasks.pop(ip)
385
+
386
+ async def close(self) -> None:
387
+ """close all tasks"""
388
+ for task in self.tasks + [tunnel_task.task for tunnel_task in self.tunnel_tasks.values()]:
389
+ task.cancel()
390
+ with suppress(asyncio.CancelledError):
391
+ await task
392
+
393
+ def get_tunnels_ips(self) -> dict:
394
+ """Retrieve the available tunnel tasks and format them as {UDID: [IP]}"""
395
+ tunnels_ips = {}
396
+ for ip, active_tunnel in self.tunnel_tasks.items():
397
+ if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
398
+ continue
399
+ if active_tunnel.udid not in tunnels_ips:
400
+ tunnels_ips[active_tunnel.udid] = [ip]
401
+ else:
402
+ tunnels_ips[active_tunnel.udid].append(ip)
403
+ return tunnels_ips
404
+
405
+ def cancel(self, udid: str) -> None:
406
+ """Cancel active tunnels"""
407
+ for tunnel_ip in self.get_tunnels_ips().get(udid, []):
408
+ self.tunnel_tasks.pop(tunnel_ip).task.cancel()
409
+ logger.info(f"Canceling tunnel {tunnel_ip}")
410
+
411
+ def clear(self) -> None:
412
+ """Clear active tunnels"""
413
+ for _udid, tunnel in self.tunnel_tasks.items():
414
+ logger.info(f"Removing tunnel {tunnel}")
415
+ tunnel.task.cancel()
416
+ self.tunnel_tasks = {}
417
+
418
+
419
+ class TunneldRunner:
420
+ """TunneldRunner orchestrate between the webserver and TunneldCore"""
421
+
422
+ @classmethod
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
+ ):
453
+ @asynccontextmanager
454
+ async def lifespan(app: FastAPI):
455
+ self._tunneld_core.start()
456
+ yield
457
+ logger.info("Closing tunneld tasks...")
458
+ await self._tunneld_core.close()
459
+
460
+ self.host = host
461
+ self.port = port
462
+ self.protocol = protocol
463
+ self._app = FastAPI(lifespan=lifespan)
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("/")
473
+ async def list_tunnels() -> dict[str, list[dict]]:
474
+ """Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}"""
475
+ tunnels = {}
476
+ for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
477
+ if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
478
+ continue
479
+ if active_tunnel.udid not in tunnels:
480
+ tunnels[active_tunnel.udid] = []
481
+ tunnels[active_tunnel.udid].append({
482
+ "tunnel-address": active_tunnel.tunnel.address,
483
+ "tunnel-port": active_tunnel.tunnel.port,
484
+ "interface": ip,
485
+ })
486
+ return tunnels
487
+
488
+ @self._app.get("/shutdown")
489
+ async def shutdown() -> fastapi.Response:
490
+ """Shutdown Tunneld"""
491
+ os.kill(os.getpid(), signal.SIGINT)
492
+ data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
493
+ return generate_http_response(data)
494
+
495
+ @self._app.get("/clear_tunnels")
496
+ async def clear_tunnels() -> fastapi.Response:
497
+ self._tunneld_core.clear()
498
+ data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."}
499
+ return generate_http_response(data)
500
+
501
+ @self._app.get("/cancel")
502
+ async def cancel_tunnel(udid: str) -> fastapi.Response:
503
+ self._tunneld_core.cancel(udid=udid)
504
+ data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."}
505
+ return generate_http_response(data)
506
+
507
+ @self._app.get("/hello")
508
+ async def hello() -> fastapi.Response:
509
+ data = {"message": "Hello, I'm alive"}
510
+ return generate_http_response(data)
511
+
512
+ def generate_http_response(
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))
516
+
517
+ @self._app.get("/start-tunnel")
518
+ async def start_tunnel(
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
+ ]
524
+ if len(udid_tunnels) > 0:
525
+ data = {
526
+ "interface": udid_tunnels[0].interface,
527
+ "port": udid_tunnels[0].port,
528
+ "address": udid_tunnels[0].address,
529
+ }
530
+ return generate_http_response(data)
531
+
532
+ queue = asyncio.Queue()
533
+ created_task = False
534
+
535
+ try:
536
+ if not created_task and connection_type in ("usbmux", None):
537
+ task_identifier = f"usbmux-{udid}"
538
+ try:
539
+ with create_using_usbmux(udid) as lockdown:
540
+ service = await CoreDeviceTunnelProxy.create(lockdown)
541
+ task = asyncio.create_task(
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
+ )
547
+ self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
548
+ created_task = True
549
+ except (ConnectionFailedError, InvalidServiceError, MuxException):
550
+ pass
551
+ if connection_type in ("usb", None):
552
+ for rsd in await get_rsds(udid=udid):
553
+ rsd_ip = rsd.service.address[0]
554
+ if ip is not None and rsd_ip != ip:
555
+ await rsd.close()
556
+ continue
557
+ task = asyncio.create_task(
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
+ )
563
+ self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
564
+ created_task = True
565
+ if not created_task and connection_type in ("wifi", None):
566
+ for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
567
+ remotepairing_ip = remotepairing.hostname
568
+ if ip is not None and remotepairing_ip != ip:
569
+ await remotepairing.close()
570
+ continue
571
+ task = asyncio.create_task(
572
+ self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
573
+ name=f"start-tunnel-wifi-{remotepairing_ip}",
574
+ )
575
+ self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
576
+ task=task, udid=remotepairing.remote_identifier
577
+ )
578
+ created_task = True
579
+ except Exception as e:
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
+ )
589
+
590
+ if not created_task:
591
+ return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"}))
592
+
593
+ tunnel: Optional[TunnelResult] = await queue.get()
594
+ if tunnel is not None:
595
+ data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address}
596
+ return generate_http_response(data)
597
+ else:
598
+ return fastapi.Response(
599
+ status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"})
600
+ )
601
+
602
+ def _run_app(self) -> None:
603
+ uvicorn.run(self._app, host=self.host, port=self.port, loop="asyncio")