# Conflicts:
#	server.py
This commit is contained in:
2026-03-13 07:09:11 -04:00
parent 37c3bf0aeb
commit b5ebedb2b9
2 changed files with 82 additions and 216 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
__pycache__/*
*.pyc

248
server.py
View File

@@ -65,9 +65,7 @@ from pymobiledevice3.remote.tunnel_service import (
) )
from pymobiledevice3.remote.utils import get_rsds, stop_remoted from pymobiledevice3.remote.utils import get_rsds, stop_remoted
from pymobiledevice3.utils import asyncio_print_traceback from pymobiledevice3.utils import asyncio_print_traceback
from pymobiledevice3.services.dvt.instruments.location_simulation import ( from pymobiledevice3.services.dvt.instruments.location_simulation import LocationSimulation
LocationSimulation,
)
from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider from pymobiledevice3.services.dvt.instruments.dvt_provider import DvtProvider
from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask from pymobiledevice3.tunneld.server import TunneldCore, TunnelTask
from pymobiledevice3.tunneld.api import ( from pymobiledevice3.tunneld.api import (
@@ -206,15 +204,8 @@ class TunneldRunnerSio:
self.port = port self.port = port
self.protocol = protocol self.protocol = protocol
self.context = context self.context = context
self._tunneld_api_address = ( self._tunneld_api_address = ("127.0.0.1" if host in ("0.0.0.0", "::") else host, port)
"127.0.0.1" if host in ("0.0.0.0", "::") else host, self._app = FastAPI(title="iOS Device Management API", lifespan=lifespan, cors_allowed_origins="*")
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._asgi_app = socketio.ASGIApp(self.context.sio, self._app)
self._tunneld_core = TunneldCore( self._tunneld_core = TunneldCore(
protocol=protocol, protocol=protocol,
@@ -231,33 +222,20 @@ class TunneldRunnerSio:
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
if udid: if udid:
rsd = await get_tunneld_device_by_udid( rsd = await get_tunneld_device_by_udid(udid, self._tunneld_api_address)
udid, self._tunneld_api_address
)
if rsd is not None: if rsd is not None:
logger.info( logger.info("Connected to tunnel for udid %s after %s retries", udid, attempt)
"Connected to tunnel for udid %s after %s retries",
udid,
attempt,
)
return rsd return rsd
rsds = await get_tunneld_devices(self._tunneld_api_address) rsds = await get_tunneld_devices(self._tunneld_api_address)
if rsds: if rsds:
if udid: if udid:
logger.warning( logger.warning("Tunnel for udid %s not found; using first available tunnel", udid)
"Tunnel for udid %s not found; using first available tunnel",
udid,
)
logger.info("Connected to tunneld after %s retries", attempt) logger.info("Connected to tunneld after %s retries", attempt)
return rsds[0] return rsds[0]
except TunneldConnectionError: except TunneldConnectionError:
if attempt < max_retries - 1: if attempt < max_retries - 1:
logger.info( logger.info("Waiting for tunneld to be ready... (attempt %s/%s)", attempt + 1, max_retries)
"Waiting for tunneld to be ready... (attempt %s/%s)",
attempt + 1,
max_retries,
)
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
else: else:
logger.error("Failed to connect to tunneld after max retries") logger.error("Failed to connect to tunneld after max retries")
@@ -277,10 +255,7 @@ class TunneldRunnerSio:
break break
tun = await get_tun(self.context.udid) tun = await get_tun(self.context.udid)
if tun is not None: if tun is not None:
async with ( async with DvtProvider(tun) as dvt, LocationSimulationQueue(dvt, self.context) as locate_simulation:
DvtProvider(tun) as dvt,
LocationSimulationQueue(dvt, self.context) as locate_simulation,
):
await locate_simulation.clear() await locate_simulation.clear()
self.context.simulation_active = False self.context.simulation_active = False
@@ -290,36 +265,21 @@ class TunneldRunnerSio:
try: try:
if self.context.udid is None: if self.context.udid is None:
active_udids = sorted( 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: if len(active_udids) == 1:
self.context.udid = active_udids[0] self.context.udid = active_udids[0]
logger.info( logger.info("Simulation worker: auto-selected udid=%s from active tunnel", self.context.udid)
"Simulation worker: auto-selected udid=%s from active tunnel",
self.context.udid,
)
elif len(active_udids) == 0: elif len(active_udids) == 0:
logger.error( logger.error("Simulation worker: no active tunnel with udid available")
"Simulation worker: no active tunnel with udid available"
)
await self.context.sio.emit( await self.context.sio.emit(
"error", "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="/", namespace="/",
) )
return return
else: else:
logger.error( logger.error("Simulation worker: multiple active tunnels; explicit udid required: %s", active_udids)
"Simulation worker: multiple active tunnels; explicit udid required: %s",
active_udids,
)
await self.context.sio.emit( await self.context.sio.emit(
"error", "error",
{ {
@@ -331,27 +291,17 @@ class TunneldRunnerSio:
) )
return return
logger.info( logger.info("Simulation worker: acquiring tunnel (udid=%s)", self.context.udid)
"Simulation worker: acquiring tunnel (udid=%s)", self.context.udid
)
tun = await asyncio.wait_for( tun = await asyncio.wait_for(
get_tun(self.context.udid), get_tun(self.context.udid),
timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS, timeout=TUNNEL_ACQUIRE_TIMEOUT_SECONDS,
) )
logger.info( logger.info("Simulation worker: tunnel acquired, connecting DVT provider")
"Simulation worker: tunnel acquired, connecting DVT provider"
)
dvt_provider = DvtProvider(tun) dvt_provider = DvtProvider(tun)
dvt = await asyncio.wait_for( dvt = await asyncio.wait_for(dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS)
dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS logger.info("Simulation worker: DVT provider connected, starting queue playback")
)
logger.info(
"Simulation worker: DVT provider connected, starting queue playback"
)
try: try:
async with LocationSimulationQueue( async with LocationSimulationQueue(dvt, self.context) as locate_simulation:
dvt, self.context
) as locate_simulation:
await locate_simulation.play_queue() await locate_simulation.play_queue()
finally: finally:
logger.info("Simulation worker: closing DVT provider") logger.info("Simulation worker: closing DVT provider")
@@ -390,16 +340,12 @@ class TunneldRunnerSio:
for key, value in d.items(): for key, value in d.items():
if isinstance(value, dict): if isinstance(value, dict):
iterate_multidim(value) iterate_multidim(value)
elif ( elif isinstance(value, bytes):
isinstance(value, str) mydict[key] = 'BYTE DATA'
or isinstance(value, int)
or isinstance(value, float)
or isinstance(value, bool)
or isinstance(value, list)
):
mydict[key] = value
else: 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 return mydict
@self._app.get("/") @self._app.get("/")
@@ -431,14 +377,10 @@ class TunneldRunnerSio:
if active_tunnel.udid not in tunnels: if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = {} tunnels[active_tunnel.udid] = {}
try: try:
lockdown = await create_using_usbmux( lockdown = await create_using_usbmux(serial=active_tunnel.udid, autopair=False)
serial=active_tunnel.udid, autopair=False
)
tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values) tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
except Exception as e: except Exception as e:
logger.error( logger.error(f"Failed to create lockdown session for device {active_tunnel.udid}: {e}")
f"Failed to create lockdown session for device {active_tunnel.udid}: {e}"
)
continue continue
return tunnels return tunnels
@@ -446,32 +388,19 @@ class TunneldRunnerSio:
async def shutdown() -> fastapi.Response: async def shutdown() -> fastapi.Response:
"""Shutdown Tunneld""" """Shutdown Tunneld"""
os.kill(os.getpid(), signal.SIGINT) os.kill(os.getpid(), signal.SIGINT)
data = { data = {"operation": "shutdown", "data": True, "message": "Server shutting down..."}
"operation": "shutdown",
"data": True,
"message": "Server shutting down...",
}
return generate_http_response(data) return generate_http_response(data)
@self._app.get("/clear_tunnels") @self._app.get("/clear_tunnels")
async def clear_tunnels() -> fastapi.Response: async def clear_tunnels() -> fastapi.Response:
self._tunneld_core.clear() self._tunneld_core.clear()
data = { data = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."}
"operation": "clear_tunnels",
"data": True,
"message": "Cleared tunnels...",
}
return generate_http_response(data) return generate_http_response(data)
@self._app.get("/cancel") @self._app.get("/cancel")
async def cancel_tunnel(udid: str) -> fastapi.Response: async def cancel_tunnel(udid: str) -> fastapi.Response:
self._tunneld_core.cancel(udid=udid) self._tunneld_core.cancel(udid=udid)
data = { data = {"operation": "cancel", "udid": udid, "data": True, "message": f"tunnel {udid} Canceled ..."}
"operation": "cancel",
"udid": udid,
"data": True,
"message": f"tunnel {udid} Canceled ...",
}
return generate_http_response(data) return generate_http_response(data)
@self._app.get("/hello") @self._app.get("/hello")
@@ -482,9 +411,7 @@ class TunneldRunnerSio:
def generate_http_response( 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: ) -> fastapi.Response:
return fastapi.Response( return fastapi.Response(status_code=status_code, media_type=media_type, content=json.dumps(data))
status_code=status_code, media_type=media_type, content=json.dumps(data)
)
@self._app.get("/start-tunnel") @self._app.get("/start-tunnel")
@self.context.sio.event @self.context.sio.event
@@ -492,9 +419,7 @@ class TunneldRunnerSio:
udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None udid: str, ip: Optional[str] = None, connection_type: Optional[str] = None
) -> fastapi.Response: ) -> fastapi.Response:
udid_tunnels = [ udid_tunnels = [
t.tunnel t.tunnel for t in self._tunneld_core.tunnel_tasks.values() if t.udid == udid and t.tunnel is not None
for t in self._tunneld_core.tunnel_tasks.values()
if t.udid == udid and t.tunnel is not None
] ]
if len(udid_tunnels) > 0: if len(udid_tunnels) > 0:
self.context.udid = udid self.context.udid = udid
@@ -516,18 +441,13 @@ class TunneldRunnerSio:
service = await CoreDeviceTunnelProxy.create(lockdown) service = await CoreDeviceTunnelProxy.create(lockdown)
task = asyncio.create_task( task = asyncio.create_task(
self._tunneld_core.start_tunnel_task( self._tunneld_core.start_tunnel_task(
task_identifier, task_identifier, service, protocol=TunnelProtocol.TCP, queue=queue
service,
protocol=TunnelProtocol.TCP,
queue=queue,
), ),
name=f"start-tunnel-task-{task_identifier}", name=f"start-tunnel-task-{task_identifier}",
) )
self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask( self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(task=task, udid=udid)
task=task, udid=udid
)
created_task = True created_task = True
except ConnectionFailedError, InvalidServiceError, MuxException: except (ConnectionFailedError, InvalidServiceError, MuxException):
pass pass
if connection_type in ("usb", None): if connection_type in ("usb", None):
for rsd in await get_rsds(udid=udid): for rsd in await get_rsds(udid=udid):
@@ -537,28 +457,20 @@ class TunneldRunnerSio:
continue continue
task = asyncio.create_task( task = asyncio.create_task(
self._tunneld_core.start_tunnel_task( self._tunneld_core.start_tunnel_task(
rsd_ip, rsd_ip, await create_core_device_tunnel_service_using_rsd(rsd), queue=queue
await create_core_device_tunnel_service_using_rsd(rsd),
queue=queue,
), ),
name=f"start-tunnel-usb-{rsd_ip}", name=f"start-tunnel-usb-{rsd_ip}",
) )
self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask( self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask(task=task, udid=rsd.udid)
task=task, udid=rsd.udid
)
created_task = True created_task = True
if not created_task and connection_type in ("wifi", None): if not created_task and connection_type in ("wifi", None):
for remotepairing in await get_remote_pairing_tunnel_services( for remotepairing in await get_remote_pairing_tunnel_services(udid=udid):
udid=udid
):
remotepairing_ip = remotepairing.hostname remotepairing_ip = remotepairing.hostname
if ip is not None and remotepairing_ip != ip: if ip is not None and remotepairing_ip != ip:
await remotepairing.close() await remotepairing.close()
continue continue
task = asyncio.create_task( task = asyncio.create_task(
self._tunneld_core.start_tunnel_task( self._tunneld_core.start_tunnel_task(remotepairing_ip, remotepairing, queue=queue),
remotepairing_ip, remotepairing, queue=queue
),
name=f"start-tunnel-wifi-{remotepairing_ip}", name=f"start-tunnel-wifi-{remotepairing_ip}",
) )
self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask( self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask(
@@ -568,36 +480,25 @@ class TunneldRunnerSio:
except Exception as e: except Exception as e:
return fastapi.Response( return fastapi.Response(
status_code=501, status_code=501,
content=json.dumps( content=json.dumps({
{
"error": { "error": {
"exception": e.__class__.__name__, "exception": e.__class__.__name__,
"traceback": traceback.format_exc(), "traceback": traceback.format_exc(),
} }
} }),
),
) )
if not created_task: if not created_task:
return fastapi.Response( return fastapi.Response(status_code=501, content=json.dumps({"error": "task not created"}))
status_code=501, content=json.dumps({"error": "task not created"})
)
tunnel: Optional[TunnelResult] = await queue.get() tunnel: Optional[TunnelResult] = await queue.get()
if tunnel is not None: if tunnel is not None:
self.context.udid = udid self.context.udid = udid
data = { data = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address}
"interface": tunnel.interface,
"port": tunnel.port,
"address": tunnel.address,
}
return generate_http_response(data) return generate_http_response(data)
else: else:
return fastapi.Response( return fastapi.Response(
status_code=404, status_code=404, content=json.dumps({"error": "something went wrong during tunnel creation"})
content=json.dumps(
{"error": "something went wrong during tunnel creation"}
),
) )
@self.context.sio.event @self.context.sio.event
@@ -614,35 +515,16 @@ class TunneldRunnerSio:
@self.context.sio.event @self.context.sio.event
async def message(sid, data): async def message(sid, data):
logger.info("Received message from %s: %s", data, sid) logger.info("Received message from %s: %s", data, sid)
await self.context.sio.emit( await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
"message", f"Received message from {sid}: {data}", namespace="/"
)
@self.context.sio.event @self.context.sio.event
async def simulate_location(sid, data): async def simulate_location(sid, data):
logger.info("Simulate location request from %s: %s", sid, data) logger.info("Simulate location request from %s: %s", sid, data)
latitude = ( latitude = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None)
data.get("latitude") longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None)
if isinstance(data, dict) delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0)
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: if latitude is not None and longitude is not None:
logger.info( logger.info("Adding location %s, %s with %ss delay to the queue", latitude, longitude, delay)
"Adding location %s, %s with %ss delay to the queue",
latitude,
longitude,
delay,
)
await self.context.queue.put((latitude, longitude, delay)) await self.context.queue.put((latitude, longitude, delay))
await self.context.sio.emit( await self.context.sio.emit(
"simulation", "simulation",
@@ -658,46 +540,32 @@ class TunneldRunnerSio:
) )
else: else:
logger.warning("Invalid location data received from %s: %s", sid, data) logger.warning("Invalid location data received from %s: %s", sid, data)
await self.context.sio.emit( await self.context.sio.emit("error", "Invalid location data", namespace="/")
"error", "Invalid location data", namespace="/"
)
@self.context.sio.event @self.context.sio.event
async def start_simulate_location(sid, data): async def start_simulate_location(sid, data):
logger.info("Start location simulation request from %s", sid) logger.info("Start location simulation request from %s", sid)
if isinstance(data, dict) and data.get("udid"): if isinstance(data, dict) and data.get("udid"):
self.context.udid = data["udid"] self.context.udid = data["udid"]
if ( if self.context.simulation_task is None or self.context.simulation_task.done():
self.context.simulation_task is None
or self.context.simulation_task.done()
):
self.context.simulation_active = True self.context.simulation_active = True
self.context.simulation_task = asyncio.create_task( self.context.simulation_task = asyncio.create_task(
start_queue(), start_queue(),
name="location-simulation-worker", name="location-simulation-worker",
) )
await self.context.sio.emit( await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None},
"simulation", namespace="/")
{"status": self.context.simulation_active, "data": None},
namespace="/",
)
@self.context.sio.event @self.context.sio.event
async def end_simulate_location(sid, data): async def end_simulate_location(sid, data):
logger.info("End location simulation request from %s", sid) logger.info("End location simulation request from %s", sid)
if ( if self.context.simulation_task is not None and not self.context.simulation_task.done():
self.context.simulation_task is not None
and not self.context.simulation_task.done()
):
await self.context.queue.put((None, None, None)) await self.context.queue.put((None, None, None))
with suppress(asyncio.CancelledError): with suppress(asyncio.CancelledError):
await self.context.simulation_task await self.context.simulation_task
await empty_queue() await empty_queue()
await self.context.sio.emit( await self.context.sio.emit("simulation", {"status": self.context.simulation_active, "data": None},
"simulation", namespace="/")
{"status": self.context.simulation_active, "data": None},
namespace="/",
)
@self.context.sio.event @self.context.sio.event
async def disconnect(sid): async def disconnect(sid):
@@ -713,9 +581,7 @@ class TunneldRunnerSio:
logger.error("Error starting tunneld: %s", e) logger.error("Error starting tunneld: %s", e)
def _run_app(self) -> None: def _run_app(self) -> None:
uvicorn.run( uvicorn.run(self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1)
self._asgi_app, host=self.host, port=self.port, loop="asyncio", workers=1
)
class LocationSimulationQueue(LocationSimulation): class LocationSimulationQueue(LocationSimulation):
@@ -723,9 +589,7 @@ class LocationSimulationQueue(LocationSimulation):
super().__init__(dvt) super().__init__(dvt)
self.context = context self.context = context
async def play_queue( async def play_queue(self, disable_sleep: bool = False, timing_randomness_range: int = 0) -> None:
self, disable_sleep: bool = False, timing_randomness_range: int = 0
) -> None:
while True: while True:
latitude, longitude, delay = await self.context.queue.get() latitude, longitude, delay = await self.context.queue.get()
if (latitude, longitude, delay) == (None, None, None): if (latitude, longitude, delay) == (None, None, None):