danos/system/kernel/log.zig

117 lines
4.9 KiB
Zig

//! The kernel's multi-sink **diagnostic** log — the machine-readable stream of
//! what the kernel is doing, separate from any user-facing display.
//!
//! Output is a *diagnostic convenience, never a correctness dependency* — the
//! kernel must boot and run correctly with zero output channels. So logging fans
//! out to a set of registered **sinks**, each best-effort and self-guarding: the
//! serial UART, the 0xE9 debug console, and — later — a file on a ramdisk/USB/SSD.
//! A message reaches whatever channels exist; if none do, the kernel runs on,
//! silent but correct.
//!
//! The **framebuffer is deliberately not a sink here.** It's a separate output
//! surface (a bootstrap text console today, a graphics device driver later), so
//! the log never assumes the machine is text-based. `main.zig` mirrors a few
//! user-facing status lines and panics to it explicitly; the verbose log does not.
//!
//! No allocation: the sink table is fixed, so the log works before the heap is up
//! and inside a panic. Two channels don't go through the sink list because they
//! must survive even a total-output failure: `checkpoint` (a one-byte POST code)
//! and `recordPanic` (a breadcrumb in a fixed record).
const std = @import("std");
const architecture = @import("architecture");
pub const SinkFn = *const fn ([]const u8) void;
const maximum_sinks = 8;
var sinks: [maximum_sinks]SinkFn = undefined;
var sink_count: usize = 0;
/// Register an output sink. Every registered sink receives every message; sinks
/// must be self-guarding (safe to call when their device is absent).
pub fn addSink(sink: SinkFn) void {
if (sink_count < maximum_sinks) {
sinks[sink_count] = sink;
sink_count += 1;
}
}
/// Fan `bytes` out to every registered sink.
pub fn write(bytes: []const u8) void {
for (sinks[0..sink_count]) |sink| sink(bytes);
}
// --- the RAM sink: a retained copy of the whole diagnostic stream ------------
//
// A fixed in-image buffer that accumulates every logged byte, so a user program
// (`log-flush`, and init at shutdown) can read it back through `klog_read` and
// persist it to a file — the boot log survives on a headless/real machine that
// has no host capturing serial. It is a *sink like any other*: register it with
// `addSink(ramSink)` at boot. No allocation (works pre-heap and in a panic).
//
// It fills linearly and stops when full: the earliest output — the most valuable
// for diagnosing a boot — is kept, and the tail is still on the live serial sink.
// 256 KiB comfortably holds a full boot plus a long run (a boot is ~15 KiB).
const ram_capacity = 256 * 1024;
var ram_buffer: [ram_capacity]u8 = undefined;
var ram_len: usize = 0;
/// The RAM sink. Best-effort and self-guarding like every sink: appends what fits
/// and silently drops the rest once full. (Concurrency matches the other sinks —
/// the dominant writer, debug_write, already holds the kernel lock; a rare torn
/// append on a kernel-internal line is an accepted diagnostic imperfection.)
pub fn ramSink(bytes: []const u8) void {
const n = @min(ram_buffer.len - ram_len, bytes.len);
if (n != 0) {
@memcpy(ram_buffer[ram_len..][0..n], bytes[0..n]);
ram_len += n;
}
}
/// The accumulated log so far — what `klog_read` copies out.
pub fn ramSnapshot() []const u8 {
return ram_buffer[0..ram_len];
}
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
/// this is safe to call from interrupt context and from a panic.
pub fn print(comptime fmt: []const u8, args: anytype) void {
var buffer: [256]u8 = undefined;
write(std.fmt.bufPrint(&buffer, fmt, args) catch return);
}
/// Emit a one-byte checkpoint/POST code (I/O port 0x80) — the always-available
/// progress channel for when there is no text output at all. Independent of the
/// sink list, so it works even before any sink is registered.
pub fn checkpoint(code: u8) void {
architecture.checkpoint(code);
}
// --- persistent panic breadcrumb -------------------------------------------
//
// A fixed record in the kernel image that a panic fills in, so a post-mortem — an
// attached debugger, a RAM dump, or (later) a file/pstore reader — can recover
// what killed the kernel even when there was no live console. `magic` is written
// *last*, so a reader only trusts a fully-written record.
pub const panic_magic: u64 = 0xD1ED_B00B_5EED_F00D;
pub const PanicRecord = extern struct {
magic: u64 = 0,
len: u32 = 0,
_pad: u32 = 0,
message: [512]u8 = undefined,
};
/// Findable by symbol (`log.panic_record`) for a debugger or RAM dump.
pub var panic_record: PanicRecord = .{};
/// Stamp the panic message into the breadcrumb record.
pub fn recordPanic(message: []const u8) void {
const n: u32 = @intCast(@min(message.len, panic_record.message.len));
@memcpy(panic_record.message[0..n], message[0..n]);
panic_record.len = n;
panic_record.magic = panic_magic; // set last: a reader sees a complete record
}