From c7540c31d45dac68fa1852f5d25645d569b2da9d Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 14:09:14 +0200 Subject: klog: kernel log ring + /dev/klog + klog shell command - rsnix/src/klog.rs: RAM ring of the last 128 serial log lines (128 chars each), fed by serial_println! so every boot/low-level message is captured for free. Sequence numbers per line, wrap/drop accounting; the ring never allocates (const-constructible static). - /dev/klog char device; shell 'klog' is a thin alias for cat /dev/klog with the same filters/redirection, so 'klog -t 5', 'klog -g fat' and 'klog WRITING /disk/.../boot.log' all work. klog rejects positional args (usage) and input keywords. - Shell user-facing output (banner, prompt, term_print/term_println, pipeline sink, halt/poweroff) now writes to serial directly instead of through serial_println!, keeping echo/cat output out of the boot log (a streaming cat /dev/mem would otherwise wrap the ring). - serial_println! macro hooks klog::append_fmt via format_args! (no alloc at call sites); klog::dump formats the log for the device. Verified in QEMU: 28 boot lines captured (seq 0..27), shell output absent from the ring, klog WRITING persists a boot log (mtype-checked), wrap verified with a temporary 140-line boot loop (128 kept / 40 dropped, seq 40..167, oldest-live correct; loop reverted); full test-disk round trip still fsck-clean. --- rsnix/src/klog.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 rsnix/src/klog.rs (limited to 'rsnix/src/klog.rs') diff --git a/rsnix/src/klog.rs b/rsnix/src/klog.rs new file mode 100644 index 0000000..1e23460 --- /dev/null +++ b/rsnix/src/klog.rs @@ -0,0 +1,130 @@ +// --------------------------------------------------------------------------- +// 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; +pub const MAX_LINE: usize = 128; + +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, + lens: [u8; MAX_LINES], + bytes: [[u8; MAX_LINE]; MAX_LINES], +} + +static mut RING: Ring = Ring { + seq: 0, + dropped: 0, + count: 0, + next: 0, + 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 (line-oriented; newlines are stripped). +/// Truncated at MAX_LINE bytes. Never allocates, never logs. +pub fn append(line: &str) { + let r = ring(); + 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.bytes[r.next][..n].copy_from_slice(&b[..n]); + r.lens[r.next] = n as u8; + r.next = (r.next + 1) % MAX_LINES; + r.seq += 1; +} + +/// Append the formatted output of a `serial_println!` — the macro hook. +/// `fmt::Write` may deliver the line in several chunks, so accumulate +/// into one line buffer first, then append the whole line. +pub fn append_fmt(args: core::fmt::Arguments<'_>) { + let mut line = LineBuf::new(); + let _ = core::fmt::write(&mut line, args); + append(line.as_str()); +} + +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(""); + out += &alloc::format!("{:06} {}\n", first + i as u64, text); + } + out +} -- cgit v1.2.3