From b5ebedb2b93daf9d17da9e416c1c15e881f4427f Mon Sep 17 00:00:00 2001 From: William Bruno Date: Fri, 13 Mar 2026 07:09:11 -0400 Subject: [PATCH] merge # Conflicts: # server.py --- .gitignore | 2 + server.py | 296 +++++++++++++++-------------------------------------- 2 files changed, 82 insertions(+), 216 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d36675 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/* +*.pyc diff --git a/server.py b/server.py index 75ba07c..42c905b 100644 --- a/server.py +++ b/server.py @@ -65,9 +65,7 @@ from pymobiledevice3.remote.tunnel_service import ( ) from pymobiledevice3.remote.utils import get_rsds, stop_remoted from pymobiledevice3.utils import asyncio_print_traceback -from pymobiledevice3.services.dvt.instruments.location_simulation import ( - LocationSimulation, -) +from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask from pymobiledevice3.tunneld.api import ( @@ -161,15 +159,15 @@ class TunneldRunnerSio: @classmethod def create( - cls, - host: str, - port: int, - context: LocationSimulationState = LocationSimulationState(), - protocol: TunnelProtocol = TunnelProtocol.QUIC, - usb_monitor: bool = True, - wifi_monitor: bool = True, - usbmux_monitor: bool = True, - mobdev2_monitor: bool = True, + cls, + host: str, + port: int, + context: LocationSimulationState = LocationSimulationState(), + protocol: TunnelProtocol = TunnelProtocol.QUIC, + usb_monitor: bool = True, + wifi_monitor: bool = True, + usbmux_monitor: bool = True, + mobdev2_monitor: bool = True, ) -> None: cls( host, @@ -183,15 +181,15 @@ class TunneldRunnerSio: )._run_app() def __init__( - self, - host: str, - port: int, - context: LocationSimulationState = LocationSimulationState(), - protocol: TunnelProtocol = TunnelProtocol.QUIC, - usb_monitor: bool = True, - wifi_monitor: bool = True, - usbmux_monitor: bool = True, - mobdev2_monitor: bool = True, + self, + host: str, + port: int, + context: LocationSimulationState = LocationSimulationState(), + protocol: TunnelProtocol = TunnelProtocol.QUIC, + usb_monitor: bool = True, + wifi_monitor: bool = True, + usbmux_monitor: bool = True, + mobdev2_monitor: bool = True, ): @asynccontextmanager async def lifespan(app: FastAPI): @@ -206,15 +204,8 @@ class TunneldRunnerSio: self.port = port self.protocol = protocol self.context = context - self._tunneld_api_address = ( - "127.0.0.1" if host in ("0.0.0.0", "::") else host, - port, - ) - self._app = FastAPI( - title="iOS Device Management API", - lifespan=lifespan, - cors_allowed_origins="*", - ) + self._tunneld_api_address = ("127.0.0.1" if host in ("0.0.0.0", "::") else host, port) + self._app = FastAPI(title="iOS Device Management API", lifespan=lifespan, cors_allowed_origins="*") self._asgi_app = socketio.ASGIApp(self.context.sio, self._app) self._tunneld_core = TunneldCore( protocol=protocol, @@ -225,39 +216,26 @@ class TunneldRunnerSio: ) async def get_tun( - udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5 + udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5 ) -> RemoteServiceDiscoveryService: """Try to connect to tunneld with retries to handle startup delay.""" for attempt in range(max_retries): try: if udid: - rsd = await get_tunneld_device_by_udid( - udid, self._tunneld_api_address - ) + rsd = await get_tunneld_device_by_udid(udid, self._tunneld_api_address) if rsd is not None: - logger.info( - "Connected to tunnel for udid %s after %s retries", - udid, - attempt, - ) + logger.info("Connected to tunnel for udid %s after %s retries", udid, attempt) return rsd rsds = await get_tunneld_devices(self._tunneld_api_address) if rsds: if udid: - logger.warning( - "Tunnel for udid %s not found; using first available tunnel", - udid, - ) + logger.warning("Tunnel for udid %s not found; using first available tunnel", udid) logger.info("Connected to tunneld after %s retries", attempt) return rsds[0] except TunneldConnectionError: if attempt < max_retries - 1: - logger.info( - "Waiting for tunneld to be ready... (attempt %s/%s)", - attempt + 1, - max_retries, - ) + logger.info("Waiting for tunneld to be ready... (attempt %s/%s)", attempt + 1, max_retries) await asyncio.sleep(retry_delay) else: logger.error("Failed to connect to tunneld after max retries") @@ -277,10 +255,7 @@ class TunneldRunnerSio: break tun = await get_tun(self.context.udid) if tun is not None: - async with ( - DvtProvider(tun) as dvt, - LocationSimulationQueue(dvt, self.context) as locate_simulation, - ): + async with DvtProvider(tun) as dvt, LocationSimulationQueue(dvt, self.context) as locate_simulation: await locate_simulation.clear() self.context.simulation_active = False @@ -290,36 +265,21 @@ class TunneldRunnerSio: try: if self.context.udid is None: active_udids = sorted( - { - t.udid - for t in self._tunneld_core.tunnel_tasks.values() - if t.udid is not None and t.tunnel is not None - } + {t.udid for t in self._tunneld_core.tunnel_tasks.values() if t.udid is not None and t.tunnel is not None} ) if len(active_udids) == 1: self.context.udid = active_udids[0] - logger.info( - "Simulation worker: auto-selected udid=%s from active tunnel", - self.context.udid, - ) + logger.info("Simulation worker: auto-selected udid=%s from active tunnel", self.context.udid) elif len(active_udids) == 0: - logger.error( - "Simulation worker: no active tunnel with udid available" - ) + logger.error("Simulation worker: no active tunnel with udid available") await self.context.sio.emit( "error", - { - "type": "simulation_no_tunnel", - "message": "No active tunnel found. Start tunnel first.", - }, + {"type": "simulation_no_tunnel", "message": "No active tunnel found. Start tunnel first."}, namespace="/", ) return else: - logger.error( - "Simulation worker: multiple active tunnels; explicit udid required: %s", - active_udids, - ) + logger.error("Simulation worker: multiple active tunnels; explicit udid required: %s", active_udids) await self.context.sio.emit( "error", { @@ -331,27 +291,17 @@ class TunneldRunnerSio: ) return - logger.info( - "Simulation worker: acquiring tunnel (udid=%s)", self.context.udid - ) + logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid) tun = await asyncio.wait_for( get_tun(self.context.udid), timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS, ) - logger.info( - "Simulation worker: tunnel acquired, connecting DVT provider" - ) + logger.info("Simulation worker: tunnel acquired, connecting DVT provider") dvt_provider = DvtProvider(tun) - dvt = await asyncio.wait_for( - dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS - ) - logger.info( - "Simulation worker: DVT provider connected, starting queue playback" - ) + dvt = await asyncio.wait_for(dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS) + logger.info("Simulation worker: DVT provider connected, starting queue playback") try: - async with LocationSimulationQueue( - dvt, self.context - ) as locate_simulation: + async with LocationSimulationQueue(dvt, self.context) as locate_simulation: await locate_simulation.play_queue() finally: logger.info("Simulation worker: closing DVT provider") @@ -390,16 +340,12 @@ class TunneldRunnerSio: for key, value in d.items(): if isinstance(value, dict): iterate_multidim(value) - elif ( - isinstance(value, str) - or isinstance(value, int) - or isinstance(value, float) - or isinstance(value, bool) - or isinstance(value, list) - ): - mydict[key] = value + elif isinstance(value, bytes): + mydict[key] = 'BYTE DATA' else: - mydict[key] = "" + mydict[key] = value +# elif isinstance(value, str) or isinstance(value, int) or isinstance(value, float) or isinstance(value, bool) or isinstance(value, list): +# mydict[key] = type(value).__name__ return mydict @self._app.get("/") @@ -431,14 +377,10 @@ class TunneldRunnerSio: if active_tunnel.udid not in tunnels: tunnels[active_tunnel.udid] = {} try: - lockdown = await create_using_usbmux( - serial=active_tunnel.udid, autopair=False - ) + lockdown = await create_using_usbmux(serial=active_tunnel.udid, autopair=False) tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values) except Exception as e: - logger.error( - f"Failed to create lockdown session for device {active_tunnel.udid}: {e}" - ) + logger.error(f"Failed to create lockdown session for device {active_tunnel.udid}: {e}") continue return tunnels @@ -446,32 +388,19 @@ class TunneldRunnerSio: async def shutdown() -> fastapi.Response: """Shutdown Tunneld""" os.kill(os.getpid(), signal.SIGINT) - data = { - "operation": "shutdown", - "data": True, - "message": "Server shutting down...", - } + data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."} return generate_http_response(data) @self._app.get("/clear_tunnels") async def clear_tunnels() -> fastapi.Response: self._tunneld_core.clear() - data = { - "operation": "clear_tunnels", - "data": True, - "message": "Cleared tunnels...", - } + data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."} return generate_http_response(data) @self._app.get("/cancel") async def cancel_tunnel(udid: str) -> fastapi.Response: self._tunneld_core.cancel(udid=udid) - data = { - "operation": "cancel", - "udid": udid, - "data": True, - "message": f"tunnel {udid} Canceled ...", - } + data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."} return generate_http_response(data) @self._app.get("/hello") @@ -480,21 +409,17 @@ class TunneldRunnerSio: return generate_http_response(data) def generate_http_response( - data: dict, status_code: int = 200, media_type: str = "application/json" + data: dict, status_code: int = 200, media_type: str = "application/json" ) -> fastapi.Response: - return fastapi.Response( - status_code=status_code, media_type=media_type, content=json.dumps(data) - ) + return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data)) @self._app.get("/start-tunnel") @self.context.sio.event async def start_tunnel( - udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None + udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None ) -> fastapi.Response: udid_tunnels = [ - t.tunnel - for t in self._tunneld_core.tunnel_tasks.values() - if t.udid == udid and t.tunnel is not None + t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None ] if len(udid_tunnels) > 0: self.context.udid = udid @@ -516,18 +441,13 @@ class TunneldRunnerSio: service = await CoreDeviceTunnelProxy.create(lockdown) task = asyncio.create_task( self._tunneld_core.start_tunnel_task( - task_identifier, - service, - protocol=TunnelProtocol.TCP, - queue=queue, + task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue ), name=f"start-tunnel-task-{task_identifier}", ) - self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask( - task=task, udid=udid - ) + self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid) created_task = True - except ConnectionFailedError, InvalidServiceError, MuxException: + except (ConnectionFailedError, InvalidServiceError, MuxException): pass if connection_type in ("usb", None): for rsd in await get_rsds(udid=udid): @@ -537,28 +457,20 @@ class TunneldRunnerSio: continue task = asyncio.create_task( self._tunneld_core.start_tunnel_task( - rsd_ip, - await create_core_device_tunnel_service_using_rsd(rsd), - queue=queue, + rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue ), name=f"start-tunnel-usb-{rsd_ip}", ) - self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask( - task=task, udid=rsd.udid - ) + self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid) created_task = True if not created_task and connection_type in ("wifi", None): - for remotepairing in await get_remote_pairing_tunnel_services( - udid=udid - ): + for remotepairing in await get_remote_pairing_tunnel_services(udid=udid): remotepairing_ip = remotepairing.hostname if ip is not None and remotepairing_ip != ip: await remotepairing.close() continue task = asyncio.create_task( - self._tunneld_core.start_tunnel_task( - remotepairing_ip, remotepairing, queue=queue - ), + self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue), name=f"start-tunnel-wifi-{remotepairing_ip}", ) self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask( @@ -568,36 +480,25 @@ class TunneldRunnerSio: except Exception as e: return fastapi.Response( status_code=501, - content=json.dumps( - { - "error": { - "exception": e.__class__.__name__, - "traceback": traceback.format_exc(), - } + content=json.dumps({ + "error": { + "exception": e.__class__.__name__, + "traceback": traceback.format_exc(), } - ), + }), ) if not created_task: - return fastapi.Response( - status_code=501, content=json.dumps({"error": "task not created"}) - ) + return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"})) tunnel: Optional[TunnelResult] = await queue.get() if tunnel is not None: self.context.udid = udid - data = { - "interface": tunnel.interface, - "port": tunnel.port, - "address": tunnel.address, - } + data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address} return generate_http_response(data) else: return fastapi.Response( - status_code=404, - content=json.dumps( - {"error": "something went wrong during tunnel creation"} - ), + status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"}) ) @self.context.sio.event @@ -614,35 +515,16 @@ class TunneldRunnerSio: @self.context.sio.event async def message(sid, data): logger.info("Received message from %s: %s", data, sid) - await self.context.sio.emit( - "message", f"Received message from {sid}: {data}", namespace="/" - ) + await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/") @self.context.sio.event async def simulate_location(sid, data): logger.info("Simulate location request from %s: %s", sid, data) - latitude = ( - data.get("latitude") - if isinstance(data, dict) - else getattr(data, "latitude", None) - ) - longitude = ( - data.get("longitude") - if isinstance(data, dict) - else getattr(data, "longitude", None) - ) - delay = ( - data.get("delay", 0) - if isinstance(data, dict) - else getattr(data, "delay", 0) - ) + latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None) + longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None) + delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0) if latitude is not None and longitude is not None: - logger.info( - "Adding location %s, %s with %ss delay to the queue", - latitude, - longitude, - delay, - ) + logger.info("Adding location %s, %s with %ss delay to the queue", latitude, longitude, delay) await self.context.queue.put((latitude, longitude, delay)) await self.context.sio.emit( "simulation", @@ -658,46 +540,32 @@ class TunneldRunnerSio: ) else: logger.warning("Invalid location data received from %s: %s", sid, data) - await self.context.sio.emit( - "error", "Invalid location data", namespace="/" - ) + await self.context.sio.emit("error", "Invalid location data", namespace="/") @self.context.sio.event async def start_simulate_location(sid, data): logger.info("Start location simulation request from %s", sid) if isinstance(data, dict) and data.get("udid"): self.context.udid = data["udid"] - if ( - self.context.simulation_task is None - or self.context.simulation_task.done() - ): + if self.context.simulation_task is None or self.context.simulation_task.done(): self.context.simulation_active = True self.context.simulation_task = asyncio.create_task( start_queue(), name="location-simulation-worker", ) - await self.context.sio.emit( - "simulation", - {"status": self.context.simulation_active, "data": None}, - namespace="/", - ) + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None}, + namespace="/") @self.context.sio.event async def end_simulate_location(sid, data): logger.info("End location simulation request from %s", sid) - if ( - self.context.simulation_task is not None - and not self.context.simulation_task.done() - ): + if self.context.simulation_task is not None and not self.context.simulation_task.done(): await self.context.queue.put((None, None, None)) with suppress(asyncio.CancelledError): await self.context.simulation_task await empty_queue() - await self.context.sio.emit( - "simulation", - {"status": self.context.simulation_active, "data": None}, - namespace="/", - ) + await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None}, + namespace="/") @self.context.sio.event async def disconnect(sid): @@ -713,9 +581,7 @@ class TunneldRunnerSio: logger.error("Error starting tunneld: %s", e) def _run_app(self) -> None: - uvicorn.run( - self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1 - ) + uvicorn.run(self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1) class LocationSimulationQueue(LocationSimulation): @@ -723,9 +589,7 @@ class LocationSimulationQueue(LocationSimulation): super().__init__(dvt) self.context = context - async def play_queue( - self, disable_sleep: bool = False, timing_randomness_range: int = 0 - ) -> None: + async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None: while True: latitude, longitude, delay = await self.context.queue.get() if (latitude, longitude, delay) == (None, None, None):