feat(fbcond): bidirectional serial console mirror for headless login

fbcond now mirrors all framebuffer-console output to the kernel debug (serial)
scheme AND feeds serial input back into the active VT's input queue. Previously,
once fbcond took over the console after display handoff the serial line went
silent (headless operators saw the boot stop at 'Performing handoff' and never
saw the getty login prompt, which draws only to the framebuffer VT). Now the
same console — boot log, login prompt, and shell — is fully usable over serial,
reusing the working framebuffer getty for both surfaces.
This commit is contained in:
Red Bear OS
2026-07-18 07:23:22 +09:00
parent 0120017484
commit e9342b78e9
3 changed files with 80 additions and 0 deletions
+41
View File
@@ -45,6 +45,20 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! {
)
.expect("fbcond: failed to subscribe to scheme events");
// Open the kernel debug (serial) scheme and subscribe to its readable
// events, so serial-line input can be fed into the console. Combined with
// the output mirror in TextScreen, this makes the framebuffer console fully
// usable over a serial line (headless login).
let serial_fd = libredox::call::open(
"/scheme/debug",
(libredox::flag::O_RDWR | libredox::flag::O_NONBLOCK) as _,
0,
)
.ok();
if let Some(fd) = serial_fd {
let _ = event_queue.subscribe(fd, VtIndex::SERIAL_SENTINEL, event::EventFlags::READ);
}
let mut state = SchemeState::new();
let mut scheme = FbconScheme::new(&vt_ids, &mut event_queue);
@@ -70,6 +84,33 @@ fn daemon(daemon: daemon::SchemeDaemon) -> ! {
for event in event_queue {
let event = event.expect("fbcond: failed to read event from event queue");
if let Some(fd) = serial_fd {
if event.user_data == VtIndex::SERIAL_SENTINEL {
let mut buf = [0u8; 256];
loop {
match libredox::call::read(fd, &mut buf) {
Ok(0) => break,
Ok(n) => {
for vt in scheme.vts.values_mut() {
vt.push_input_bytes(&buf[..n]);
}
}
Err(err) if err.errno() == EAGAIN => break,
Err(_) => break,
}
}
// Run a scheme pass so blocked getty reads get their fevent
// notification now that input is available.
handle_event(
&mut socket,
&mut scheme,
&mut state,
&mut blocked,
VtIndex::SCHEMA_SENTINEL,
);
continue;
}
}
handle_event(
&mut socket,
&mut scheme,
+3
View File
@@ -16,6 +16,9 @@ pub struct VtIndex(usize);
impl VtIndex {
pub const SCHEMA_SENTINEL: VtIndex = VtIndex(usize::MAX);
/// Event-queue sentinel for readable input on the kernel debug (serial)
/// scheme, used to feed serial-line input into the console.
pub const SERIAL_SENTINEL: VtIndex = VtIndex(usize::MAX - 1);
}
impl UserData for VtIndex {
+36
View File
@@ -21,10 +21,24 @@ pub struct TextScreen {
scrollback: VecDeque<Vec<u8>>,
/// Active keyboard layout. Defaults to US QWERTY.
pub keymap: Keymap,
/// Optional mirror of all rendered console output to the kernel serial/debug
/// scheme. Without this, once fbcond takes over the framebuffer console the
/// serial line goes silent — so a headless operator (QEMU `-serial`, a real
/// serial header) sees the boot stop right after "Performing handoff" and
/// never sees the `getty` login prompt (which is drawn to the framebuffer
/// VT). Mirroring makes the same console — boot log and login prompt —
/// visible on serial too.
serial_mirror: Option<std::fs::File>,
}
impl TextScreen {
pub fn new(display: Display) -> TextScreen {
// Open the kernel debug (serial) scheme for write-only mirroring. Best
// effort: if it is unavailable, console output simply isn't mirrored.
let serial_mirror = std::fs::OpenOptions::new()
.write(true)
.open("/scheme/debug")
.ok();
TextScreen {
display,
inner: console_draw::TextScreen::new(),
@@ -33,6 +47,7 @@ impl TextScreen {
pending_writes: Vec::new(),
scrollback: VecDeque::with_capacity(SCROLLBACK_LINES),
keymap: Keymap::us(),
serial_mirror,
}
}
@@ -94,6 +109,18 @@ impl TextScreen {
Ok(i)
}
/// Inject raw bytes into this console's input queue, exactly as if they had
/// been typed on the keyboard. Used to feed serial-line input (from the
/// kernel debug scheme) into the console so a headless operator can log in
/// over serial through the same getty that serves the framebuffer VT.
pub fn push_input_bytes(&mut self, bytes: &[u8]) {
for &b in bytes {
// Translate a bare CR (what most serial terminals send on Enter)
// into LF, which is what the tty/line-discipline and login expect.
self.input.push_back(if b == b'\r' { b'\n' } else { b });
}
}
pub fn write(&mut self, buf: &[u8]) -> Result<usize> {
// Retain output for scrollback (ring-buffered by line). Captured
// regardless of display-map state so the boot log before handoff is
@@ -105,6 +132,15 @@ impl TextScreen {
self.scrollback.pop_front();
}
// Mirror everything rendered to the framebuffer console onto the serial
// line so a headless operator sees the same output (boot log + the getty
// login prompt) instead of silence after the display handoff.
if let Some(serial) = &mut self.serial_mirror {
use std::io::Write;
let _ = serial.write_all(buf);
let _ = serial.flush();
}
if let Some(map) = &mut self.display.map {
Display::handle_resize(map, &mut self.inner);