lots of changes

This commit is contained in:
2026-03-21 07:32:43 -04:00
parent 21933cdef9
commit 47aeebd86f
7 changed files with 1463 additions and 107 deletions

323
server.py
View File

@@ -1,5 +1,6 @@
import asyncio
from icloud import FindMyMonitor
from datetime import datetime, timezone, timedelta
import json
import uuid
@@ -9,11 +10,13 @@ import signal
import traceback
import warnings
import random
from operator import truediv
from pydantic import BaseModel, RootModel
import socketio
from contextlib import asynccontextmanager, suppress
from typing import Optional
from typing import Optional, Dict
from pymobiledevice3.services.dvt.instruments.location_simulation_base import LocationSimulationBase
@@ -129,24 +132,37 @@ class SimulationRequestResponse(BaseModel):
status: bool
data: Optional[SimulationRequestResponseData]
class SimulationQueueDict(BaseModel):
location_id: Dict[str, SimulationRequestResponseData]
class iCloudLocationData(BaseModel):
latitude: number
longitude: number
timestamp: string
class LocationSimulationState:
def __init__(self):
self.current_location: Optional[Dict[str, SimulationRequestResponseData]] = None
self.next_location: Optional[Dict[str, SimulationRequestResponseData]] = None
self.loc_id: Optional[str] = None
self.latitude: Optional[float] = None
self.longitude: Optional[float] = None
self.next_move: Optional[float] = None
self.udid: Optional[str] = None
self.simulation_active: bool = False
self.loc_id: Optional[str] = None
self.set_location_enabled: bool = True
self.queue: asyncio.Queue = asyncio.Queue()
self.queue_list: list[SimulationRequestResponseData] = []
self.queue_order: list[str] = []
self.queue_data: Dict = {}
self.queue_status: Optional[asyncio.Event] = asyncio.Event()
self.queue_state: str = "STOPPED"
self.test_mode: bool = True
self.test_mode: bool = False
self.simulation_task: Optional[asyncio.Task] = None
self.sio: socketio.AsyncServer = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
self.tunnel: Optional[RemoteServiceDiscoveryService] = None
self.fmf_queue: asyncio.Queue = asyncio.Queue
self.fmf_location: Optional[iCloudLocationData] = None
class TunneldRunnerSio:
@@ -325,10 +341,7 @@ class TunneldRunnerSio:
return
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,
# )
tun = await get_tun(self.context.udid)
logger.info("Simulation worker: tunnel acquired, connecting DVT provider")
dvt_provider = DvtProvider(tun)
@@ -369,6 +382,19 @@ class TunneldRunnerSio:
self.context.simulation_active = False
self.context.simulation_task = None
async def start_icloud_monitor()
"""Start Apple iCloud Find My Monitor to retreive actual reported device location"""
monitor = FindMyMonitor(apple_id, apple_pw, self.context.fmf_queue)
monitor_task = asyncio.create_task(monitor.run_monitor(interval=30))
while True:
updated_location = await self.context.fmf_queue.get()
if self.context.fmf_location !== updated_location:
self.context.fmf_location = update_location
self.context.sio.emit("fmf_update", updated_location, namespace="/",)
async def pause_simulation_queue():
"""Pauses asyncio.Queue playback"""
self.context.queue_state = "PAUSED"
@@ -390,23 +416,95 @@ class TunneldRunnerSio:
except asyncio.QueueEmpty:
break
async def end_simulation_queue() -> str:
def add_item(item_id, payload):
self.context.queue_data[item_id] = payload
self.context.queue_order.append(item_id)
def remove_item(item_id):
if item_id in self.context.queue_order:
self.context.queue_order.remove(item_id)
def get_item(item_id):
return self.context.queue_data[item_id]
def update_item(item_id, **updates):
if item_id in self.context.queue_data:
self.context.queue_data[item_id].update(updates)
def get_item_index(item_id):
return self.context.queue_order.index(item_id)
def get_item_id_by_index(index):
return self.context.queue_order[index]
def get_items_in_order():
return [self.context.queue_data[i] for i in self.context.queue_order]
async def end_simulation_queue() -> bool:
"""Ends asyncio.Queue playback and closes tunnel"""
logger.info("End location simulation request from %s", sid)
if self.context.simulation_task is not None and not self.context.simulation_task.done():
q = self.context.queue
if q.qsize() > 0:
await empty_simulation_queue()
while q.empty() and q.qsize() == 0:
try:
if self.context.test_mode:
q = self.context.queue
if q.qsize() > 0:
self.context.set_location_enabled = False
while not q.empty():
try:
item = q.get_nowait()
q.task_done()
logger.info("Discarding item from queue: %s", item)
except asyncio.QueueEmpty:
break
await q.join()
with suppress(asyncio.CancelledError):
await self.context.simulation_task
if self.context.tunnel is not None:
async with DvtProvider(self.context.tunnel) as dvt, LocationSimulation(dvt) as locate_simulation:
await locate_simulation.clear()
self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN"
return "ended"
# with suppress(asyncio.CancelledError):
# await self.context.simulation_task
self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN"
return True
if not self.context.test_mode:
if self.context.simulation_task is not None and not self.context.simulation_task.done():
q = self.context.queue
if q.qsize() > 0:
await empty_simulation_queue()
while q.empty() and q.qsize() == 0:
await q.join()
with suppress(asyncio.CancelledError):
await self.context.simulation_task
if self.context.tunnel is not None:
async with DvtProvider(self.context.tunnel) as dvt, LocationSimulation(dvt) as locate_simulation:
await locate_simulation.clear()
self.context.simulation_active = False
self.context.queue_state = "SHUTDOWN"
return True
except Exception as e:
logger.error(f"Error ending simulation queue: {e}")
return False
def get_status():
data = {
"current_location": self.context.current_location,
"next_location": self.context.next_location,
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_move": self.context.next_move,
"udid": self.context.udid,
"simulation_active": self.context.simulation_active,
"loc_id": self.context.loc_id,
"set_location_enable": self.context.set_location_enabled,
"queue_length": self.context.queue.qsize() if self.context.queue else 0,
"queue_state": self.context.queue_state,
"queue_order": self.context.queue_order,
"queue_data": self.context.queue_data,
"queue_status": self.context.queue_status.is_set() if self.context.queue_status else False,
"test_mode": self.context.test_mode,
"simulation_task": self.context.simulation_task.get_name() if self.context.simulation_task else None,
"tunnel": self.context.tunnel.service.address[0] if self.context.tunnel else None,
}
return data
""" FastAPI HTTP Functions"""
def generate_http_response(
@@ -646,27 +744,22 @@ class TunneldRunnerSio:
@self._app.get("/context-status")
async def app_context_status() -> fastapi.Response:
data = {
"latitude": self.context.latitude,
"longitude": self.context.longitude,
"next_more": self.context.next_move,
"udid": self.context.udid,
"simulation_active": self.context.simulation_active,
"loc_id": self.context.loc_id,
"set_location_enable": self.context.set_location_enabled,
"queue_state": self.context.queue_state,
"queue_list": self.context.queue_list
}
data = get_status()
return generate_http_response(data)
""" Socket.IO Functions"""
async def sio_send_status(sid):
""" Send Current Status"""
await self.context.sio.emit("status", get_status(), namespace="/", to=sid)
"""Socket.IO Connection Events"""
@self.context.sio.event
async def connect(sid, environ):
"""Client connection event handler."""
logger.info("Client connected: %s", sid)
return('%s connected' % sid)
await sio_send_status(sid)
return '%s connected' % sid
@self.context.sio.event
async def disconnect(sid):
@@ -680,8 +773,9 @@ 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="/")
logger.info("Received message from %s: %s", sid, data)
return True, "Message received"
# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/")
""" Device Control"""
@self.context.sio.event
@@ -708,83 +802,86 @@ class TunneldRunnerSio:
return { "command": command, "status": "error", "message": f"Invalid command: {command}" }
@self.context.sio.event
async def simulate_control(sid, data):
async def simulation_control(sid, data):
""" Simulation Control """
command = data.get("command") if isinstance(data, dict) else getattr(data, "command", None)
logger.info("Simulation Control command: %s requested from %s", command, sid)
match command:
case "add":
""" Add a location to the simulation queue"""
loc_id = str(uuid.uuid4())
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)
delay = 0 if delay is None else delay
if latitude is not None and longitude is not None:
logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude,
delay)
await self.context.queue.put((loc_id, latitude, longitude, delay))
if delay == 0:
start_time = datetime.now(timezone.utc).isoformat()
else:
try:
match command:
case "add":
""" Add a location to the simulation queue"""
loc_id = str(uuid.uuid4())
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)
delay = 0 if delay is None else delay
if latitude is not None and longitude is not None:
logger.info("Adding location %s (%s, %s) with %s delay to the queue", loc_id, latitude, longitude,
delay)
accrued_delay = 0
if self.context.queue_data:
accrued_delay = sum(item.get('delay', 0) for item in self.context.queue_data.values())
now_time = datetime.now(timezone.utc)
new_time = now_time + timedelta(seconds=delay)
new_time = now_time + timedelta(seconds=accrued_delay) + timedelta(seconds=delay)
start_time = new_time.isoformat()
location_item = {
loc_id: {
location_item = {
"loc_id": loc_id,
"latitude": latitude,
"longitude": longitude,
"delay": delay,
"start": start_time
}
}
ack = {
"command": command,
"status": "added",
"message": f"Location {loc_id} added to the queue",
"item": location_item
}
self.context.queue_list.append(location_item)
logger.info("Location %s added to the queue", loc_id)
return ack
else:
logger.warning("Invalid location data received from %s: %s", sid, data)
return {"command": command, "status": "error", "message": "Invalid location data"}
case "clear":
""" Clear the simulation queue"""
await empty_simulation_queue()
return {"command": command, "status": "cleared", "message": "Simulation cleared"}
case "pause":
""" Pause the simulation queue"""
await pause_simulation_queue()
return {"command": command, "status": "paused", "message": "Simulation paused"}
case "resume":
""" Resume the simulation queue"""
await resume_simulation_queue()
return {"command": command, "status": "resumed", "message": "Simulation resumed"}
case "end":
""" End the simulation queue"""
logger.info("End location simulation request from %s", sid)
end_task = asyncio.create_task(end_simulation_queue(), name="end-simulation-worker")
result = await end_task
return {"command": command, "status": result, "message": "Simulation ended"}
case "start":
""" Start the simulation queue"""
logger.info("Start location simulation request from %s", sid)
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_simulation_queue(),
name="location-simulation-worker",
)
return {"command": command, "status": "started", "message": "Simulation started"}
else:
return {"command": command, "status": "error", "message": "Simulation already running"}
case _:
logger.warning("Invalid command received from %s: %s", sid, command)
return {"status": "error", "message": "Invalid command"}
ack = {
"command": command,
"status": "added",
"message": f"Location {loc_id} added to the queue",
"item": location_item
}
await self.context.queue.put(loc_id)
add_item(loc_id, location_item)
logger.info("Location %s added to the queue", loc_id)
return ack
else:
logger.warning("Invalid location data received from %s: %s", sid, data)
return {"command": command, "status": "error", "message": "Invalid location data", "data": location_item}
case "clear":
""" Clear the simulation queue"""
await empty_simulation_queue()
return {"command": command, "status": "cleared", "message": "Simulation cleared"}
case "pause":
""" Pause the simulation queue"""
await pause_simulation_queue()
return {"command": command, "status": "paused", "message": "Simulation paused"}
case "resume":
""" Resume the simulation queue"""
await resume_simulation_queue()
return {"command": command, "status": "resumed", "message": "Simulation resumed"}
case "end":
""" End the simulation queue"""
logger.info("End location simulation request from %s", sid)
end_task = asyncio.create_task(end_simulation_queue(), name="end-simulation-worker")
result = await end_task
simstatus = not result
return {"command": command, "status": simstatus, "message": "Simulation ended"}
case "start":
""" Start the simulation queue"""
logger.info("Start location simulation request from %s", sid)
if self.context.simulation_task is None or self.context.simulation_task.done():
self.context.simulation_active = True
self.context.queue_state = "RUNNING"
self.context.simulation_task = asyncio.create_task(
start_simulation_queue(),
name="location-simulation-worker",
)
return {"command": command, "status": self.context.queue_state, "message": "Simulation started"}
else:
return {"command": command, "status": "error", "message": "Simulation already running"}
case _:
logger.warning("Invalid command received from %s: %s", sid, command)
return {"status": "error", "message": "Invalid command"}
finally:
await sio_send_status(sid)
""" Tunnel Control """
@self.context.sio.event
@@ -798,7 +895,7 @@ class TunneldRunnerSio:
try:
self._tunneld_core.start()
logger.info("Tunneld started successfully")
return {"status": "started", "message": "Tunneld started successfully"}
return {"status": "running", "message": "Tunneld started successfully"}
except Exception as e:
logger.error("Error starting tunneld: %s", e)
return {"command": command, "status": "error", "message": f"Error starting tunneld: {e}"}
@@ -855,9 +952,15 @@ class LocationSimulationQueue(LocationSimulation):
continue
if self.context.queue_state == "SHUTDOWN":
break
loc_id, latitude, longitude, delay = await self.context.queue.get()
if (loc_id, latitude, longitude, delay) == (None, None, None, None):
loc_id = await self.context.queue.get()
if loc_id == None:
break
location_item = self.context.queue_data.get(loc_id)
latitude = location_item.get("latitude")
longitude = location_item.get("longitude")
delay = location_item.get("delay")
delay = 0 if delay is None else delay
start_time = location_item.get("start_time")
if self.context.set_location_enabled:
if delay > 0 and not disable_sleep:
if timing_randomness_range > 0:
@@ -941,9 +1044,14 @@ class LocationSimulationTestQueue(LocationSimulationBase):
await asyncio.sleep(0.1)
if self.context.queue_state == "SHUTDOWN":
break
loc_id, latitude, longitude, delay = await self.context.queue.get()
if (loc_id, latitude, longitude, delay) == (None, None, None, None):
loc_id = await self.context.queue.get()
if loc_id == None:
break
location_item = self.context.queue_data.get(loc_id)
latitude = location_item.get("latitude")
longitude = location_item.get("longitude")
delay = location_item.get("delay")
start_time = location_item.get("start_time")
if self.context.set_location_enabled:
if delay > 0 and not disable_sleep:
if timing_randomness_range > 0:
@@ -969,6 +1077,7 @@ class LocationSimulationTestQueue(LocationSimulationBase):
self.context.longitude = longitude
self.context.loc_id = loc_id
await self.context.sio.emit(
"simulation_status",
{
"status": self.context.simulation_active,