danos/docs/logging.md

105 lines
5.1 KiB
Markdown

# Logging: the diagnostic log vs. the display
danos separates two things that are easy to conflate: the **diagnostic log** — the
machine-readable stream of *what the kernel is doing* — and the **display**, the
framebuffer surface the OS draws on. They are different concerns with different
lifetimes, so they're different code paths.
The guiding rule: **output is a diagnostic convenience, never a correctness
dependency.** The kernel must boot and run correctly with *zero* output channels —
no serial, no screen. Logging that can take the kernel down isn't robust; it's a
liability. This is the same [resilience](resilience.md) posture the rest of the
kernel follows.
## The log is multi-sink
`system/kernel/log.zig` is the diagnostic log. It fans a message out to a set of
registered **sinks**, each best-effort and self-guarding:
```zig
log.addSink(arch.serialWrite); // the serial UART
if (arch.debugconPresent()) log.addSink(arch.debugconWrite); // 0xE9 debug console
// later: log.addSink(fs.logWrite); // a file on a ramdisk / USB / SSD
log.write("…"); log.print("x={d}\n", .{x});
```
Properties that matter:
- **No allocation.** The sink table is a fixed array, so the log works before the
heap is up and inside a panic.
- **Best-effort.** A sink whose device is absent is a no-op (e.g. writing to a
missing UART just goes nowhere — the TX-wait is bounded so it can't hang). A
message reaches whatever channels exist; if none do, the kernel runs on, silent.
- **Order-independent.** Every registered sink gets every message. Adding the file
logger later is one `addSink` call and **zero** changes to call sites.
## The framebuffer is *not* a log sink
The framebuffer is a general graphics surface, **not inherently a text terminal**.
Today `system/kernel/console.zig` paints a text grid on it as a *bootstrap* console, but
that's a stop-gap: once the driver machinery exists the framebuffer becomes a proper
**graphics device driver**, and the text crutch goes away. So the log must not assume
it — routing the verbose log through a pixel console would bake in "the OS is text".
Instead the two paths are explicit:
```
verbose diagnostics ──► log ──► serial, debugcon, (file later)
user status / panics ──► status() ──► log (above) + framebuffer (if present)
```
A handful of user-facing lines (`kernel initialised`, a panic) go through
`main.zig`'s `status()` / `statusPrint()`, which write to the log **and** paint the
framebuffer when one is present. Everything else uses `log.*` and never touches the
screen. `console.write` is a no-op when the firmware gave us no framebuffer.
## Optional framebuffer (headless machines)
A framebuffer is not guaranteed — a headless server exposes no UEFI Graphics Output
Protocol. That used to be *fatal* (the loader failed the boot). Now the loader hands
over a "no framebuffer" descriptor (`base == 0`) rather than failing, and
`Framebuffer.present()` (in `system/boot-handoff.zig`) gates every on-screen path. A headless,
serial-less machine boots and runs correctly — it just goes quiet.
## Last-resort channels (no text output at all)
Two signals bypass the sink list, because they must survive even a total
output-channel failure:
- **`log.checkpoint(code)`** — a one-byte **POST code** to I/O port `0x80` (a POST
card or BMC shows it). `main.zig` emits one at each boot milestone (`cp_paging`,
`cp_heap`, …) and on a fault/panic, so "where did it hang?" is answerable with no
text output whatsoever. Writing `0x80` is universally safe.
- **`log.recordPanic(msg)`** — stamps the panic message into a fixed record
(`log.panic_record`, with a `magic` written last). A post-mortem — an attached
debugger, a RAM dump, or a future file/pstore reader — recovers *what killed it*
even though nothing was on screen.
The panic and CPU-exception handlers fan out to every sink, emit a POST code, and
drop the breadcrumb — they never assume a console.
## The 0xE9 debug console
Port `0xE9` is the Bochs/QEMU debug console. It's detected safely: the port returns
`0xE9` when read if present, and `0xFF` on real hardware, so `debugconPresent()`
only enables the sink when it's really there. Under QEMU it's captured with
`-debugcon file:…`, giving CI a log channel independent of `-serial`.
## The robustness spectrum
The result handles every combination — framebuffer only, serial only, both, or
**neither**. With no channels at all the kernel still boots and runs; port-`0x80`
checkpoints track progress and the panic breadcrumb captures failures. *Runs blind
but correct* is the goal, not *always has output*.
## Related
- [framebuffer.md](framebuffer.md) — the display surface itself (pitch, format), the
thing that becomes a graphics device driver.
- [efi.md](efi.md) — where the loader captures (or, headless, doesn't capture) the
framebuffer before `ExitBootServices`.
- [device-interrupts.md](device-interrupts.md) — the serial UART bring-up the log's
primary sink rides on.
- [resilience.md](resilience.md) — why "never let a missing peripheral take the
kernel down" is a core design stance.