tahamajs's picture
download
raw
8.66 kB
import asyncio
import json
import time
import socket
import traceback
from typing import Dict, Callable, Optional, Tuple
from .mouse_controller import MouseController
from .notification_manager import send_notification
from .config import CONFIG
class AirMouseTCPServer:
def __init__(self, log_callback, stats_callback, connections_callback=None, move_callback=None):
self.host = CONFIG.get("host", "0.0.0.0")
self.port = CONFIG.get("port", 9090)
self.mouse = MouseController()
self.log = log_callback or (lambda msg, level="info": None)
self.stats_callback = stats_callback
self.connections_callback = connections_callback
self.move_callback = move_callback
self._server = None
self._tasks: Dict[asyncio.Task, Tuple[str, int]] = {} # task -> (ip, port)
self.active_connections: Dict[Tuple[str, int], dict] = {}
# ---------- Public API ----------
async def start(self):
retry_delay = 2
while True:
try:
self._server = await asyncio.start_server(
self._handle_client, self.host, self.port
)
self.log(f"🚀 TCP server listening on {self.host}:{self.port}", "info")
async with self._server:
await self._server.serve_forever()
break
except OSError as e:
self.log(f"❌ Failed to start server: {e}. Retrying in {retry_delay}s...", "error")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, 30)
async def stop(self):
if self._server:
self._server.close()
await self._server.wait_closed()
# Cancel all client tasks
for task in list(self._tasks.keys()):
task.cancel()
await asyncio.gather(*self._tasks.keys(), return_exceptions=True)
self._tasks.clear()
self.active_connections.clear()
self.log("🛑 TCP server stopped", "info")
def disconnect_client(self, addr: Tuple[str, int]):
"""Request a synchronous disconnect for `addr` from calling thread.
Prefer scheduling `await close_connection(addr)` on the server loop for async safety.
"""
info = self.active_connections.get(addr)
if not info:
return
writer = info.get('writer')
try:
# best-effort close; caller may be outside event loop
writer.close()
except Exception:
pass
async def close_connection(self, addr: Tuple[str, int]):
"""Asynchronously close and cleanup a specific active connection.
Safe to call from the server's asyncio loop or via `asyncio.run_coroutine_threadsafe`.
"""
info = self.active_connections.get(addr)
if not info:
return
writer = info.get('writer')
try:
writer.close()
try:
await writer.wait_closed()
except Exception:
pass
except Exception:
pass
# final cleanup; remove from active map and update UI
try:
del self.active_connections[addr]
except KeyError:
pass
self.log(f"🔌 Server closed connection {addr[0]}:{addr[1]}", "info")
self._update_connections_list()
def _update_connections_list(self):
if self.connections_callback:
addrs = []
for addr, info in self.active_connections.items():
name = info.get('name', 'Unknown')
uptime = int(time.time() - info['start_time'])
idle = int(time.time() - info['last_activity'])
addrs.append(f"{name} | {addr[0]}:{addr[1]} | up {uptime}s | idle {idle}s")
self.connections_callback(addrs)
# ---------- Client handler ----------
async def _handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
addr = writer.get_extra_info('peername')
if not addr:
self.log("⚠️ Connection with no address rejected", "warning")
writer.close()
return
start_time = time.time()
client_info = {
'writer': writer,
'start_time': start_time,
'bytes_sent': 0,
'bytes_recv': 0,
'last_activity': start_time,
'name': None
}
self.active_connections[addr] = client_info
self.log(f"✅ Connected: {addr[0]}:{addr[1]}", "info")
send_notification("Client connected", f"{addr[0]}:{addr[1]} connected")
self._update_connections_list()
# Start watchdog with 60s timeout
watchdog_task = asyncio.create_task(self._connection_watchdog(addr, writer, timeout=60))
try:
while True:
try:
data = await asyncio.wait_for(reader.readline(), timeout=10.0)
except asyncio.TimeoutError:
continue
except asyncio.CancelledError:
break
if not data:
break
client_info['last_activity'] = time.time()
client_info['bytes_recv'] += len(data)
try:
msg = json.loads(data.decode().strip())
except json.JSONDecodeError:
self.log(f"⚠️ Invalid JSON from {addr[0]}:{addr[1]}", "warning")
continue
# Process message
await self._process_message(msg, writer, addr, client_info)
except (ConnectionResetError, BrokenPipeError, OSError):
self.log(f"🔌 Connection reset by {addr[0]}:{addr[1]}", "info")
except Exception as e:
self.log(f"❌ Unhandled error with {addr[0]}:{addr[1]}: {e}", "error")
traceback.print_exc()
finally:
watchdog_task.cancel()
try:
del self.active_connections[addr]
except KeyError:
pass
writer.close()
await writer.wait_closed()
uptime = int(time.time() - start_time)
self.log(f"🔌 Disconnected: {addr[0]}:{addr[1]} (after {uptime}s)", "info")
send_notification("Client disconnected", f"{addr[0]}:{addr[1]} disconnected after {uptime}s")
self._update_connections_list()
async def _connection_watchdog(self, addr, writer, timeout=60):
while True:
await asyncio.sleep(timeout)
info = self.active_connections.get(addr)
if info and time.time() - info['last_activity'] > timeout:
self.log(f"🕒 Watchdog triggered for {addr[0]}:{addr[1]}, disconnecting", "warning")
writer.close()
break
# ---------- Message processing ----------
async def _process_message(self, msg: dict, writer, addr, client_info):
t = msg.get('type')
if t == 'move':
dx, dy = msg.get('dx', 0.0), msg.get('dy', 0.0)
self.mouse.move(dx, dy)
if self.move_callback:
self.move_callback(dx, dy)
elif t == 'hello':
name = msg.get('name', 'Unknown')
client_info['name'] = name
self.log(f"🖥️ Client {addr[0]}:{addr[1]} identified as '{name}'", "info")
self._update_connections_list()
elif t in ('click', 'doubleclick', 'rightclick', 'scroll'):
# Handle click/scroll with ACK
if t == 'click':
self.mouse.click()
elif t == 'doubleclick':
self.mouse.double_click()
elif t == 'rightclick':
self.mouse.click(button='right')
elif t == 'scroll':
self.mouse.scroll(msg.get('delta', 0))
await self._send_ack(msg.get('id'), writer)
self.log(f"🖱️ {t} from {addr[0]}:{addr[1]}", "info")
else:
self.log(f"⚠️ Unknown message type '{t}' from {addr[0]}:{addr[1]}", "warning")
self.stats_callback(self.mouse.get_stats())
async def _send_ack(self, msg_id, writer):
if msg_id is not None:
ack = json.dumps({'type': 'ack', 'id': msg_id})
writer.write(ack.encode() + b'\n')
await writer.drain()
# Update bytes sent (approximate)
for addr, info in self.active_connections.items():
if info['writer'] is writer:
info['bytes_sent'] += len(ack) + 1
break

Xet Storage Details

Size:
8.66 kB
·
Xet hash:
eefec5ca233dd38d76d9f59e6bc2f77830072784e6fb0795bb12c55102b7250d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.