Persist the kernel boot log to the USB FAT volume
On a headless or real board nothing captures serial, so the boot log — the whole diagnostic stream — is lost at power-off. This retains it in the kernel and copies it to the boot USB volume as /mnt/usb/DANOS.LOG, the on-disk equivalent of QEMU's `-serial file:`. Pull the stick, read DANOS.LOG on another machine. How it fits together: - Kernel RAM sink (log.zig): a fixed 256 KiB in-image buffer registered as a log sink in kmain, right after serial. Because userspace debug_write funnels through log.write, it captures the entire stream — kernel lines and every service's output — from the first line. Fills linearly and stops when full (earliest boot output, the most valuable, is kept); no allocation, so it is panic-safe. - klog_read syscall (32): copies that buffer out to a user buffer, the mirror of debug_write — same overflow-safe user-half bounds check, kernel -> user copy, under the kernel lock so the snapshot can't grow mid-copy. Wrapped by runtime.system.klogRead. - log-flush (new one-shot, in the initial-ramdisk): waits for the fat server to mount /mnt/usb, then copies the whole log to /mnt/usb/DANOS.LOG. init spawns it once the boot services are up (fire-and-forget; it polls the mount itself). If no volume is mounted — no stick, or the no-VFS ramdisk sweep — it exits silently. - init shutdown flush: init repeats the copy inline at the top of shutDown(), BEFORE it tears down the storage services (the fat server is stopped first), so a clean poweroff captures the fullest log while /mnt/usb is still writable. - unistd.writeAll: loops write() past the 224-byte VFS payload cap; both flush paths use it. The filename is 8.3 (DANOS.LOG) at the mount root — the FAT short-name rule, and there is no mkdir on the FAT path yet. Extend-only writes mean the two same-session flushes never leave stale bytes (the shutdown log is a superset of the boot log); a shorter log on a later boot of the same stick can leave a stale tail — a noted, cosmetic limitation, not worth pulling O_TRUNC into the FAT write path for now. Verified end to end under QEMU: a new `log-flush` case (reusing the orderly-shutdown build) asserts both markers then S5, and DANOS.LOG is read back out of the image afterwards (11804 bytes, containing the kernel init line, the FAT mount line, and the boot flush marker). Regression stays green: zig build, zig build test, zig build check-fat-image, and a sequential QEMU sweep — smoke, init, initial-ramdisk (log-flush silent in the bare sweep), orderly-shutdown, fat-mount, usb-storage, usb-hid, vfs, input, device-manager, process, signals, dma, fault-pf.
This commit is contained in:
@@ -19,8 +19,14 @@
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const unistd = @import("posix").unistd;
|
||||
const power = runtime.power_protocol;
|
||||
|
||||
/// Where the kernel boot log is persisted on the USB FAT volume — an 8.3 name at
|
||||
/// the mount root (see system/services/log-flush). init writes it at shutdown;
|
||||
/// the log-flush one-shot writes it once at boot.
|
||||
const log_path = "/mnt/usb/DANOS.LOG";
|
||||
|
||||
/// The system services init brings up at boot, in order. This is init's policy — the
|
||||
/// microkernel keeps such choices in user space, not the kernel. Drivers are absent
|
||||
/// on purpose: the device manager owns those. (A future init reads this from a
|
||||
@@ -65,6 +71,14 @@ pub fn main() void {
|
||||
}
|
||||
}
|
||||
|
||||
// Once the storage stack is up, a one-shot copies the boot log to the USB
|
||||
// volume (/mnt/usb/DANOS.LOG) so it can be read on another machine — the only
|
||||
// way to see it on a headless/real board with no host capturing serial. Fire
|
||||
// and forget: it polls for the mount itself, and is deliberately NOT one of
|
||||
// init's supervised children (a transient one-shot must not be stopped-and-
|
||||
// waited-for during shutdown).
|
||||
_ = runtime.system.spawn("log-flush");
|
||||
|
||||
// Subscribe to power events (retry: the power service registers well after
|
||||
// init starts). Best-effort — without it, a `terminate` signal still
|
||||
// triggers the same shutdown path.
|
||||
@@ -115,11 +129,36 @@ fn subscribePower() void {
|
||||
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
||||
}
|
||||
|
||||
/// The stop sequence: terminate each child in reverse spawn order (vfs last —
|
||||
/// other services may flush through it), waiting up to a deadline for each to
|
||||
/// exit before killing it, then ask the power service to enter S5.
|
||||
/// Copy the whole kernel log to /mnt/usb/DANOS.LOG (the same file log-flush
|
||||
/// writes at boot), so a poweroff captures the fullest log. Best-effort: if the
|
||||
/// USB volume is not mounted, the open fails and it does nothing. Must run while
|
||||
/// the storage services are still alive (see shutDown).
|
||||
fn flushKernelLog() void {
|
||||
const fd = unistd.open(log_path, unistd.O_CREAT);
|
||||
if (fd < 0) return; // no USB volume mounted — nothing to persist to
|
||||
defer unistd.close(fd);
|
||||
var chunk: [4096]u8 = undefined;
|
||||
var offset: usize = 0;
|
||||
while (true) {
|
||||
const got = runtime.system.klogRead(offset, &chunk);
|
||||
if (got == 0) break; // reached the end of the accumulated log
|
||||
if (unistd.writeAll(fd, chunk[0..got]) < 0) break; // storage went away
|
||||
offset += got;
|
||||
}
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, "/system/services/init: flushed log to {s} ({d} bytes)\n", .{ log_path, offset }) catch "");
|
||||
}
|
||||
|
||||
/// The stop sequence: persist the log while storage is still up, then terminate
|
||||
/// each child in reverse spawn order (vfs last — other services may flush through
|
||||
/// it), waiting up to a deadline for each to exit before killing it, then ask the
|
||||
/// power service to enter S5.
|
||||
fn shutDown() void {
|
||||
_ = runtime.system.write("/system/services/init: shutting down\n");
|
||||
// Persist the fullest log to the USB volume BEFORE tearing anything down: the
|
||||
// reverse-order stop loop below kills the fat server (children[3]) first, so
|
||||
// /mnt/usb must be written while it is still mounted.
|
||||
flushKernelLog();
|
||||
var i = child_count;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! system/services/log-flush — a one-shot that copies the kernel's in-memory
|
||||
//! diagnostic log to a file on the mounted USB FAT volume, so the boot log
|
||||
//! survives to be read on another machine. On a headless or real board there is
|
||||
//! no host capturing serial, so without this the log is lost at power-off; this
|
||||
//! is the on-disk equivalent of QEMU's `-serial file:`.
|
||||
//!
|
||||
//! It reads the whole kernel log back through `klog_read` (the RAM sink in
|
||||
//! system/kernel/log.zig) and writes it to /mnt/usb/DANOS.LOG. The name is 8.3
|
||||
//! (FAT short-name rule: base <= 8, extension <= 3) and lives at the mount root
|
||||
//! (there is no mkdir on the FAT path yet). init spawns this once the boot
|
||||
//! services are up; init itself repeats the flush at shutdown for a fuller log.
|
||||
//!
|
||||
//! If no USB volume is mounted — no stick, or the initial-ramdisk sweep that
|
||||
//! spawns every bundled binary bare with no VFS — it waits briefly, then exits
|
||||
//! silently, deranging no other test's output.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const unistd = @import("posix").unistd;
|
||||
|
||||
const log_path = "/mnt/usb/DANOS.LOG";
|
||||
|
||||
/// Copy the whole kernel log to the open fd, looping klog_read -> write until the
|
||||
/// log is exhausted. Returns the number of bytes written.
|
||||
fn drainKernelLog(fd: i32) usize {
|
||||
var chunk: [4096]u8 = undefined;
|
||||
var offset: usize = 0;
|
||||
while (true) {
|
||||
const got = runtime.system.klogRead(offset, &chunk);
|
||||
if (got == 0) break; // reached the end of the accumulated log
|
||||
if (unistd.writeAll(fd, chunk[0..got]) < 0) break; // storage went away
|
||||
offset += got;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// Wait for the fat server to mount /mnt/usb (it must bring up the whole USB
|
||||
// storage chain first, so it races us at boot). Bounded: if the mount never
|
||||
// appears — no volume, or the no-VFS ramdisk sweep — give up silently.
|
||||
var dir: i32 = -1;
|
||||
var tries: u32 = 0;
|
||||
while (dir < 0 and tries < 1400) : (tries += 1) {
|
||||
dir = unistd.opendir("/mnt/usb");
|
||||
if (dir < 0) runtime.system.sleep(50);
|
||||
}
|
||||
if (dir < 0) return; // /mnt/usb never became available — nothing to persist to
|
||||
unistd.closedir(dir);
|
||||
|
||||
const fd = unistd.open(log_path, unistd.O_CREAT);
|
||||
if (fd < 0) return; // could not create the file — exit quietly
|
||||
const written = drainKernelLog(fd);
|
||||
unistd.close(fd);
|
||||
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, "log-flush: wrote {d} bytes to {s}\n", .{ written, log_path }) catch return);
|
||||
}
|
||||
|
||||
pub const panic = runtime.panic;
|
||||
comptime {
|
||||
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||
}
|
||||
Reference in New Issue
Block a user