diff --git a/.gitignore b/.gitignore index 7a605bc..02378cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ __pycache__/ +.venv*/ diff --git a/HNroute.txt b/HNroute.txt index 59d3883..f8df75c 100644 --- a/HNroute.txt +++ b/HNroute.txt @@ -1 +1 @@ -{"lng":"120.7335575167566","lat":"30.52802386594508"},{"lng":"120.7335664998117","lat":"30.527949984828833"},{"lng":"120.73357997439435","lat":"30.527887769107984"},{"lng":"120.73361590661474","lat":"30.527825553346897"},{"lng":"120.7336653134178","lat":"30.527771114522935"},{"lng":"120.73372370327594","lat":"30.527720564158813"},{"lng":"120.73379107618919","lat":"30.527689456229226"},{"lng":"120.73387641521265","lat":"30.527662236782586"},{"lng":"120.73397073729119","lat":"30.52765057130309"},{"lng":"120.73407404242484","lat":"30.527646682809618"},{"lng":"120.73416387297584","lat":"30.527677790753035"},{"lng":"120.73428514421968","lat":"30.52773611811983"},{"lng":"120.73436150018803","lat":"30.52781388788721"},{"lng":"120.73441539851864","lat":"30.527922765455916"},{"lng":"120.73444234768392","lat":"30.52803941985705"},{"lng":"120.73445133073903","lat":"30.528156074116726"},{"lng":"120.73445582226658","lat":"30.5283038359759"},{"lng":"120.73446031379413","lat":"30.528420489914918"},{"lng":"120.73446031379413","lat":"30.52852158988096"},{"lng":"120.73446480532168","lat":"30.528618801286576"},{"lng":"120.73446480532168","lat":"30.528719901044198"},{"lng":"120.73445582226658","lat":"30.52881322380307"},{"lng":"120.73442887310128","lat":"30.528902658028688"},{"lng":"120.73439294088088","lat":"30.52898820373192"},{"lng":"120.73434353407784","lat":"30.529034864992525"},{"lng":"120.73427616116459","lat":"30.529081526230495"},{"lng":"120.73420429672379","lat":"30.52912429901208"},{"lng":"120.73414590686563","lat":"30.529143741179244"},{"lng":"120.73408302547993","lat":"30.52915929491014"},{"lng":"120.73399319492894","lat":"30.52915540647765"},{"lng":"120.73390785590549","lat":"30.52915540647765"},{"lng":"120.73383599146469","lat":"30.529135964312847"},{"lng":"120.733773110079","lat":"30.529108745275526"},{"lng":"120.73372370327594","lat":"30.529065972487018"},{"lng":"120.73365633036269","lat":"30.529003757487974"},{"lng":"120.73361590661474","lat":"30.528925988682573"},{"lng":"120.7335754828668","lat":"30.528855996703964"},{"lng":"120.7335664998117","lat":"30.528782116226836"},{"lng":"120.7335575167566","lat":"30.528719901044198"},{"lng":"120.7335575167566","lat":"30.52863435510235"},{"lng":"120.7335575167566","lat":"30.528541032169947"},{"lng":"120.7335575167566","lat":"30.528451597608097"},{"lng":"120.7335485337015","lat":"30.528350497568482"},{"lng":"120.73355302522906","lat":"30.528261062829475"},{"lng":"120.7335575167566","lat":"30.52818329342363"},{"lng":"120.73356200828415","lat":"30.528128854802127"} \ No newline at end of file +{"lng":"120.08826462728848","lat":"30.311727137354364"},{"lng":"120.08818377979257","lat":"30.31171544577161"},{"lng":"120.08809844076913","lat":"30.311656987836667"},{"lng":"120.08805801702118","lat":"30.31163360465283"},{"lng":"120.08804005091098","lat":"30.31161022146335"},{"lng":"120.08801310174569","lat":"30.311579043868623"},{"lng":"120.08799513563548","lat":"30.311528380255805"},{"lng":"120.08797716952529","lat":"30.311469922208154"},{"lng":"120.08796369494263","lat":"30.31143874456834"},{"lng":"120.08796818647018","lat":"30.311384183674555"},{"lng":"120.08796818647018","lat":"30.311345211588783"},{"lng":"120.08795920341508","lat":"30.311298445065198"},{"lng":"120.08795920341508","lat":"30.31125167851906"},{"lng":"120.08796818647018","lat":"30.31117373422542"},{"lng":"120.08798166105284","lat":"30.311138659272842"},{"lng":"120.08799064410793","lat":"30.311091892649703"},{"lng":"120.08799064410793","lat":"30.31104902322535"},{"lng":"120.08800411869058","lat":"30.311017845450287"},{"lng":"120.08801310174569","lat":"30.310978873217366"},{"lng":"120.08801310174569","lat":"30.31093600374308"},{"lng":"120.08803106785588","lat":"30.310900928704577"},{"lng":"120.08803106785588","lat":"30.31084636750836"},{"lng":"120.08804454243852","lat":"30.31081129243745"},{"lng":"120.08804903396609","lat":"30.310760628423754"},{"lng":"120.08806250854873","lat":"30.31070216991354"},{"lng":"120.08807149160383","lat":"30.310659300316996"},{"lng":"120.08808047465892","lat":"30.310604738984914"},{"lng":"120.08809394924158","lat":"30.310581355547484"},{"lng":"120.08809844076913","lat":"30.31054238313925"},{"lng":"120.08811191535177","lat":"30.31048002725351"},{"lng":"120.08811191535177","lat":"30.31042936306685"},{"lng":"120.08813437298953","lat":"30.310386493349778"},{"lng":"120.08814784757217","lat":"30.31033972636409"},{"lng":"120.08817030520993","lat":"30.31028516485231"},{"lng":"120.08819725437523","lat":"30.310253986831796"},{"lng":"120.08824666117827","lat":"30.31023450056389"},{"lng":"120.08828259339867","lat":"30.310195528016337"},{"lng":"120.08832301714662","lat":"30.310179938992935"},{"lng":"120.08840835617008","lat":"30.310164349967025"},{"lng":"120.08847572908333","lat":"30.310172144480294"},{"lng":"120.08852962741392","lat":"30.310172144480294"},{"lng":"120.08859250879962","lat":"30.310183836249024"},{"lng":"120.08866437324042","lat":"30.310218911546745"},{"lng":"120.08874522073631","lat":"30.310253986831796"},{"lng":"120.08879462753936","lat":"30.3103202401134"},{"lng":"120.08883954281487","lat":"30.310386493349778"},{"lng":"120.08886649198016","lat":"30.31050341071536"},{"lng":"120.08883505128732","lat":"30.310620327940075"},{"lng":"120.08882606823221","lat":"30.31073724502389"},{"lng":"120.08878564448426","lat":"30.31085026473768"},{"lng":"120.08875420379141","lat":"30.310982770441356"},{"lng":"120.08873174615367","lat":"30.311119173182615"},{"lng":"120.08869581393327","lat":"30.31123608966534"},{"lng":"120.08868233935061","lat":"30.311364697633625"},{"lng":"120.08865089865776","lat":"30.311501099837947"},{"lng":"120.08861496643736","lat":"30.311625810256956"},{"lng":"120.08857454268941","lat":"30.311719342966015"},{"lng":"120.08848920366597","lat":"30.311758314901514"},{"lng":"120.08838140700477","lat":"30.311770006479115"} \ No newline at end of file diff --git a/driver/connect.py b/driver/connect.py index b4be45d..1260419 100644 --- a/driver/connect.py +++ b/driver/connect.py @@ -1,55 +1,98 @@ -import logging +import asyncio +import inspect import multiprocessing from pymobiledevice3.lockdown import create_using_usbmux, LockdownClient -from pymobiledevice3.cli.remote import install_driver_if_required -from pymobiledevice3.cli.remote import select_device, RemoteServiceDiscoveryService -from pymobiledevice3.cli.remote import start_tunnel -from pymobiledevice3.cli.remote import verify_tunnel_imports - from pymobiledevice3.services.amfi import AmfiService from pymobiledevice3.exceptions import NoDeviceConnectedError +from util.pymobiledevice3_compat import ( + RemoteServiceDiscoveryService, + get_tunnel_service, + install_driver_if_required, + start_tunnel as start_tunnel_session, + verify_tunnel_imports, +) + + +def _wait_if_needed(result): + if inspect.isawaitable(result): + return asyncio.run(result) + return result + + +def _create_using_usbmux(): + return _wait_if_needed(create_using_usbmux()) + + +async def _create_using_usbmux_async(): + lockdown = create_using_usbmux() + if inspect.isawaitable(lockdown): + lockdown = await lockdown + return lockdown + + def get_usbmux_lockdownclient(): while True: try: - lockdown = create_using_usbmux() + lockdown = _create_using_usbmux() except NoDeviceConnectedError: print("请连接设备后按回车...") input() else: break while True: - lockdown = create_using_usbmux() + lockdown = _create_using_usbmux() if lockdown.all_values.get("PasswordProtected"): print("请解锁设备后按回车...") input() else: break - return create_using_usbmux() + return lockdown def get_version(lockdown: LockdownClient): return lockdown.all_values.get("ProductVersion") def get_developer_mode_status(lockdown: LockdownClient): - return lockdown.developer_mode_status + if hasattr(lockdown, "developer_mode_status"): + return lockdown.developer_mode_status + + async def get_status(): + fresh_lockdown = await _create_using_usbmux_async() + return await fresh_lockdown.get_developer_mode_status() + + return asyncio.run(get_status()) def reveal_developer_mode(lockdown: LockdownClient): - AmfiService(lockdown).create_amfi_show_override_path_file() + service = AmfiService(lockdown) + if hasattr(service, "create_amfi_show_override_path_file"): + return _wait_if_needed(service.create_amfi_show_override_path_file()) + + async def reveal(): + fresh_lockdown = await _create_using_usbmux_async() + await AmfiService(fresh_lockdown).reveal_developer_mode_option_in_ui() + + return asyncio.run(reveal()) def enable_developer_mode(lockdown: LockdownClient): - AmfiService(lockdown).enable_developer_mode() + service = AmfiService(lockdown) + if not inspect.iscoroutinefunction(service.enable_developer_mode): + return service.enable_developer_mode() + + async def enable(): + fresh_lockdown = await _create_using_usbmux_async() + await AmfiService(fresh_lockdown).enable_developer_mode() + + return asyncio.run(enable()) def get_serverrsd(): install_driver_if_required() if not verify_tunnel_imports(): exit(1) - return select_device(None) + return get_tunnel_service() async def tunnel(rsd: RemoteServiceDiscoveryService, queue: multiprocessing.Queue): - async with start_tunnel(rsd, None) as tunnel_result: - queue.put((tunnel_result.address, tunnel_result.port)) - await tunnel_result.client.wait_closed() + await start_tunnel_session(rsd, queue) diff --git a/driver/location.py b/driver/location.py index 6a602fc..8d0de44 100644 --- a/driver/location.py +++ b/driver/location.py @@ -1,7 +1,6 @@ -from pymobiledevice3.cli.developer import LocationSimulation +def set_location(location_client, lat: float, lng: float): + location_client.set(lat, lng) -def set_location(dvt, lat: float, lng: float): - LocationSimulation(dvt).set(lat, lng) -def clear_location(dvt): - LocationSimulation(dvt).clear() +def clear_location(location_client): + location_client.clear() diff --git a/init/tunnel.py b/init/tunnel.py index 5119d90..eb0250a 100644 --- a/init/tunnel.py +++ b/init/tunnel.py @@ -1,11 +1,16 @@ import asyncio import multiprocessing +import traceback from driver import connect def tunnel_proc(queue: multiprocessing.Queue): - server_rsd = connect.get_serverrsd() - asyncio.run(connect.tunnel(server_rsd, queue)) + try: + server_rsd = connect.get_serverrsd() + asyncio.run(connect.tunnel(server_rsd, queue)) + except Exception: + queue.put(("error", traceback.format_exc())) + raise def tunnel(): @@ -16,5 +21,8 @@ def tunnel(): # get the address and port of the tunnel address, port = queue.get() + if address == "error": + process.join(timeout=1) + raise RuntimeError(f"Failed to start tunnel:\n{port}") - return process, address, port \ No newline at end of file + return process, address, port diff --git a/main.py b/main.py index cb273db..804c37a 100644 --- a/main.py +++ b/main.py @@ -3,11 +3,6 @@ import coloredlogs import os -from driver import location - -from pymobiledevice3.cli.remote import RemoteServiceDiscoveryService -from pymobiledevice3.cli.developer import DvtSecureSocketProxyService - from init import init from init import tunnel from init import route @@ -15,6 +10,10 @@ import run import config +from util.pymobiledevice3_compat import ( + open_location_session, + open_remote_service_discovery, +) debug = os.environ.get("DEBUG", False) @@ -60,21 +59,25 @@ def main(): logger.info(f"got route from {config.config.routeConfig}") - with RemoteServiceDiscoveryService((address, port)) as rsd: - with DvtSecureSocketProxyService(rsd) as dvt: + with open_remote_service_discovery(address, port) as rsd: + with open_location_session(rsd) as location_client: try: print(f"已开始模拟跑步,速度大约为 {config.config.v} m/s") print("会无限循环,按 Ctrl+C 退出") print("请勿直接关闭窗口,否则无法还原正常定位") - run.run(dvt, loc, config.config.v) + run.run(location_client, loc, config.config.v) except KeyboardInterrupt: logger.debug("get KeyboardInterrupt (inner)") logger.debug(f"Is process alive? {process.is_alive()}") finally: logger.debug(f"Is process alive? {process.is_alive()}") logger.debug("Start to clear location") - location.clear_location(dvt) - logger.info("Location cleared") + try: + location_client.clear() + except Exception: + logger.exception("Failed to clear location") + else: + logger.info("Location cleared") except KeyboardInterrupt: @@ -90,4 +93,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/requirements.txt b/requirements.txt index a35c754..57641f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -pymobiledevice3==2.46.1 +# Recommended on Windows: Python 3.11/3.12. Python 3.13 may require MSVC Build Tools for lzfse. +pymobiledevice3>=9.20.0,<10 +qh3>=1.9.2,<2 PyYAML==6.0.1 geopy==2.4.1 diff --git a/run.py b/run.py index 917299e..55dec95 100644 --- a/run.py +++ b/run.py @@ -134,22 +134,34 @@ def fixLockT(loc: list, v, dt): t += dt return fixedLoc -def run1(dvt, loc: list, v, dt=0.2): +def run1(location_client, loc: list, v, dt=0.5): fixedLoc = fixLockT(loc, v, dt) nList = (5, 6, 7, 8, 9) n = nList[random.randint(0, len(nList)-1)] fixedLoc = randLoc(fixedLoc, n=n) # a path will be divided into n parts for random route clock = time.time() + failures = 0 for i in fixedLoc: # utils.setLoc(bd09Towgs84(i)) - location.set_location(dvt, **bd09Towgs84(i)) - while time.time()-clock < dt: - pass + try: + location.set_location(location_client, **bd09Towgs84(i)) + failures = 0 + except Exception as error: + failures += 1 + print(f"定位下发失败,继续尝试 ({failures}/10): {error}") + if failures >= 10: + raise + remaining = dt - (time.time() - clock) + if remaining > 0: + time.sleep(remaining) clock = time.time() -def run(dvt, loc: list, v, d=15): +def run(location_client, loc: list, v, d=15): random.seed(time.time()) while True: vRand = 1000/(1000/v-(2*random.random()-1)*d) - run1(dvt, loc, vRand) - print("跑完一圈了") \ No newline at end of file + run1(location_client, loc, vRand) + refresh_session = getattr(location_client, "refresh_session", None) + if refresh_session is not None: + refresh_session() + print("跑完一圈了") diff --git a/util/pymobiledevice3_compat.py b/util/pymobiledevice3_compat.py new file mode 100644 index 0000000..1d3c23d --- /dev/null +++ b/util/pymobiledevice3_compat.py @@ -0,0 +1,359 @@ +import asyncio +import inspect +import logging +import os +from contextlib import AsyncExitStack, contextmanager, suppress +from importlib import import_module + +from pymobiledevice3.exceptions import NoDeviceConnectedError +from pymobiledevice3.lockdown import create_using_usbmux + + +def _import_attr(module_name: str, attr_name: str): + module = import_module(module_name) + return getattr(module, attr_name) + + +def _import_first(*candidates): + last_error = None + for module_name, attr_name in candidates: + try: + return _import_attr(module_name, attr_name) + except (ImportError, AttributeError) as error: + last_error = error + if last_error is not None: + raise last_error + raise ImportError("No import candidates were provided") + + +RemoteServiceDiscoveryService = _import_first( + ("pymobiledevice3.remote.remote_service_discovery", "RemoteServiceDiscoveryService"), + ("pymobiledevice3.cli.remote", "RemoteServiceDiscoveryService"), +) + +LocationSimulation = _import_first( + ("pymobiledevice3.services.dvt.instruments.location_simulation", "LocationSimulation"), + ("pymobiledevice3.cli.developer", "LocationSimulation"), +) + +_start_tunnel = _import_first( + ("pymobiledevice3.remote.module_imports", "start_tunnel"), + ("pymobiledevice3.cli.remote", "start_tunnel"), +) + +try: + _verify_tunnel_imports = _import_first( + ("pymobiledevice3.remote.module_imports", "verify_tunnel_imports"), + ("pymobiledevice3.cli.remote", "verify_tunnel_imports"), + ) +except (ImportError, AttributeError): + _verify_tunnel_imports = None + +try: + _install_driver_if_required = _import_first( + ("pymobiledevice3.cli.remote", "install_driver_if_required"), + ) +except (ImportError, AttributeError): + _install_driver_if_required = None + +try: + _select_device = _import_first( + ("pymobiledevice3.cli.remote", "select_device"), + ) +except (ImportError, AttributeError): + _select_device = None + +try: + _prompt_device_list = _import_first( + ("pymobiledevice3.cli.cli_common", "prompt_device_list"), + ) +except (ImportError, AttributeError): + _prompt_device_list = None + +try: + _get_core_device_tunnel_services = _import_first( + ("pymobiledevice3.remote.tunnel_service", "get_core_device_tunnel_services"), + ) +except (ImportError, AttributeError): + _get_core_device_tunnel_services = None + +try: + CoreDeviceTunnelProxy = _import_first( + ("pymobiledevice3.remote.tunnel_service", "CoreDeviceTunnelProxy"), + ) +except (ImportError, AttributeError): + CoreDeviceTunnelProxy = None + +try: + TunnelProtocol = _import_first( + ("pymobiledevice3.remote.common", "TunnelProtocol"), + ) +except (ImportError, AttributeError): + TunnelProtocol = None + +try: + DvtProvider = _import_first( + ("pymobiledevice3.services.dvt.instruments.dvt_provider", "DvtProvider"), + ) +except (ImportError, AttributeError): + DvtProvider = None + +try: + DvtSecureSocketProxyService = _import_first( + ("pymobiledevice3.cli.developer", "DvtSecureSocketProxyService"), + ) +except (ImportError, AttributeError): + DvtSecureSocketProxyService = None + +try: + DtSimulateLocation = _import_first( + ("pymobiledevice3.services.simulate_location", "DtSimulateLocation"), + ) +except (ImportError, AttributeError): + DtSimulateLocation = None + +try: + ConnectionTerminatedError = _import_first( + ("pymobiledevice3.exceptions", "ConnectionTerminatedError"), + ) +except (ImportError, AttributeError): + ConnectionTerminatedError = None + +_TRANSIENT_LOCATION_ERRORS = tuple( + error + for error in ( + ConnectionTerminatedError, + ConnectionResetError, + BrokenPipeError, + OSError, + ) + if error is not None +) + +_LOCATION_RETRIES = int(os.environ.get("LOCATION_RETRIES", "5")) +_DVT_SESSION_CALLS = int(os.environ.get("DVT_SESSION_CALLS", "250")) +logger = logging.getLogger(__name__) + + +class UsbTunnelService: + pass + + +def install_driver_if_required(): + if _install_driver_if_required is not None: + _install_driver_if_required() + + +def verify_tunnel_imports() -> bool: + if _verify_tunnel_imports is None: + return True + return bool(_verify_tunnel_imports()) + + +def _preferred_protocol(): + if TunnelProtocol is None: + return None + return getattr(TunnelProtocol, "TCP", None) + + +def _wait_if_needed(result): + if inspect.isawaitable(result): + return asyncio.run(result) + return result + + +async def _await_if_needed(result): + if inspect.isawaitable(result): + return await result + return result + + +async def _select_tunnel_service(): + if CoreDeviceTunnelProxy is not None: + return UsbTunnelService() + + if _get_core_device_tunnel_services is None: + if _select_device is not None: + return _select_device(None) + raise ImportError("No compatible tunnel discovery API was found") + + services = [] + for _ in range(5): + services = await _get_core_device_tunnel_services() + if services: + break + await asyncio.sleep(1) + + if not services: + raise NoDeviceConnectedError() + + if len(services) == 1 or _prompt_device_list is None: + return services[0] + return _prompt_device_list(services) + + +def get_tunnel_service(): + return asyncio.run(_select_tunnel_service()) + + +def _build_tunnel_kwargs(): + kwargs = {} + parameters = inspect.signature(_start_tunnel).parameters + if "secrets" in parameters: + kwargs["secrets"] = None + protocol = _preferred_protocol() + if protocol is not None and "protocol" in parameters: + kwargs["protocol"] = protocol + return kwargs + + +async def start_tunnel(service, queue): + if isinstance(service, UsbTunnelService): + lockdown = await _await_if_needed(create_using_usbmux()) + service = await CoreDeviceTunnelProxy.create(lockdown) + + async with _start_tunnel(service, **_build_tunnel_kwargs()) as tunnel_result: + queue.put((tunnel_result.address, tunnel_result.port)) + await tunnel_result.client.wait_closed() + + +class _LocationClient: + def __init__(self, service, loop=None): + self._service = service + self._loop = loop + + def set(self, latitude: float, longitude: float): + result = self._service.set(latitude, longitude) + if inspect.isawaitable(result) and self._loop is not None: + return self._loop.run_until_complete(result) + return _wait_if_needed(result) + + def clear(self): + result = self._service.clear() + if inspect.isawaitable(result) and self._loop is not None: + return self._loop.run_until_complete(result) + return _wait_if_needed(result) + + +class _DvtLocationClient: + def __init__(self, service_provider, loop): + self._service_provider = service_provider + self._loop = loop + self._exit_stack = None + self._location = None + self._calls_since_open = 0 + + async def _open(self): + self._exit_stack = AsyncExitStack() + dvt = await self._exit_stack.enter_async_context(DvtProvider(self._service_provider)) + self._location = await self._exit_stack.enter_async_context(LocationSimulation(dvt)) + self._calls_since_open = 0 + + def open(self): + self._loop.run_until_complete(self._open()) + + async def _reopen(self): + await self._close() + await self._open() + + async def _close(self): + if self._exit_stack is not None: + with suppress(Exception): + await self._exit_stack.aclose() + self._exit_stack = None + self._location = None + + async def _refresh_if_needed(self): + if self._calls_since_open >= _DVT_SESSION_CALLS: + logger.info("Refreshing DVT location session after %s updates", self._calls_since_open) + await self._reopen() + + async def _run_with_reconnect(self, operation): + last_error = None + for attempt in range(1, _LOCATION_RETRIES + 1): + try: + await self._refresh_if_needed() + result = await operation() + self._calls_since_open += 1 + return result + except Exception as error: + last_error = error + logger.warning( + "Location channel failed, reconnecting (%s/%s): %r", + attempt, + _LOCATION_RETRIES, + error, + ) + await asyncio.sleep(min(0.2 * attempt, 1.0)) + await self._reopen() + raise last_error + + def set(self, latitude: float, longitude: float): + async def set_location(): + await self._location.set(latitude, longitude) + + self._loop.run_until_complete(self._run_with_reconnect(set_location)) + + def clear(self): + async def clear_location(): + await self._location.clear() + + self._loop.run_until_complete(self._run_with_reconnect(clear_location)) + + def close(self): + self._loop.run_until_complete(self._close()) + + def refresh_session(self): + self._loop.run_until_complete(self._reopen()) + + +class _LegacyLocationClient: + def __init__(self, dvt): + self._dvt = dvt + + def set(self, latitude: float, longitude: float): + return _wait_if_needed(LocationSimulation(self._dvt).set(latitude, longitude)) + + def clear(self): + return _wait_if_needed(LocationSimulation(self._dvt).clear()) + + +@contextmanager +def open_remote_service_discovery(address, port): + rsd = RemoteServiceDiscoveryService((address, port)) + if hasattr(rsd, "__enter__"): + with rsd as connected_rsd: + yield connected_rsd + return + + loop = asyncio.new_event_loop() + rsd._compat_loop = loop + try: + loop.run_until_complete(rsd.connect()) + yield rsd + finally: + loop.run_until_complete(rsd.close()) + loop.close() + + +@contextmanager +def open_location_session(service_provider): + loop = getattr(service_provider, "_compat_loop", None) + if DvtProvider is not None and loop is not None: + client = _DvtLocationClient(service_provider, loop) + client.open() + try: + yield client + finally: + client.close() + return + + if DtSimulateLocation is not None: + yield _LocationClient(DtSimulateLocation(service_provider), loop=loop) + return + + if DvtSecureSocketProxyService is None: + raise ImportError("No compatible location simulation API was found") + + with DvtSecureSocketProxyService(service_provider) as dvt: + yield _LegacyLocationClient(dvt)