From 37c3bf0aeb23f43306ba88b83b375a7b55e2e861 Mon Sep 17 00:00:00 2001 From: William Bruno Date: Thu, 12 Mar 2026 21:33:18 -0400 Subject: [PATCH] debug --- __pycache__/main.cpython-314.pyc | Bin 0 -> 5304 bytes __pycache__/server.cpython-314.pyc | Bin 0 -> 40578 bytes server.py | 404 ++++++++++++++++++++++------- 3 files changed, 305 insertions(+), 99 deletions(-) create mode 100644 __pycache__/main.cpython-314.pyc create mode 100644 __pycache__/server.cpython-314.pyc diff --git a/__pycache__/main.cpython-314.pyc b/__pycache__/main.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1ebd9af79321deb4ce4a91f47813c1d2d96f2ad GIT binary patch literal 5304 zcmbVQU2Gf25#A#y{*(F_DT$O$l7A%IqHW24@lPB@KR!CLM4u?x24tTp@+6&2@|eA) zV-bDuLs6tbf%KtIfq?`;fVgi$P@sJ(ng(cJ=|rlTvsAZ8fi!P)E!Rc*(%HQ`Nl_{| z$jRKB-I<-8o!k9p#s`A|FM{Xh#wW^H5TUPV$9fnCI?w*eA#@MjKq41M#CUUrV|^R3 zvA3Pr+1o)J>|H}@*xN~*?Cl~hc-!J`!9zTqREIt86@0|UXh+;H1W16(a7B~hV+ zbTGar-YM{eXS6roC3KT+M*HGD!XC0m*h}`ZzCXTC*iZHgy`-1*1MxoL068EWBnO2< z*m~+JLK*n75p@0BmL#ms^{`)G+c$LfL?5U7^2Ka6t>%e* zmq6E5NMWL+vOqc#DYDpS*F8h|yh>6;&gkyR0#Vd_Dyut_ARnIuv0yxZOHLCdzmQxi z$hgm@hek%nhNj1p((98G7bmAhDH*>sIys%xd*-nEmL^MB&Z$H$q!bLZq+BXrOl2jr z+vn7q#i@}g8Q)RTassP&l#Gl=b#4hOWxC zt!1bMq2%<$#OU~lWSxMakr8opY6@6BT+B;KUdpO!fo{^+c1EOo!c&T>hsM=(ik_}1 zC0EQE6g_piGpjBv$T)3X0{X6jpB;YBcxdjSS->vi5#fA`wv#ra2PfX%gZdmL*O3_e zQ(o&t2)=@2QH3Mopq^uwAe~iAe`bD-p3%v=E0paebyV+Y}<`JuUo)*Q9J zy!Juzk>db900U#mG&R98BLp-v@84j^Jv4)E*iQWvt|W{_&}D9s6S-LsF`ik|ScGOB z)>y)6QCsgWfL76d(p+QE_8q`*Y}4nq=Dbv2&93^K+w@r>;^p?aM7QVxopkT;?6A?h zO(*ptdtPdzPxOlcF(}raG`+J!Yuz@jKity#U3cE_qx3cIs;}us>1*CqU(1iu7ur={ zc$+>*4u0TQY28&{o7jHX3zshP4L#R*A2g+XLzxVgHH{}KKd0m~JXw@^14=SH#Tyv| zD+Vy(Sd_B`yq`|$NxG_y%~8vRm_spd~!I!t9d@L zL>A%o>bb$#(ZNH!3TxrF6iwzYPsfM*Vifq(DF}UItKx9pww!LnEmg_ zn32CUV4*LsZNe{aGU+#f=xI0rd}w^InC`?fDdPM@pBq!S((M|-dJUmjfbKLh1>FuY zT6fH;Y8F#a#68f!XLf=!b19-xFzdz?S9QN6rOZS_lJI#Lq8`%DgUCJf0v+O7pVDS6 z7`ku2KmGpfd$Z-kS5~4|%i*i5!E04J@cyx#mt2WXm&4Pm!I^3e@HU*NDSAKo(eaPZ zet32{@Hf7$``Kab&|A+qa!1xM(soZvHwQi!?Nx3{~Xup5#qvnqzA4bZ96DwVl z<@U+d`b4#ha=U@M(mwE;(cg}LGG3`4-LP}VFK|`FMJ{m95!WDa)gF+m?n6%R)BQlP z2$lALH?4=FFJQFe+25FVx5B5DRMHDbp9L1NXxcbyV*1OX)C`*8X6!SLnVK2rjBCaX z!Ps26;F+;mdNX%vBy2ZI1cy*l@OGlvI%7?Q5)Br$^@iwIbIgo=XE=!~+KP5*GYKyE zlU|T;CXpYBt|aG21CFFW(X=(+qLLobZN;ZTV7A2?PlPRM>kS$`+cjG2ccNq<(Y_^T zQI_{;qD}vfnLaZT?-|d|RGde%n{m*IGO+6iD)?q=Eq$WHS~2Ncw2557ciyLP3OZwt zB8B52AF=$o09YGAimekxQ78MO-^`Hf^Og=w(FBq=b1rs)A;V)A+DKSUU<25h=yT$u zV1VueM3GE5()|>RHhVz>Y&QF~CbC!q_#`NRTkaXz1n0vRG{X|Sq5))Pmw2sMpb#r( zD1sLA%3Zzyi64aqO}D*8N^Z+wojVOVqdiq9==Oq=(VZzMIMjTpo+ZXqr6t)H#I&x$ zG*`ggbRW(%mLZMAN9f2X9r49k%FJ?ky>UV%(9g;_8EPOS!PnhH&J}3Vt~X3T+MY>5 zSH&s3G)Bi9G?mADs49n&pI7x7W(F)xgB=TA#%k1$c#t;qWZ>hpIYArhA3bOW6A3b7 z$bk*pI2po{VpI&e({P$b!}31s;pxL56ZcYGv|&j3S0L`8jRVNv^~l%#xv%?6cW|va zvgQsya(6y-cUIhf`mesV(iEyRw^l;YO1QI97usm_wm9!iyl6r0`l`+Dxyh~j4+2~U zTOI{^KM(XiXnGWgtpsAryF#(zHki-{L56g>Pe5PRxHp3q<1;s0#Z zqPq4C#KE#_4SUP`u6)sOwR}S=`=$R@?R50puLChKpd5&Ot5Ki--ue1D^lA5ydpJ__ z$8)~nR{Lje|8SH2v!)tKM|{Km9^EZLSqasK)HkHNSltK}hA)#MsdP4_X;9oaSmEfR zwFM2fPKfszH3uh$n!YW=3Bwbxpzbx}U{;yK^hWER`C>jzAf{>ToYA70EK)8l zOLQkhKv2nIB6h(DrU^MdO?g2U9ia}QC9{e#g+@J8Sb_@#C(9^b2V-+;{+3$An5MJX zM@2ezia@~~4OUyKpA}z}S^u4q1=KS2YRhT|Ti;A7PA82K0xc#OP{Q7!yiN8Rfvx{kt6Q0yU!eTDq%sPAiZ^M%dD z*+{$8YDbPXw$J|oE3P~5 literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-314.pyc b/__pycache__/server.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..396ca7424ceb0845d63a88c9bc4147f7627fa34b GIT binary patch literal 40578 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 None: cls( host, @@ -147,15 +183,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): @@ -170,8 +206,15 @@ 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, @@ -182,26 +225,39 @@ 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") @@ -221,7 +277,10 @@ 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 @@ -231,21 +290,36 @@ 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", { @@ -257,17 +331,27 @@ 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") @@ -306,10 +390,16 @@ 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): + elif ( + isinstance(value, str) + or isinstance(value, int) + or isinstance(value, float) + or isinstance(value, bool) + or isinstance(value, list) + ): mydict[key] = value else: - mydict[key] = '' + mydict[key] = "" return mydict @self._app.get("/") @@ -322,11 +412,13 @@ class TunneldRunnerSio: continue if active_tunnel.udid not in tunnels: tunnels[active_tunnel.udid] = [] - tunnels[active_tunnel.udid].append({ - "tunnel-address": active_tunnel.tunnel.address, - "tunnel-port": active_tunnel.tunnel.port, - "interface": ip, - }) + tunnels[active_tunnel.udid].append( + { + "tunnel-address": active_tunnel.tunnel.address, + "tunnel-port": active_tunnel.tunnel.port, + "interface": ip, + } + ) return tunnels @self._app.get("/device_info") @@ -339,10 +431,14 @@ 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 @@ -350,19 +446,32 @@ 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") @@ -371,17 +480,21 @@ 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 @@ -403,13 +516,18 @@ 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): @@ -419,20 +537,28 @@ 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( @@ -442,25 +568,36 @@ 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 @@ -477,49 +614,90 @@ 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", {"status": self.context.simulation_active, - "data": {"latitude": self.context.latitude, - "cur_longitude": longitude, "next_move": delay}}, - namespace="/") + await self.context.sio.emit( + "simulation", + { + "status": self.context.simulation_active, + "data": { + "latitude": self.context.latitude, + "cur_longitude": longitude, + "next_move": delay, + }, + }, + namespace="/", + ) 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): @@ -535,7 +713,9 @@ 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): @@ -543,26 +723,52 @@ 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): break if delay > 0 and not disable_sleep: 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): - await self.context.sio.emit("simulation", {"status": self.context.simulation_active, - "data": {"latitude": self.context.latitude, - "longitude": self.context.longitude, - "next_move": i}}, namespace="/") + self.context.next_move = i + await self.context.sio.emit( + "simulation", + { + "status": self.context.simulation_active, + "data": { + "latitude": self.context.latitude, + "longitude": self.context.longitude, + "next_move": i, + }, + }, + namespace="/", + ) await asyncio.sleep(1) await self.set(latitude, longitude) self.context.latitude = latitude self.context.longitude = longitude - await self.context.sio.emit("simulation", {"status": self.context.simulation_active, - "data": {"latitude": self.context.latitude, - "longitude": self.context.longitude, - "next_move": None}}, namespace="/") - logger.info("Set simulated location to %s, %s after %ss delay", latitude, longitude, delay) + await self.context.sio.emit( + "simulation", + { + "status": self.context.simulation_active, + "data": { + "latitude": self.context.latitude, + "longitude": self.context.longitude, + "next_move": None, + }, + }, + namespace="/", + ) + logger.info( + "Set simulated location to %s, %s after %ss delay", + latitude, + longitude, + delay, + ) self.context.queue.task_done()