blob: 9faad8c684c33286a1d5cead839a04cc1e0ca224 [file] [log] [blame]
awlane1cec2332025-04-24 17:24:47 -05001import os
2import logging
3import msgpack
4import fcntl
5import struct
6import termios
7import random
8import shutil
9
10from contextlib import redirect_stdout, redirect_stderr
11from code import InteractiveConsole
12
13from mininet.net import Mininet
14from mininet.cli import CLI
15from minindn.util import is_valid_hostid, host_home, getPopen
16from minindn.minindn_play.consts import WSKeys, WSFunctions
17from minindn.minindn_play.socket import PlaySocket
18from minindn.minindn_play.term.pty_manager import Pty, PtyManager
19
20class TermExecutor:
21 pty_list: dict[str, Pty] = {}
22 pty_manager: PtyManager
23
24 def __init__(self, net: Mininet, socket: PlaySocket):
25 self.net = net
26 self.socket = socket
27 self.pty_manager = PtyManager(self)
28
29 def start_cli(self):
30 """UI Function: Start CLI"""
31 # Send logs to UI
32 class WsCliHandler():
33 def __init__(self, parent: TermExecutor):
34 self.parent = parent
35
36 def write(self, msg: str):
37 if "cli" not in self.parent.pty_list:
38 return
39 mb = msg.encode("utf-8")
40 self.parent.send_pty_out(mb, "cli")
41 self.parent.pty_list["cli"].buffer.write(mb)
42
43 lg = logging.getLogger("mininet")
44 handler = logging.StreamHandler(WsCliHandler(self))
45 handler.terminator = ""
46 lg.addHandler(handler)
47
48 # Create pty for cli
49 cpty = Pty(self, "cli", "Mininet CLI")
50 self.pty_manager.register(cpty)
51
52 # Start cli
53 CLI.use_rawinput = False
54 CLI(self.net, stdin=os.fdopen(cpty.slave, "r"), stdout=os.fdopen(cpty.slave, "w"))
55
56 def start_repl(self):
57 """UI Function: Start REPL"""
58
59 cpty = Pty(self, "repl", "Python REPL")
60 self.pty_manager.register(cpty)
61
62 try:
63 with os.fdopen(cpty.slave, "w") as fout, os.fdopen(cpty.slave, "r") as fin, redirect_stdout(fout), redirect_stderr(fout):
64 def raw_input(prompt="") -> str:
65 print(prompt, end="", flush=True)
66 return fin.readline()
67 repl = InteractiveConsole({
68 "net": self.net,
69 })
70 repl.raw_input = raw_input
71 repl.interact(None, None)
72 except OSError:
73 pass
74
75 async def open_all_ptys(self):
76 """UI Function: Open all ptys currently active"""
77 for key in self.pty_list:
78 cpty = self.pty_list[key]
79 self.socket.send_all(msgpack.dumps({
80 WSKeys.MSG_KEY_FUN: WSFunctions.OPEN_TERMINAL,
81 WSKeys.MSG_KEY_RESULT: self._open_term_response(cpty)
82 }))
83
84 async def open_term(self, nodeId: str):
85 """UI Function: Open new bash terminal"""
86 if not is_valid_hostid(self.net, nodeId):
87 return
88
89 # Get directory of node
90 node_home = host_home(self.net[nodeId])
91
92 # Copy .bashrc to node
93 if node_home is not None:
94 path = os.path.expanduser("~/.bashrc")
95 if os.path.isfile(path):
96 # Do this copy every time to make sure the file is up to date
97 target = node_home + "/.bashrc"
98 shutil.copy(path, target)
99
100 # Append extra commands
101 with open(target, "a") as f:
102 # Shell prompt
103 f.write("\nexport PS1='\\[\\033[01;32m\\]\\u@{}\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\$ '\n".format(nodeId))
104
105 # Create pty
106 pty_id = nodeId + str(int(random.random() * 100000))
107 pty_name = f"bash [{nodeId}]"
108 cpty = Pty(self, pty_id, pty_name)
109 self.pty_manager.register(cpty)
110
111 # Start bash
112 cpty.process = getPopen(self.net[nodeId], "bash --noprofile -i", stdin=cpty.slave, stdout=cpty.slave, stderr=cpty.slave)
113
114 return self._open_term_response(cpty)
115
116 async def pty_in(self, pty_id: str, msg: msgpack.ExtType):
117 """UI Function: Send input to pty"""
118 if pty_id not in self.pty_list:
119 return
120
121 if pty_id == "cli" and msg.data == b"\x03":
122 # interrupt
123 for node in self.net.hosts:
124 if node.waiting:
125 node.sendInt()
126
127 self.pty_list[pty_id].stdin.write(msg.data)
128 self.pty_list[pty_id].stdin.flush()
129
130 async def pty_resize(self, pty_id, rows, cols):
131 """UI Function: Resize pty"""
132 if pty_id not in self.pty_list:
133 return
134
135 winsize = struct.pack("HHHH", rows, cols, 0, 0)
136 fcntl.ioctl(self.pty_list[pty_id].master, termios.TIOCSWINSZ, winsize)
137
138 def send_pty_out(self, msg: bytes, pty_id: str):
139 """Send output to UI"""
140 self.socket.send_all(msgpack.dumps({
141 WSKeys.MSG_KEY_FUN: WSFunctions.PTY_OUT,
142 WSKeys.MSG_KEY_ID: pty_id,
143 WSKeys.MSG_KEY_RESULT: msg,
144 }))
145
146 def _open_term_response(self, cpty: Pty):
147 """Return response for open terminal"""
148 return { "id": cpty.id, "name": cpty.name, "buf": cpty.buffer.read() }