This commit is contained in:
2026-03-12 21:33:18 -04:00
parent 5b2d5655d8
commit 37c3bf0aeb
3 changed files with 305 additions and 99 deletions

Binary file not shown.

Binary file not shown.

346
server.py
View File

@@ -1,9 +1,11 @@
import asyncio import asyncio
import dataclasses import dataclasses
from http.cookies import BaseCookie
import json import json
import logging import logging
import os import os
import signal import signal
from sys import float_repr_style
import traceback import traceback
import warnings import warnings
import fastapi import fastapi
@@ -44,13 +46,16 @@ from pymobiledevice3.exceptions import (
PairingError, PairingError,
QuicProtocolNotSupportedError, QuicProtocolNotSupportedError,
StreamClosedError, StreamClosedError,
TunneldConnectionError TunneldConnectionError,
) )
from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns from pymobiledevice3.lockdown import create_using_usbmux, get_mobdev2_lockdowns
from pymobiledevice3.osu.os_utils import get_os_utils from pymobiledevice3.osu.os_utils import get_os_utils
from pymobiledevice3.remote.common import TunnelProtocol from pymobiledevice3.remote.common import TunnelProtocol
from pymobiledevice3.remote.module_imports import start_tunnel from pymobiledevice3.remote.module_imports import start_tunnel
from pymobiledevice3.remote.remote_service_discovery import RSD_PORT, RemoteServiceDiscoveryService from pymobiledevice3.remote.remote_service_discovery import (
RSD_PORT,
RemoteServiceDiscoveryService,
)
from pymobiledevice3.remote.tunnel_service import ( from pymobiledevice3.remote.tunnel_service import (
CoreDeviceTunnelProxy, CoreDeviceTunnelProxy,
RemotePairingProtocol, RemotePairingProtocol,
@@ -60,7 +65,9 @@ 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 LocationSimulation from pymobiledevice3.services.dvt.instruments.location_simulation import (
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 (
@@ -109,15 +116,44 @@ TUNNEL_ACQUIRE_TIMEOUT_SECONDS = 15
DVT_CONNECT_TIMEOUT_SECONDS = 20 DVT_CONNECT_TIMEOUT_SECONDS = 20
class SimulationStatusData:
latitude: float
longitude: float
start: float
end: Optional[float]
next_move: Optional[float]
class SimulationStatus:
status: bool
data: Optional[SimulationStatusData]
class SimulationRequestData:
latitude: float
longitude: float
delay: int
start: str
end: Optional[str]
class SimulationRequest:
status: bool
data: Optional[SimulationRequestData]
class LocationSimulationState: class LocationSimulationState:
def __init__(self): def __init__(self):
self.latitude: Optional[float] = None self.latitude: Optional[float] = None
self.longitude: Optional[float] = None self.longitude: Optional[float] = None
self.next_move: Optional[float] = None
self.udid: Optional[str] = None self.udid: Optional[str] = None
self.simulation_active: bool = False self.simulation_active: bool = False
self.queue: asyncio.Queue = asyncio.Queue() self.queue: asyncio.Queue = asyncio.Queue()
self.simulation_task: Optional[asyncio.Task] = None self.simulation_task: Optional[asyncio.Task] = None
self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") self.sio: socketio.AsyncServer = socketio.AsyncServer(
async_mode="asgi", cors_allowed_origins="*"
)
class TunneldRunnerSio: class TunneldRunnerSio:
@@ -170,8 +206,15 @@ class TunneldRunnerSio:
self.port = port self.port = port
self.protocol = protocol self.protocol = protocol
self.context = context self.context = context
self._tunneld_api_address = ("127.0.0.1" if host in ("0.0.0.0", "::") else host, port) self._tunneld_api_address = (
self._app = FastAPI(title="iOS Device Management API", lifespan=lifespan, cors_allowed_origins="*") "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._asgi_app = socketio.ASGIApp(self.context.sio, self._app)
self._tunneld_core = TunneldCore( self._tunneld_core = TunneldCore(
protocol=protocol, protocol=protocol,
@@ -188,20 +231,33 @@ 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(udid, self._tunneld_api_address) rsd = await get_tunneld_device_by_udid(
udid, self._tunneld_api_address
)
if rsd is not None: 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 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("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) 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("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) 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")
@@ -221,7 +277,10 @@ 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 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() await locate_simulation.clear()
self.context.simulation_active = False self.context.simulation_active = False
@@ -231,21 +290,36 @@ 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("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: 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( 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("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( await self.context.sio.emit(
"error", "error",
{ {
@@ -257,17 +331,27 @@ class TunneldRunnerSio:
) )
return 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( 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("Simulation worker: tunnel acquired, connecting DVT provider") logger.info(
"Simulation worker: tunnel acquired, connecting DVT provider"
)
dvt_provider = DvtProvider(tun) dvt_provider = DvtProvider(tun)
dvt = await asyncio.wait_for(dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS) dvt = await asyncio.wait_for(
logger.info("Simulation worker: DVT provider connected, starting queue playback") dvt_provider.__aenter__(), timeout=DVT_CONNECT_TIMEOUT_SECONDS
)
logger.info(
"Simulation worker: DVT provider connected, starting queue playback"
)
try: try:
async with LocationSimulationQueue(dvt, self.context) as locate_simulation: async with LocationSimulationQueue(
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")
@@ -306,10 +390,16 @@ 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 isinstance(value, str) or isinstance(value, int) or isinstance(value, float) or isinstance(value, bool) or isinstance(value, list): elif (
isinstance(value, str)
or isinstance(value, int)
or isinstance(value, float)
or isinstance(value, bool)
or isinstance(value, list)
):
mydict[key] = value mydict[key] = value
else: else:
mydict[key] = '' mydict[key] = ""
return mydict return mydict
@self._app.get("/") @self._app.get("/")
@@ -322,11 +412,13 @@ class TunneldRunnerSio:
continue continue
if active_tunnel.udid not in tunnels: if active_tunnel.udid not in tunnels:
tunnels[active_tunnel.udid] = [] tunnels[active_tunnel.udid] = []
tunnels[active_tunnel.udid].append({ tunnels[active_tunnel.udid].append(
{
"tunnel-address": active_tunnel.tunnel.address, "tunnel-address": active_tunnel.tunnel.address,
"tunnel-port": active_tunnel.tunnel.port, "tunnel-port": active_tunnel.tunnel.port,
"interface": ip, "interface": ip,
}) }
)
return tunnels return tunnels
@self._app.get("/device_info") @self._app.get("/device_info")
@@ -339,10 +431,14 @@ 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(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) tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values)
except Exception as e: 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 continue
return tunnels return tunnels
@@ -350,19 +446,32 @@ 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 = {"operation": "shutdown", "data": True, "message": "Server shutting down..."} data = {
"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 = {"operation": "clear_tunnels", "data": True, "message": "Cleared tunnels..."} data = {
"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 = {"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) return generate_http_response(data)
@self._app.get("/hello") @self._app.get("/hello")
@@ -373,7 +482,9 @@ 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(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._app.get("/start-tunnel")
@self.context.sio.event @self.context.sio.event
@@ -381,7 +492,9 @@ 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 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: if len(udid_tunnels) > 0:
self.context.udid = udid self.context.udid = udid
@@ -403,13 +516,18 @@ 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, service, protocol=TunnelProtocol.TCP, queue=queue task_identifier,
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(task=task, udid=udid) self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask(
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):
@@ -419,20 +537,28 @@ 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, 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}", 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 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(udid=udid): for remotepairing in await get_remote_pairing_tunnel_services(
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(remotepairing_ip, remotepairing, queue=queue), self._tunneld_core.start_tunnel_task(
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(
@@ -442,25 +568,36 @@ 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(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() 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 = {"interface": tunnel.interface, "port": tunnel.port, "address": tunnel.address} data = {
"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, 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 @self.context.sio.event
@@ -477,49 +614,90 @@ 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("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 @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 = data.get("latitude") if isinstance(data, dict) else getattr(data, "latitude", None) latitude = (
longitude = data.get("longitude") if isinstance(data, dict) else getattr(data, "longitude", None) data.get("latitude")
delay = data.get("delay", 0) if isinstance(data, dict) else getattr(data, "delay", 0) 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: 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.queue.put((latitude, longitude, delay))
await self.context.sio.emit("simulation", {"status": self.context.simulation_active, await self.context.sio.emit(
"data": {"latitude": self.context.latitude, "simulation",
"cur_longitude": longitude, "next_move": delay}}, {
namespace="/") "status": self.context.simulation_active,
"data": {
"latitude": self.context.latitude,
"cur_longitude": longitude,
"next_move": delay,
},
},
namespace="/",
)
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("error", "Invalid location data", namespace="/") await self.context.sio.emit(
"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 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_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("simulation", {"status": self.context.simulation_active, "data": None}, await self.context.sio.emit(
namespace="/") "simulation",
{"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 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)) 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("simulation", {"status": self.context.simulation_active, "data": None}, await self.context.sio.emit(
namespace="/") "simulation",
{"status": self.context.simulation_active, "data": None},
namespace="/",
)
@self.context.sio.event @self.context.sio.event
async def disconnect(sid): async def disconnect(sid):
@@ -535,7 +713,9 @@ 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(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): class LocationSimulationQueue(LocationSimulation):
@@ -543,26 +723,52 @@ class LocationSimulationQueue(LocationSimulation):
super().__init__(dvt) super().__init__(dvt)
self.context = context 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: 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):
break break
if delay > 0 and not disable_sleep: if delay > 0 and not disable_sleep:
if timing_randomness_range > 0: if timing_randomness_range > 0:
delay = delay + random.uniform(-timing_randomness_range, timing_randomness_range) delay = delay + random.uniform(
-timing_randomness_range, timing_randomness_range
)
for i in range(delay, 0, -1): for i in range(delay, 0, -1):
await self.context.sio.emit("simulation", {"status": self.context.simulation_active, self.context.next_move = i
"data": {"latitude": self.context.latitude, await self.context.sio.emit(
"simulation",
{
"status": self.context.simulation_active,
"data": {
"latitude": self.context.latitude,
"longitude": self.context.longitude, "longitude": self.context.longitude,
"next_move": i}}, namespace="/") "next_move": i,
},
},
namespace="/",
)
await asyncio.sleep(1) await asyncio.sleep(1)
await self.set(latitude, longitude) await self.set(latitude, longitude)
self.context.latitude = latitude self.context.latitude = latitude
self.context.longitude = longitude self.context.longitude = longitude
await self.context.sio.emit("simulation", {"status": self.context.simulation_active, await self.context.sio.emit(
"data": {"latitude": self.context.latitude, "simulation",
{
"status": self.context.simulation_active,
"data": {
"latitude": self.context.latitude,
"longitude": self.context.longitude, "longitude": self.context.longitude,
"next_move": None}}, namespace="/") "next_move": None,
logger.info("Set simulated location to %s, %s after %ss delay", latitude, longitude, delay) },
},
namespace="/",
)
logger.info(
"Set simulated location to %s, %s after %ss delay",
latitude,
longitude,
delay,
)
self.context.queue.task_done() self.context.queue.task_done()