1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/static/js/console_serial.js
catborise 2b54198965 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).
2026-10-27 08:12:33 +03:00

89 lines
2.9 KiB
JavaScript

// The serial console page: xterm.js on socketiod (Socket.IO behind nginx's
// /socket.io/), driven from the panel's bar: status, keys, reconnect.
import { bindFullscreen, bindKeyAction } from './console_keys.js';
const status = document.getElementById('status');
const connectionButton = document.getElementById('connection_button');
// the page's signed token is the credential, good for two minutes: a new
// connection takes a new page (Reconnect), not an automatic retry
const token = JSON.parse(document.getElementById('serial-token').textContent);
const socket = io({ transports: ['websocket', 'polling'], auth: { token }, reconnection: false });
const term = new Terminal({ cursorBlink: true });
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById('terminal'));
function send(input) {
if (socket.connected) {
socket.emit('pty_input', { input });
}
}
function resize() {
fitAddon.fit();
if (socket.connected) {
socket.emit('resize', { cols: term.cols, rows: term.rows });
}
}
function showStatus(text, color) {
status.textContent = text;
status.className = 'badge text-bg-' + color;
connectionButton.textContent = socket.connected ? connectionButton.dataset.disconnect : connectionButton.dataset.reconnect;
}
let output = false;
let takenBy = null;
// typed and pasted input
term.onData(send);
socket.on('pty_output', (data) => {
output = true;
term.write(data.output);
});
socket.on('connect', () => {
showStatus(status.dataset.connected, 'success');
resize();
term.focus();
// a serial console shows nothing until the VM writes
setTimeout(() => {
if (!output && socket.connected) {
term.write('\x1b[2m' + status.dataset.hint + '\x1b[0m\r\n');
}
}, 1500);
});
// a superuser opened this console: the server ends this session next
socket.on('taken_over', (data) => {
takenBy = data.by;
});
socket.on('disconnect', () => {
if (takenBy) {
const text = status.dataset.takenOver.replace('%(name)s', takenBy);
term.write('\r\n[' + text + ']\r\n');
showStatus(text, 'danger');
} else {
showStatus(status.dataset.disconnected, 'danger');
}
});
// the server says why: serial console off, an expired link, the console open elsewhere
socket.on('connect_error', (err) => showStatus(err.message, 'danger'));
connectionButton.addEventListener('click', () => {
if (socket.connected) {
socket.disconnect();
} else {
window.location.reload();
}
});
const keys = { ctrlc: '\x03', ctrld: '\x04', ctrlz: '\x1a', ctrlbackslash: '\x1c', sendtab: '\t', sendesc: '\x1b' };
for (const [id, input] of Object.entries(keys)) {
bindKeyAction(id, () => send(input));
}
bindFullscreen();
// back to the terminal after the menu
document.addEventListener('hidden.bs.dropdown', () => term.focus());
new ResizeObserver(resize).observe(document.getElementById('terminal'));