blob: 69e054a0453512c79d38161e55ef82c3270767b7 [file] [log] [blame]
awlane1cec2332025-04-24 17:24:47 -05001import os
2import asyncio
3import urllib
4import msgpack
5import secrets
6import time
7import webbrowser
8from threading import Thread
9
10import websockets
11
12from mininet.log import error
13from minindn.minindn_play.consts import Config, WSKeys
14
15class 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)