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.
- misc/plist_sniffer.py +15 -15
- misc/remotexpc_sniffer.py +29 -28
- misc/understanding_idevice_protocol_layers.md +15 -10
- pymobiledevice3/__main__.py +317 -127
- pymobiledevice3/_version.py +22 -4
- pymobiledevice3/bonjour.py +358 -113
- pymobiledevice3/ca.py +253 -16
- pymobiledevice3/cli/activation.py +31 -23
- pymobiledevice3/cli/afc.py +49 -40
- pymobiledevice3/cli/amfi.py +16 -21
- pymobiledevice3/cli/apps.py +87 -42
- pymobiledevice3/cli/backup.py +160 -90
- pymobiledevice3/cli/bonjour.py +44 -40
- pymobiledevice3/cli/cli_common.py +204 -198
- pymobiledevice3/cli/companion_proxy.py +14 -14
- pymobiledevice3/cli/crash.py +105 -56
- pymobiledevice3/cli/developer/__init__.py +62 -0
- pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
- pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
- pymobiledevice3/cli/developer/arbitration.py +50 -0
- pymobiledevice3/cli/developer/condition.py +33 -0
- pymobiledevice3/cli/developer/core_device.py +294 -0
- pymobiledevice3/cli/developer/debugserver.py +244 -0
- pymobiledevice3/cli/developer/dvt/__init__.py +438 -0
- pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
- pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
- pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
- pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
- pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
- pymobiledevice3/cli/developer/simulate_location.py +51 -0
- pymobiledevice3/cli/diagnostics/__init__.py +75 -0
- pymobiledevice3/cli/diagnostics/battery.py +47 -0
- pymobiledevice3/cli/idam.py +42 -0
- pymobiledevice3/cli/lockdown.py +108 -103
- pymobiledevice3/cli/mounter.py +158 -99
- pymobiledevice3/cli/notification.py +38 -26
- pymobiledevice3/cli/pcap.py +45 -24
- pymobiledevice3/cli/power_assertion.py +18 -17
- pymobiledevice3/cli/processes.py +17 -23
- pymobiledevice3/cli/profile.py +165 -109
- pymobiledevice3/cli/provision.py +35 -34
- pymobiledevice3/cli/remote.py +217 -129
- pymobiledevice3/cli/restore.py +159 -143
- pymobiledevice3/cli/springboard.py +63 -53
- pymobiledevice3/cli/syslog.py +193 -86
- pymobiledevice3/cli/usbmux.py +73 -33
- pymobiledevice3/cli/version.py +5 -7
- pymobiledevice3/cli/webinspector.py +376 -214
- pymobiledevice3/common.py +3 -1
- pymobiledevice3/exceptions.py +182 -58
- pymobiledevice3/irecv.py +52 -53
- pymobiledevice3/irecv_devices.py +1489 -464
- pymobiledevice3/lockdown.py +473 -275
- pymobiledevice3/lockdown_service_provider.py +15 -8
- pymobiledevice3/osu/os_utils.py +27 -9
- pymobiledevice3/osu/posix_util.py +34 -15
- pymobiledevice3/osu/win_util.py +14 -8
- pymobiledevice3/pair_records.py +102 -21
- pymobiledevice3/remote/common.py +8 -4
- pymobiledevice3/remote/core_device/app_service.py +94 -67
- pymobiledevice3/remote/core_device/core_device_service.py +17 -14
- pymobiledevice3/remote/core_device/device_info.py +5 -5
- pymobiledevice3/remote/core_device/diagnostics_service.py +19 -4
- pymobiledevice3/remote/core_device/file_service.py +53 -23
- pymobiledevice3/remote/remote_service_discovery.py +79 -45
- pymobiledevice3/remote/remotexpc.py +73 -44
- pymobiledevice3/remote/tunnel_service.py +442 -317
- pymobiledevice3/remote/utils.py +14 -13
- pymobiledevice3/remote/xpc_message.py +145 -125
- pymobiledevice3/resources/dsc_uuid_map.py +19 -19
- pymobiledevice3/resources/firmware_notifications.py +20 -16
- pymobiledevice3/resources/notifications.txt +144 -0
- pymobiledevice3/restore/asr.py +27 -27
- pymobiledevice3/restore/base_restore.py +110 -21
- pymobiledevice3/restore/consts.py +87 -66
- pymobiledevice3/restore/device.py +59 -12
- pymobiledevice3/restore/fdr.py +46 -48
- pymobiledevice3/restore/ftab.py +19 -19
- pymobiledevice3/restore/img4.py +163 -0
- pymobiledevice3/restore/mbn.py +587 -0
- pymobiledevice3/restore/recovery.py +151 -151
- pymobiledevice3/restore/restore.py +562 -544
- pymobiledevice3/restore/restore_options.py +131 -110
- pymobiledevice3/restore/restored_client.py +51 -31
- pymobiledevice3/restore/tss.py +385 -267
- pymobiledevice3/service_connection.py +252 -59
- pymobiledevice3/services/accessibilityaudit.py +202 -120
- pymobiledevice3/services/afc.py +962 -365
- pymobiledevice3/services/amfi.py +24 -30
- pymobiledevice3/services/companion.py +23 -19
- pymobiledevice3/services/crash_reports.py +71 -47
- pymobiledevice3/services/debugserver_applist.py +3 -3
- pymobiledevice3/services/device_arbitration.py +8 -8
- pymobiledevice3/services/device_link.py +101 -79
- pymobiledevice3/services/diagnostics.py +973 -967
- pymobiledevice3/services/dtfetchsymbols.py +8 -8
- pymobiledevice3/services/dvt/dvt_secure_socket_proxy.py +4 -4
- pymobiledevice3/services/dvt/dvt_testmanaged_proxy.py +4 -4
- pymobiledevice3/services/dvt/instruments/activity_trace_tap.py +85 -74
- pymobiledevice3/services/dvt/instruments/application_listing.py +2 -3
- pymobiledevice3/services/dvt/instruments/condition_inducer.py +7 -6
- pymobiledevice3/services/dvt/instruments/core_profile_session_tap.py +466 -384
- pymobiledevice3/services/dvt/instruments/device_info.py +20 -11
- pymobiledevice3/services/dvt/instruments/energy_monitor.py +1 -1
- pymobiledevice3/services/dvt/instruments/graphics.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation.py +1 -1
- pymobiledevice3/services/dvt/instruments/location_simulation_base.py +10 -10
- pymobiledevice3/services/dvt/instruments/network_monitor.py +17 -17
- pymobiledevice3/services/dvt/instruments/notifications.py +1 -1
- pymobiledevice3/services/dvt/instruments/process_control.py +35 -10
- pymobiledevice3/services/dvt/instruments/screenshot.py +2 -2
- pymobiledevice3/services/dvt/instruments/sysmontap.py +15 -15
- pymobiledevice3/services/dvt/testmanaged/xcuitest.py +42 -52
- pymobiledevice3/services/file_relay.py +10 -10
- pymobiledevice3/services/heartbeat.py +9 -8
- pymobiledevice3/services/house_arrest.py +16 -15
- pymobiledevice3/services/idam.py +20 -0
- pymobiledevice3/services/installation_proxy.py +173 -81
- pymobiledevice3/services/lockdown_service.py +20 -10
- pymobiledevice3/services/misagent.py +22 -19
- pymobiledevice3/services/mobile_activation.py +147 -64
- pymobiledevice3/services/mobile_config.py +331 -294
- pymobiledevice3/services/mobile_image_mounter.py +141 -113
- pymobiledevice3/services/mobilebackup2.py +203 -145
- pymobiledevice3/services/notification_proxy.py +11 -11
- pymobiledevice3/services/os_trace.py +134 -74
- pymobiledevice3/services/pcapd.py +314 -302
- pymobiledevice3/services/power_assertion.py +10 -9
- pymobiledevice3/services/preboard.py +4 -4
- pymobiledevice3/services/remote_fetch_symbols.py +21 -14
- pymobiledevice3/services/remote_server.py +176 -146
- pymobiledevice3/services/restore_service.py +16 -16
- pymobiledevice3/services/screenshot.py +15 -12
- pymobiledevice3/services/simulate_location.py +7 -7
- pymobiledevice3/services/springboard.py +15 -15
- pymobiledevice3/services/syslog.py +5 -5
- pymobiledevice3/services/web_protocol/alert.py +11 -11
- pymobiledevice3/services/web_protocol/automation_session.py +251 -239
- pymobiledevice3/services/web_protocol/cdp_screencast.py +46 -37
- pymobiledevice3/services/web_protocol/cdp_server.py +19 -19
- pymobiledevice3/services/web_protocol/cdp_target.py +411 -373
- pymobiledevice3/services/web_protocol/driver.py +114 -111
- pymobiledevice3/services/web_protocol/element.py +124 -111
- pymobiledevice3/services/web_protocol/inspector_session.py +106 -102
- pymobiledevice3/services/web_protocol/selenium_api.py +49 -49
- pymobiledevice3/services/web_protocol/session_protocol.py +18 -12
- pymobiledevice3/services/web_protocol/switch_to.py +30 -27
- pymobiledevice3/services/webinspector.py +189 -155
- pymobiledevice3/tcp_forwarder.py +87 -69
- pymobiledevice3/tunneld/__init__.py +0 -0
- pymobiledevice3/tunneld/api.py +63 -0
- pymobiledevice3/tunneld/server.py +603 -0
- pymobiledevice3/usbmux.py +198 -147
- pymobiledevice3/utils.py +14 -11
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/METADATA +55 -28
- pymobiledevice3-7.0.6.dist-info/RECORD +188 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/WHEEL +1 -1
- pymobiledevice3/cli/developer.py +0 -1215
- pymobiledevice3/cli/diagnostics.py +0 -99
- pymobiledevice3/tunneld.py +0 -524
- pymobiledevice3-4.14.6.dist-info/RECORD +0 -168
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/entry_points.txt +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info/licenses}/LICENSE +0 -0
- {pymobiledevice3-4.14.6.dist-info → pymobiledevice3-7.0.6.dist-info}/top_level.txt +0 -0
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import time
|
|
3
|
-
|
|
4
|
-
import click
|
|
5
|
-
|
|
6
|
-
from pymobiledevice3.cli.cli_common import Command, print_json
|
|
7
|
-
from pymobiledevice3.lockdown import LockdownClient
|
|
8
|
-
from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
|
|
9
|
-
from pymobiledevice3.services.diagnostics import DiagnosticsService
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@click.group()
|
|
15
|
-
def cli() -> None:
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@cli.group()
|
|
20
|
-
def diagnostics() -> None:
|
|
21
|
-
""" Reboot/Shutdown device or access other diagnostics services """
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@diagnostics.command('restart', cls=Command)
|
|
26
|
-
def diagnostics_restart(service_provider: LockdownClient):
|
|
27
|
-
""" Restart device """
|
|
28
|
-
DiagnosticsService(lockdown=service_provider).restart()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@diagnostics.command('shutdown', cls=Command)
|
|
32
|
-
def diagnostics_shutdown(service_provider: LockdownClient):
|
|
33
|
-
""" Shutdown device """
|
|
34
|
-
DiagnosticsService(lockdown=service_provider).shutdown()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
@diagnostics.command('sleep', cls=Command)
|
|
38
|
-
def diagnostics_sleep(service_provider: LockdownClient):
|
|
39
|
-
""" Put device into sleep """
|
|
40
|
-
DiagnosticsService(lockdown=service_provider).sleep()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@diagnostics.command('info', cls=Command)
|
|
44
|
-
def diagnostics_info(service_provider: LockdownClient):
|
|
45
|
-
""" Get diagnostics info """
|
|
46
|
-
print_json(DiagnosticsService(lockdown=service_provider).info())
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@diagnostics.command('ioregistry', cls=Command)
|
|
50
|
-
@click.option('--plane')
|
|
51
|
-
@click.option('--name')
|
|
52
|
-
@click.option('--ioclass')
|
|
53
|
-
def diagnostics_ioregistry(service_provider: LockdownClient, plane, name, ioclass):
|
|
54
|
-
""" Get ioregistry info """
|
|
55
|
-
print_json(DiagnosticsService(lockdown=service_provider).ioregistry(plane=plane, name=name, ioclass=ioclass))
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@diagnostics.command('mg', cls=Command)
|
|
59
|
-
@click.argument('keys', nargs=-1, default=None)
|
|
60
|
-
def diagnostics_mg(service_provider: LockdownClient, keys):
|
|
61
|
-
""" Get MobileGestalt key values from given list. If empty, return all known. """
|
|
62
|
-
print_json(DiagnosticsService(lockdown=service_provider).mobilegestalt(keys=keys))
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
@diagnostics.group('battery')
|
|
66
|
-
def diagnostics_battery():
|
|
67
|
-
""" Battery options """
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
@diagnostics_battery.command('single', cls=Command)
|
|
72
|
-
def diagnostics_battery_single(service_provider: LockdownClient):
|
|
73
|
-
""" get single snapshot of battery data """
|
|
74
|
-
raw_info = DiagnosticsService(lockdown=service_provider).get_battery()
|
|
75
|
-
print_json(raw_info)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@diagnostics_battery.command('monitor', cls=Command)
|
|
79
|
-
def diagnostics_battery_monitor(service_provider: LockdownClient):
|
|
80
|
-
""" monitor battery usage """
|
|
81
|
-
diagnostics = DiagnosticsService(lockdown=service_provider)
|
|
82
|
-
while True:
|
|
83
|
-
raw_info = diagnostics.get_battery()
|
|
84
|
-
info = {
|
|
85
|
-
'InstantAmperage': raw_info.get('InstantAmperage'),
|
|
86
|
-
'Temperature': raw_info.get('Temperature'),
|
|
87
|
-
'Voltage': raw_info.get('Voltage'),
|
|
88
|
-
'IsCharging': raw_info.get('IsCharging'),
|
|
89
|
-
'CurrentCapacity': raw_info.get('CurrentCapacity'),
|
|
90
|
-
}
|
|
91
|
-
logger.info(info)
|
|
92
|
-
time.sleep(1)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
@diagnostics.command('wifi', cls=Command)
|
|
96
|
-
def diagnostics_wifi(service_provider: LockdownServiceProvider) -> None:
|
|
97
|
-
""" Query WiFi info from IORegistry """
|
|
98
|
-
raw_info = DiagnosticsService(lockdown=service_provider).get_wifi()
|
|
99
|
-
print_json(raw_info)
|
pymobiledevice3/tunneld.py
DELETED
|
@@ -1,524 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import dataclasses
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import os
|
|
6
|
-
import signal
|
|
7
|
-
import traceback
|
|
8
|
-
from contextlib import asynccontextmanager, suppress
|
|
9
|
-
from typing import Optional, Union
|
|
10
|
-
|
|
11
|
-
import construct
|
|
12
|
-
import fastapi
|
|
13
|
-
import requests
|
|
14
|
-
import uvicorn
|
|
15
|
-
from construct import StreamError
|
|
16
|
-
from fastapi import FastAPI
|
|
17
|
-
from packaging.version import Version
|
|
18
|
-
|
|
19
|
-
from pymobiledevice3 import usbmux
|
|
20
|
-
from pymobiledevice3.bonjour import REMOTED_SERVICE_NAMES, browse
|
|
21
|
-
from pymobiledevice3.exceptions import ConnectionFailedError, ConnectionFailedToUsbmuxdError, DeviceNotFoundError, \
|
|
22
|
-
GetProhibitedError, InvalidServiceError, MuxException, PairingError, TunneldConnectionError
|
|
23
|
-
from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
|
|
24
|
-
from pymobiledevice3.osu.os_utils import get_os_utils
|
|
25
|
-
from pymobiledevice3.remote.common import TunnelProtocol
|
|
26
|
-
from pymobiledevice3.remote.module_imports import start_tunnel
|
|
27
|
-
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService
|
|
28
|
-
from pymobiledevice3.remote.tunnel_service import CoreDeviceTunnelProxy, RemotePairingProtocol, TunnelResult, \
|
|
29
|
-
create_core_device_tunnel_service_using_rsd, get_remote_pairing_tunnel_services
|
|
30
|
-
from pymobiledevice3.remote.utils import get_rsds, stop_remoted
|
|
31
|
-
from pymobiledevice3.utils import asyncio_print_traceback, get_asyncio_loop
|
|
32
|
-
|
|
33
|
-
logger = logging.getLogger(__name__)
|
|
34
|
-
|
|
35
|
-
TUNNELD_DEFAULT_ADDRESS = ('127.0.0.1', 49151)
|
|
36
|
-
|
|
37
|
-
# bugfix: after the device reboots, it might take some time for remoted to start answering the bonjour queries
|
|
38
|
-
REATTEMPT_INTERVAL = 5
|
|
39
|
-
REATTEMPT_COUNT = 5
|
|
40
|
-
|
|
41
|
-
REMOTEPAIRING_INTERVAL = 5
|
|
42
|
-
MOVDEV2_INTERVAL = 5
|
|
43
|
-
|
|
44
|
-
USBMUX_INTERVAL = 2
|
|
45
|
-
OSUTILS = get_os_utils()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclasses.dataclass
|
|
49
|
-
class TunnelTask:
|
|
50
|
-
task: asyncio.Task
|
|
51
|
-
udid: Optional[str] = None
|
|
52
|
-
tunnel: Optional[TunnelResult] = None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class TunneldCore:
|
|
56
|
-
def __init__(self, protocol: TunnelProtocol = TunnelProtocol.QUIC, wifi_monitor: bool = True,
|
|
57
|
-
usb_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True) -> None:
|
|
58
|
-
self.protocol = protocol
|
|
59
|
-
self.tasks: list[asyncio.Task] = []
|
|
60
|
-
self.tunnel_tasks: dict[str, TunnelTask] = {}
|
|
61
|
-
self.usb_monitor = usb_monitor
|
|
62
|
-
self.wifi_monitor = wifi_monitor
|
|
63
|
-
self.usbmux_monitor = usbmux_monitor
|
|
64
|
-
self.mobdev2_monitor = mobdev2_monitor
|
|
65
|
-
|
|
66
|
-
def start(self) -> None:
|
|
67
|
-
""" Register all tasks """
|
|
68
|
-
self.tasks = []
|
|
69
|
-
if self.usb_monitor:
|
|
70
|
-
self.tasks.append(asyncio.create_task(self.monitor_usb_task(), name='monitor-usb-task'))
|
|
71
|
-
if self.wifi_monitor:
|
|
72
|
-
self.tasks.append(asyncio.create_task(self.monitor_wifi_task(), name='monitor-wifi-task'))
|
|
73
|
-
if self.usbmux_monitor:
|
|
74
|
-
self.tasks.append(asyncio.create_task(self.monitor_usbmux_task(), name='monitor-usbmux-task'))
|
|
75
|
-
if self.mobdev2_monitor:
|
|
76
|
-
self.tasks.append(asyncio.create_task(self.monitor_mobdev2_task(), name='monitor-mobdev2-task'))
|
|
77
|
-
|
|
78
|
-
def tunnel_exists_for_udid(self, udid: str) -> bool:
|
|
79
|
-
for task in self.tunnel_tasks.values():
|
|
80
|
-
if (task.udid == udid) and (task.tunnel is not None):
|
|
81
|
-
return True
|
|
82
|
-
return False
|
|
83
|
-
|
|
84
|
-
@asyncio_print_traceback
|
|
85
|
-
async def monitor_usb_task(self) -> None:
|
|
86
|
-
previous_ips = []
|
|
87
|
-
while True:
|
|
88
|
-
current_ips = OSUTILS.get_ipv6_ips()
|
|
89
|
-
added = [ip for ip in current_ips if ip not in previous_ips]
|
|
90
|
-
removed = [ip for ip in previous_ips if ip not in current_ips]
|
|
91
|
-
|
|
92
|
-
previous_ips = current_ips
|
|
93
|
-
|
|
94
|
-
logger.debug(f'added interfaces: {added}')
|
|
95
|
-
logger.debug(f'removed interfaces: {removed}')
|
|
96
|
-
|
|
97
|
-
for ip in removed:
|
|
98
|
-
if ip in self.tunnel_tasks:
|
|
99
|
-
self.tunnel_tasks[ip].task.cancel()
|
|
100
|
-
await self.tunnel_tasks[ip].task
|
|
101
|
-
|
|
102
|
-
for ip in added:
|
|
103
|
-
self.tunnel_tasks[ip] = TunnelTask(
|
|
104
|
-
task=asyncio.create_task(self.handle_new_potential_usb_cdc_ncm_interface_task(ip),
|
|
105
|
-
name=f'handle-new-potential-usb-cdc-ncm-interface-task-{ip}'))
|
|
106
|
-
|
|
107
|
-
# wait before re-iterating
|
|
108
|
-
await asyncio.sleep(1)
|
|
109
|
-
|
|
110
|
-
@asyncio_print_traceback
|
|
111
|
-
async def monitor_wifi_task(self) -> None:
|
|
112
|
-
try:
|
|
113
|
-
while True:
|
|
114
|
-
for service in await get_remote_pairing_tunnel_services():
|
|
115
|
-
if service.hostname in self.tunnel_tasks:
|
|
116
|
-
# skip tunnel if already exists for this ip
|
|
117
|
-
await service.close()
|
|
118
|
-
continue
|
|
119
|
-
if self.tunnel_exists_for_udid(service.remote_identifier):
|
|
120
|
-
# skip tunnel if already exists for this udid
|
|
121
|
-
await service.close()
|
|
122
|
-
continue
|
|
123
|
-
self.tunnel_tasks[service.hostname] = TunnelTask(
|
|
124
|
-
task=asyncio.create_task(self.start_tunnel_task(service.hostname, service),
|
|
125
|
-
name=f'start-tunnel-task-wifi-{service.hostname}'),
|
|
126
|
-
udid=service.remote_identifier
|
|
127
|
-
)
|
|
128
|
-
await asyncio.sleep(REMOTEPAIRING_INTERVAL)
|
|
129
|
-
except asyncio.CancelledError:
|
|
130
|
-
pass
|
|
131
|
-
|
|
132
|
-
@asyncio_print_traceback
|
|
133
|
-
async def monitor_usbmux_task(self) -> None:
|
|
134
|
-
try:
|
|
135
|
-
while True:
|
|
136
|
-
try:
|
|
137
|
-
for mux_device in usbmux.list_devices():
|
|
138
|
-
task_identifier = f'usbmux-{mux_device.serial}-{mux_device.connection_type}'
|
|
139
|
-
if self.tunnel_exists_for_udid(mux_device.serial):
|
|
140
|
-
continue
|
|
141
|
-
try:
|
|
142
|
-
service = CoreDeviceTunnelProxy(create_using_usbmux(mux_device.serial))
|
|
143
|
-
except (MuxException, InvalidServiceError, GetProhibitedError, construct.core.StreamError,
|
|
144
|
-
ConnectionAbortedError, DeviceNotFoundError):
|
|
145
|
-
continue
|
|
146
|
-
self.tunnel_tasks[task_identifier] = TunnelTask(
|
|
147
|
-
udid=mux_device.serial,
|
|
148
|
-
task=asyncio.create_task(
|
|
149
|
-
self.start_tunnel_task(task_identifier,
|
|
150
|
-
service,
|
|
151
|
-
protocol=TunnelProtocol.TCP),
|
|
152
|
-
name=f'start-tunnel-task-{task_identifier}'))
|
|
153
|
-
except ConnectionFailedToUsbmuxdError:
|
|
154
|
-
# This is exception is expected to occur repeatedly on linux running usbmuxd
|
|
155
|
-
# as long as there isn't any physical iDevice connected
|
|
156
|
-
logger.debug('failed to connect to usbmux. waiting for it to restart')
|
|
157
|
-
finally:
|
|
158
|
-
await asyncio.sleep(USBMUX_INTERVAL)
|
|
159
|
-
except asyncio.CancelledError:
|
|
160
|
-
pass
|
|
161
|
-
|
|
162
|
-
@asyncio_print_traceback
|
|
163
|
-
async def monitor_mobdev2_task(self) -> None:
|
|
164
|
-
try:
|
|
165
|
-
while True:
|
|
166
|
-
async for ip, lockdown in get_mobdev2_lockdowns(only_paired=True):
|
|
167
|
-
if self.tunnel_exists_for_udid(lockdown.udid):
|
|
168
|
-
# skip tunnel if already exists for this udid
|
|
169
|
-
continue
|
|
170
|
-
task_identifier = f'mobdev2-{lockdown.udid}-{ip}'
|
|
171
|
-
try:
|
|
172
|
-
tunnel_service = CoreDeviceTunnelProxy(lockdown)
|
|
173
|
-
except InvalidServiceError:
|
|
174
|
-
logger.warning(f'[{task_identifier}] failed to start CoreDeviceTunnelProxy - skipping')
|
|
175
|
-
continue
|
|
176
|
-
self.tunnel_tasks[task_identifier] = TunnelTask(
|
|
177
|
-
task=asyncio.create_task(self.start_tunnel_task(task_identifier, tunnel_service),
|
|
178
|
-
name=f'start-tunnel-task-{task_identifier}'),
|
|
179
|
-
udid=lockdown.udid
|
|
180
|
-
)
|
|
181
|
-
await asyncio.sleep(MOVDEV2_INTERVAL)
|
|
182
|
-
except asyncio.CancelledError:
|
|
183
|
-
pass
|
|
184
|
-
|
|
185
|
-
@asyncio_print_traceback
|
|
186
|
-
async def start_tunnel_task(
|
|
187
|
-
self, task_identifier: str, protocol_handler: Union[RemotePairingProtocol, CoreDeviceTunnelProxy],
|
|
188
|
-
queue: Optional[asyncio.Queue] = None, protocol: Optional[TunnelProtocol] = None) -> None:
|
|
189
|
-
if protocol is None:
|
|
190
|
-
protocol = self.protocol
|
|
191
|
-
if isinstance(protocol_handler, CoreDeviceTunnelProxy):
|
|
192
|
-
protocol = TunnelProtocol.TCP
|
|
193
|
-
tun = None
|
|
194
|
-
bailed_out = False
|
|
195
|
-
try:
|
|
196
|
-
if self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
|
|
197
|
-
# cancel current tunnel creation
|
|
198
|
-
raise asyncio.CancelledError()
|
|
199
|
-
|
|
200
|
-
async with start_tunnel(protocol_handler, protocol=protocol) as tun:
|
|
201
|
-
if not self.tunnel_exists_for_udid(protocol_handler.remote_identifier):
|
|
202
|
-
self.tunnel_tasks[task_identifier].tunnel = tun
|
|
203
|
-
self.tunnel_tasks[task_identifier].udid = protocol_handler.remote_identifier
|
|
204
|
-
if queue is not None:
|
|
205
|
-
queue.put_nowait(tun)
|
|
206
|
-
# avoid sending another message if succeeded
|
|
207
|
-
queue = None
|
|
208
|
-
logger.info(f'[{asyncio.current_task().get_name()}] Created tunnel --rsd {tun.address} {tun.port}')
|
|
209
|
-
await tun.client.wait_closed()
|
|
210
|
-
else:
|
|
211
|
-
bailed_out = True
|
|
212
|
-
logger.debug(
|
|
213
|
-
f'not establishing tunnel from {asyncio.current_task().get_name()} '
|
|
214
|
-
f'since there is already an active one for same udid')
|
|
215
|
-
except asyncio.CancelledError:
|
|
216
|
-
pass
|
|
217
|
-
except (ConnectionResetError, StreamError) as e:
|
|
218
|
-
logger.debug(f'got {e.__class__.__name__} from {asyncio.current_task().get_name()}')
|
|
219
|
-
except (asyncio.exceptions.IncompleteReadError, TimeoutError, OSError) as e:
|
|
220
|
-
logger.debug(f'got {e.__class__.__name__} from tunnel --rsd {tun.address} {tun.port}')
|
|
221
|
-
except Exception:
|
|
222
|
-
logger.error(f'got exception from {asyncio.current_task().get_name()}: {traceback.format_exc()}')
|
|
223
|
-
finally:
|
|
224
|
-
if queue is not None:
|
|
225
|
-
# notify something went wrong
|
|
226
|
-
queue.put_nowait(None)
|
|
227
|
-
|
|
228
|
-
if tun is not None and not bailed_out:
|
|
229
|
-
logger.info(f'disconnected from tunnel --rsd {tun.address} {tun.port}')
|
|
230
|
-
await tun.client.stop_tunnel()
|
|
231
|
-
|
|
232
|
-
if protocol_handler is not None:
|
|
233
|
-
try:
|
|
234
|
-
await protocol_handler.close()
|
|
235
|
-
except OSError:
|
|
236
|
-
pass
|
|
237
|
-
|
|
238
|
-
if task_identifier in self.tunnel_tasks:
|
|
239
|
-
# in case the tunnel was removed just now
|
|
240
|
-
self.tunnel_tasks.pop(task_identifier)
|
|
241
|
-
|
|
242
|
-
@asyncio_print_traceback
|
|
243
|
-
async def handle_new_potential_usb_cdc_ncm_interface_task(self, ip: str) -> None:
|
|
244
|
-
rsd = None
|
|
245
|
-
try:
|
|
246
|
-
answers = None
|
|
247
|
-
for i in range(REATTEMPT_COUNT):
|
|
248
|
-
answers = await browse(REMOTED_SERVICE_NAMES, [ip])
|
|
249
|
-
if answers:
|
|
250
|
-
break
|
|
251
|
-
logger.debug(f'No addresses found for: {ip}')
|
|
252
|
-
await asyncio.sleep(REATTEMPT_INTERVAL)
|
|
253
|
-
|
|
254
|
-
if not answers:
|
|
255
|
-
raise asyncio.CancelledError()
|
|
256
|
-
|
|
257
|
-
peer_address = answers[0].ips[0]
|
|
258
|
-
|
|
259
|
-
# establish an untrusted RSD handshake
|
|
260
|
-
rsd = RemoteServiceDiscoveryService((peer_address, RSD_PORT))
|
|
261
|
-
|
|
262
|
-
with stop_remoted():
|
|
263
|
-
try:
|
|
264
|
-
await rsd.connect()
|
|
265
|
-
except (ConnectionRefusedError, TimeoutError):
|
|
266
|
-
raise asyncio.CancelledError()
|
|
267
|
-
|
|
268
|
-
if (self.protocol == TunnelProtocol.QUIC) and (Version(rsd.product_version) < Version('17.0.0')):
|
|
269
|
-
await rsd.close()
|
|
270
|
-
raise asyncio.CancelledError()
|
|
271
|
-
|
|
272
|
-
await asyncio.create_task(
|
|
273
|
-
self.start_tunnel_task(ip, await create_core_device_tunnel_service_using_rsd(rsd)),
|
|
274
|
-
name=f'start-tunnel-task-usb-{ip}')
|
|
275
|
-
except asyncio.CancelledError:
|
|
276
|
-
pass
|
|
277
|
-
except PairingError as e:
|
|
278
|
-
logger.error(f'Failed to pair with {ip} with error: {e}')
|
|
279
|
-
except RuntimeError:
|
|
280
|
-
logger.debug(f'Got RuntimeError from: {asyncio.current_task().get_name()}')
|
|
281
|
-
except Exception:
|
|
282
|
-
logger.error(f'Error raised from: {asyncio.current_task().get_name()}: {traceback.format_exc()}')
|
|
283
|
-
finally:
|
|
284
|
-
if rsd is not None:
|
|
285
|
-
try:
|
|
286
|
-
await rsd.close()
|
|
287
|
-
except OSError:
|
|
288
|
-
pass
|
|
289
|
-
|
|
290
|
-
if ip in self.tunnel_tasks:
|
|
291
|
-
# in case the tunnel was removed just now
|
|
292
|
-
self.tunnel_tasks.pop(ip)
|
|
293
|
-
|
|
294
|
-
async def close(self) -> None:
|
|
295
|
-
""" close all tasks """
|
|
296
|
-
for task in self.tasks + [tunnel_task.task for tunnel_task in self.tunnel_tasks.values()]:
|
|
297
|
-
task.cancel()
|
|
298
|
-
with suppress(asyncio.CancelledError):
|
|
299
|
-
await task
|
|
300
|
-
|
|
301
|
-
def get_tunnels_ips(self) -> dict:
|
|
302
|
-
""" Retrieve the available tunnel tasks and format them as {UDID: [IP]} """
|
|
303
|
-
tunnels_ips = {}
|
|
304
|
-
for ip, active_tunnel in self.tunnel_tasks.items():
|
|
305
|
-
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
306
|
-
continue
|
|
307
|
-
if active_tunnel.udid not in tunnels_ips:
|
|
308
|
-
tunnels_ips[active_tunnel.udid] = [ip]
|
|
309
|
-
else:
|
|
310
|
-
tunnels_ips[active_tunnel.udid].append(ip)
|
|
311
|
-
return tunnels_ips
|
|
312
|
-
|
|
313
|
-
def cancel(self, udid: str) -> None:
|
|
314
|
-
""" Cancel active tunnels """
|
|
315
|
-
for tunnel_ip in self.get_tunnels_ips().get(udid, []):
|
|
316
|
-
self.tunnel_tasks.pop(tunnel_ip).task.cancel()
|
|
317
|
-
logger.info(f'canceling tunnel {tunnel_ip}')
|
|
318
|
-
|
|
319
|
-
def clear(self) -> None:
|
|
320
|
-
""" Clear active tunnels """
|
|
321
|
-
for udid, tunnel in self.tunnel_tasks.items():
|
|
322
|
-
logger.info(f'Removing tunnel {tunnel}')
|
|
323
|
-
tunnel.task.cancel()
|
|
324
|
-
self.tunnel_tasks = {}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
class TunneldRunner:
|
|
328
|
-
""" TunneldRunner orchestrate between the webserver and TunneldCore """
|
|
329
|
-
|
|
330
|
-
@classmethod
|
|
331
|
-
def create(cls, host: str, port: int, protocol: TunnelProtocol = TunnelProtocol.QUIC, usb_monitor: bool = True,
|
|
332
|
-
wifi_monitor: bool = True, usbmux_monitor: bool = True, mobdev2_monitor: bool = True) -> None:
|
|
333
|
-
cls(host, port, protocol=protocol, usb_monitor=usb_monitor, wifi_monitor=wifi_monitor,
|
|
334
|
-
usbmux_monitor=usbmux_monitor, mobdev2_monitor=mobdev2_monitor)._run_app()
|
|
335
|
-
|
|
336
|
-
def __init__(self, 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):
|
|
338
|
-
@asynccontextmanager
|
|
339
|
-
async def lifespan(app: FastAPI):
|
|
340
|
-
logging.getLogger('zeroconf').disabled = True
|
|
341
|
-
self._tunneld_core.start()
|
|
342
|
-
yield
|
|
343
|
-
logger.info('Closing tunneld tasks...')
|
|
344
|
-
await self._tunneld_core.close()
|
|
345
|
-
|
|
346
|
-
self.host = host
|
|
347
|
-
self.port = port
|
|
348
|
-
self.protocol = protocol
|
|
349
|
-
self._app = FastAPI(lifespan=lifespan)
|
|
350
|
-
self._tunneld_core = TunneldCore(protocol=protocol, wifi_monitor=wifi_monitor, usb_monitor=usb_monitor,
|
|
351
|
-
usbmux_monitor=usbmux_monitor, mobdev2_monitor=mobdev2_monitor)
|
|
352
|
-
|
|
353
|
-
@self._app.get('/')
|
|
354
|
-
async def list_tunnels() -> dict[str, list[dict]]:
|
|
355
|
-
""" Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS} """
|
|
356
|
-
tunnels = {}
|
|
357
|
-
for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items():
|
|
358
|
-
if (active_tunnel.udid is None) or (active_tunnel.tunnel is None):
|
|
359
|
-
continue
|
|
360
|
-
if active_tunnel.udid not in tunnels:
|
|
361
|
-
tunnels[active_tunnel.udid] = []
|
|
362
|
-
tunnels[active_tunnel.udid].append({
|
|
363
|
-
'tunnel-address': active_tunnel.tunnel.address,
|
|
364
|
-
'tunnel-port': active_tunnel.tunnel.port,
|
|
365
|
-
'interface': ip})
|
|
366
|
-
return tunnels
|
|
367
|
-
|
|
368
|
-
@self._app.get('/shutdown')
|
|
369
|
-
async def shutdown() -> fastapi.Response:
|
|
370
|
-
""" Shutdown Tunneld """
|
|
371
|
-
os.kill(os.getpid(), signal.SIGINT)
|
|
372
|
-
data = {'operation': 'shutdown', 'data': True, 'message': 'Server shutting down...'}
|
|
373
|
-
return generate_http_response(data)
|
|
374
|
-
|
|
375
|
-
@self._app.get('/clear_tunnels')
|
|
376
|
-
async def clear_tunnels() -> fastapi.Response:
|
|
377
|
-
self._tunneld_core.clear()
|
|
378
|
-
data = {'operation': 'clear_tunnels', 'data': True, 'message': 'Cleared tunnels...'}
|
|
379
|
-
return generate_http_response(data)
|
|
380
|
-
|
|
381
|
-
@self._app.get('/cancel')
|
|
382
|
-
async def cancel_tunnel(udid: str) -> fastapi.Response:
|
|
383
|
-
self._tunneld_core.cancel(udid=udid)
|
|
384
|
-
data = {'operation': 'cancel', 'udid': udid, 'data': True, 'message': f'tunnel {udid} Canceled ...'}
|
|
385
|
-
return generate_http_response(data)
|
|
386
|
-
|
|
387
|
-
@self._app.get('/hello')
|
|
388
|
-
async def hello() -> fastapi.Response:
|
|
389
|
-
data = {'message': 'Hello, I\'m alive'}
|
|
390
|
-
return generate_http_response(data)
|
|
391
|
-
|
|
392
|
-
def generate_http_response(
|
|
393
|
-
data: dict, status_code: int = 200, media_type: str = "application/json") -> fastapi.Response:
|
|
394
|
-
return fastapi.Response(
|
|
395
|
-
status_code=status_code,
|
|
396
|
-
media_type=media_type,
|
|
397
|
-
content=json.dumps(data))
|
|
398
|
-
|
|
399
|
-
@self._app.get('/start-tunnel')
|
|
400
|
-
async def start_tunnel(
|
|
401
|
-
udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None) -> fastapi.Response:
|
|
402
|
-
udid_tunnels = [t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid]
|
|
403
|
-
if len(udid_tunnels) > 0:
|
|
404
|
-
data = {
|
|
405
|
-
'interface': udid_tunnels[0].interface,
|
|
406
|
-
'port': udid_tunnels[0].port,
|
|
407
|
-
'address': udid_tunnels[0].address
|
|
408
|
-
}
|
|
409
|
-
return generate_http_response(data)
|
|
410
|
-
|
|
411
|
-
queue = asyncio.Queue()
|
|
412
|
-
created_task = False
|
|
413
|
-
|
|
414
|
-
try:
|
|
415
|
-
if not created_task and connection_type in ('usbmux', None):
|
|
416
|
-
task_identifier = f'usbmux-{udid}'
|
|
417
|
-
try:
|
|
418
|
-
service = CoreDeviceTunnelProxy(create_using_usbmux(udid))
|
|
419
|
-
task = asyncio.create_task(
|
|
420
|
-
self._tunneld_core.start_tunnel_task(task_identifier, service, protocol=TunnelProtocol.TCP,
|
|
421
|
-
queue=queue),
|
|
422
|
-
name=f'start-tunnel-task-{task_identifier}')
|
|
423
|
-
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
|
|
424
|
-
created_task = True
|
|
425
|
-
except (ConnectionFailedError, InvalidServiceError, MuxException):
|
|
426
|
-
pass
|
|
427
|
-
if connection_type in ('usb', None):
|
|
428
|
-
for rsd in await get_rsds(udid=udid):
|
|
429
|
-
rsd_ip = rsd.service.address[0]
|
|
430
|
-
if ip is not None and rsd_ip != ip:
|
|
431
|
-
await rsd.close()
|
|
432
|
-
continue
|
|
433
|
-
task = asyncio.create_task(
|
|
434
|
-
self._tunneld_core.start_tunnel_task(rsd_ip,
|
|
435
|
-
await create_core_device_tunnel_service_using_rsd(rsd),
|
|
436
|
-
queue=queue),
|
|
437
|
-
name=f'start-tunnel-usb-{rsd_ip}')
|
|
438
|
-
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
|
|
439
|
-
created_task = True
|
|
440
|
-
if not created_task and connection_type in ('wifi', None):
|
|
441
|
-
for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
|
|
442
|
-
remotepairing_ip = remotepairing.hostname
|
|
443
|
-
if ip is not None and remotepairing_ip != ip:
|
|
444
|
-
await remotepairing.close()
|
|
445
|
-
continue
|
|
446
|
-
task = asyncio.create_task(
|
|
447
|
-
self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
|
|
448
|
-
name=f'start-tunnel-wifi-{remotepairing_ip}')
|
|
449
|
-
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
|
|
450
|
-
task=task, udid=remotepairing.remote_identifier)
|
|
451
|
-
created_task = True
|
|
452
|
-
except Exception as e:
|
|
453
|
-
return fastapi.Response(status_code=501,
|
|
454
|
-
content=json.dumps({'error': {
|
|
455
|
-
'exception': e.__class__.__name__,
|
|
456
|
-
'traceback': traceback.format_exc(),
|
|
457
|
-
}}))
|
|
458
|
-
|
|
459
|
-
if not created_task:
|
|
460
|
-
return fastapi.Response(status_code=501, content=json.dumps({'error': 'task not not created'}))
|
|
461
|
-
|
|
462
|
-
tunnel: Optional[TunnelResult] = await queue.get()
|
|
463
|
-
if tunnel is not None:
|
|
464
|
-
data = {
|
|
465
|
-
'interface': tunnel.interface,
|
|
466
|
-
'port': tunnel.port,
|
|
467
|
-
'address': tunnel.address
|
|
468
|
-
}
|
|
469
|
-
return generate_http_response(data)
|
|
470
|
-
else:
|
|
471
|
-
return fastapi.Response(status_code=404,
|
|
472
|
-
content=json.dumps({'error': 'something went wrong during tunnel creation'}))
|
|
473
|
-
|
|
474
|
-
def _run_app(self) -> None:
|
|
475
|
-
uvicorn.run(self._app, host=self.host, port=self.port, loop='asyncio')
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
async def async_get_tunneld_devices(tunneld_address: tuple[str, int] = TUNNELD_DEFAULT_ADDRESS) \
|
|
479
|
-
-> list[RemoteServiceDiscoveryService]:
|
|
480
|
-
tunnels = _list_tunnels(tunneld_address)
|
|
481
|
-
return await _create_rsds_from_tunnels(tunnels)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
def get_tunneld_devices(tunneld_address: tuple[str, int] = TUNNELD_DEFAULT_ADDRESS) \
|
|
485
|
-
-> list[RemoteServiceDiscoveryService]:
|
|
486
|
-
return get_asyncio_loop().run_until_complete(async_get_tunneld_devices(tunneld_address))
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
async def async_get_tunneld_device_by_udid(udid: str, tunneld_address: tuple[str, int] = TUNNELD_DEFAULT_ADDRESS) \
|
|
490
|
-
-> Optional[RemoteServiceDiscoveryService]:
|
|
491
|
-
tunnels = _list_tunnels(tunneld_address)
|
|
492
|
-
if udid not in tunnels:
|
|
493
|
-
return None
|
|
494
|
-
rsds = await _create_rsds_from_tunnels({udid: tunnels[udid]})
|
|
495
|
-
return rsds[0]
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
def get_tunneld_device_by_udid(udid: str, tunneld_address: tuple[str, int] = TUNNELD_DEFAULT_ADDRESS) \
|
|
499
|
-
-> Optional[RemoteServiceDiscoveryService]:
|
|
500
|
-
return get_asyncio_loop().run_until_complete(async_get_tunneld_device_by_udid(udid, tunneld_address))
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
def _list_tunnels(tunneld_address: tuple[str, int] = TUNNELD_DEFAULT_ADDRESS) -> dict[str, list[dict]]:
|
|
504
|
-
try:
|
|
505
|
-
# Get the list of tunnels from the specified address
|
|
506
|
-
resp = requests.get(f'http://{tunneld_address[0]}:{tunneld_address[1]}')
|
|
507
|
-
tunnels = resp.json()
|
|
508
|
-
except requests.exceptions.ConnectionError:
|
|
509
|
-
raise TunneldConnectionError()
|
|
510
|
-
return tunnels
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
async def _create_rsds_from_tunnels(tunnels: dict[str, list[dict]]) -> list[RemoteServiceDiscoveryService]:
|
|
514
|
-
rsds = []
|
|
515
|
-
for udid, details in tunnels.items():
|
|
516
|
-
for tunnel_details in details:
|
|
517
|
-
rsd = RemoteServiceDiscoveryService((tunnel_details['tunnel-address'], tunnel_details['tunnel-port']),
|
|
518
|
-
name=tunnel_details['interface'])
|
|
519
|
-
try:
|
|
520
|
-
await rsd.connect()
|
|
521
|
-
rsds.append(rsd)
|
|
522
|
-
except (TimeoutError, ConnectionError):
|
|
523
|
-
continue
|
|
524
|
-
return rsds
|