use std::collections::VecDeque; use orbclient::{Event, EventOption}; use syscall::error::*; use crate::display::Display; use crate::keymap::Keymap; /// Number of most-recent output lines retained for scrollback, exposed via the /// `/scheme/fbcon//scrollback` read path. const SCROLLBACK_LINES: usize = 1000; pub struct TextScreen { pub display: Display, inner: console_draw::TextScreen, ctrl: bool, input: VecDeque, pending_writes: Vec>, /// Ring buffer of the most recent output lines (each ends with '\n'), /// capped at SCROLLBACK_LINES. 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, /// In-flight handoff: a worker performing the (blocking) open of the new /// display. See `handle_handoff`. pending_handoff: 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(), ctrl: false, input: VecDeque::new(), pending_writes: Vec::new(), scrollback: VecDeque::with_capacity(SCROLLBACK_LINES), keymap: Keymap::us(), serial_mirror, pending_handoff: None, } } /// Pick up a handoff started by `handle_handoff`, if the worker has finished. /// /// Never blocks. Called from the event loop, so the console keeps being /// serviced while the open is outstanding; writes made in the meantime are /// buffered in `pending_writes` and flushed once the display is mapped. pub fn poll_pending_handoff(&mut self) { let Some(rx) = &self.pending_handoff else { return; }; match rx.try_recv() { Ok(Ok(display_file)) => { self.pending_handoff = None; if self.display.install_display_file(display_file) { self.flush_pending_writes(); } else { log::warn!( "fbcond: handoff display opened but could not be mapped; \ framebuffer console stays on the previous display" ); } } Ok(Err(err)) => { self.pending_handoff = None; log::warn!( "fbcond: handoff open failed ({err}); framebuffer console stays \ on the previous display" ); } Err(std::sync::mpsc::TryRecvError::Empty) => {} Err(std::sync::mpsc::TryRecvError::Disconnected) => { self.pending_handoff = None; log::warn!("fbcond: handoff worker exited without a result"); } } } pub fn handle_handoff(&mut self) { log::info!("fbcond: Performing handoff"); if self.pending_handoff.is_some() { log::debug!("fbcond: handoff already in flight; ignoring repeat signal"); return; } // Resolving the path only talks to inputd and returns promptly; the // subsequent open of the graphics driver's scheme is what can block // indefinitely (O_NONBLOCK does not bound the scheme-open handshake, so // a driver that has registered its path but is not yet serving leaves us // waiting on it). fbcond is single-threaded and owns the console, so // doing that open inline stops us draining console writers -- getty, // login and the login shell then block on their writes, and if the // driver is itself waiting on the console the two deadlock and the whole // console dies right after this message. Hand the open to a worker and // keep serving the console; writes are buffered until it lands. let display_path = match self.display.display_path() { Ok(path) => path, Err(err) => { log::warn!("fbcond: could not resolve display path for handoff: {err}"); return; } }; let (tx, rx) = std::sync::mpsc::channel(); match std::thread::Builder::new() .name("fbcond-handoff".to_owned()) .spawn(move || { let _ = tx.send(inputd::ConsumerHandle::open_display_path_v2(&display_path)); }) { Ok(_) => self.pending_handoff = Some(rx), Err(err) => log::warn!("fbcond: could not spawn handoff worker: {err}"), } } pub fn input(&mut self, event: &Event) { let mut buf = vec![]; match event.to_option() { EventOption::Key(key_event) => { if key_event.scancode == 0x1D { self.ctrl = key_event.pressed; } else if key_event.pressed { let sc = key_event.scancode as u8; if let Some(seq) = self.keymap.lookup(sc) { buf.extend_from_slice(seq); } else { let c = match key_event.character { c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char, c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char, c => c, }; if c != '\0' { let mut b = [0; 4]; buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes()); } } } } _ => (), //TODO: Mouse in terminal } for &b in buf.iter() { // Translate a bare CR (what the Enter key produces on most keymaps) // to LF, matching the serial input path (push_input_bytes). Without // this, line-oriented readers such as a shell's read_line never see // a completed line from keyboard input and appear to hang. self.input.push_back(if b == b'\r' { b'\n' } else { b }); } } pub fn can_read(&self) -> bool { !self.input.is_empty() } } impl TextScreen { pub fn read(&mut self, buf: &mut [u8]) -> Result { let mut i = 0; while i < buf.len() && !self.input.is_empty() { buf[i] = self.input.pop_front().unwrap(); i += 1; } 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 // also scrollable. for line in buf.split_inclusive(|&b| b == b'\n') { self.scrollback.push_back(line.to_vec()); } while self.scrollback.len() > SCROLLBACK_LINES { 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); let damage = self.inner.write(map, buf, &mut self.input); self.display.sync_rect(damage); } else { log::warn!( "fbcond: TextScreen::write() called while display map is None; \ buffering {} bytes ({} pending chunks). Will flush after handoff.", buf.len(), self.pending_writes.len() + 1, ); self.pending_writes.push(buf.to_vec()); } Ok(buf.len()) } /// Return the retained scrollback buffer (oldest line first) as a flat byte /// stream. Read via the `/scheme/fbcon//scrollback` handle. pub fn read_scrollback(&self) -> Vec { let mut result = Vec::new(); for line in &self.scrollback { result.extend_from_slice(line); } result } pub fn flush_pending_writes(&mut self) { if self.pending_writes.is_empty() { return; } let count = self.pending_writes.len(); log::info!( "fbcond: Flushing {} pending write chunks after display handoff", count, ); if let Some(map) = &mut self.display.map { for pending in self.pending_writes.drain(..) { Display::handle_resize(map, &mut self.inner); let damage = self.inner.write(map, &pending, &mut self.input); map.dirty_fb(damage).unwrap(); } } else { log::error!( "fbcond: flush_pending_writes called but display map is still None; \ keeping {} buffered chunks", self.pending_writes.len(), ); } } }