// --------------------------------------------------------------------------- // klog — kernel log ring buffer. // // A RAM ring of the last MAX_LINES serial log lines, fed by // `serial_println!` (one choke point, so every boot/low-level message is // captured for free). Exposed to the shell as the `/dev/klog` char // device, so `klog` (alias for `cat /dev/klog`) shows the boot sequence // and `klog WRITING /disk/.../boot.log` persists it via normal // redirection. // // Line-based: the ring stores whole lines up to MAX_LINE bytes; longer // lines are truncated. Each line gets a sequence number; when the ring // wraps it drops the oldest line and counts it, so a dump can report how // much history was lost. The ring never allocates (const-constructible // static), so it works from the very first log line; only `dump()` // allocates, and that is called at shell time. // --------------------------------------------------------------------------- extern crate alloc; use alloc::string::String; pub const MAX_LINES: usize = 128; /// Max bytes stored per line. Wide enough that no real line truncates /// at any plausible terminal width (a 240-col 4K framebuffer row is /// still under this) — long lines flow in full and the terminal wraps /// them at the screen edge instead of cutting them off. pub const MAX_LINE: usize = 512; struct Ring { /// Sequence counter for the next line to be appended (total ever). seq: u64, /// Lines dropped because the ring was full (wrap overwrites). dropped: u64, /// Live lines currently in the ring (0..=MAX_LINES). count: usize, /// Slot the next line will be written to (oldest once full). next: usize, /// Highest timestamp seen, so the log stays monotonic even before /// the timer is running (early-boot PIT reads are uninitialised). last_ms: u64, ms: [u64; MAX_LINES], lens: [u16; MAX_LINES], bytes: [[u8; MAX_LINE]; MAX_LINES], } static mut RING: Ring = Ring { seq: 0, dropped: 0, count: 0, next: 0, last_ms: 0, ms: [0; MAX_LINES], lens: [0; MAX_LINES], bytes: [[0; MAX_LINE]; MAX_LINES], }; fn ring() -> &'static mut Ring { unsafe { &mut *core::ptr::addr_of_mut!(RING) } } /// Append one line to the ring with its timestamp in ms (line-oriented; /// newlines are stripped). Truncated at MAX_LINE bytes. Never /// allocates, never logs. Timestamps are clamped to be monotonic. pub fn append(line: &str, mut ms: u64) { let r = ring(); if ms > r.last_ms { r.last_ms = ms; } else { ms = r.last_ms; } let b = line.as_bytes(); let n = b.len().min(MAX_LINE); if r.count == MAX_LINES { r.dropped += 1; // overwrite the oldest line } else { r.count += 1; } r.ms[r.next] = ms; r.bytes[r.next][..n].copy_from_slice(&b[..n]); r.lens[r.next] = n as u16; r.next = (r.next + 1) % MAX_LINES; r.seq += 1; } /// Format one serial line, write it to the serial port, and append it /// to the ring — all under the scheduler lock, so a line from one task /// never interleaves with another task's line (line atomicity). /// `serial_println!` funnels through here. pub fn emit(args: core::fmt::Arguments<'_>) { let mut line = LineBuf::new(); let _ = core::fmt::write(&mut line, args); let ms = crate::interrupts::uptime_ms(); crate::sched::lock(); crate::serial::write_raw(line.as_str()); crate::serial::write_raw("\r\n"); append(line.as_str(), ms); crate::sched::unlock(); } struct LineBuf { bytes: [u8; MAX_LINE], len: usize, } impl LineBuf { fn new() -> Self { LineBuf { bytes: [0; MAX_LINE], len: 0, } } fn as_str(&self) -> &str { core::str::from_utf8(&self.bytes[..self.len]).unwrap_or("") } } impl core::fmt::Write for LineBuf { fn write_str(&mut self, s: &str) -> core::fmt::Result { let n = s.len().min(MAX_LINE - self.len); self.bytes[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]); self.len += n; Ok(()) } } /// The whole log as text: a header with the wrap/drop accounting, then /// one line per entry, each prefixed with its sequence number. pub fn dump() -> String { let r = ring(); let mut out = String::new(); if r.count == 0 { out += "klog: no messages logged yet\n"; return out; } let first = r.seq - r.count as u64; let last = r.seq - 1; out += &alloc::format!( "klog: {} line(s) kept, {} dropped on wrap, seq {}..{}\n", r.count, r.dropped, first, last ); let start = (r.next + MAX_LINES - r.count) % MAX_LINES; for i in 0..r.count { let idx = (start + i) % MAX_LINES; let n = r.lens[idx] as usize; let text = core::str::from_utf8(&r.bytes[idx][..n]).unwrap_or(""); let s = r.ms[idx] / 1000; let m = r.ms[idx] % 1000; out += &alloc::format!("[{:04}.{:03}] {:06} {}\n", s, m, first + i as u64, text); } out }