Skip to main content

strat9_kernel/arch/x86_64/
serial.rs

1use core::{
2    fmt,
3    sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
4};
5use spin::Mutex;
6use uart_16550::SerialPort;
7
8/// Global serial port instance
9static SERIAL1: Mutex<SerialPort> = Mutex::new(unsafe { SerialPort::new(0x3F8) });
10
11/// Fixed-size buffer for kernel cmdline (up to 2KB).
12/// SAFETY: Written once during early boot (single-threaded, IRQs disabled),
13/// then read-only. Safe for concurrent reads after initialization.
14static CMDLINE_BUF: [u8; 2048] = [0; 2048];
15static CMDLINE_LEN: AtomicUsize = AtomicUsize::new(0);
16static CMDLINE_READY: AtomicBool = AtomicBool::new(false);
17
18/// Flag indicating if the kernel is in a panic state.
19/// When true, serial output bypasses all locks to ensure messages are displayed.
20static PANIC_IN_PROGRESS: AtomicBool = AtomicBool::new(false);
21static BOOT_LOG_PREFIX_ENABLED: AtomicBool = AtomicBool::new(false);
22static SERIAL_AT_LINE_START: AtomicBool = AtomicBool::new(true);
23
24/// Raw spinlock for `_print_force` to prevent multi-core character interleaving.
25/// Uses a ticket-style test-and-set: 0 = free, 1 = locked.
26///
27/// **Interrupt safety**: `force_lock_acquire` saves and disables IRQs before
28/// spinning, and `force_lock_release` restores them. This prevents a nested
29/// timer IRQ on the same CPU from trying to acquire `FORCE_LOCK` while it is
30/// already held by an outer `serial_force_println!` call, which would deadlock.
31static FORCE_LOCK: AtomicU8 = AtomicU8::new(0);
32
33#[inline(always)]
34fn force_lock_acquire() -> u64 {
35    // Disable IRQs before spinning to prevent a timer IRQ on this CPU from
36    // re-entering _print_force while FORCE_LOCK is held (nested IRQ deadlock).
37    let saved = crate::arch::x86_64::save_flags_and_cli();
38    while FORCE_LOCK
39        .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
40        .is_err()
41    {
42        core::hint::spin_loop();
43    }
44    saved
45}
46
47#[inline(always)]
48fn force_lock_release(saved_flags: u64) {
49    FORCE_LOCK.store(0, Ordering::Release);
50    // Restore RFLAGS (re-enables IRQs if they were enabled before the acquire).
51    crate::arch::x86_64::restore_flags(saved_flags);
52}
53
54const ANSI_RESET: &str = "\x1b[0m";
55const ANSI_RED: &str = "\x1b[31m";
56const ANSI_GREEN: &str = "\x1b[32m";
57const ANSI_VIOLET: &str = "\x1b[35m";
58const TOKEN_BUF_CAP: usize = 64;
59
60/// Signal that the kernel has entered an emergency panic state.
61pub fn enter_emergency_mode() {
62    PANIC_IN_PROGRESS.store(true, Ordering::SeqCst);
63}
64
65/// Enable or disable Linux-style boot timestamps at the beginning of each line.
66pub fn set_boot_log_prefix_enabled(enabled: bool) {
67    BOOT_LOG_PREFIX_ENABLED.store(enabled, Ordering::SeqCst);
68    SERIAL_AT_LINE_START.store(true, Ordering::SeqCst);
69}
70
71/// Returns whether token char.
72#[inline]
73fn is_token_char(b: u8) -> bool {
74    b.is_ascii_alphanumeric() || b == b'_'
75}
76
77/// Returns whether hex word.
78#[inline]
79fn is_hex_word(s: &str) -> bool {
80    if s.len() <= 2 {
81        return false;
82    }
83    let bytes = s.as_bytes();
84    if bytes[0] != b'0' || (bytes[1] != b'x' && bytes[1] != b'X') {
85        return false;
86    }
87    bytes[2..].iter().all(|b| b.is_ascii_hexdigit())
88}
89
90struct AnsiStylingWriter<'a, W: fmt::Write> {
91    inner: &'a mut W,
92    in_escape: bool,
93    token_buf: [u8; TOKEN_BUF_CAP],
94    token_len: usize,
95    token_passthrough: bool,
96}
97
98impl<'a, W: fmt::Write> AnsiStylingWriter<'a, W> {
99    /// Creates a new instance.
100    fn new(inner: &'a mut W) -> Self {
101        Self {
102            inner,
103            in_escape: false,
104            token_buf: [0u8; TOKEN_BUF_CAP],
105            token_len: 0,
106            token_passthrough: false,
107        }
108    }
109
110    /// Performs the flush token operation.
111    fn flush_token(&mut self) -> fmt::Result {
112        if self.token_len == 0 {
113            return Ok(());
114        }
115        let token = unsafe { core::str::from_utf8_unchecked(&self.token_buf[..self.token_len]) };
116        if token == "PASS" {
117            self.inner.write_str(ANSI_GREEN)?;
118            self.inner.write_str(token)?;
119            self.inner.write_str(ANSI_RESET)?;
120        } else if token == "FAIL" {
121            self.inner.write_str(ANSI_RED)?;
122            self.inner.write_str(token)?;
123            self.inner.write_str(ANSI_RESET)?;
124        } else if is_hex_word(token) {
125            self.inner.write_str(ANSI_VIOLET)?;
126            self.inner.write_str(token)?;
127            self.inner.write_str(ANSI_RESET)?;
128        } else {
129            self.inner.write_str(token)?;
130        }
131        self.token_len = 0;
132        Ok(())
133    }
134
135    /// Writes byte raw.
136    fn write_byte_raw(&mut self, b: u8) -> fmt::Result {
137        self.inner.write_char(b as char)
138    }
139
140    /// Performs the finish operation.
141    fn finish(&mut self) -> fmt::Result {
142        self.flush_token()
143    }
144}
145
146impl<W: fmt::Write> fmt::Write for AnsiStylingWriter<'_, W> {
147    /// Writes str.
148    fn write_str(&mut self, s: &str) -> fmt::Result {
149        for &b in s.as_bytes() {
150            if self.in_escape {
151                self.write_byte_raw(b)?;
152                if (b as char).is_ascii_alphabetic() {
153                    self.in_escape = false;
154                }
155                continue;
156            }
157
158            if b == 0x1b {
159                self.flush_token()?;
160                self.token_passthrough = false;
161                self.in_escape = true;
162                self.write_byte_raw(b)?;
163                continue;
164            }
165
166            if is_token_char(b) {
167                if self.token_passthrough {
168                    self.write_byte_raw(b)?;
169                    continue;
170                }
171                if self.token_len < TOKEN_BUF_CAP {
172                    self.token_buf[self.token_len] = b;
173                    self.token_len += 1;
174                } else {
175                    self.flush_token()?;
176                    self.token_passthrough = true;
177                    self.write_byte_raw(b)?;
178                }
179            } else {
180                self.flush_token()?;
181                self.token_passthrough = false;
182                self.write_byte_raw(b)?;
183            }
184        }
185        Ok(())
186    }
187}
188
189struct BootPrefixWriter<'a, W: fmt::Write> {
190    inner: &'a mut W,
191    line_start: bool,
192    prefix_enabled: bool,
193}
194
195impl<'a, W: fmt::Write> BootPrefixWriter<'a, W> {
196    fn new(inner: &'a mut W) -> Self {
197        Self {
198            inner,
199            line_start: SERIAL_AT_LINE_START.load(Ordering::Relaxed),
200            prefix_enabled: BOOT_LOG_PREFIX_ENABLED.load(Ordering::Relaxed),
201        }
202    }
203
204    fn write_prefix(&mut self) -> fmt::Result {
205        if !self.prefix_enabled {
206            return Ok(());
207        }
208        let elapsed_us = crate::arch::x86_64::boot_timestamp::elapsed_us();
209        let secs = elapsed_us / 1_000_000;
210        let micros = elapsed_us % 1_000_000;
211        write!(self.inner, "[{:>5}.{:06}] ", secs, micros)
212    }
213
214    fn finish(&mut self) {
215        SERIAL_AT_LINE_START.store(self.line_start, Ordering::Relaxed);
216    }
217}
218
219impl<W: fmt::Write> fmt::Write for BootPrefixWriter<'_, W> {
220    fn write_str(&mut self, s: &str) -> fmt::Result {
221        for ch in s.chars() {
222            if self.line_start && ch != '\n' {
223                self.write_prefix()?;
224                self.line_start = false;
225            }
226            self.inner.write_char(ch)?;
227            if ch == '\n' {
228                self.line_start = true;
229            }
230        }
231        Ok(())
232    }
233}
234
235/// Initialize the serial port
236pub fn init() {
237    SERIAL1.lock().init();
238}
239
240/// Parse kernel cmdline from Limine boot arguments.
241///
242/// `ptr` is a pointer to a null-terminated C string provided by the bootloader.
243/// `len` is the length of the cmdline string (including the null terminator).
244///
245/// This function:
246/// - Stores the cmdline globally for `/proc/cmdline` access.
247/// - Detects `console=ttyS0,baud` parameters and logs the configuration.
248pub unsafe fn parse_cmdline(ptr: u64, len: u64) {
249    if ptr == 0 || len == 0 {
250        return;
251    }
252
253    // Convert C string to Rust &str and copy into static buffer.
254    let cstr = core::ffi::CStr::from_ptr(ptr as *const core::ffi::c_char);
255    let cmdline = cstr.to_str().unwrap_or("");
256
257    let copy_len = cmdline.len().min(2047);
258    // SAFETY: Single-threaded early boot, IRQs disabled. No concurrent access.
259    let buf_ptr = CMDLINE_BUF.as_ptr() as *mut u8;
260    core::ptr::copy_nonoverlapping(cmdline.as_ptr(), buf_ptr, copy_len);
261    CMDLINE_LEN.store(copy_len, Ordering::Release);
262    CMDLINE_READY.store(true, Ordering::Release);
263
264    // Parse console parameters.
265    let cmdline_str =
266        core::str::from_utf8_unchecked(core::slice::from_raw_parts(buf_ptr, copy_len));
267
268    let mut has_serial_console = false;
269    let mut baud: Option<u32> = None;
270
271    let mut pos = 0;
272    while pos < cmdline_str.len() {
273        while pos < cmdline_str.len() && cmdline_str.as_bytes()[pos].is_ascii_whitespace() {
274            pos += 1;
275        }
276        if pos >= cmdline_str.len() {
277            break;
278        }
279        let start = pos;
280        while pos < cmdline_str.len() && !cmdline_str.as_bytes()[pos].is_ascii_whitespace() {
281            pos += 1;
282        }
283        let token = &cmdline_str[start..pos];
284
285        if let Some(value) = token.strip_prefix("console=") {
286            if value.starts_with("ttyS0") {
287                has_serial_console = true;
288                if let Some((_, baud_str)) = value.split_once(',') {
289                    if let Ok(b) = baud_str.parse::<u32>() {
290                        baud = Some(b);
291                    }
292                }
293            }
294        }
295    }
296
297    if has_serial_console {
298        if let Some(b) = baud {
299            crate::serial_force_println!("[cmdline] console=ttyS0,{}", b);
300        } else {
301            crate::serial_force_println!("[cmdline] console=ttyS0 (115200 baud)");
302        }
303    } else {
304        crate::serial_force_println!("[cmdline] no serial console detected");
305    }
306}
307
308/// Returns the stored kernel cmdline for `/proc/cmdline`.
309pub fn get_cmdline() -> &'static str {
310    if !CMDLINE_READY.load(Ordering::Acquire) {
311        return "";
312    }
313    let len = CMDLINE_LEN.load(Ordering::Acquire);
314    // SAFETY: CMDLINE_READY guarantees CMDLINE_BUF has been written.
315    unsafe { core::str::from_utf8_unchecked(&CMDLINE_BUF[..len]) }
316}
317
318/// Print to serial port
319#[doc(hidden)]
320pub fn _print(args: fmt::Arguments) {
321    use core::fmt::Write;
322
323    // Check if we are in emergency panic mode.
324    if PANIC_IN_PROGRESS.load(Ordering::Relaxed) {
325        // SAFETY: In emergency mode, we bypass the mutex to ensure output.
326        // We re-initialize a local SerialPort instance pointing to the same IO port.
327        let mut port = unsafe { SerialPort::new(0x3F8) };
328        // We don't use AnsiStylingWriter here to minimize risk of further panics/complex logic.
329        let _ = port.write_fmt(args);
330        return;
331    }
332
333    // Normal mode: Use try_lock to avoid deadlock in interrupt handlers.
334    if let Some(mut port) = SERIAL1.try_lock() {
335        let mut prefix_writer = BootPrefixWriter::new(&mut *port);
336        let mut writer = AnsiStylingWriter::new(&mut prefix_writer);
337        let _ = writer.write_fmt(args);
338        let _ = writer.finish();
339        prefix_writer.finish();
340    }
341}
342
343/// Print to serial port bypassing the shared mutex.
344///
345/// Uses a dedicated raw spinlock (with IRQs disabled) so that multiple CPUs
346/// cannot interleave their output at the character level, and so that a timer
347/// IRQ firing on the same CPU while this function is in progress cannot cause
348/// a deadlock by trying to re-acquire `FORCE_LOCK`.
349#[doc(hidden)]
350pub fn _print_force(args: fmt::Arguments) {
351    use core::fmt::Write;
352
353    // Check if we are in emergency panic mode.
354    if PANIC_IN_PROGRESS.load(Ordering::Relaxed) {
355        // SAFETY: In emergency mode, we bypass the lock to ensure output.
356        let mut port = unsafe { SerialPort::new(0x3F8) };
357        let _ = port.write_fmt(args);
358        return;
359    }
360
361    // Acquire the raw force-lock (saves + clears IF, then spins until free).
362    let saved_flags = force_lock_acquire();
363    // SAFETY: We hold `FORCE_LOCK` with IRQs disabled, giving exclusive UART access.
364    let mut port = unsafe { SerialPort::new(0x3F8) };
365    let mut prefix_writer = BootPrefixWriter::new(&mut port);
366    let _ = prefix_writer.write_fmt(args);
367    prefix_writer.finish();
368    // Release lock and restore RFLAGS (re-enables IRQs if they were on before).
369    force_lock_release(saved_flags);
370}
371
372/// Print to serial port
373#[macro_export]
374macro_rules! serial_print {
375    ($($arg:tt)*) => {
376        $crate::arch::x86_64::serial::_print(format_args!($($arg)*))
377    };
378}
379
380/// Print to serial port with newline
381#[macro_export]
382macro_rules! serial_println {
383    () => ($crate::serial_print!("\n"));
384    ($($arg:tt)*) => ($crate::serial_print!("{}\n", format_args!($($arg)*)));
385}
386
387/// Print to serial port with newline, bypassing the shared mutex.
388#[macro_export]
389macro_rules! serial_force_println {
390    () => ($crate::arch::x86_64::serial::_print_force(format_args!("\n")));
391    ($($arg:tt)*) => ($crate::arch::x86_64::serial::_print_force(format_args!("{}\n", format_args!($($arg)*))));
392}