diff --git a/drivers/graphics/fbcond/src/main.rs b/drivers/graphics/fbcond/src/main.rs index 56b23d39da..0139450fc7 100644 --- a/drivers/graphics/fbcond/src/main.rs +++ b/drivers/graphics/fbcond/src/main.rs @@ -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, diff --git a/drivers/graphics/fbcond/src/scheme.rs b/drivers/graphics/fbcond/src/scheme.rs index 1593757040..352ebec735 100644 --- a/drivers/graphics/fbcond/src/scheme.rs +++ b/drivers/graphics/fbcond/src/scheme.rs @@ -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 { diff --git a/drivers/graphics/fbcond/src/text.rs b/drivers/graphics/fbcond/src/text.rs index c7b234b003..58a34b0f73 100644 --- a/drivers/graphics/fbcond/src/text.rs +++ b/drivers/graphics/fbcond/src/text.rs @@ -21,10 +21,24 @@ pub struct TextScreen { scrollback: VecDeque>, /// 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, } 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 { // 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);