1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 04:36:48 +00:00

feat(console): the serial console page on the panel's bar; a taken-over user learns by whom

The serial page had a white strip of inline styles with an untranslated
status and a browser-default button above the terminal, sent input while
disconnected, sized the terminal only on window load and resize, and left
the keyboard elsewhere after the key menu. When a superuser opened a
console open in another window, its user saw "[An administrator opened
this console]" and a plain "disconnected".

- console-xterm.html shares the VNC page's bar: status badge (Connecting,
  Connected, Disconnected, the server's refusal), Disconnect, then
  Reconnect (a new page for a new token). The script is
  static/js/console_serial.js; the token reaches it as json_script data.
- The terminal takes the keyboard on connect and after the key menu; input
  is sent only while connected; it fits its box on connect and whenever
  the box changes size (ResizeObserver).
- A console that writes nothing yet gets a dim local hint: press Enter.
- socketiod sends the displaced client "taken_over" with the superuser's
  username; the page shows "Taken over by <name>" in the bar and the
  terminal. The superuser is not asked first (owner's decision).
- Fullscreen uses the shared console_keys.js handler (keyboard lock).
This commit is contained in:
catborise 2026-10-27 00:31:36 +03:00
parent 0532471759
commit 2b54198965
7 changed files with 125 additions and 98 deletions

View file

@ -83,8 +83,8 @@ class LiveSerialConsoleTestCase(TransactionTestCase):
with self.assertRaises(socketio.exceptions.ConnectionError):
console(self.port, token)
admin_client, admin_received = console(self.port, self.admin_token(token))
wait_for(lambda: "An administrator opened this console" in "".join(received["output"]))
self.assertIn("An administrator opened this console", "".join(received["output"]))
wait_for(lambda: received["taken_over"])
self.assertEqual(received["taken_over"], ["serial-live-admin"])
client.disconnect()
# the VM stops: the terminal says so
dom.destroy()

View file

@ -154,13 +154,17 @@ class SocketiodTestCase(Base):
admin = get_user_model().objects.create_superuser("root", "root@example.com", "pw")
self.d.connect("owner-tab", {}, {"token": self.token()})
old = self.d.sessions["owner-tab"]
with patch.object(self.d.sio, "emit") as emit, patch.object(self.d.sio, "disconnect") as disconnect:
killed_at_emit = []
with patch.object(self.d.sio, "emit", side_effect=lambda *a, **k: killed_at_emit.append(self.d.os.kill.call_count)) as emit, \
patch.object(self.d.sio, "disconnect") as disconnect:
self.assertIsNone(self.d.connect("admin-tab", {}, {"token": self.token(admin)}))
self.assertEqual(list(self.d.sessions), ["admin-tab"])
self.assertFalse(old["open"])
# the old worker was killed before the new one started: libvirt gives the console to one
self.assertEqual(self.killed_before_fork, [0, 1])
self.assertIn("An administrator opened this console", emit.call_args.args[1]["output"])
emit.assert_called_once_with("taken_over", {"by": "root"}, to="owner-tab")
# told before its worker was killed: the old page's forward disconnects it then
self.assertEqual(killed_at_emit, [0])
disconnect.assert_called_once_with("owner-tab")
@override_settings(SERIAL_CONSOLE_ENABLED=True)

View file

@ -87,8 +87,9 @@ def start_socketiod(test):
def console(port, token):
"""A connected client and what it receives until the server disconnects it."""
client = socketio.Client(reconnection=False)
received = {"output": [], "gone": threading.Event()}
received = {"output": [], "taken_over": [], "gone": threading.Event()}
client.on("pty_output", lambda data: received["output"].append(data["output"]))
client.on("taken_over", lambda data: received["taken_over"].append(data["by"]))
client.on("disconnect", lambda *args: received["gone"].set())
# the same origin as the server, as the page through nginx
url = "http://127.0.0.1:%d" % port

View file

@ -126,6 +126,10 @@ class ConsoleViewsTestCase(TestCase):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "console-xterm.html")
# the page's credential for socketiod, as data
signed = json.loads(html.fromstring(response.content).get_element_by_id("serial-token").text)
self.assertEqual(TimestampSigner(salt="console.novnc").unsign(signed),
f"{self.compute.id}:{self.instance.uuid}:{self.admin_user.id}")
@override_settings(SERIAL_CONSOLE_ENABLED=False)
@patch("console.views.wvmInstance")