summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTEXT.md36
-rw-r--r--rsnix/src/fs.rs8
-rw-r--r--rsnix/src/klog.rs130
-rw-r--r--rsnix/src/lib.rs1
-rw-r--r--rsnix/src/serial.rs11
-rw-r--r--shell/src/lib.rs64
6 files changed, 226 insertions, 24 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index c6bd5f9..86822d1 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -255,6 +255,34 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target:
255 monitor (`--sock`, US layout, named keys for punctuation; bare 255 monitor (`--sock`, US layout, named keys for punctuation; bare
256 `-`/`.`/`/` are not valid sendkey names) 256 `-`/`.`/`/` are not valid sendkey names)
257 257
258✅ **`klog` kernel log ring** — a RAM ring of the last 128 serial log
259 lines (128 chars each, ~16 KB), fed by `serial_println!` (one choke
260 point — every boot/low-level message captured for free). Sequence
261 numbers per line + wrap/drop accounting; a dump header reports how
262 many lines were lost to wrap. Exposed as the `/dev/klog` char
263 device; the shell `klog` command is a thin alias for `cat
264 /dev/klog` (same filters/redirection), so `klog -t 5`, `klog -g
265 fat`, and `klog WRITING /disk/…/boot.log` (persists the boot log)
266 all work. The ring never allocates (const-constructible static);
267 only the dump allocates, at shell time. `klog` rejects positional
268 args (usage) and input keywords ("does not accept input").
269
270 **Design consequence: shell output must not feed the ring.** The
271 plan's "hook `serial_println!`" only works if that macro is
272 kernel-logging-only — but the shell printed prompts/errors/results
273 through it, which would have flooded the boot log with echo/cat
274 output (a `cat /dev/mem` would wrap the ring). The shell now
275 writes user-facing output (banner, prompt, `term_print`/
276 `term_println`, pipeline sink, halt/poweroff) directly to the
277 serial port via `serial::write_str`; tty echo already did. Only
278 kernel messages (rsnix modules + `rsdos/main.rs`) go through
279 `serial_println!` now. Verified in QEMU: 28 boot lines captured
280 (seq 0..27, first `=== rsnix boot ===`, last `boot: entering
281 shell`), `echo hello` output absent from the ring, `klog WRITING`
282 persists a boot log (mtype-verified), wrap behavior verified with a
283 temporary 140-line boot loop (128 kept / 40 dropped, seq 40..167,
284 oldest live line correct — loop reverted after the test).
285
258## Known issues / perf notes 286## Known issues / perf notes
259 287
260- **Scroll cost** — scrolling still shifts the whole framebuffer (a 288- **Scroll cost** — scrolling still shifts the whole framebuffer (a
@@ -289,14 +317,6 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target:
289 317
290## Pending decisions (agreed, not yet implemented) 318## Pending decisions (agreed, not yet implemented)
291 319
292- **`klog` kernel log ring**: capture all boot/low-level messages by
293 appending to a RAM ring buffer inside `serial_println!` (one choke
294 point, every existing message captured for free). Ring: 128 lines ×
295 128 chars ≈ 16 KB, sequence numbers, wrap/drop accounting.
296- **`klog` command** = thin alias for `cat /dev/klog` plus filters
297 `-h` (head) and `-t` (tail); no log levels, no timestamps, nothing
298 fancier. `/dev/klog` is a char device so `klog WRITING /disk/…/boot.log`
299 persists a boot log via the existing redirection.
300- **Disk recognition scope — keep simple for now**: no pre-EBS UEFI 320- **Disk recognition scope — keep simple for now**: no pre-EBS UEFI
301 block-device census, no GPT/MBR partition parsing, no NVMe driver. 321 block-device census, no GPT/MBR partition parsing, no NVMe driver.
302 (Revisit later: Linux-style recognition = PCI scan → ahci/nvme → 322 (Revisit later: Linux-style recognition = PCI scan → ahci/nvme →
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs
index a71e51a..063560a 100644
--- a/rsnix/src/fs.rs
+++ b/rsnix/src/fs.rs
@@ -41,6 +41,7 @@ enum CharDevice {
41 Pci, 41 Pci,
42 Version, 42 Version,
43 Meminfo, 43 Meminfo,
44 Klog,
44 Null, 45 Null,
45 Zero, 46 Zero,
46 Console, 47 Console,
@@ -141,6 +142,12 @@ pub fn init() -> (usize, usize) {
141 NodeData::Char(CharDevice::Meminfo), 142 NodeData::Char(CharDevice::Meminfo),
142 ); 143 );
143 add( 144 add(
145 "klog",
146 Some(dev),
147 FileType::Char,
148 NodeData::Char(CharDevice::Klog),
149 );
150 add(
144 "null", 151 "null",
145 Some(dev), 152 Some(dev),
146 FileType::Char, 153 FileType::Char,
@@ -650,6 +657,7 @@ fn dev_text(dev: CharDevice, _pos: u64) -> String {
650 CharDevice::Pci => pci_listing(), 657 CharDevice::Pci => pci_listing(),
651 CharDevice::Version => version_text(), 658 CharDevice::Version => version_text(),
652 CharDevice::Meminfo => meminfo_text(), 659 CharDevice::Meminfo => meminfo_text(),
660 CharDevice::Klog => crate::klog::dump(),
653 CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(), 661 CharDevice::Null | CharDevice::Console | CharDevice::Serial => String::new(),
654 CharDevice::Zero => String::new(), 662 CharDevice::Zero => String::new(),
655 } 663 }
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 @@
1// ---------------------------------------------------------------------------
2// klog — kernel log ring buffer.
3//
4// A RAM ring of the last MAX_LINES serial log lines, fed by
5// `serial_println!` (one choke point, so every boot/low-level message is
6// captured for free). Exposed to the shell as the `/dev/klog` char
7// device, so `klog` (alias for `cat /dev/klog`) shows the boot sequence
8// and `klog WRITING /disk/.../boot.log` persists it via normal
9// redirection.
10//
11// Line-based: the ring stores whole lines up to MAX_LINE bytes; longer
12// lines are truncated. Each line gets a sequence number; when the ring
13// wraps it drops the oldest line and counts it, so a dump can report how
14// much history was lost. The ring never allocates (const-constructible
15// static), so it works from the very first log line; only `dump()`
16// allocates, and that is called at shell time.
17// ---------------------------------------------------------------------------
18
19extern crate alloc;
20
21use alloc::string::String;
22
23pub const MAX_LINES: usize = 128;
24pub const MAX_LINE: usize = 128;
25
26struct Ring {
27 /// Sequence counter for the next line to be appended (total ever).
28 seq: u64,
29 /// Lines dropped because the ring was full (wrap overwrites).
30 dropped: u64,
31 /// Live lines currently in the ring (0..=MAX_LINES).
32 count: usize,
33 /// Slot the next line will be written to (oldest once full).
34 next: usize,
35 lens: [u8; MAX_LINES],
36 bytes: [[u8; MAX_LINE]; MAX_LINES],
37}
38
39static mut RING: Ring = Ring {
40 seq: 0,
41 dropped: 0,
42 count: 0,
43 next: 0,
44 lens: [0; MAX_LINES],
45 bytes: [[0; MAX_LINE]; MAX_LINES],
46};
47
48fn ring() -> &'static mut Ring {
49 unsafe { &mut *core::ptr::addr_of_mut!(RING) }
50}
51
52/// Append one line to the ring (line-oriented; newlines are stripped).
53/// Truncated at MAX_LINE bytes. Never allocates, never logs.
54pub fn append(line: &str) {
55 let r = ring();
56 let b = line.as_bytes();
57 let n = b.len().min(MAX_LINE);
58 if r.count == MAX_LINES {
59 r.dropped += 1; // overwrite the oldest line
60 } else {
61 r.count += 1;
62 }
63 r.bytes[r.next][..n].copy_from_slice(&b[..n]);
64 r.lens[r.next] = n as u8;
65 r.next = (r.next + 1) % MAX_LINES;
66 r.seq += 1;
67}
68
69/// Append the formatted output of a `serial_println!` — the macro hook.
70/// `fmt::Write` may deliver the line in several chunks, so accumulate
71/// into one line buffer first, then append the whole line.
72pub fn append_fmt(args: core::fmt::Arguments<'_>) {
73 let mut line = LineBuf::new();
74 let _ = core::fmt::write(&mut line, args);
75 append(line.as_str());
76}
77
78struct LineBuf {
79 bytes: [u8; MAX_LINE],
80 len: usize,
81}
82
83impl LineBuf {
84 fn new() -> Self {
85 LineBuf {
86 bytes: [0; MAX_LINE],
87 len: 0,
88 }
89 }
90 fn as_str(&self) -> &str {
91 core::str::from_utf8(&self.bytes[..self.len]).unwrap_or("")
92 }
93}
94
95impl core::fmt::Write for LineBuf {
96 fn write_str(&mut self, s: &str) -> core::fmt::Result {
97 let n = s.len().min(MAX_LINE - self.len);
98 self.bytes[self.len..self.len + n].copy_from_slice(&s.as_bytes()[..n]);
99 self.len += n;
100 Ok(())
101 }
102}
103
104/// The whole log as text: a header with the wrap/drop accounting, then
105/// one line per entry, each prefixed with its sequence number.
106pub fn dump() -> String {
107 let r = ring();
108 let mut out = String::new();
109 if r.count == 0 {
110 out += "klog: no messages logged yet\n";
111 return out;
112 }
113 let first = r.seq - r.count as u64;
114 let last = r.seq - 1;
115 out += &alloc::format!(
116 "klog: {} line(s) kept, {} dropped on wrap, seq {}..{}\n",
117 r.count,
118 r.dropped,
119 first,
120 last
121 );
122 let start = (r.next + MAX_LINES - r.count) % MAX_LINES;
123 for i in 0..r.count {
124 let idx = (start + i) % MAX_LINES;
125 let n = r.lens[idx] as usize;
126 let text = core::str::from_utf8(&r.bytes[idx][..n]).unwrap_or("");
127 out += &alloc::format!("{:06} {}\n", first + i as u64, text);
128 }
129 out
130}
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index fc823ea..a2d41da 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -8,6 +8,7 @@ pub mod font;
8pub mod fs; 8pub mod fs;
9pub mod gop; 9pub mod gop;
10pub mod interrupts; 10pub mod interrupts;
11pub mod klog;
11pub mod memory; 12pub mod memory;
12pub mod paging; 13pub mod paging;
13pub mod pci; 14pub mod pci;
diff --git a/rsnix/src/serial.rs b/rsnix/src/serial.rs
index 03fa570..484a114 100644
--- a/rsnix/src/serial.rs
+++ b/rsnix/src/serial.rs
@@ -91,12 +91,21 @@ macro_rules! serial_print {
91 }}; 91 }};
92} 92}
93 93
94/// Prints a line to serial and appends it to the kernel log ring
95/// (klog — see /dev/klog). Every kernel-side message funnels through
96/// here, so the ring captures the whole boot sequence for free.
97/// User-facing shell output must NOT use this macro (shell output would
98/// pollute the boot log); it writes to the serial port directly.
94#[macro_export] 99#[macro_export]
95macro_rules! serial_println { 100macro_rules! serial_println {
96 () => { $crate::serial::write_str("\r\n"); }; 101 () => {
102 $crate::serial::write_str("\r\n");
103 $crate::klog::append_fmt(core::format_args!(""));
104 };
97 ($($arg:tt)*) => {{ 105 ($($arg:tt)*) => {{
98 $crate::serial_print!($($arg)*); 106 $crate::serial_print!($($arg)*);
99 $crate::serial::write_str("\r\n"); 107 $crate::serial::write_str("\r\n");
108 $crate::klog::append_fmt(core::format_args!($($arg)*));
100 }}; 109 }};
101} 110}
102 111
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index cf46cb0..ddd8688 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -28,7 +28,7 @@ use alloc::collections::VecDeque;
28use alloc::string::{String, ToString}; 28use alloc::string::{String, ToString};
29use alloc::vec::Vec; 29use alloc::vec::Vec;
30use rsnix::tty::{self, LineInput}; 30use rsnix::tty::{self, LineInput};
31use rsnix::{gop_print, gop_println, serial_print, serial_println}; 31use rsnix::{gop_print, gop_println};
32 32
33/// Run the shell forever. 33/// Run the shell forever.
34pub fn run() -> ! { 34pub fn run() -> ! {
@@ -36,18 +36,17 @@ pub fn run() -> ! {
36 "rs-dos v{} — type 'help' for commands", 36 "rs-dos v{} — type 'help' for commands",
37 env!("CARGO_PKG_VERSION") 37 env!("CARGO_PKG_VERSION")
38 ); 38 );
39 serial_println!( 39 term_println(&alloc::format!(
40 "========== rs-dos v{} ==========", 40 "========== rs-dos v{} ==========",
41 env!("CARGO_PKG_VERSION") 41 env!("CARGO_PKG_VERSION")
42 ); 42 ));
43 43
44 loop { 44 loop {
45 // Push any deferred scrolls so the prompt renders on a current 45 // Push any deferred scrolls so the prompt renders on a current
46 // framebuffer (e.g. after a streaming command's batch of output). 46 // framebuffer (e.g. after a streaming command's batch of output).
47 rsnix::vt::sync_scroll(); 47 rsnix::vt::sync_scroll();
48 // Print prompt 48 // Print prompt
49 gop_print!("> "); 49 term_print("> ");
50 serial_print!("> ");
51 50
52 // Read one line from keyboard (tty canonical mode) 51 // Read one line from keyboard (tty canonical mode)
53 match tty::read_line() { 52 match tty::read_line() {
@@ -65,13 +64,26 @@ pub fn run() -> ! {
65} 64}
66 65
67// --------------------------------------------------------------------------- 66// ---------------------------------------------------------------------------
68// Diagnostics go straight to terminal + serial, never through the filter 67// Diagnostics and command output go straight to terminal + serial, never
69// pipeline and never into a file sink. 68// through the filter pipeline and never into a file sink. These helpers
69// write to the serial port directly — NOT via `serial_println!`, which
70// feeds the klog ring — so user-facing shell output (prompts, echoes,
71// command results) never lands in the boot log.
70// --------------------------------------------------------------------------- 72// ---------------------------------------------------------------------------
71 73
74fn term_print(s: &str) {
75 gop_print!("{}", s);
76 rsnix::serial::write_str(s);
77}
78
79fn term_println(s: &str) {
80 term_print(s);
81 gop_println!();
82 rsnix::serial::write_str("\r\n");
83}
84
72fn term_err(msg: &str) { 85fn term_err(msg: &str) {
73 gop_println!("{}", msg); 86 term_println(msg);
74 serial_println!("{}", msg);
75} 87}
76 88
77// --------------------------------------------------------------------------- 89// ---------------------------------------------------------------------------
@@ -541,8 +553,7 @@ impl Pipeline {
541 fn emit(&mut self, l: &str) { 553 fn emit(&mut self, l: &str) {
542 match &self.sink { 554 match &self.sink {
543 OutSink::Term => { 555 OutSink::Term => {
544 gop_println!("{}", l); 556 term_println(l);
545 serial_println!("{}", l);
546 } 557 }
547 OutSink::File { .. } => {} 558 OutSink::File { .. } => {}
548 } 559 }
@@ -672,6 +683,7 @@ write `{} <path> TIL <marker>`",
672 "pci" => cmd_pci(&parsed), 683 "pci" => cmd_pci(&parsed),
673 "ls" => cmd_ls(&parsed), 684 "ls" => cmd_ls(&parsed),
674 "cat" => cmd_cat(&parsed), 685 "cat" => cmd_cat(&parsed),
686 "klog" => cmd_klog(&parsed),
675 "hexdump" => cmd_hexdump(&parsed), 687 "hexdump" => cmd_hexdump(&parsed),
676 "mkdir" => cmd_mkdir(&parsed), 688 "mkdir" => cmd_mkdir(&parsed),
677 "touch" => cmd_touch(&parsed), 689 "touch" => cmd_touch(&parsed),
@@ -700,6 +712,7 @@ fn cmd_help(_p: &Parsed) {
700 pci — list PCI devices 712 pci — list PCI devices
701 ls — list a directory 713 ls — list a directory
702 cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) 714 cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc)
715 klog — kernel boot log (cat /dev/klog; WRITING <dst> persists it)
703 hexdump — hex-dump a file or input stream 716 hexdump — hex-dump a file or input stream
704 mkdir — create a directory 717 mkdir — create a directory
705 touch — create an empty file 718 touch — create an empty file
@@ -789,6 +802,29 @@ fn cmd_ls(p: &Parsed) {
789} 802}
790 803
791// --------------------------------------------------------------------------- 804// ---------------------------------------------------------------------------
805// klog — kernel log (thin alias for `cat /dev/klog`)
806// ---------------------------------------------------------------------------
807
808fn cmd_klog(p: &Parsed) {
809 if !p.args.is_empty() {
810 term_err("klog: usage: klog [-h N] [-t N] [-g PAT] [-p] [WRITING <dst> | TO <dst>]");
811 return;
812 }
813 // Alias for `cat /dev/klog` — same filters and redirection, so
814 // `klog -t 5` shows the last 5 log lines and `klog WRITING
815 // /disk/.../boot.log` persists the boot log. Input keywords were
816 // already rejected (klog does not accept input).
817 let q = Parsed {
818 cmd: alloc::string::String::from("klog"),
819 args: alloc::vec!["/dev/klog".into()],
820 input: Input::None,
821 filters: p.filters.clone(),
822 output: p.output.clone(),
823 };
824 cmd_cat(&q);
825}
826
827// ---------------------------------------------------------------------------
792// cat — file and stream consumers 828// cat — file and stream consumers
793// --------------------------------------------------------------------------- 829// ---------------------------------------------------------------------------
794 830
@@ -1471,8 +1507,7 @@ fn cmd_stress(p: &Parsed) {
1471} 1507}
1472 1508
1473fn cmd_halt() -> ! { 1509fn cmd_halt() -> ! {
1474 gop_println!("halting..."); 1510 term_println("halting...");
1475 serial_println!("halting...");
1476 rsnix::power::halt() 1511 rsnix::power::halt()
1477} 1512}
1478 1513
@@ -1515,7 +1550,6 @@ fn cmd_pci(p: &Parsed) {
1515} 1550}
1516 1551
1517fn cmd_poweroff() -> ! { 1552fn cmd_poweroff() -> ! {
1518 gop_println!("shutting down..."); 1553 term_println("shutting down...");
1519 serial_println!("shutting down...");
1520 rsnix::power::poweroff() 1554 rsnix::power::poweroff()
1521} 1555}