awlane | 1cec233 | 2025-04-24 17:24:47 -0500 | [diff] [blame^] | 1 | import os |
| 2 | import asyncio |
| 3 | import urllib |
| 4 | import msgpack |
| 5 | import secrets |
| 6 | import time |
| 7 | import webbrowser |
| 8 | from threading import Thread |
| 9 | |
| 10 | import websockets |
| 11 | |
| 12 | from mininet.log import error |
| 13 | from minindn.minindn_play.consts import Config, WSKeys |
| 14 | |
| 15 | class PlaySocket: |
| 16 | conn_list: dict = {} |
| 17 | executors: list = [] |
| 18 | AUTH_TOKEN: str | None = None |
| 19 | |
| 20 | def __init__(self): |
| 21 | """Initialize the PlaySocket. |
| 22 | This starts the background loop and creates the websocket server. |
| 23 | Calls to UI async functions are made from this class. |
| 24 | """ |
| 25 | self._set_auth_token() |
| 26 | self.loop = asyncio.new_event_loop() |
| 27 | Thread(target=self.loop.run_forever, args=(), daemon=True).start() |
| 28 | self.loop.call_soon_threadsafe(self.loop.create_task, self._run()) |
| 29 | |
| 30 | def add_executor(self, executor): |
| 31 | self.executors.append(executor) |
| 32 | |
| 33 | def send(self, websocket, msg): |
| 34 | """Send message to UI threadsafe""" |
| 35 | if websocket.state == websockets.protocol.State.OPEN: |
| 36 | self.loop.call_soon_threadsafe(self.loop.create_task, websocket.send(msg)) |
| 37 | |
| 38 | def send_all(self, msg): |
| 39 | """Send message to all UI threadsafe""" |
| 40 | for websocket in self.conn_list.copy(): |
| 41 | try: |
| 42 | self.send(websocket, msg) |
| 43 | except Exception as err: |
| 44 | error(f'Failed to send to {websocket.remote_address} with error {err}\n') |
| 45 | del self.conn_list[websocket] |
| 46 | |
| 47 | def _set_auth_token(self): |
| 48 | """Create auth token if it doesn't exist.""" |
| 49 | # Perist auth token so you don't need to refresh every time |
| 50 | # Check if AUTH_FILE was modified less than a day ago |
| 51 | if os.path.exists(Config.AUTH_FILE) and time.time() - os.path.getmtime(Config.AUTH_FILE) < 24 * 60 * 60: |
| 52 | with open(Config.AUTH_FILE, 'r') as f: |
| 53 | self.AUTH_TOKEN = f.read().strip() |
| 54 | |
| 55 | if not self.AUTH_TOKEN or len(self.AUTH_TOKEN) < 10: |
| 56 | self.AUTH_TOKEN = secrets.token_hex(16) |
| 57 | with open(Config.AUTH_FILE, 'w') as f: |
| 58 | f.write(self.AUTH_TOKEN) |
| 59 | |
| 60 | async def _run(self) -> None: |
| 61 | """Runs in separate thread from main""" |
| 62 | # Show the URL to the user |
| 63 | ws_url = f"ws://{Config.SERVER_HOST_URL}:{Config.SERVER_PORT}" |
| 64 | ws_url_q = urllib.parse.quote(ws_url.encode('utf8')) |
| 65 | full_url = f"{Config.PLAY_URL}/?minindn={ws_url_q}&auth={self.AUTH_TOKEN}" |
| 66 | print(f"Opened NDN Play GUI at {full_url}") |
| 67 | webbrowser.open(full_url, 1) |
| 68 | |
| 69 | # Start server |
| 70 | asyncio.set_event_loop(self.loop) |
| 71 | |
| 72 | server = await websockets.serve(self._serve, Config.SERVER_HOST, Config.SERVER_PORT) |
| 73 | await server.serve_forever() |
| 74 | |
| 75 | async def _serve(self, websocket): |
| 76 | """Handle websocket connection""" |
| 77 | |
| 78 | try: |
| 79 | path = websocket.request.path |
| 80 | auth = urllib.parse.parse_qs(urllib.parse.urlparse(path).query)['auth'][0] |
| 81 | if auth != self.AUTH_TOKEN: |
| 82 | raise Exception("Invalid auth token") |
| 83 | except Exception: |
| 84 | print(f"Rejected connection from {websocket.remote_address}") |
| 85 | await websocket.close() |
| 86 | return |
| 87 | |
| 88 | print(f"Accepted connection from {websocket.remote_address}") |
| 89 | self.conn_list[websocket] = 1 |
| 90 | while True: |
| 91 | try: |
| 92 | fcall = msgpack.loads(await websocket.recv()) |
| 93 | loop = asyncio.get_event_loop() |
| 94 | loop.create_task(self._call_fun(websocket, fcall)) |
| 95 | except websockets.exceptions.ConnectionClosedOK: |
| 96 | print(f"Closed connection gracefully from {websocket.remote_address}") |
| 97 | break |
| 98 | except websockets.exceptions.ConnectionClosedError: |
| 99 | print(f"Closed connection with error from {websocket.remote_address}") |
| 100 | break |
| 101 | |
| 102 | del self.conn_list[websocket] |
| 103 | |
| 104 | async def _call_fun(self, websocket, fcall): |
| 105 | """Call function and return result to UI asynchronously""" |
| 106 | |
| 107 | # Get function from any executor |
| 108 | fun = None |
| 109 | for executor in self.executors: |
| 110 | fun = getattr(executor, fcall[WSKeys.MSG_KEY_FUN], None) |
| 111 | if fun is not None: |
| 112 | break |
| 113 | |
| 114 | # Function not found |
| 115 | if fun is None: |
| 116 | error(f"Function {fcall[WSKeys.MSG_KEY_FUN]} not found\n") |
| 117 | return # function not found |
| 118 | |
| 119 | # Call function |
| 120 | res = await fun(*fcall[WSKeys.MSG_KEY_ARGS]) |
| 121 | if res is not None: |
| 122 | pack = msgpack.dumps({ |
| 123 | WSKeys.MSG_KEY_FUN: fcall[WSKeys.MSG_KEY_FUN], |
| 124 | WSKeys.MSG_KEY_RESULT: res, |
| 125 | }) |
| 126 | await websocket.send(pack) |