Compare commits
12
Commits
ec6e888076
...
ad0fd52cb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad0fd52cb8 | ||
|
|
65a44a5568 | ||
|
|
e186858315 | ||
|
|
7c5645fe48 | ||
|
|
127ea2dad9 | ||
|
|
30d6ea622a | ||
|
|
d0c1b3e45b | ||
|
|
f480c5d790 | ||
|
|
9f18d8340e | ||
|
|
0a84e52bf8 | ||
|
|
1cf9985da6 | ||
|
|
2d0858caf6 |
+126
-43
@@ -2,6 +2,7 @@ const std = @import("std");
|
||||
const uefi = std.os.uefi;
|
||||
const elf = std.elf;
|
||||
const boot_handoff = @import("boot-handoff");
|
||||
const initial_ramdisk = @import("initial-ramdisk");
|
||||
const build_options = @import("build_options");
|
||||
const BootInformation = boot_handoff.BootInformation;
|
||||
const GraphicsOutput = uefi.protocol.GraphicsOutput;
|
||||
@@ -15,11 +16,11 @@ const MemoryMapSlice = uefi.tables.MemoryMapSlice;
|
||||
/// The kernel image: /system/kernel.
|
||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
|
||||
|
||||
/// The init program: /system/services/init.
|
||||
const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\services\\init");
|
||||
|
||||
/// The initial-ramdisk (the VFS server + drivers), in /boot.
|
||||
const initial_ramdisk_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\initial-ramdisk.img");
|
||||
/// The user binaries: everything under /system except the kernel itself. The
|
||||
/// loader walks this tree and packs it into the in-RAM initial_ramdisk image —
|
||||
/// the volume's file structure is the single source of truth (no packed image
|
||||
/// artifact on disk).
|
||||
const system_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("system");
|
||||
|
||||
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
||||
const page_size = 4096;
|
||||
@@ -64,20 +65,14 @@ fn boot() !noreturn {
|
||||
|
||||
const entry = try loadKernel(bs, &boot_information);
|
||||
|
||||
// Best effort: a volume without /system/services/init still boots (kernel-only).
|
||||
loadInit(bs, &boot_information) catch |err| {
|
||||
log("EFI: no /system/services/init (");
|
||||
// Best effort: a volume without a /system tree of user binaries still boots
|
||||
// (kernel-only). The tree — init included — becomes the initial_ramdisk.
|
||||
loadSystemTree(bs, &boot_information) catch |err| {
|
||||
log("EFI: no /system binaries (");
|
||||
logBytes(@errorName(err));
|
||||
log(") - booting without user space\r\n");
|
||||
};
|
||||
|
||||
// Best effort: the initial_ramdisk (VFS server + drivers) is optional too.
|
||||
loadInitialRamdisk(bs, &boot_information) catch |err| {
|
||||
log("EFI: no initial_ramdisk (");
|
||||
logBytes(@errorName(err));
|
||||
log(")\r\n");
|
||||
};
|
||||
|
||||
// Build the page tables the kernel starts life on: identity + a physmap of
|
||||
// low RAM, plus the higher-half kernel image once it links high. Allocated
|
||||
// now, while boot services (and the memory map) are still stable — nothing
|
||||
@@ -369,11 +364,28 @@ fn handoff(cr3: u64, entry: usize, boot_information: *const BootInformation) nor
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Read a whole file off the boot volume into a pool buffer that outlives the
|
||||
/// loader. The buffer is deliberately NOT freed: it's LoaderData, which the
|
||||
/// memory-map conversion classifies as reserved, so the kernel identity-maps it
|
||||
/// and reads from there. Returns the buffer (pointer + length).
|
||||
fn loadFile(bs: *uefi.tables.BootServices, name: [*:0]const u16) ![]u8 {
|
||||
// --- the /system tree -> initial_ramdisk ------------------------------------
|
||||
|
||||
/// Cap on bundled binaries. Generous: the tree carries ~30 today.
|
||||
const maximum_bundled = 64;
|
||||
|
||||
/// How deep the walk goes below /system ("/system/services/x" is depth 1).
|
||||
const maximum_tree_depth = 3;
|
||||
|
||||
/// One binary discovered under /system: its FHS path (UTF-8, '/'-separated,
|
||||
/// NUL-free) and its contents in a transient pool buffer.
|
||||
const Bundled = struct {
|
||||
path: [initial_ramdisk.maximum_name]u8,
|
||||
path_len: usize,
|
||||
data: []align(8) u8,
|
||||
};
|
||||
|
||||
/// Walk the boot volume's /system tree and pack every regular file (except the
|
||||
/// kernel image itself — the only top-level file) into an in-RAM v2
|
||||
/// initial_ramdisk image, entries named by full FHS path. This is what makes the
|
||||
/// volume's file structure the single source of truth: there is no packed
|
||||
/// ramdisk artifact on disk, and init travels in the table like everything else.
|
||||
fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
|
||||
return error.NoLoadedImage;
|
||||
const device = loaded.device_handle orelse return error.NoBootDevice;
|
||||
@@ -383,40 +395,111 @@ fn loadFile(bs: *uefi.tables.BootServices, name: [*:0]const u16) ![]u8 {
|
||||
const root = try fs.openVolume();
|
||||
defer _ = root.close() catch {};
|
||||
|
||||
const file = try root.open(name, .read, .{});
|
||||
defer _ = file.close() catch {};
|
||||
const system_directory = try root.open(system_directory_name, .read, .{});
|
||||
defer _ = system_directory.close() catch {};
|
||||
|
||||
var list: [maximum_bundled]Bundled = undefined;
|
||||
var count: usize = 0;
|
||||
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
|
||||
if (count == 0) return error.NoBinaries;
|
||||
|
||||
// Assemble the v2 image: header, entry table, then the blobs.
|
||||
const table_end = @sizeOf(initial_ramdisk.Header) + count * @sizeOf(initial_ramdisk.Entry);
|
||||
var total: usize = table_end;
|
||||
for (list[0..count]) |e| total += e.data.len;
|
||||
|
||||
const image = try bs.allocatePool(.loader_data, total); // survives the handoff
|
||||
std.mem.bytesAsValue(initial_ramdisk.Header, image[0..@sizeOf(initial_ramdisk.Header)]).* = .{
|
||||
.magic = initial_ramdisk.magic,
|
||||
.count = @intCast(count),
|
||||
};
|
||||
var offset: usize = table_end;
|
||||
for (list[0..count], 0..) |e, i| {
|
||||
var record = initial_ramdisk.Entry{ .name = @splat(0), .offset = offset, .len = e.data.len };
|
||||
@memcpy(record.name[0..e.path_len], e.path[0..e.path_len]);
|
||||
const slot = image[@sizeOf(initial_ramdisk.Header) + i * @sizeOf(initial_ramdisk.Entry) ..][0..@sizeOf(initial_ramdisk.Entry)];
|
||||
std.mem.bytesAsValue(initial_ramdisk.Entry, slot).* = record;
|
||||
@memcpy(image[offset..][0..e.data.len], e.data);
|
||||
offset += e.data.len;
|
||||
_ = bs.freePool(e.data.ptr) catch {};
|
||||
}
|
||||
|
||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||
boot_information.initial_ramdisk_len = total;
|
||||
progress("EFI: /system tree loaded\r\n");
|
||||
}
|
||||
|
||||
/// Recursively collect the regular files below `directory` into `list`. Top-level
|
||||
/// files (depth 0) are skipped: the only one is /system/kernel, which loadKernel
|
||||
/// has already consumed and which is not a spawnable user binary.
|
||||
fn walkDirectory(
|
||||
bs: *uefi.tables.BootServices,
|
||||
directory: *uefi.protocol.File,
|
||||
prefix: []const u8,
|
||||
depth: usize,
|
||||
list: *[maximum_bundled]Bundled,
|
||||
count: *usize,
|
||||
) !void {
|
||||
// Each read() on a directory yields one EFI_FILE_INFO; zero bytes means done.
|
||||
var info_buffer: [1024]u8 align(8) = undefined;
|
||||
while (true) {
|
||||
const n = try directory.read(&info_buffer);
|
||||
if (n == 0) return;
|
||||
const info: *const uefi.protocol.File.Info.File = @ptrCast(@alignCast(&info_buffer));
|
||||
const name16 = info.getFileName();
|
||||
|
||||
// Convert the (ASCII in practice) UTF-16 name; skip "." and "..".
|
||||
var name_buffer: [initial_ramdisk.maximum_name]u8 = undefined;
|
||||
var name_length: usize = 0;
|
||||
while (name16[name_length] != 0) : (name_length += 1) {
|
||||
if (name_length == name_buffer.len) return error.NameTooLong;
|
||||
const c = name16[name_length];
|
||||
if (c > 0x7F) return error.UnsupportedName;
|
||||
name_buffer[name_length] = @intCast(c);
|
||||
}
|
||||
const name = name_buffer[0..name_length];
|
||||
if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue;
|
||||
|
||||
if (info.attribute.directory) {
|
||||
if (depth == maximum_tree_depth) continue;
|
||||
var child_prefix: [initial_ramdisk.maximum_name]u8 = undefined;
|
||||
const child = try std.fmt.bufPrint(&child_prefix, "{s}/{s}", .{ prefix, name });
|
||||
const child_directory = try directory.open(name16, .read, .{});
|
||||
defer _ = child_directory.close() catch {};
|
||||
try walkDirectory(bs, child_directory, child, depth + 1, list, count);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth == 0) continue; // /system/kernel — already loaded, not bundled
|
||||
if (count.* == maximum_bundled) return error.TooManyBinaries;
|
||||
|
||||
var entry: *Bundled = &list[count.*];
|
||||
const path = try std.fmt.bufPrint(&entry.path, "{s}/{s}", .{ prefix, name });
|
||||
entry.path_len = path.len;
|
||||
|
||||
const file = try directory.open(name16, .read, .{});
|
||||
defer _ = file.close() catch {};
|
||||
entry.data = try readWholeFile(bs, file);
|
||||
count.* += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an open file completely into a fresh pool buffer that survives the
|
||||
/// handoff (LoaderData is classified reserved, so the kernel identity-maps it).
|
||||
fn readWholeFile(bs: *uefi.tables.BootServices, file: *uefi.protocol.File) ![]align(8) u8 {
|
||||
try file.setPosition(seek_end);
|
||||
const size: usize = @intCast(try file.getPosition());
|
||||
try file.setPosition(0);
|
||||
if (size == 0) return error.EmptyFile;
|
||||
|
||||
const image = try bs.allocatePool(.loader_data, size); // survives the handoff
|
||||
|
||||
const buffer = try bs.allocatePool(.loader_data, size);
|
||||
var read_total: usize = 0;
|
||||
while (read_total < size) {
|
||||
const n = try file.read(image[read_total..]);
|
||||
const n = try file.read(buffer[read_total..]);
|
||||
if (n == 0) return error.UnexpectedEof;
|
||||
read_total += n;
|
||||
}
|
||||
return image[0..size];
|
||||
}
|
||||
|
||||
/// Ferry the init program (/system/services/init) to the kernel. The kernel does the ELF
|
||||
/// loading itself (into ring-3 mappings) — the loader just carries the bytes.
|
||||
fn loadInit(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const image = try loadFile(bs, init_file_name);
|
||||
boot_information.init_base = @intFromPtr(image.ptr);
|
||||
boot_information.init_len = image.len;
|
||||
progress("EFI: /system/services/init loaded\r\n");
|
||||
}
|
||||
|
||||
/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init.
|
||||
fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const image = try loadFile(bs, initial_ramdisk_file_name);
|
||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||
boot_information.initial_ramdisk_len = image.len;
|
||||
progress("EFI: initial_ramdisk loaded\r\n");
|
||||
return buffer[0..size];
|
||||
}
|
||||
|
||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
||||
|
||||
@@ -221,18 +221,21 @@ fn addKernel(
|
||||
return exe;
|
||||
}
|
||||
|
||||
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding what
|
||||
/// the firmware and loader need off the ESP: the EFI stub, `kernel`, `init`, and
|
||||
/// the initial-ramdisk. Factored so the serial-enabled `run-x86-64` variant can
|
||||
/// bundle its own serial kernel while sharing the loader, init, and ramdisk — all
|
||||
/// built once per invocation (the loader's boot breadcrumbs and init's heartbeat
|
||||
/// both follow the top-level -Dserial). Returns the image's LazyPath.
|
||||
/// One user binary and its FHS home on the boot volume (and in zig-out).
|
||||
const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath };
|
||||
|
||||
/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the
|
||||
/// EFI stub, the kernel, and every user binary at its FHS path — the volume's
|
||||
/// /system tree IS the system image; the EFI loader walks it at boot and builds
|
||||
/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled
|
||||
/// `run-x86-64` variant can bundle its own serial kernel while sharing the
|
||||
/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both
|
||||
/// follow the top-level -Dserial). Returns the image's LazyPath.
|
||||
fn addBootImage(
|
||||
b: *std.Build,
|
||||
kernel_bin: std.Build.LazyPath,
|
||||
efi_bin: std.Build.LazyPath,
|
||||
init_bin: std.Build.LazyPath,
|
||||
initial_ramdisk_img: std.Build.LazyPath,
|
||||
bundled: []const BundledBinary,
|
||||
) std.Build.LazyPath {
|
||||
const mk_fat = b.addSystemCommand(&.{"python3"});
|
||||
mk_fat.addFileArg(b.path("tools/make-fat-image.py"));
|
||||
@@ -242,10 +245,10 @@ fn addBootImage(
|
||||
mk_fat.addFileArg(efi_bin);
|
||||
mk_fat.addArg("system/kernel");
|
||||
mk_fat.addFileArg(kernel_bin);
|
||||
mk_fat.addArg("system/services/init");
|
||||
mk_fat.addFileArg(init_bin);
|
||||
mk_fat.addArg("boot/initial-ramdisk.img");
|
||||
mk_fat.addFileArg(initial_ramdisk_img);
|
||||
for (bundled) |item| {
|
||||
mk_fat.addArg(item.path);
|
||||
mk_fat.addFileArg(item.binary);
|
||||
}
|
||||
return fat_image;
|
||||
}
|
||||
|
||||
@@ -361,7 +364,7 @@ pub fn build(b: *std.Build) void {
|
||||
// is the first "protocol module" (see docs/driver-model.md); usb/block will
|
||||
// expose theirs the same way.
|
||||
const vfs_protocol_module = b.addModule("vfs-protocol", .{
|
||||
.root_source_file = b.path("system/services/vfs/protocol.zig"),
|
||||
.root_source_file = b.path("system/vfs-protocol.zig"),
|
||||
});
|
||||
|
||||
// The input wire protocol: the input service's public interface, exposed as its own
|
||||
@@ -443,8 +446,9 @@ pub fn build(b: *std.Build) void {
|
||||
},
|
||||
});
|
||||
|
||||
// The initial_ramdisk container format, shared by the kernel (unpacks it) and the
|
||||
// build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies.
|
||||
// The initial_ramdisk container format, shared by the kernel (unpacks it) and
|
||||
// the EFI loader (packs it in RAM from the boot volume's /system tree). No
|
||||
// dependencies.
|
||||
const initial_ramdisk_module = b.addModule("initial-ramdisk", .{
|
||||
.root_source_file = b.path("system/initial-ramdisk.zig"),
|
||||
});
|
||||
@@ -502,15 +506,12 @@ pub fn build(b: *std.Build) void {
|
||||
const init_options = b.addOptions();
|
||||
init_options.addOption(bool, "serial", serial);
|
||||
programModule(init_exe).addImport("build_options", init_options.createModule());
|
||||
const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } });
|
||||
b.getInstallStep().dependOn(&init_install.step);
|
||||
|
||||
// --- initial_ramdisk: a bundle of extra user binaries (VFS server + drivers) ---
|
||||
// Each is built by the same user-binary recipe, then packed into one image by
|
||||
// the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel,
|
||||
// which unpacks it and spawns each program (system/initial-ramdisk.zig).
|
||||
const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig");
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig");
|
||||
// --- the rest of the /system tree: services, drivers, test fixtures ---
|
||||
// Each is built by the same user-binary recipe and laid out at its FHS path on
|
||||
// the boot volume (see `bundled` below). The EFI loader walks the tree at boot
|
||||
// and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig).
|
||||
const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs-test/vfs-test.zig");
|
||||
const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||
const ps2_mouse_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-mouse", "system/drivers/ps2-bus/mouse.zig");
|
||||
@@ -579,99 +580,55 @@ pub fn build(b: *std.Build) void {
|
||||
const input_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "input-test", "system/services/input-test/input-test.zig");
|
||||
const args_echo_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "args-echo", "system/services/args-echo/args-echo.zig");
|
||||
const process_test_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "process-test", "system/services/process-test/process-test.zig");
|
||||
const log_flush_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "log-flush", "system/services/log-flush/log-flush.zig");
|
||||
const logger_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "logger", "system/services/logger/logger.zig");
|
||||
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI
|
||||
// (docs/threading.md). Built threaded so its shared-memory poll is real.
|
||||
const thread_test_exe = addThreadedUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "thread-test", "system/services/thread-test/thread-test.zig");
|
||||
|
||||
// Pack the user binaries into the initial_ramdisk image with the host-side Python tool
|
||||
// (the container format is trivial, and Python sidesteps std API churn). Args:
|
||||
// make-initial-ramdisk.py <out> [<name> <file>]... — one name/file pair per binary.
|
||||
const mk_run = b.addSystemCommand(&.{"python3"});
|
||||
mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py"));
|
||||
const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img");
|
||||
mk_run.addArg("vfs");
|
||||
mk_run.addFileArg(vfs_exe.getEmittedBin());
|
||||
mk_run.addArg("vfs-test");
|
||||
mk_run.addFileArg(vfstest_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-bus");
|
||||
mk_run.addFileArg(ps2_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-keyboard");
|
||||
mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin());
|
||||
mk_run.addArg("ps2-mouse");
|
||||
mk_run.addFileArg(ps2_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-xhci-bus");
|
||||
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-hid-keyboard");
|
||||
mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-hid-mouse");
|
||||
mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin());
|
||||
mk_run.addArg("usb-storage");
|
||||
mk_run.addFileArg(usb_storage_exe.getEmittedBin());
|
||||
mk_run.addArg("fat");
|
||||
mk_run.addFileArg(fat_exe.getEmittedBin());
|
||||
mk_run.addArg("fat-test");
|
||||
mk_run.addFileArg(fat_test_exe.getEmittedBin());
|
||||
mk_run.addArg("display");
|
||||
mk_run.addFileArg(display_exe.getEmittedBin());
|
||||
mk_run.addArg("display-demo");
|
||||
mk_run.addFileArg(display_demo_exe.getEmittedBin());
|
||||
mk_run.addArg("virtio-gpu");
|
||||
mk_run.addFileArg(virtio_gpu_exe.getEmittedBin());
|
||||
mk_run.addArg("shm-server");
|
||||
mk_run.addFileArg(shm_server_exe.getEmittedBin());
|
||||
mk_run.addArg("shm-client");
|
||||
mk_run.addFileArg(shm_client_exe.getEmittedBin());
|
||||
mk_run.addArg("pci-bus");
|
||||
mk_run.addFileArg(pci_bus_exe.getEmittedBin());
|
||||
mk_run.addArg("crash-test");
|
||||
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||
mk_run.addArg("thread-test");
|
||||
mk_run.addFileArg(thread_test_exe.getEmittedBin());
|
||||
mk_run.addArg("device-list");
|
||||
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
||||
mk_run.addArg("discovery");
|
||||
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
||||
mk_run.addArg("device-manager");
|
||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||
mk_run.addArg("input");
|
||||
mk_run.addFileArg(input_exe.getEmittedBin());
|
||||
mk_run.addArg("input-source");
|
||||
mk_run.addFileArg(input_source_exe.getEmittedBin());
|
||||
mk_run.addArg("input-test");
|
||||
mk_run.addFileArg(input_test_exe.getEmittedBin());
|
||||
mk_run.addArg("args-echo");
|
||||
mk_run.addFileArg(args_echo_exe.getEmittedBin());
|
||||
mk_run.addArg("process-test");
|
||||
mk_run.addFileArg(process_test_exe.getEmittedBin());
|
||||
mk_run.addArg("log-flush");
|
||||
mk_run.addFileArg(log_flush_exe.getEmittedBin());
|
||||
// Every user binary and its FHS home on the boot volume. There is no packed
|
||||
// ramdisk artifact any more: make-fat-image.py lays each binary out at this
|
||||
// path on the image, and the EFI loader walks /system at boot and builds the
|
||||
// in-RAM initial_ramdisk table from the tree — the volume's file structure is
|
||||
// the single source of truth. Entry names (and hence argv[0] and task names)
|
||||
// are these paths with a leading slash.
|
||||
const bundled = [_]BundledBinary{
|
||||
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/display", .binary = display_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/display-demo", .binary = display_demo_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/device-manager", .binary = device_manager_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/input", .binary = input_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/discovery", .binary = discovery_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/logger", .binary = logger_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/usb-xhci-bus", .binary = usb_xhci_bus_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/usb-hid-keyboard", .binary = usb_hid_keyboard_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/usb-hid-mouse", .binary = usb_hid_mouse_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/usb-storage", .binary = usb_storage_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/virtio-gpu", .binary = virtio_gpu_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/pci-bus", .binary = pci_bus_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/vfs-test", .binary = vfstest_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/fat-test", .binary = fat_test_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/shm-server", .binary = shm_server_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/shm-client", .binary = shm_client_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/crash-test", .binary = crash_test_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/device-list", .binary = device_list_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/input-source", .binary = input_source_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/input-test", .binary = input_test_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/args-echo", .binary = args_echo_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/process-test", .binary = process_test_exe.getEmittedBin() },
|
||||
.{ .path = "system/tests/thread-test", .binary = thread_test_exe.getEmittedBin() },
|
||||
};
|
||||
|
||||
// Also install the packed binaries to their FHS homes, so zig-out is a true image
|
||||
// of the filesystem — even though at boot they arrive inside the initial-ramdisk.
|
||||
for ([_]struct { *std.Build.Step.Compile, []const u8 }{
|
||||
.{ vfs_exe, "system/services" },
|
||||
.{ device_manager_exe, "system/services" },
|
||||
.{ input_exe, "system/services" },
|
||||
.{ ps2_bus_exe, "system/drivers" },
|
||||
.{ ps2_keyboard_exe, "system/drivers" },
|
||||
.{ ps2_mouse_exe, "system/drivers" },
|
||||
.{ usb_xhci_bus_exe, "system/drivers" },
|
||||
.{ usb_hid_keyboard_exe, "system/drivers" },
|
||||
.{ usb_hid_mouse_exe, "system/drivers" },
|
||||
.{ usb_storage_exe, "system/drivers" },
|
||||
.{ fat_exe, "system/services" },
|
||||
.{ display_exe, "system/services" },
|
||||
.{ log_flush_exe, "system/services" },
|
||||
}) |entry| {
|
||||
const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } });
|
||||
b.getInstallStep().dependOn(&step.step);
|
||||
// Install every bundled binary to its FHS home, so zig-out is a true image of
|
||||
// the filesystem — the same tree make-fat-image.py lays out on the boot volume.
|
||||
for (bundled) |item| {
|
||||
const install = b.addInstallFileWithDir(item.binary, .prefix, item.path);
|
||||
b.getInstallStep().dependOn(&install.step);
|
||||
}
|
||||
|
||||
// The initial-ramdisk itself installs to /boot (with the loaders).
|
||||
const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "boot/initial-ramdisk.img");
|
||||
b.getInstallStep().dependOn(&initial_ramdisk_install.step);
|
||||
|
||||
// Boot methods live in boot/, one per way of getting the kernel running.
|
||||
// Each is its own binary/entry (a loader is built for its own target); today
|
||||
// that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis.
|
||||
@@ -692,8 +649,10 @@ pub fn build(b: *std.Build) void {
|
||||
}),
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
// The bootloader speaks only the handoff contract — never the user ABI.
|
||||
// The bootloader speaks the handoff contract and the ramdisk
|
||||
// container it packs the /system tree into — never the user ABI.
|
||||
.{ .name = "boot-handoff", .module = boot_handoff_module },
|
||||
.{ .name = "initial-ramdisk", .module = initial_ramdisk_module },
|
||||
.{ .name = "build_options", .module = loader_options_module },
|
||||
},
|
||||
}),
|
||||
@@ -706,11 +665,11 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
// --- danos-usb.img: the bootable FAT32 USB image ---
|
||||
// Format a real FAT32 image (the in-repo Python builder, no external tools)
|
||||
// holding exactly what the firmware and bootloader need off the ESP: the EFI
|
||||
// stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as
|
||||
// a USB mass-storage device the guest boots from (see run-x86-64 and the test
|
||||
// harness), and the danos fat driver mounts the same image at /mnt/usb.
|
||||
const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||
// holding the EFI stub, the kernel, and the whole /system tree of user
|
||||
// binaries at their FHS paths. QEMU presents this image as a USB mass-storage
|
||||
// device the guest boots from (see run-x86-64 and the test harness), and the
|
||||
// danos fat driver mounts the same image at /mnt/usb.
|
||||
const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), &bundled);
|
||||
const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img");
|
||||
b.getInstallStep().dependOn(&fat_image_install.step);
|
||||
|
||||
@@ -719,7 +678,7 @@ pub fn build(b: *std.Build) void {
|
||||
// log captured to serial0 — without baking serial into the image users flash.
|
||||
// Built lazily (only when `run-x86-64` is requested), and never installed.
|
||||
const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true);
|
||||
const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img);
|
||||
const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), &bundled);
|
||||
|
||||
// `zig build check-fat-image` — validate the produced image is a real FAT32
|
||||
// with the EFI stub present (the builder's own --verify, no external tools).
|
||||
@@ -914,6 +873,7 @@ pub fn build(b: *std.Build) void {
|
||||
for ([_][]const u8{
|
||||
"system/boot-handoff.zig",
|
||||
"system/abi.zig",
|
||||
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
|
||||
"system/devices/device-abi.zig",
|
||||
"system/devices/pci-class.zig", // class/subclass/prog-IF name decoding
|
||||
"system/devices/acpi-ids.zig", // _HID name decoding
|
||||
@@ -926,8 +886,7 @@ pub fn build(b: *std.Build) void {
|
||||
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
|
||||
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
|
||||
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
|
||||
"system/services/vfs/path.zig", // mount-prefix path matching
|
||||
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||
"system/vfs-protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||
"system/services/fat/on-disk.zig", // FAT on-disk struct sizes + type detection
|
||||
"system/services/fat/engine.zig", // FAT read/write over a RAM-backed image
|
||||
"system/services/display/compositor.zig", // Rect math + fill/composite/blit-tile
|
||||
@@ -960,6 +919,21 @@ pub fn build(b: *std.Build) void {
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(xkb_tests).step);
|
||||
|
||||
// The tagged kernel log ring: append/wrap/reclaim/sequence-gap behavior over
|
||||
// a RAM buffer. Needs the `abi` module (record header layout), so it doesn't
|
||||
// fit the plain loop above.
|
||||
const log_ring_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("system/kernel/log-ring.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "abi", .module = abi_module },
|
||||
},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(log_ring_tests).step);
|
||||
|
||||
// runtime.time's Instant/Duration arithmetic. time.zig pulls in system.zig (the
|
||||
// syscall wrappers), which needs the `abi` module, so it doesn't fit the plain
|
||||
// loop above.
|
||||
|
||||
+78
-86
@@ -1,104 +1,96 @@
|
||||
# Logging: the diagnostic log vs. the display
|
||||
# Logging
|
||||
|
||||
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.
|
||||
Output is a *diagnostic convenience, never a correctness dependency*: the kernel
|
||||
and every service must run correctly with zero output channels. On top of that
|
||||
rule, danos has **per-process logging** — every process's output is attributed
|
||||
by the kernel and lands in its own file on the flash volume, which is what makes
|
||||
a headless real machine (no serial port) debuggable. The display (the
|
||||
framebuffer surface) is a separate concern and deliberately not a log sink;
|
||||
`main.zig` mirrors a few user-facing status lines and panics to it explicitly.
|
||||
|
||||
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:
|
||||
## The pipeline
|
||||
|
||||
```
|
||||
verbose diagnostics ──► log ──► serial, debugcon, (file later)
|
||||
user status / panics ──► status() ──► log (above) + framebuffer (if present)
|
||||
process std.log ──▶ debug_write(level) ──▶ tagged kernel ring ──▶ logger service ──▶ /var/log/<boot-stamp>/<binary-path>.log
|
||||
kernel log.print ─┘ │
|
||||
└▶ serial / 0xE9 sinks (QEMU, -Dserial)
|
||||
```
|
||||
|
||||
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.
|
||||
1. **Emit.** A program calls `std.log.info("mounted {s}", .{path})` — the
|
||||
runtime's `logFn` (installed for every binary by the root shim,
|
||||
`library/runtime/log.zig`) formats one line and issues one `debug_write`
|
||||
carrying the level. The payload does NOT contain the process's name.
|
||||
`runtime.system.write` remains as the raw/bring-up path (panics, test
|
||||
fixtures); raw bytes ride the same ring, attributed all the same.
|
||||
|
||||
## Optional framebuffer (headless machines)
|
||||
2. **Stamp.** The kernel wraps every payload LINE in a record stamped with the
|
||||
sender's pid, task name (its binary path, e.g. `/system/services/fat`),
|
||||
level, a per-boot sequence number, and a monotonic timestamp
|
||||
(`system/kernel/log.zig` + `log-ring.zig`). Attribution is structural — a
|
||||
payload cannot forge another sender's tag, and an embedded newline just ends
|
||||
the record, so the forged "prefix" lands inside the forger's own next line.
|
||||
|
||||
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.
|
||||
3. **Retain.** The 512 KiB ring overwrites oldest-first; sequence gaps make any
|
||||
loss countable. `klog_read` (#32) copies stream bytes from a free-running
|
||||
offset; `klog_status` (#45) returns the cursors plus the wall-clock time of
|
||||
boot. The framing (`abi.KlogRecordHeader`) is 32 bytes + name + payload,
|
||||
8-byte aligned.
|
||||
|
||||
## Last-resort channels (no text output at all)
|
||||
4. **Render.** Registered sinks (serial under `-Dserial`, the 0xE9 debug
|
||||
console) get a live transcript: kernel/raw output verbatim, leveled records
|
||||
as `<binary path>: message` — one composed write per line, under the log's
|
||||
own spinlock (never the big kernel lock; panic paths try-acquire with a
|
||||
bound and fall back to sinks-only). Sinks are best-effort and self-guarding;
|
||||
a serial-less machine just goes quiet.
|
||||
|
||||
Two signals bypass the sink list, because they must survive even a total
|
||||
output-channel failure:
|
||||
5. **Persist.** The **logger service** (`system/services/logger`) drains the
|
||||
ring every 250 ms and demultiplexes records into one file per source under
|
||||
`/var/log/<boot-stamp>/`, e.g.
|
||||
|
||||
- **`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.
|
||||
```
|
||||
/var/log/2026-07-21T150434Z/kernel.log
|
||||
/var/log/2026-07-21T150434Z/system/services/fat.log
|
||||
/var/log/2026-07-21T150434Z/system/drivers/usb-storage.log
|
||||
```
|
||||
|
||||
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 boot stamp is the RTC anchor from `klog_status` (FAT-safe: no colons; a
|
||||
dead RTC yields the 1970 directory rather than no logs). Each line carries
|
||||
the record's monotonic timestamp and level. Storage is best-effort and late:
|
||||
the ring buffers a whole boot many times over, and the first successful
|
||||
`makePath` of the per-boot directory (also the readiness probe) triggers a
|
||||
full backlog write. Files close — which is the fat server's SCSI cache
|
||||
flush — after a ~2 s quiet period, bounding data-at-risk without per-record
|
||||
flush thrash. At shutdown init stops the logger FIRST (it is last in the
|
||||
boot order), so its final drain runs over a live storage chain.
|
||||
|
||||
## The 0xE9 debug console
|
||||
## Why a ring in the kernel, not a logging server
|
||||
|
||||
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 storage stack must be able to log. If the fat server wrote its own log file
|
||||
through the VFS it would rendezvous-deadlock on itself; if processes sent
|
||||
records to a logging server over IPC, early boot would need a buffer that is —
|
||||
a ring, one hop later. The kernel ring is that buffer, placed where every
|
||||
process (and the kernel itself) can reach it with one syscall, before any
|
||||
service exists. The logger service is a *reader*, not a hop.
|
||||
|
||||
## The robustness spectrum
|
||||
Two disciplines keep it honest:
|
||||
|
||||
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*.
|
||||
- the logger announces itself **once** — a periodic status line would feed the
|
||||
very stream it drains;
|
||||
- lost records surface as an explicit `-- N records lost --` line, computed
|
||||
from sequence gaps, never silently.
|
||||
|
||||
## Related
|
||||
## Last-resort channels
|
||||
|
||||
- [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.
|
||||
Unchanged, and independent of the sink list so they survive a total output
|
||||
failure: `checkpoint` (a one-byte POST code on port 0x80) and `recordPanic`
|
||||
(a fixed breadcrumb record, `log.panic_record`, findable in a RAM dump; magic
|
||||
written last so a reader only trusts a complete record).
|
||||
|
||||
## Accepted gaps
|
||||
|
||||
- A write-spamming process can evict other processes' unread records from the
|
||||
ring (a per-process quota is future work); the loss is at least visible via
|
||||
sequence gaps in every affected file.
|
||||
- `/var/log` files have no privacy until the VFS grows permissions.
|
||||
- Records emitted after the logger's final shutdown drain reach serial and the
|
||||
ring but not the files.
|
||||
|
||||
+10
-7
@@ -1,15 +1,18 @@
|
||||
# System Calls
|
||||
System calls (syscalls) are the bridge between your programs and the operating system's restricted core (kernel).
|
||||
|
||||
> **Status:** danos has real user processes (M3). User programs enter the kernel
|
||||
> **Status:** danos has real user processes. User programs enter the kernel
|
||||
> via the `syscall` instruction (STAR/LSTAR/SFMASK set per core; the entry stub in
|
||||
> `isr.s` does the `swapgs` + kernel-stack switch and reuses the interrupt
|
||||
> dispatcher). The `int 0x80` gate is kept alongside as a minimal test path. The
|
||||
> current call set is still a placeholder — `0 = exit(code)`, `1 = ping`,
|
||||
> `2 = write(ptr, len)`, `3 = sleep(ms)` (see `system/kernel/process.zig`); the
|
||||
> handler dispatches on whether the caller is a scheduled process (its own address
|
||||
> space) or a borrowed test thread. The microkernel set below (IPC_Call /
|
||||
> IPC_ReplyWait / Yield) replaces it once a second user server exists.
|
||||
> dispatcher); the `int 0x80` gate is kept alongside as a minimal test path.
|
||||
> The live table is `system/abi.zig` (private, renumberable — see
|
||||
> [vdso.md](vdso.md) for the public boundary): process lifecycle + threads,
|
||||
> memory (mmap/dma/shm), synchronous + async IPC with capability passing,
|
||||
> device access, time, the tagged-log diagnostics (`debug_write` with a level,
|
||||
> `klog_read`/`klog_status`), and filesystem NAMING (`fs_resolve`/`fs_node`/
|
||||
> `fs_mount`/`fs_unmount` — the kernel VFS root routes paths and serves the
|
||||
> read-only /system initrd mount; file DATA stays with userspace filesystem
|
||||
> servers over the vfs-protocol, docs/vfs-protocol.md).
|
||||
|
||||
## The Mechanism of a Syscall
|
||||
|
||||
|
||||
+6
-3
@@ -133,7 +133,8 @@ Grouped as `abi.zig` groups them:
|
||||
| ipc | `danos_endpoint_create`, `danos_ipc_register`, `danos_ipc_lookup`, `danos_ipc_call`, `danos_ipc_reply_wait`, `danos_ipc_send` |
|
||||
| devices | `danos_device_enumerate`, `danos_device_claim`, `danos_device_register`, `danos_mmio_map`, `danos_irq_bind`, `danos_irq_ack`, `danos_msi_bind`, `danos_io_read`, `danos_io_write` |
|
||||
| time | `danos_clock`, `danos_wall_clock`, `danos_timer_bind` |
|
||||
| diagnostics | `danos_debug_write`, `danos_klog_read` |
|
||||
| diagnostics | `danos_debug_write` (leveled, kernel-stamped records), `danos_klog_read`, `danos_klog_status` |
|
||||
| filesystem naming | `danos_fs_resolve`, `danos_fs_node`, `danos_fs_mount`, `danos_fs_unmount` (naming only — file DATA still crosses the vfs-protocol IPC, see below) |
|
||||
|
||||
The constants that ride alongside the calls — mmap protection bits, DMA
|
||||
flags, notification badge bits, `ExitReason`, `Signal`, well-known service
|
||||
@@ -197,8 +198,10 @@ Phased so every step ships alone (the M-milestone discipline):
|
||||
across processes stays what it is today: a service behind IPC, or source
|
||||
compiled into each binary.
|
||||
- **No file/device I/O in the vDSO.** The microkernel line doesn't move: the
|
||||
vDSO wraps the same deliberately tiny table (docs/syscall.md); files are
|
||||
still the VFS server's business over IPC.
|
||||
vDSO wraps the same deliberately tiny table (docs/syscall.md). The kernel
|
||||
resolves file NAMES (`fs_resolve` — the mount table moved in-kernel), but
|
||||
file data is still the filesystem server's business over the vfs-protocol
|
||||
IPC; the kernel never blocks on a userspace filesystem.
|
||||
- **No fast-path user-mode implementations yet.** Linux's vDSO exists mostly
|
||||
to answer `gettimeofday` without a kernel entry. `danos_clock` could one
|
||||
day read the calibrated TSC in user mode the same way — the blob is where
|
||||
|
||||
+26
-18
@@ -1,20 +1,24 @@
|
||||
# The VFS wire protocol
|
||||
|
||||
> **Status:** built and spoken today between `runtime.fs` (the client) and the
|
||||
> VFS server (`system/services/vfs`), with mounted backends (the FAT server)
|
||||
> speaking the same protocol behind the router. The Zig source of truth is
|
||||
> `system/services/vfs/protocol.zig` (the `vfs-protocol` module), whose unit
|
||||
> tests pin the sizes and values below. This page is the **language-neutral
|
||||
> wire specification** of that contract — what a Rust or C client implements
|
||||
> ([vdso.md](vdso.md) explains why the IPC protocols, not the syscall
|
||||
> numbers, are danos's public ABI).
|
||||
> filesystem BACKENDS (the FAT server). The mount router lives in the
|
||||
> **kernel** (`system/kernel/vfs.zig`): `fs_resolve` routes a path and either
|
||||
> serves it directly (the read-only /system initrd mount, via `fs_node`) or
|
||||
> redirects the caller to the owning backend's endpoint plus the rewritten
|
||||
> mount-relative path — after which the client speaks THIS protocol to the
|
||||
> backend, unchanged. The Zig source of truth is `system/vfs-protocol.zig`
|
||||
> (the `vfs-protocol` module), whose unit tests pin the sizes and values
|
||||
> below. This page is the **language-neutral wire specification** of that
|
||||
> contract — what a Rust or C client implements ([vdso.md](vdso.md) explains
|
||||
> why the IPC protocols, not the syscall numbers, are danos's public ABI).
|
||||
|
||||
## Transport
|
||||
|
||||
A VFS exchange is one synchronous IPC rendezvous (`ipc_call`,
|
||||
docs/ipc.md): the client sends one message and blocks; the server replies
|
||||
with one message. The endpoint is found by well-known service id
|
||||
(`ipc_lookup`, service id **1** = vfs).
|
||||
with one message. The endpoint comes from the kernel's `fs_resolve` — which
|
||||
also hands back the path rewritten relative to the mount — not from a
|
||||
registry lookup. (Service id 1, the old userspace router, is retired.)
|
||||
|
||||
- A message is at most **256 bytes** (`message_maximum`).
|
||||
- A request is a fixed 32-byte **Request** header followed by an inline
|
||||
@@ -26,8 +30,11 @@ with one message. The endpoint is found by well-known service id
|
||||
- All integers are **little-endian**; layouts are C layout for x86-64
|
||||
(`extern struct`), offsets given below so nothing need be inferred.
|
||||
|
||||
The kernel never parses any of this — it only moves the bytes
|
||||
(docs/syscall.md); files are entirely a user-space affair.
|
||||
The kernel resolves NAMES (the mount table) but never parses these
|
||||
messages — it moves the bytes; file state is entirely the backend's affair.
|
||||
With clients holding backend node ids directly, a backend records each open
|
||||
handle's owner and sweeps a dead client's handles via the published process
|
||||
exit events.
|
||||
|
||||
## Request header — 32 bytes
|
||||
|
||||
@@ -90,13 +97,14 @@ Notes per operation:
|
||||
position. Each call returns exactly one entry; the client increments the
|
||||
cursor by 1. A reply with `len` 0 is end-of-directory. The directory must
|
||||
have been opened with the `directory` flag.
|
||||
- **mount** — the one operation that passes a **capability**: the caller
|
||||
(a filesystem server, e.g. FAT) sends its own request endpoint as the
|
||||
`ipc_call` capability argument, and the router forwards everything under
|
||||
the mount point to it — speaking this same protocol, with paths rewritten
|
||||
relative to the mount. Prefixes match at path boundaries only
|
||||
(`/mnt/usb` never captures `/mnt/usbextra`); the longest matching prefix
|
||||
wins.
|
||||
- **mount / unmount** — RETIRED from the wire: mounting is the `fs_mount`
|
||||
syscall now (a filesystem server passes its endpoint handle; possession is
|
||||
the capability, exactly the trust of the old cap-passing op). The op
|
||||
numbers stay reserved. Mount-prefix semantics are unchanged: prefixes
|
||||
match at path boundaries only (`/mnt/usb` never captures `/mnt/usbextra`),
|
||||
the longest matching prefix wins, and an optional backend-side rewrite
|
||||
prefix maps a mount into the backend's namespace (fat serves `/mnt/usb`
|
||||
from its volume root and `/var` from its `/var` subtree).
|
||||
- **rename** — same-directory rename only (the router requires old and new to
|
||||
resolve under one mount).
|
||||
|
||||
|
||||
+129
-49
@@ -1,5 +1,5 @@
|
||||
//! runtime.fs — the danos-native file API. A program opens, reads, writes, and
|
||||
//! lists files served by the user-space VFS (system/services/vfs), each call
|
||||
//! lists files through the kernel VFS root (resolve + redirect), each call
|
||||
//! marshalling a vfs-protocol request over IPC. This is the danos-native layer
|
||||
//! danos programs use directly; it is also where the file operations that later
|
||||
//! become `std.os.danos` are staged (see docs/zig-self-hosting.md). It replaces
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const ipc = @import("ipc.zig");
|
||||
const system = @import("system.zig");
|
||||
const protocol = @import("vfs-protocol");
|
||||
|
||||
/// The kind of a filesystem node — re-exported so a caller need not import the
|
||||
@@ -60,23 +61,37 @@ pub const OpenOptions = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// The VFS server endpoint, looked up once by well-known id and cached.
|
||||
var vfs_handle: ipc.Handle = 0;
|
||||
var vfs_resolved = false;
|
||||
fn vfs() ?ipc.Handle {
|
||||
if (!vfs_resolved) {
|
||||
vfs_handle = ipc.lookup(.vfs) orelse return null;
|
||||
vfs_resolved = true;
|
||||
// The route to a path: the kernel resolves (fs_resolve) and either serves the
|
||||
// node itself (the initrd at /system — a permanent token) or redirects us to
|
||||
// the owning filesystem backend's endpoint, to which we speak the vfs-protocol
|
||||
// rendezvous directly with the rewritten mount-relative path.
|
||||
const Route = union(enum) {
|
||||
kernel: u64,
|
||||
backend: struct { handle: ipc.Handle, path: [224]u8, path_len: usize },
|
||||
|
||||
fn backendPath(self: *const Route) []const u8 {
|
||||
return self.backend.path[0..self.backend.path_len];
|
||||
}
|
||||
};
|
||||
|
||||
fn resolve(path: []const u8, flags: usize) ?Route {
|
||||
var out: [224]u8 = undefined;
|
||||
const route = system.fsResolve(path, flags, &out) orelse return null;
|
||||
switch (route) {
|
||||
.kernel => |token| return .{ .kernel = token },
|
||||
.backend => |b| {
|
||||
var r: Route = .{ .backend = .{ .handle = b.handle, .path = undefined, .path_len = b.path_len } };
|
||||
@memcpy(r.backend.path[0..b.path_len], out[0..b.path_len]);
|
||||
return r;
|
||||
},
|
||||
}
|
||||
return vfs_handle;
|
||||
}
|
||||
|
||||
const Result = struct { reply: protocol.Reply, payload: []u8 };
|
||||
|
||||
// One request/reply round trip: [Request header][send payload] -> VFS ->
|
||||
// One request/reply round trip: [Request header][send payload] -> backend ->
|
||||
// [Reply header][receive payload]. The receive payload lands in `out`.
|
||||
fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||
const h = vfs() orelse return null;
|
||||
fn transact(h: ipc.Handle, request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const slen = @min(send.len, protocol.maximum_payload);
|
||||
@@ -95,13 +110,21 @@ fn transact(request: protocol.Request, send: []const u8, out: []u8) ?Result {
|
||||
pub const File = struct {
|
||||
node: u64,
|
||||
offset: u64 = 0,
|
||||
/// The owning backend's endpoint, or null for a kernel-served node (the
|
||||
/// read-only /system tree), whose `node` is a permanent fs_node token.
|
||||
backend: ?ipc.Handle = null,
|
||||
|
||||
/// Read up to `buffer.len` bytes at the current offset; returns the count, or
|
||||
/// null on error.
|
||||
pub fn read(self: *File, buffer: []u8) ?usize {
|
||||
const h = self.backend orelse {
|
||||
const n = system.fsNodeRead(self.node, self.offset, buffer) orelse return null;
|
||||
self.offset += n;
|
||||
return n;
|
||||
};
|
||||
const want: u32 = @intCast(@min(buffer.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .read, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, &.{}, buffer) orelse return null;
|
||||
const r = transact(h, request, &.{}, buffer) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
self.offset += r.reply.len;
|
||||
return r.reply.len;
|
||||
@@ -109,11 +132,13 @@ pub const File = struct {
|
||||
|
||||
/// Write `data` at the current offset; returns the count written. A single
|
||||
/// call is capped at the VFS payload size, so the return may be short — use
|
||||
/// `writeAll` to write the whole slice. Null on error.
|
||||
/// `writeAll` to write the whole slice. Null on error (kernel-served nodes
|
||||
/// are read-only).
|
||||
pub fn write(self: *File, data: []const u8) ?usize {
|
||||
const h = self.backend orelse return null;
|
||||
const want: u32 = @intCast(@min(data.len, protocol.maximum_payload));
|
||||
const request = protocol.Request{ .operation = .write, .node = self.node, .offset = self.offset, .len = want, .flags = 0 };
|
||||
const r = transact(request, data[0..want], &.{}) orelse return null;
|
||||
const r = transact(h, request, data[0..want], &.{}) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
self.offset += r.reply.len;
|
||||
return r.reply.len;
|
||||
@@ -138,27 +163,40 @@ pub const File = struct {
|
||||
|
||||
/// This file's metadata.
|
||||
pub fn attributes(self: *File) ?Attributes {
|
||||
const h = self.backend orelse {
|
||||
const a = system.fsNodeStatus(self.node) orelse return null;
|
||||
return .{ .size = a.size, .kind = if (a.kind == system.file_kind_directory) .directory else .regular, .mtime = a.mtime };
|
||||
};
|
||||
const request = protocol.Request{ .operation = .status, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var buffer: [@sizeOf(protocol.FileStatus)]u8 = undefined;
|
||||
const r = transact(request, &.{}, &buffer) orelse return null;
|
||||
const r = transact(h, request, &.{}, &buffer) orelse return null;
|
||||
if (r.reply.status != 0 or r.payload.len < @sizeOf(protocol.FileStatus)) return null;
|
||||
const status = std.mem.bytesToValue(protocol.FileStatus, buffer[0..@sizeOf(protocol.FileStatus)]);
|
||||
return .{ .size = status.size, .kind = kindFromWire(status.kind), .mtime = status.mtime };
|
||||
}
|
||||
|
||||
/// Release the VFS's open handle for this file.
|
||||
/// Release the backend's open handle for this file. Kernel-served node
|
||||
/// tokens are permanent — nothing to release.
|
||||
pub fn close(self: *File) void {
|
||||
const h = self.backend orelse return;
|
||||
const request = protocol.Request{ .operation = .close, .node = self.node, .offset = 0, .len = 0, .flags = 0 };
|
||||
_ = transact(request, &.{}, &.{});
|
||||
_ = transact(h, request, &.{}, &.{});
|
||||
}
|
||||
};
|
||||
|
||||
/// Open (or create, with `.create`) `path`. Returns the open file, or null.
|
||||
pub fn open(path: []const u8, options: OpenOptions) ?File {
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = options.wireFlags() };
|
||||
const r = transact(request, path, &.{}) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
return .{ .node = r.reply.node };
|
||||
const route = resolve(path, options.wireFlags()) orelse return null;
|
||||
switch (route) {
|
||||
.kernel => |token| return .{ .node = token, .backend = null },
|
||||
.backend => |b| {
|
||||
const relative = route.backendPath();
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = options.wireFlags() };
|
||||
const r = transact(b.handle, request, relative, &.{}) orelse return null;
|
||||
if (r.reply.status != 0) return null;
|
||||
return .{ .node = r.reply.node, .backend = b.handle };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// A path's metadata without keeping it open (open -> status -> close).
|
||||
@@ -190,13 +228,27 @@ pub const Entry = struct {
|
||||
pub const Directory = struct {
|
||||
node: u64,
|
||||
cursor: u64 = 0,
|
||||
backend: ?ipc.Handle = null,
|
||||
|
||||
/// Fill `entry` with the next directory entry; false at end of directory or
|
||||
/// on error.
|
||||
pub fn next(self: *Directory, entry: *Entry) bool {
|
||||
const h = self.backend orelse {
|
||||
var buffer: [@sizeOf(system.DirectoryEntryHeader) + 64]u8 = undefined;
|
||||
const n = system.fsNodeReaddir(self.node, self.cursor, &buffer) orelse return false;
|
||||
if (n < @sizeOf(system.DirectoryEntryHeader)) return false; // end
|
||||
const header = std.mem.bytesToValue(system.DirectoryEntryHeader, buffer[0..@sizeOf(system.DirectoryEntryHeader)]);
|
||||
entry.kind = if (header.kind == system.file_kind_directory) .directory else .regular;
|
||||
entry.size = header.size;
|
||||
const nlen = @min(@as(usize, header.name_len), entry.name_buffer.len);
|
||||
@memcpy(entry.name_buffer[0..nlen], buffer[@sizeOf(system.DirectoryEntryHeader)..][0..nlen]);
|
||||
entry.name_len = nlen;
|
||||
self.cursor += 1;
|
||||
return true;
|
||||
};
|
||||
const request = protocol.Request{ .operation = .readdir, .node = self.node, .offset = self.cursor, .len = 0, .flags = 0 };
|
||||
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||
const r = transact(request, &.{}, &buffer) orelse return false;
|
||||
const r = transact(h, request, &.{}, &buffer) orelse return false;
|
||||
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
||||
if (r.payload.len < protocol.directory_entry_size) return false;
|
||||
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
|
||||
@@ -210,9 +262,9 @@ pub const Directory = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Release the VFS's open handle for this directory.
|
||||
/// Release the backend's open handle for this directory.
|
||||
pub fn close(self: *Directory) void {
|
||||
var f = File{ .node = self.node };
|
||||
var f = File{ .node = self.node, .backend = self.backend };
|
||||
f.close();
|
||||
}
|
||||
};
|
||||
@@ -220,13 +272,18 @@ pub const Directory = struct {
|
||||
/// Open `path` as a directory for listing. Returns null if it isn't one / on error.
|
||||
pub fn openDirectory(path: []const u8) ?Directory {
|
||||
const file = open(path, .{ .directory = true }) orelse return null;
|
||||
return .{ .node = file.node };
|
||||
return .{ .node = file.node, .backend = file.backend };
|
||||
}
|
||||
|
||||
// A path-based request that returns only a status (mkdir, unlink).
|
||||
// A path-based request that returns only a status (mkdir, unlink). Kernel-served
|
||||
// paths (the read-only /system) refuse mutation by construction: the resolve
|
||||
// must land on a backend.
|
||||
fn pathOperation(operation: protocol.Operation, path: []const u8) bool {
|
||||
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(path.len), .flags = 0 };
|
||||
const r = transact(request, path, &.{}) orelse return false;
|
||||
const route = resolve(path, 0) orelse return false;
|
||||
if (route != .backend) return false;
|
||||
const relative = route.backendPath();
|
||||
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
|
||||
const r = transact(route.backend.handle, request, relative, &.{}) orelse return false;
|
||||
return r.reply.status == 0;
|
||||
}
|
||||
|
||||
@@ -236,39 +293,62 @@ pub fn makeDirectory(path: []const u8) bool {
|
||||
return pathOperation(.mkdir, path);
|
||||
}
|
||||
|
||||
/// Create every missing directory along `path` (mkdir -p). Probes each prefix
|
||||
/// with `exists` first — a FAT mkdir of an existing name is refused, and the
|
||||
/// probe keeps the common "already there" case cheap. Returns true when the
|
||||
/// whole path exists afterwards.
|
||||
pub fn makePath(path: []const u8) bool {
|
||||
var end: usize = 0;
|
||||
while (end < path.len) {
|
||||
end += 1;
|
||||
while (end < path.len and path[end] != '/') end += 1;
|
||||
const prefix = path[0..end];
|
||||
if (prefix.len == 0 or (prefix.len == 1 and prefix[0] == '/')) continue;
|
||||
// Best-effort per prefix: components at or above a mount point ("/mnt")
|
||||
// are router names, not filesystem nodes — they neither exist as nodes
|
||||
// nor accept mkdir, and that is fine. Only the final verdict counts.
|
||||
if (!exists(prefix)) _ = makeDirectory(prefix);
|
||||
}
|
||||
return exists(path);
|
||||
}
|
||||
|
||||
/// Remove the file at `path`. Returns true on success. Directories are refused
|
||||
/// (a separate directory-removal would have to check emptiness).
|
||||
pub fn remove(path: []const u8) bool {
|
||||
return pathOperation(.unlink, path);
|
||||
}
|
||||
|
||||
/// Rename `old_path` to `new_path`. Both must be in the same directory (same-
|
||||
/// directory, 8.3-name rename only for now). Returns true on success.
|
||||
/// Rename `old_path` to `new_path`. Both must resolve to the SAME filesystem
|
||||
/// backend (same-directory, 8.3-name rename only for now). Returns true on
|
||||
/// success.
|
||||
pub fn rename(old_path: []const u8, new_path: []const u8) bool {
|
||||
const total = old_path.len + 1 + new_path.len;
|
||||
const old_route = resolve(old_path, 0) orelse return false;
|
||||
const new_route = resolve(new_path, 0) orelse return false;
|
||||
if (old_route != .backend or new_route != .backend) return false;
|
||||
if (old_route.backend.handle != new_route.backend.handle) return false; // cross-filesystem
|
||||
const old_relative = old_route.backendPath();
|
||||
const new_relative = new_route.backendPath();
|
||||
const total = old_relative.len + 1 + new_relative.len;
|
||||
if (total > protocol.maximum_payload) return false;
|
||||
var payload: [protocol.maximum_payload]u8 = undefined;
|
||||
@memcpy(payload[0..old_path.len], old_path);
|
||||
payload[old_path.len] = 0;
|
||||
@memcpy(payload[old_path.len + 1 ..][0..new_path.len], new_path);
|
||||
@memcpy(payload[0..old_relative.len], old_relative);
|
||||
payload[old_relative.len] = 0;
|
||||
@memcpy(payload[old_relative.len + 1 ..][0..new_relative.len], new_relative);
|
||||
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||
const r = transact(request, payload[0..total], &.{}) orelse return false;
|
||||
const r = transact(old_route.backend.handle, request, payload[0..total], &.{}) orelse return false;
|
||||
return r.reply.status == 0;
|
||||
}
|
||||
|
||||
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||
/// the VFS then routes everything under `target` to that backend. This is the one
|
||||
/// call that hands the VFS a capability (the backend endpoint). Returns true on
|
||||
/// success.
|
||||
/// the kernel VFS then routes everything under `target` to that backend.
|
||||
/// Possession of the endpoint handle is the capability. Returns true on success.
|
||||
pub fn mount(target: []const u8, backend: ipc.Handle) bool {
|
||||
const h = vfs() orelse return false;
|
||||
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const tlen = @min(target.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
|
||||
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return false;
|
||||
if (result.len < protocol.reply_size) return false;
|
||||
return std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0;
|
||||
return system.fsMount(target, backend, "");
|
||||
}
|
||||
|
||||
/// As `mount`, with a backend-side rewrite prefix: a path under `target` reaches
|
||||
/// the backend as `rewrite` + the mount-relative tail. How one volume serves two
|
||||
/// mounts ("/mnt/usb" from its root, "/var" from its /var subtree).
|
||||
pub fn mountRewritten(target: []const u8, backend: ipc.Handle, rewrite: []const u8) bool {
|
||||
return system.fsMount(target, backend, rewrite);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! The per-process logger: std.log wired to the tagged kernel log ring.
|
||||
//!
|
||||
//! A program just calls `std.log.info("mounted {s}", .{path})` (or a scoped
|
||||
//! logger); this backend formats the line into a fixed buffer and emits ONE
|
||||
//! `debug_write` record carrying the level. The kernel stamps the record with
|
||||
//! the sender's pid and task name (its binary path) — the process does NOT put
|
||||
//! its own name in the payload; attribution is the kernel's, structural and
|
||||
//! unforgeable. Serial shows the kernel-rendered `<path>: message` line, and
|
||||
//! the logger service demultiplexes the ring into one file per process.
|
||||
//!
|
||||
//! Installed for every user binary by the root shim (library/runtime/root.zig)
|
||||
//! via `std_options`; a program can override by declaring its own
|
||||
//! `pub const std_options`.
|
||||
|
||||
const std = @import("std");
|
||||
const system = @import("system.zig");
|
||||
|
||||
fn levelOf(comptime level: std.log.Level) system.KlogLevel {
|
||||
return switch (level) {
|
||||
.err => .err,
|
||||
.warn => .warn,
|
||||
.info => .info,
|
||||
.debug => .debug,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logFn(
|
||||
comptime level: std.log.Level,
|
||||
comptime scope: @EnumLiteral(),
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
// One record = one line = at most klog_maximum_message bytes of payload.
|
||||
// On overflow keep what fits and end with "~" so the record is still a
|
||||
// whole line (the kernel would split an embedded rest anyway).
|
||||
var buffer: [256]u8 = undefined;
|
||||
const prefix = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ") ";
|
||||
const line = std.fmt.bufPrint(&buffer, prefix ++ format, args) catch truncated: {
|
||||
buffer[buffer.len - 1] = '~';
|
||||
break :truncated buffer[0..];
|
||||
};
|
||||
_ = system.writeRecord(levelOf(level), line);
|
||||
}
|
||||
|
||||
/// The std.Options the root shim installs unless the program overrides it.
|
||||
/// Debug level: filtering is the log *reader's* job here — the ring is cheap,
|
||||
/// serial is a dev convenience, and the logger service keeps everything.
|
||||
pub const default_options: std.Options = .{
|
||||
.log_level = .debug,
|
||||
.logFn = logFn,
|
||||
};
|
||||
@@ -14,6 +14,12 @@ pub const main = program.main;
|
||||
/// The panic handler for every safety check in the image (runtime.start.panic).
|
||||
pub const panic = runtime.panic;
|
||||
|
||||
/// std.log for every user binary goes to the tagged kernel log ring (the kernel
|
||||
/// stamps the sender; see runtime.log). A program overrides by declaring its
|
||||
/// own `pub const std_options`.
|
||||
pub const std_options: @import("std").Options =
|
||||
if (@hasDecl(program, "std_options")) program.std_options else runtime.log.default_options;
|
||||
|
||||
comptime {
|
||||
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! nothing to declare per source file.
|
||||
|
||||
pub const system = @import("system.zig");
|
||||
pub const log = @import("log.zig");
|
||||
/// Monotonic time, delays, and deadlines over the kernel clock/sleep/timer syscalls
|
||||
/// — an `Instant`/`Duration` front door, no time service (docs/timers.md).
|
||||
pub const time = @import("time.zig");
|
||||
|
||||
+112
-11
@@ -21,10 +21,34 @@ pub fn yield() void {
|
||||
_ = sc.systemCall0(.yield);
|
||||
}
|
||||
|
||||
/// Write raw bytes to the kernel log (a bring-up diagnostic; real output goes
|
||||
/// through the console/VFS later). Returns the byte count, or a wrapped -1.
|
||||
/// The tagged-log level of a record — re-exported so runtime.log and the logger
|
||||
/// service don't import `abi` themselves.
|
||||
pub const KlogLevel = abi.KlogLevel;
|
||||
pub const KlogStatus = abi.KlogStatus;
|
||||
pub const KlogRecordHeader = abi.KlogRecordHeader;
|
||||
pub const klog_record_header_size = abi.klog_record_header_size;
|
||||
pub const klog_record_alignment = abi.klog_record_alignment;
|
||||
pub const klog_record_magic = abi.klog_record_magic;
|
||||
pub const klog_flag_truncated = abi.klog_flag_truncated;
|
||||
pub const klog_maximum_message = abi.klog_maximum_message;
|
||||
pub const maximum_process_name = abi.maximum_process_name;
|
||||
pub const FileAttributes = abi.FileAttributes;
|
||||
pub const DirectoryEntryHeader = abi.DirectoryEntryHeader;
|
||||
pub const file_kind_regular = abi.file_kind_regular;
|
||||
pub const file_kind_directory = abi.file_kind_directory;
|
||||
|
||||
/// Write raw bytes to the kernel log (bring-up/panic diagnostics; ordinary
|
||||
/// output goes through std.log -> writeRecord). The kernel stamps the record
|
||||
/// with this process's id and name. Returns the byte count, or a wrapped -1.
|
||||
pub fn write(message: []const u8) usize {
|
||||
return sc.systemCall2(.debug_write, @intFromPtr(message.ptr), message.len);
|
||||
return writeRecord(.raw, message);
|
||||
}
|
||||
|
||||
/// Emit one leveled record into the tagged kernel log ring. The kernel stamps
|
||||
/// pid/name/sequence/timestamp; the payload should be a single line (embedded
|
||||
/// newlines split into further records).
|
||||
pub fn writeRecord(level: KlogLevel, message: []const u8) usize {
|
||||
return sc.systemCall3(.debug_write, @intFromPtr(message.ptr), message.len, @intFromEnum(level));
|
||||
}
|
||||
|
||||
/// Block the caller for `ms` milliseconds.
|
||||
@@ -59,14 +83,91 @@ pub fn wallClock() u64 {
|
||||
return @intCast(sc.systemCall0(.wall_clock));
|
||||
}
|
||||
|
||||
/// Copy bytes out of the kernel's in-memory diagnostic log — the accumulated
|
||||
/// stream of everything `write` (and the kernel itself) has emitted — starting at
|
||||
/// `offset`, into `out`. Returns the number of bytes copied (0 at end of buffer).
|
||||
/// A program reads the whole log by looping from offset 0, advancing by the return
|
||||
/// value, until it gets 0. This is how the boot log is persisted to disk on a
|
||||
/// headless/real machine where serial output is otherwise lost.
|
||||
pub fn klogRead(offset: usize, out: []u8) usize {
|
||||
return sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
||||
/// Copy bytes out of the tagged kernel log ring — framed records of everything
|
||||
/// every process (and the kernel) has emitted — starting at stream offset
|
||||
/// `offset`, into `out`. Returns the byte count (0 = caught up), or null when
|
||||
/// `offset` fell behind the ring's tail (those records were overwritten) or
|
||||
/// lies past its head; re-sync via `klogStatus`. A reader parses
|
||||
/// [KlogRecordHeader][name][message] frames (8-byte aligned) from the bytes.
|
||||
pub fn klogRead(offset: u64, out: []u8) ?usize {
|
||||
const r = sc.systemCall3(.klog_read, offset, @intFromPtr(out.ptr), out.len);
|
||||
if (@as(isize, @bitCast(r)) < 0) return null;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// The log ring's live cursors (oldest retained offset, end of stream, next
|
||||
/// sequence number) plus the wall-clock time of boot — how a log reader starts,
|
||||
/// detects loss, and names a per-boot log directory.
|
||||
pub fn klogStatus() ?KlogStatus {
|
||||
var status: KlogStatus = undefined;
|
||||
if (@as(isize, @bitCast(sc.systemCall1(.klog_status, @intFromPtr(&status)))) != 0) return null;
|
||||
return status;
|
||||
}
|
||||
|
||||
/// Where fs_resolve routed a path: served by the kernel (a permanent node
|
||||
/// token for fs_node) or by a userspace filesystem backend (an endpoint handle
|
||||
/// plus the rewritten mount-relative path, returned in the caller's buffer).
|
||||
pub const FsRoute = union(enum) {
|
||||
kernel: u64,
|
||||
backend: struct { handle: usize, path_len: usize },
|
||||
};
|
||||
|
||||
/// Route `path` through the kernel VFS. For a backend route the rewritten
|
||||
/// mount-relative path lands in `out` (behind a kernel-written length prefix,
|
||||
/// already stripped here: out[0..path_len] is the path).
|
||||
pub fn fsResolve(path: []const u8, flags: usize, out: []u8) ?FsRoute {
|
||||
var rax: usize = undefined;
|
||||
var rdx: usize = flags; // in: flags (arg #3); out: node token / backend handle
|
||||
asm volatile ("syscall"
|
||||
: [rax] "={rax}" (rax),
|
||||
[rdx] "+{rdx}" (rdx),
|
||||
: [n] "{rax}" (@intFromEnum(abi.SystemCall.fs_resolve)),
|
||||
[a0] "{rdi}" (@intFromPtr(path.ptr)),
|
||||
[a1] "{rsi}" (path.len),
|
||||
[a3] "{r10}" (@intFromPtr(out.ptr)),
|
||||
[a4] "{r8}" (out.len),
|
||||
: .{ .rcx = true, .r11 = true, .memory = true });
|
||||
if (@as(isize, @bitCast(rax)) < 0) return null;
|
||||
if (rax == abi.fs_route_kernel) return .{ .kernel = rdx };
|
||||
if (rax != abi.fs_route_backend) return null;
|
||||
const path_len = @as(usize, out[0]) | (@as(usize, out[1]) << 8);
|
||||
if (path_len + 2 > out.len) return null;
|
||||
std.mem.copyForwards(u8, out[0..path_len], out[2..][0..path_len]);
|
||||
return .{ .backend = .{ .handle = rdx, .path_len = path_len } };
|
||||
}
|
||||
|
||||
/// Read `out.len` bytes of a kernel-served node at `offset` (fs_node read).
|
||||
pub fn fsNodeRead(node_token: u64, offset: u64, out: []u8) ?usize {
|
||||
const r = sc.systemCall5(.fs_node, abi.fs_node_read, node_token, offset, @intFromPtr(out.ptr), out.len);
|
||||
if (@as(isize, @bitCast(r)) < 0) return null;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// A kernel-served node's metadata (fs_node status).
|
||||
pub fn fsNodeStatus(node_token: u64) ?abi.FileAttributes {
|
||||
var attributes: abi.FileAttributes = undefined;
|
||||
const r = sc.systemCall5(.fs_node, abi.fs_node_status, node_token, 0, @intFromPtr(&attributes), @sizeOf(abi.FileAttributes));
|
||||
if (@as(isize, @bitCast(r)) < 0) return null;
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// The `cursor`th child of a kernel-served directory (fs_node readdir): fills
|
||||
/// `out` with [DirectoryEntryHeader][name]; returns total bytes (0 = end).
|
||||
pub fn fsNodeReaddir(node_token: u64, cursor: u64, out: []u8) ?usize {
|
||||
const r = sc.systemCall5(.fs_node, abi.fs_node_readdir, node_token, cursor, @intFromPtr(out.ptr), out.len);
|
||||
if (@as(isize, @bitCast(r)) < 0) return null;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// Mount a userspace filesystem's endpoint at `prefix`, with an optional
|
||||
/// backend-side `rewrite` prefix ("" = none). Possession of the endpoint
|
||||
/// handle is the capability.
|
||||
pub fn fsMount(prefix: []const u8, backend: usize, rewrite: []const u8) bool {
|
||||
return sc.systemCall5(.fs_mount, @intFromPtr(prefix.ptr), prefix.len, backend, @intFromPtr(rewrite.ptr), rewrite.len) == 0;
|
||||
}
|
||||
|
||||
pub fn fsUnmount(prefix: []const u8) bool {
|
||||
return sc.systemCall2(.fs_unmount, @intFromPtr(prefix.ptr), prefix.len) == 0;
|
||||
}
|
||||
|
||||
/// End the process. Never returns.
|
||||
|
||||
+93
-2
@@ -58,7 +58,7 @@ pub const SystemCall = enum(u64) {
|
||||
signal_bind = 29, // signal_bind(endpoint) -> 0/-errno: nominate the endpoint this process's signals arrive on
|
||||
process_signal = 30, // process_signal(id, signal) -> 0/-errno: post a signal to a child (or to yourself)
|
||||
timer_bind = 31, // timer_bind(endpoint, ms) -> 0/-errno: one-shot timer — posts a notification when ms elapse
|
||||
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy the kernel RAM log buffer out to a user buffer (for persisting the boot log to disk)
|
||||
klog_read = 32, // klog_read(offset, ptr, len) -> bytes copied: copy tagged log-ring stream bytes from `offset` out to a user buffer; fails once `offset` falls behind the ring's tail (re-sync via klog_status)
|
||||
wall_clock = 33, // wall_clock() -> Unix epoch seconds (UTC): the RTC wall-clock time, for filesystem timestamps (mtime). Monotonic time is `clock`.
|
||||
shm_create = 34, // shm_create(len) -> virtual_address (rax), handle (rdx): a shareable, zeroed, cacheable RAM region mapped into this AS; the handle is a capability passed to another process as an ipc_call send_cap (docs/display-v2.md)
|
||||
shm_map = 35, // shm_map(cap) -> virtual_address: map the shared region named by a received capability into this address space (the same physical pages the creator sees)
|
||||
@@ -71,6 +71,11 @@ pub const SystemCall = enum(u64) {
|
||||
thread_self = 42, // thread_self() -> tid: the calling thread's kernel task id (runtime.Thread.getCurrentId)
|
||||
thread_join = 43, // thread_join(tid) -> 0: block until the thread with id `tid` has exited (runtime.Thread.join; no per-thread IPC endpoint) (docs/threading.md)
|
||||
set_thread_pointer = 44, // set_thread_pointer(addr) -> 0: set the caller's thread pointer (user-space TLS base; x86_64 IA32_FS_BASE, aarch64 TPIDR_EL0); restored per task across context switches (docs/threading-plan.md M10)
|
||||
klog_status = 45, // klog_status(ptr) -> 0: copy a KlogStatus (ring cursors + the boot wall-clock anchor) out to a user buffer
|
||||
fs_resolve = 46, // fs_resolve(path_ptr, path_len, flags, out_ptr, out_cap) -> route tag (rax: fs_route_*) + node token or backend handle (rdx); a backend resolve writes the rewritten mount-relative path into out (length in r8 via third result)
|
||||
fs_node = 47, // fs_node(op, node_token, offset, buf_ptr, buf_len) -> bytes/0/-errno: read/status/readdir on a kernel-served node (op values mirror the vfs-protocol Operation numbers)
|
||||
fs_mount = 48, // fs_mount(prefix_ptr, prefix_len, backend_handle, rewrite_ptr, rewrite_len) -> 0/-errno: mount a userspace filesystem's endpoint at an absolute prefix (possession of the handle is the capability)
|
||||
fs_unmount = 49, // fs_unmount(prefix_ptr, prefix_len) -> 0/-errno: remove a backend mount
|
||||
_,
|
||||
};
|
||||
|
||||
@@ -187,11 +192,97 @@ pub const ProcessDescriptor = extern struct {
|
||||
name: [maximum_process_name]u8, // argv[0] at spawn; empty for kernel tasks
|
||||
};
|
||||
|
||||
// --- the tagged kernel log ring (klog) ---------------------------------------
|
||||
// Every `debug_write` becomes one RECORD per payload line, stamped by the kernel
|
||||
// with the sender's pid, task name (its binary path), level, a per-boot sequence
|
||||
// number, and a monotonic timestamp. `klog_read` copies raw stream bytes — a
|
||||
// reader parses [KlogRecordHeader][name][message] frames, each padded to
|
||||
// `klog_record_alignment`. Sequence gaps tell a reader exactly how many records
|
||||
// the ring overwrote while it wasn't looking.
|
||||
|
||||
/// Log level of a klog record — std.log's levels plus `raw` (untagged bytes:
|
||||
/// kernel prints and legacy runtime.system.write output).
|
||||
pub const KlogLevel = enum(u8) { err = 0, warn = 1, info = 2, debug = 3, raw = 4 };
|
||||
|
||||
/// "RK" — leads every record; a parser's resync/corruption guard.
|
||||
pub const klog_record_magic: u16 = 0x4B52;
|
||||
|
||||
/// KlogRecordHeader.flags bit: the emitter truncated the payload to fit.
|
||||
pub const klog_flag_truncated: u8 = 1;
|
||||
|
||||
/// Header of one ring record, followed by `name_len` bytes of task name and
|
||||
/// `message_len` bytes of payload; the whole record is padded to 8 bytes.
|
||||
pub const KlogRecordHeader = extern struct {
|
||||
magic: u16, // klog_record_magic
|
||||
level: KlogLevel,
|
||||
name_len: u8, // 0..maximum_process_name
|
||||
pid: u32, // sender process id; 0 = the kernel itself
|
||||
sequence: u64, // per-boot monotonic record number (gaps = lost records)
|
||||
timestamp_ns: u64, // monotonic ns since boot (the `clock` timebase)
|
||||
message_len: u16, // payload bytes (excludes the record's trailing pad)
|
||||
flags: u8, // klog_flag_* bits
|
||||
_reserved: [5]u8,
|
||||
};
|
||||
|
||||
pub const klog_record_header_size: usize = 32; // @sizeOf(KlogRecordHeader), pinned by a test
|
||||
pub const klog_record_alignment: usize = 8;
|
||||
/// Per-record payload cap (one line; longer emitter lines are truncated).
|
||||
pub const klog_maximum_message: usize = 256;
|
||||
|
||||
// --- the kernel VFS root (resolve + redirect) --------------------------------
|
||||
// fs_resolve routes a path through the kernel mount table. Kernel-backed mounts
|
||||
// (the initrd at /system) resolve to a permanent node TOKEN served by fs_node;
|
||||
// userspace mounts resolve to the backend's endpoint handle (installed in the
|
||||
// caller's table, deduplicated) plus the rewritten mount-relative path — the
|
||||
// caller then speaks the vfs-protocol to the backend directly. The kernel never
|
||||
// blocks on a userspace filesystem.
|
||||
|
||||
/// fs_resolve result tags (rax).
|
||||
pub const fs_route_kernel: u64 = 0; // rdx = node token; serve via fs_node
|
||||
pub const fs_route_backend: u64 = 1; // rdx = endpoint handle; speak vfs-protocol
|
||||
|
||||
/// fs_node operations — the same numbers as the vfs-protocol Operation enum, so
|
||||
/// client code shares one vocabulary.
|
||||
pub const fs_node_read: u64 = 2;
|
||||
pub const fs_node_status: u64 = 4;
|
||||
pub const fs_node_readdir: u64 = 5;
|
||||
|
||||
/// fs_resolve flags (same values as the vfs-protocol open flags).
|
||||
pub const fs_flag_create: u64 = 1;
|
||||
|
||||
/// FileStatus-shaped node metadata (matches the vfs-protocol payload layout).
|
||||
pub const file_kind_regular: u32 = 0;
|
||||
pub const file_kind_directory: u32 = 1;
|
||||
pub const FileAttributes = extern struct {
|
||||
size: u64,
|
||||
kind: u32,
|
||||
_pad: u32 = 0,
|
||||
mtime: u64 = 0,
|
||||
};
|
||||
|
||||
/// One fs_node readdir result: the header, followed by `name_len` name bytes in
|
||||
/// the caller's buffer (matches the vfs-protocol DirectoryEntry layout).
|
||||
pub const DirectoryEntryHeader = extern struct {
|
||||
kind: u32,
|
||||
name_len: u32,
|
||||
size: u64,
|
||||
};
|
||||
|
||||
/// The klog_status copy-out: the ring's live cursors plus the wall-clock time
|
||||
/// of boot — the anchor a log persister names its per-boot directory with and
|
||||
/// combines with record timestamps for wall-clock line stamps.
|
||||
pub const KlogStatus = extern struct {
|
||||
tail: u64, // oldest retained stream offset — always a record boundary
|
||||
head: u64, // next byte to be written (end of stream)
|
||||
next_sequence: u64, // the sequence the next record will get
|
||||
boot_unix_seconds: u64, // wall-clock time of boot (RTC anchor)
|
||||
};
|
||||
|
||||
/// Well-known IPC service ids for the bootstrap name registry (create_ipc_endpoint +
|
||||
/// ipc_register/ipc_lookup). Small integers, so no string interning is needed
|
||||
/// during bring-up. The VFS server registers under `vfs`; clients look it up.
|
||||
pub const ServiceId = enum(u32) {
|
||||
vfs = 1,
|
||||
vfs = 1, // RETIRED: the router moved into the kernel (fs_resolve); the slot stays reserved
|
||||
input = 2,
|
||||
ps2_bus = 3, // the 8042 owner; child device drivers attach here for raw bytes
|
||||
device_manager = 4, // the tree, the matcher, the supervisor (docs/device-manager.md)
|
||||
|
||||
@@ -147,15 +147,11 @@ pub const BootInformation = extern struct {
|
||||
/// A device-tree boot path leaves this 0 and (later) fills a `device_tree_blob`
|
||||
/// field instead, so the kernel discovers devices without knowing what booted it.
|
||||
acpi_rsdp: u64 = 0,
|
||||
/// The raw `/system/services/init` ELF image, read off the boot volume by the loader
|
||||
/// into memory that survives the handoff (classified reserved, so the kernel
|
||||
/// identity-maps it and never allocates over it). 0/0 = no init found — the
|
||||
/// kernel boots without user space. Grows into a full initial_ramdisk handoff later.
|
||||
init_base: u64 = 0,
|
||||
init_len: u64 = 0,
|
||||
/// The initial_ramdisk image (a bundle of extra user binaries — the VFS server and
|
||||
/// device drivers), read off the boot volume into memory that survives the
|
||||
/// handoff, same as `init` above. 0/0 = no initial_ramdisk. See system/initial-ramdisk.zig.
|
||||
/// The initial_ramdisk image: every user binary from the boot volume's /system
|
||||
/// tree (init included), packed by the loader into memory that survives the
|
||||
/// handoff (classified reserved, so the kernel identity-maps it and never
|
||||
/// allocates over it). Entries are named by full FHS path. 0/0 = no binaries
|
||||
/// found — the kernel boots without user space. See system/initial-ramdisk.zig.
|
||||
initial_ramdisk_base: u64 = 0,
|
||||
initial_ramdisk_len: u64 = 0,
|
||||
};
|
||||
|
||||
@@ -16,11 +16,6 @@ const protocol = runtime.device_manager_protocol;
|
||||
const device = runtime.device;
|
||||
const pci_class = @import("pci-class");
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Log a discovered function with its (class / subclass / prog-IF) triple decoded
|
||||
/// to human names — the boot-log breadcrumb that says *what* the hardware is, so
|
||||
/// "class 0x01 (Mass Storage Controller) subclass 0x06 (Serial ATA Controller)
|
||||
@@ -74,7 +69,7 @@ fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) voi
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
if (!device.claim(bridge_id)) {
|
||||
writeLine("/system/drivers/pci-bus: unable to claim bridge device {d}\n", .{bridge_id});
|
||||
std.log.info("unable to claim bridge device {d}", .{bridge_id});
|
||||
return false;
|
||||
}
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
@@ -85,7 +80,7 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.id == bridge_id) break d;
|
||||
} else {
|
||||
writeLine("/system/drivers/pci-bus: device {d} not in the device tree\n", .{bridge_id});
|
||||
std.log.info("device {d} not in the device tree", .{bridge_id});
|
||||
return false;
|
||||
};
|
||||
// Resource 0 is the ECAM window (1 MiB of config space per bus); the bus
|
||||
@@ -159,7 +154,7 @@ fn scan() void {
|
||||
}
|
||||
}
|
||||
}
|
||||
writeLine("/system/drivers/pci-bus: {d} functions found\n", .{found});
|
||||
std.log.info("{d} functions found", .{found});
|
||||
}
|
||||
|
||||
/// Register one function under the bridge and report it to the manager. The
|
||||
@@ -229,7 +224,7 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void
|
||||
}
|
||||
|
||||
const registered = device.register(bridge_id, &descriptor) orelse {
|
||||
writeLine("/system/drivers/pci-bus: register refused for {d}:{d}.{d}\n", .{ bus, dev, function });
|
||||
std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
|
||||
return;
|
||||
};
|
||||
const report = protocol.ChildAdded{
|
||||
@@ -240,7 +235,7 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void
|
||||
};
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = runtime.ipc.call(manager_handle, std.mem.asBytes(&report), &reply) catch {
|
||||
writeLine("/system/drivers/pci-bus: child report for {d}:{d}.{d} failed\n", .{ bus, dev, function });
|
||||
std.log.info("child report for {d}:{d}.{d} failed", .{ bus, dev, function });
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,7 +250,7 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const argument = init.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
||||
bridge_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/pci-bus: malformed bridge device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed bridge device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
|
||||
@@ -23,11 +23,6 @@ const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
const protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||
/// registering) is still coming up.
|
||||
fn lookupBus() ?ipc.Handle {
|
||||
@@ -75,14 +70,14 @@ pub fn main(init: runtime.process.Init) void {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: starting for hid {s}\n", .{hid});
|
||||
std.log.info("starting for hid {s}", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/keyboard: out of memory\n");
|
||||
return;
|
||||
};
|
||||
if (device.findDeviceDescriptorByHid(buffer, hid) == null) {
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: no device for hid {s}\n", .{hid});
|
||||
std.log.info("no device for hid {s}", .{hid});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,7 +85,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
// absent (as today) it defaults to us.
|
||||
const layout_name = init.arguments.get(2) orelse "us";
|
||||
const layout = xkb.byName(layout_name) orelse xkb.us;
|
||||
writeLine("/system/drivers/ps2-bus/keyboard: layout {s}\n", .{layout.name});
|
||||
std.log.info("layout {s}", .{layout.name});
|
||||
|
||||
// Attach to the bus: hand it our endpoint, and it forwards every byte the
|
||||
// keyboard sends (it owns the controller; we own the decoding).
|
||||
|
||||
@@ -22,11 +22,6 @@ const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
const protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Look up the ps2-bus service, retrying while the bus (which spawned us before
|
||||
/// registering) is still coming up.
|
||||
fn lookupBus() ?ipc.Handle {
|
||||
@@ -54,14 +49,14 @@ pub fn main(init: runtime.process.Init) void {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: no HID argument\n");
|
||||
return;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus/mouse: starting for hid {s}\n", .{hid});
|
||||
std.log.info("starting for hid {s}", .{hid});
|
||||
|
||||
const buffer = runtime.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||
_ = runtime.system.write("/system/drivers/ps2-bus/mouse: out of memory\n");
|
||||
return;
|
||||
};
|
||||
if (ps2.findMouseDescriptor(buffer) == null) {
|
||||
writeLine("/system/drivers/ps2-bus/mouse: no device for hid {s}\n", .{hid});
|
||||
std.log.info("no device for hid {s}", .{hid});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,6 @@ const ps2 = @import("ps2-library.zig");
|
||||
const device = runtime.device;
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the child drivers (which run concurrently) can never interleave with it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// Ask the device on `port` what it is, then spawn the matching driver from the
|
||||
/// initial-ramdisk, handing it the device's HID as argv[1]. The driver is chosen
|
||||
/// from what the device reports, not from the port number. Returns the identified
|
||||
@@ -30,19 +23,19 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
/// attaches, or null if nothing was spawned.
|
||||
fn spawnIdentifiedDriver(controller: ps2.Controller, port: ps2.Port) ?ps2.DeviceType {
|
||||
const device_type = controller.identifyDevice(port) orelse {
|
||||
writeLine("/system/drivers/ps2-bus: identify timed out on port {s}\n", .{@tagName(port)});
|
||||
std.log.info("identify timed out on port {s}", .{@tagName(port)});
|
||||
return null;
|
||||
};
|
||||
const driver_name = device_type.driverName() orelse {
|
||||
writeLine("/system/drivers/ps2-bus: unrecognized device on port {s}\n", .{@tagName(port)});
|
||||
std.log.info("unrecognized device on port {s}", .{@tagName(port)});
|
||||
return null;
|
||||
};
|
||||
const hid = device_type.hid() orelse "";
|
||||
if (runtime.system.spawnWithArguments(driver_name, &.{hid}) != null) {
|
||||
writeLine("/system/drivers/ps2-bus: port {s} is a {s}, spawned {s}\n", .{ @tagName(port), hid, driver_name });
|
||||
std.log.info("port {s} is a {s}, spawned {s}", .{ @tagName(port), hid, driver_name });
|
||||
return device_type;
|
||||
}
|
||||
writeLine("/system/drivers/ps2-bus: failed to spawn {s}\n", .{driver_name});
|
||||
std.log.info("failed to spawn {s}", .{driver_name});
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -83,7 +76,7 @@ fn handleAttach(message: []const u8, got: ipc.Received, out: []u8) usize {
|
||||
const device_type = maybe_type orelse continue;
|
||||
if (@intFromEnum(device_type) != request.device_type) continue;
|
||||
port_endpoints[port_index] = endpoint;
|
||||
writeLine("/system/drivers/ps2-bus: {s} driver attached\n", .{@tagName(device_type)});
|
||||
std.log.info("{s} driver attached", .{@tagName(device_type)});
|
||||
return reply.write(out, .ok);
|
||||
}
|
||||
return reply.write(out, .no_such_device);
|
||||
|
||||
@@ -23,11 +23,6 @@ const ipc = runtime.ipc;
|
||||
const process = runtime.process;
|
||||
const input_protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// The modifier state a character lookup needs — derived from the report's
|
||||
// modifier byte, plus the driver-tracked caps-lock toggle.
|
||||
const ModifierSnapshot = struct {
|
||||
@@ -71,7 +66,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-hid/keyboard: malformed device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us;
|
||||
@@ -82,7 +77,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
}
|
||||
var device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-hid/keyboard: could not open device {d}\n", .{device_id});
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return;
|
||||
};
|
||||
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||
@@ -104,7 +99,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
_ = process.bindSignals(device.endpoint);
|
||||
writeLine("/system/drivers/usb-hid/keyboard: ok (device {d}, interface {d}, layout {s})\n", .{ device_id, device.interface_number, layout.name });
|
||||
std.log.info("ok (device {d}, interface {d}, layout {s})", .{ device_id, device.interface_number, layout.name });
|
||||
|
||||
var decoder = hid.KeyboardDecoder{};
|
||||
var caps_lock = false;
|
||||
|
||||
@@ -18,11 +18,6 @@ const ipc = runtime.ipc;
|
||||
const process = runtime.process;
|
||||
const input_protocol = runtime.input_protocol;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// The current pressed-button bitmask in input-protocol terms.
|
||||
fn buttonMask(buttons: u8) u32 {
|
||||
var mask: u32 = 0;
|
||||
@@ -38,7 +33,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
const device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-hid/mouse: malformed device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -47,7 +42,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
}
|
||||
var device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-hid/mouse: could not open device {d}\n", .{device_id});
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return;
|
||||
};
|
||||
const endpoint = device.findEndpoint(runtime.usb.transfer_type_interrupt, true) orelse {
|
||||
@@ -67,7 +62,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
_ = process.bindSignals(device.endpoint);
|
||||
writeLine("/system/drivers/usb-hid/mouse: ok (device {d}, interface {d})\n", .{ device_id, device.interface_number });
|
||||
std.log.info("ok (device {d}, interface {d})", .{ device_id, device.interface_number });
|
||||
|
||||
var previous_buttons: u8 = 0;
|
||||
var receive: [64]u8 = undefined;
|
||||
|
||||
@@ -18,11 +18,6 @@ const bot = @import("bulk-only-transport.zig");
|
||||
const block_protocol = @import("block-protocol");
|
||||
const dma = runtime.dma;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
var device_id: u64 = 0;
|
||||
var device: runtime.usb.Device = undefined;
|
||||
var bulk_in: runtime.usb.Endpoint = undefined;
|
||||
@@ -73,7 +68,7 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
return false;
|
||||
}
|
||||
device = runtime.usb.open(device_id) orelse {
|
||||
writeLine("/system/drivers/usb-storage: could not open device {d}\n", .{device_id});
|
||||
std.log.info("could not open device {d}", .{device_id});
|
||||
return false;
|
||||
};
|
||||
bulk_in = device.findEndpoint(runtime.usb.transfer_type_bulk, true) orelse {
|
||||
@@ -112,14 +107,14 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
const capacity = scsi.parseCapacity(capacity_bytes);
|
||||
block_size = capacity.block_size;
|
||||
block_count = @as(u64, capacity.last_lba) + 1;
|
||||
writeLine("/system/drivers/usb-storage: ready ({d} blocks x {d} bytes)\n", .{ block_count, block_size });
|
||||
std.log.info("ready ({d} blocks x {d} bytes)", .{ block_count, block_size });
|
||||
|
||||
// Self-check: read block 0 and log its trailing signature (0x55AA for a boot
|
||||
// sector) — proof READ(10) works end to end over the bulk path.
|
||||
const read0 = scsi.read10(0, 1);
|
||||
if (block_size <= 4096 and transact(&read0, true, command_data.physical, block_size)) {
|
||||
const sector: [*]const u8 = @ptrFromInt(command_data.virtual);
|
||||
writeLine("/system/drivers/usb-storage: block 0 signature 0x{x:0>2}{x:0>2}\n", .{ sector[510], sector[511] });
|
||||
std.log.info("block 0 signature 0x{x:0>2}{x:0>2}", .{ sector[510], sector[511] });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -171,7 +166,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-storage: malformed device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(block_protocol.message_maximum, .{
|
||||
|
||||
@@ -64,13 +64,6 @@ fn reportEndpointFor(device_token: u64) ?usize {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so
|
||||
/// concurrent instances (one per controller) can never interleave mid-line.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
var controller_id: u64 = protocol.no_device;
|
||||
|
||||
/// Claim the assigned controller, find its register window, and hello the
|
||||
@@ -79,7 +72,7 @@ var controller_id: u64 = protocol.no_device;
|
||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
service_endpoint = endpoint;
|
||||
if (!device.claim(controller_id)) {
|
||||
writeLine("/system/drivers/usb-xhci-bus: unable to claim controller device {d}\n", .{controller_id});
|
||||
std.log.info("unable to claim controller device {d}", .{controller_id});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -92,7 +85,7 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
const descriptor = for (buffer[0..@min(total, buffer.len)]) |d| {
|
||||
if (d.id == controller_id) break d;
|
||||
} else {
|
||||
writeLine("/system/drivers/usb-xhci-bus: device {d} not in the device tree\n", .{controller_id});
|
||||
std.log.info("device {d} not in the device tree", .{controller_id});
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -105,10 +98,10 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
break resource;
|
||||
}
|
||||
} else {
|
||||
writeLine("/system/drivers/usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id});
|
||||
std.log.info("controller device {d} has no register BAR", .{controller_id});
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{
|
||||
std.log.info("claimed controller device {d} (registers at 0x{x}, {d} bytes)", .{
|
||||
controller_id,
|
||||
register_window.start,
|
||||
register_window.len,
|
||||
@@ -124,7 +117,7 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller reset/bring-up failed\n");
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: controller running ({d} slots, {d}-byte contexts)\n", .{
|
||||
std.log.info("controller running ({d} slots, {d}-byte contexts)", .{
|
||||
controller.?.max_slots,
|
||||
controller.?.context_size,
|
||||
});
|
||||
@@ -198,7 +191,7 @@ fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||
_ = runtime.system.write("/system/drivers/usb-xhci-bus: controller not initialised\n");
|
||||
return;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: {d} root-hub ports\n", .{engine.max_ports});
|
||||
std.log.info("{d} root-hub ports", .{engine.max_ports});
|
||||
|
||||
var port: u32 = 1;
|
||||
var connected: u32 = 0;
|
||||
@@ -207,17 +200,17 @@ fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
||||
connected += 1;
|
||||
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} connected — {s} (speed class {d})\n", .{ port, speedName(speed), speed });
|
||||
std.log.info("port {d} connected — {s} (speed class {d})", .{ port, speedName(speed), speed });
|
||||
|
||||
const usb_device = engine.setupDevice(port, speed) orelse {
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} device setup failed\n", .{port});
|
||||
std.log.info("port {d} device setup failed", .{port});
|
||||
continue;
|
||||
};
|
||||
if (!engine.enumerate(usb_device)) {
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} enumeration failed\n", .{port});
|
||||
std.log.info("port {d} enumeration failed", .{port});
|
||||
continue;
|
||||
}
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)\n", .{
|
||||
std.log.info("port {d} device vendor 0x{x:0>4} product 0x{x:0>4}, {d} interface(s)", .{
|
||||
port,
|
||||
usb_device.device_descriptor.vendor_id,
|
||||
usb_device.device_descriptor.product_id,
|
||||
@@ -259,7 +252,7 @@ fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.In
|
||||
descriptor.hid_len = hid_text.len;
|
||||
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
|
||||
const registered = device.register(controller_id, &descriptor) orelse {
|
||||
writeLine("/system/drivers/usb-xhci-bus: register refused for port {d} interface {d}\n", .{ port, interface.number });
|
||||
std.log.info("register refused for port {d} interface {d}", .{ port, interface.number });
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -271,10 +264,10 @@ fn reportInterface(manager: runtime.ipc.Handle, port: u32, interface: library.In
|
||||
};
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
|
||||
writeLine("/system/drivers/usb-xhci-bus: child report for port {d} interface {d} failed\n", .{ port, interface.number });
|
||||
std.log.info("child report for port {d} interface {d} failed", .{ port, interface.number });
|
||||
return null;
|
||||
};
|
||||
writeLine("/system/drivers/usb-xhci-bus: port {d} interface {d} class {d}/{d}/{d} registered as device {d}\n", .{
|
||||
std.log.info("port {d} interface {d} class {d}/{d}/{d} registered as device {d}", .{
|
||||
port,
|
||||
interface.number,
|
||||
interface.class,
|
||||
@@ -407,7 +400,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
controller_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
writeLine("/system/drivers/usb-xhci-bus: malformed controller device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed controller device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(transfer.message_maximum, .{
|
||||
|
||||
@@ -103,13 +103,6 @@ var surface: shm.Region = undefined;
|
||||
var avail_shadow: u16 = 0;
|
||||
var used_shadow: u16 = 0;
|
||||
|
||||
/// Format one whole log line and emit it in a single `write`, so this driver's output can
|
||||
/// never interleave mid-line with the other drivers the manager runs concurrently.
|
||||
fn log(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [160]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// --- common-config register access (little-endian MMIO at `common_base`) ---------------
|
||||
|
||||
fn cfgRead(comptime T: type, comptime field: []const u8) T {
|
||||
@@ -154,7 +147,7 @@ fn mapBar(config: usize, descriptor: *const device.DeviceDescriptor, bar: u8) ?u
|
||||
return v;
|
||||
}
|
||||
}
|
||||
log("virtio-gpu: BAR {d} (physical 0x{x}) is not a mapped resource\n", .{ bar, base });
|
||||
std.log.info("BAR {d} (physical 0x{x}) is not a mapped resource", .{ bar, base });
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -162,7 +155,7 @@ fn mapBar(config: usize, descriptor: *const device.DeviceDescriptor, bar: u8) ?u
|
||||
/// notify structures (the only two V3 needs). Returns false if either is missing.
|
||||
fn walkCapabilities(config: usize, descriptor: *const device.DeviceDescriptor) bool {
|
||||
if (mmio.read(u16, config + 0x06) & 0x10 == 0) { // Status bit 4: capabilities list present
|
||||
log("virtio-gpu: device has no PCI capability list\n", .{});
|
||||
std.log.info("device has no PCI capability list", .{});
|
||||
return false;
|
||||
}
|
||||
var cap: u8 = @as(u8, @truncate(mmio.read(u8, config + 0x34))) & 0xFC;
|
||||
@@ -193,7 +186,7 @@ fn walkCapabilities(config: usize, descriptor: *const device.DeviceDescriptor) b
|
||||
cap = next;
|
||||
}
|
||||
if (common_base == 0 or notify_base == 0) {
|
||||
log("virtio-gpu: missing common-config or notify capability\n", .{});
|
||||
std.log.info("missing common-config or notify capability", .{});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -275,7 +268,7 @@ fn testPixel(index: u32) u32 {
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
_ = endpoint;
|
||||
if (!device.claim(device_id)) {
|
||||
log("virtio-gpu: unable to claim device {d}\n", .{device_id});
|
||||
std.log.info("unable to claim device {d}", .{device_id});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -284,7 +277,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
const descriptor = for (descriptors[0..@min(total, descriptors.len)]) |*d| {
|
||||
if (d.id == device_id) break d;
|
||||
} else {
|
||||
log("virtio-gpu: device {d} not in the device tree\n", .{device_id});
|
||||
std.log.info("device {d} not in the device tree", .{device_id});
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -292,13 +285,13 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
// decode + bus mastering (the device DMAs the ring and backing out of RAM); pci-bus only
|
||||
// preserves whatever the firmware left, and a secondary display is often left disabled.
|
||||
const config = device.mmioMap(device_id, 0) orelse {
|
||||
log("virtio-gpu: config-space map failed\n", .{});
|
||||
std.log.info("config-space map failed", .{});
|
||||
return false;
|
||||
};
|
||||
const vendor = mmio.read(u16, config + 0x00);
|
||||
const dev = mmio.read(u16, config + 0x02);
|
||||
if (vendor != virtio_vendor or dev != virtio_gpu_device) {
|
||||
log("virtio-gpu: not a virtio-gpu (vendor 0x{x} device 0x{x})\n", .{ vendor, dev });
|
||||
std.log.info("not a virtio-gpu (vendor 0x{x} device 0x{x})", .{ vendor, dev });
|
||||
return false;
|
||||
}
|
||||
mmio.write(u16, config + 0x04, mmio.read(u16, config + 0x04) | 0x06); // MEM + bus master
|
||||
@@ -317,7 +310,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
// High feature word: VERSION_1 (bit 32) is required for a modern device.
|
||||
cfgWrite(u32, "device_feature_select", vp.feature_version_1_word);
|
||||
if (cfgRead(u32, "device_feature") & vp.feature_version_1_bit == 0) {
|
||||
log("virtio-gpu: device does not offer VERSION_1 (not a modern device)\n", .{});
|
||||
std.log.info("device does not offer VERSION_1 (not a modern device)", .{});
|
||||
return false;
|
||||
}
|
||||
// Accept exactly VERSION_1, plus EDID when the device offered it (never a feature it didn't).
|
||||
@@ -327,7 +320,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
cfgWrite(u32, "driver_feature", vp.feature_version_1_bit);
|
||||
orStatus(vp.status_features_ok);
|
||||
if (cfgRead(u8, "device_status") & vp.status_features_ok == 0) {
|
||||
log("virtio-gpu: device rejected the negotiated features\n", .{});
|
||||
std.log.info("device rejected the negotiated features", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -335,15 +328,15 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
cfgWrite(u16, "queue_select", 0);
|
||||
const device_qsize = cfgRead(u16, "queue_size");
|
||||
if (device_qsize < queue_size) {
|
||||
log("virtio-gpu: control queue too small ({d})\n", .{device_qsize});
|
||||
std.log.info("control queue too small ({d})", .{device_qsize});
|
||||
return false;
|
||||
}
|
||||
ring = dma.alloc(4096, dma.coherent) orelse {
|
||||
log("virtio-gpu: virtqueue allocation failed\n", .{});
|
||||
std.log.info("virtqueue allocation failed", .{});
|
||||
return false;
|
||||
};
|
||||
command = dma.alloc(4096, dma.coherent) orelse {
|
||||
log("virtio-gpu: command-buffer allocation failed\n", .{});
|
||||
std.log.info("command-buffer allocation failed", .{});
|
||||
return false;
|
||||
};
|
||||
mmio.write(u16, ring.virtual + avail_offset, 1); // VIRTQ_AVAIL_F_NO_INTERRUPT: we poll
|
||||
@@ -371,7 +364,7 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
.height = max_height,
|
||||
};
|
||||
if (command_nodata(@sizeOf(vg.ResourceCreate2d)) != ok_nodata) {
|
||||
log("virtio-gpu: resource_create_2d failed\n", .{});
|
||||
std.log.info("resource_create_2d failed", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -379,11 +372,11 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
// Back the resource with a shared (shm) surface, so the compositor and the device work
|
||||
// the same physical pages. The device needs the guest-physical base for attach_backing.
|
||||
surface = shm.create(scanout_bytes) orelse {
|
||||
log("virtio-gpu: scanout surface allocation failed\n", .{});
|
||||
std.log.info("scanout surface allocation failed", .{});
|
||||
return false;
|
||||
};
|
||||
const surface_physical = shm.physical(surface.handle) orelse {
|
||||
log("virtio-gpu: could not resolve the scanout surface physical address\n", .{});
|
||||
std.log.info("could not resolve the scanout surface physical address", .{});
|
||||
return false;
|
||||
};
|
||||
{
|
||||
@@ -396,15 +389,15 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
const entry: *vg.MemEntry = @ptrFromInt(command.virtual + request_offset + @sizeOf(vg.ResourceAttachBacking));
|
||||
entry.* = .{ .addr = surface_physical, .length = @intCast(scanout_bytes) };
|
||||
if (command_nodata(@sizeOf(vg.ResourceAttachBacking) + @sizeOf(vg.MemEntry)) != ok_nodata) {
|
||||
log("virtio-gpu: resource_attach_backing failed\n", .{});
|
||||
std.log.info("resource_attach_backing failed", .{});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!setScanoutRect()) {
|
||||
log("virtio-gpu: set_scanout failed\n", .{});
|
||||
std.log.info("set_scanout failed", .{});
|
||||
return false;
|
||||
}
|
||||
log("virtio-gpu: scanout {d}x{d} online\n", .{ current_width, current_height });
|
||||
std.log.info("scanout {d}x{d} online", .{ current_width, current_height });
|
||||
|
||||
// Hello the device manager so it counts us as up (and does not stop us at the hello
|
||||
// deadline). A restarted instance re-hellos here and re-announces below — the compositor
|
||||
@@ -422,17 +415,17 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||
for (0..pixel_count) |i| pixels[i] = testPixel(@intCast(i));
|
||||
|
||||
if (!presentFull()) {
|
||||
log("virtio-gpu: initial present failed\n", .{});
|
||||
std.log.info("initial present failed", .{});
|
||||
return false;
|
||||
}
|
||||
// The scanout surface is CPU-visible RAM: read the pattern back to prove the mapping,
|
||||
// which together with the flush ack above is the automated stand-in for "it's on screen".
|
||||
mmio.rmb();
|
||||
if (pixels[0] != testPixel(0) or pixels[pixel_count / 2] != testPixel(@intCast(pixel_count / 2))) {
|
||||
log("virtio-gpu: pixel read-back mismatch\n", .{});
|
||||
std.log.info("pixel read-back mismatch", .{});
|
||||
return false;
|
||||
}
|
||||
log("virtio-gpu: flush acked, pixel check ok\n", .{});
|
||||
std.log.info("flush acked, pixel check ok", .{});
|
||||
|
||||
// Offer the shared surface to the compositor so it upgrades off the GOP floor (V4).
|
||||
announce();
|
||||
@@ -456,18 +449,18 @@ fn setScanoutRect() bool {
|
||||
/// a device that doesn't offer EDID, or a missing/short block, is logged and ignored.
|
||||
fn readEdid() void {
|
||||
if (!edid_available) {
|
||||
log("virtio-gpu: EDID not offered by device\n", .{});
|
||||
std.log.info("EDID not offered by device", .{});
|
||||
return;
|
||||
}
|
||||
const request = requestAt(vg.GetEdid);
|
||||
request.* = .{ .hdr = .{ .type = @intFromEnum(vg.CmdType.get_edid) }, .scanout = 0 };
|
||||
if (!submit(@sizeOf(vg.GetEdid), @sizeOf(vg.RespEdid))) {
|
||||
log("virtio-gpu: EDID request not acked\n", .{});
|
||||
std.log.info("EDID request not acked", .{});
|
||||
return;
|
||||
}
|
||||
const response: *vg.RespEdid = @ptrFromInt(command.virtual + response_offset);
|
||||
if (response.hdr.type != @intFromEnum(vg.CmdType.resp_ok_edid) or response.size < 64) {
|
||||
log("virtio-gpu: EDID unavailable\n", .{});
|
||||
std.log.info("EDID unavailable", .{});
|
||||
return;
|
||||
}
|
||||
// The first detailed timing descriptor (EDID base-block offset 54) is the preferred mode:
|
||||
@@ -483,7 +476,7 @@ fn readEdid() void {
|
||||
const v_blank = @as(u64, e[60]) | (@as(u64, e[61] & 0x0F) << 8);
|
||||
const total = (@as(u64, h_active) + h_blank) * (@as(u64, v_active) + v_blank);
|
||||
if (total != 0) edid_refresh_hz = @intCast((clock_hz + total / 2) / total);
|
||||
log("virtio-gpu: EDID preferred mode {d}x{d} @ {d} Hz\n", .{ h_active, v_active, edid_refresh_hz });
|
||||
std.log.info("EDID preferred mode {d}x{d} @ {d} Hz", .{ h_active, v_active, edid_refresh_hz });
|
||||
}
|
||||
|
||||
/// Present the whole surface: copy the guest backing into the host resource, then flush it to
|
||||
@@ -529,20 +522,20 @@ fn helloManager() void {
|
||||
if (ipc.lookup(.device_manager)) |h| break h;
|
||||
system.sleep(20);
|
||||
} else {
|
||||
log("virtio-gpu: no device manager to hello\n", .{});
|
||||
std.log.info("no device manager to hello", .{});
|
||||
return;
|
||||
};
|
||||
const hello = dm.Hello{ .role = @intFromEnum(dm.Role.bus), .device_id = device_id };
|
||||
var reply: [dm.reply_size]u8 = undefined;
|
||||
const n = ipc.call(manager, std.mem.asBytes(&hello), &reply) catch {
|
||||
log("virtio-gpu: hello call failed\n", .{});
|
||||
std.log.info("hello call failed", .{});
|
||||
return;
|
||||
};
|
||||
if (n < dm.reply_size or std.mem.bytesToValue(dm.HelloReply, reply[0..dm.reply_size]).status != 0) {
|
||||
log("virtio-gpu: hello refused\n", .{});
|
||||
std.log.info("hello refused", .{});
|
||||
return;
|
||||
}
|
||||
log("virtio-gpu: hello acknowledged\n", .{});
|
||||
std.log.info("hello acknowledged", .{});
|
||||
}
|
||||
|
||||
/// Announce the scanout to the display service so it upgrades off the GOP framebuffer: hand it
|
||||
@@ -556,7 +549,7 @@ fn announce() void {
|
||||
if (ipc.lookup(.display)) |h| break h;
|
||||
system.sleep(20);
|
||||
} else {
|
||||
log("virtio-gpu: no display service to announce to (scanout-only)\n", .{});
|
||||
std.log.info("no display service to announce to (scanout-only)", .{});
|
||||
return;
|
||||
};
|
||||
var request = dp.Request{
|
||||
@@ -569,10 +562,10 @@ fn announce() void {
|
||||
};
|
||||
var reply: [dp.reply_size]u8 = undefined;
|
||||
_ = ipc.callCap(display, std.mem.asBytes(&request), &reply, surface.handle) catch {
|
||||
log("virtio-gpu: announce to display failed\n", .{});
|
||||
std.log.info("announce to display failed", .{});
|
||||
return;
|
||||
};
|
||||
log("virtio-gpu: announced scanout to display\n", .{});
|
||||
std.log.info("announced scanout to display", .{});
|
||||
}
|
||||
|
||||
/// A `sp.Reply{status}` written into `reply`.
|
||||
@@ -621,7 +614,7 @@ pub fn main(init: runtime.process.Init) void {
|
||||
return;
|
||||
};
|
||||
device_id = std.fmt.parseInt(u64, argument, 10) catch {
|
||||
log("virtio-gpu: malformed device id '{s}'\n", .{argument});
|
||||
std.log.info("malformed device id '{s}'", .{argument});
|
||||
return;
|
||||
};
|
||||
runtime.service.run(256, .{
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
//! The initial_ramdisk (initial ramdisk) container format — shared by the build-time
|
||||
//! packer (tools/make-initial-ramdisk.py) and the kernel that unpacks it. Deliberately
|
||||
//! trivial: a header, a table of fixed-size entries, then the concatenated file
|
||||
//! blobs. We own both producer and consumer, so it need be no fancier.
|
||||
//! The initial_ramdisk (initial ramdisk) container format — built in RAM by the
|
||||
//! bootloader (boot/efi.zig walks the boot volume's /system tree) and unpacked by
|
||||
//! the kernel. Deliberately trivial: a header, a table of fixed-size entries, then
|
||||
//! the concatenated file blobs. We own both producer and consumer, so it need be
|
||||
//! no fancier.
|
||||
//!
|
||||
//! v2: entry names are full FHS paths ("/system/services/init"), 64 bytes — the
|
||||
//! same limit as a task name (abi.maximum_process_name), so a path-named task is
|
||||
//! never truncated. The boot volume's file tree is the single source of truth;
|
||||
//! this image is only the loader→kernel handoff snapshot of it.
|
||||
//!
|
||||
//! Layout:
|
||||
//! Header (magic, count)
|
||||
@@ -10,8 +16,14 @@
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// "DNRD" — identifies a danos initial_ramdisk image.
|
||||
pub const magic: u32 = 0x444E5244;
|
||||
/// "DNR2" — identifies a danos initial_ramdisk image, format v2 (path names).
|
||||
/// The v1 magic ("DNRD", basename entries) is rejected: a stale image should
|
||||
/// fail loudly at Reader.init, not misparse names.
|
||||
pub const magic: u32 = 0x32524E44;
|
||||
|
||||
/// Entry name capacity. Matches abi.maximum_process_name so a spawned task can
|
||||
/// always carry its full binary path as its name.
|
||||
pub const maximum_name = 64;
|
||||
|
||||
pub const Header = extern struct {
|
||||
magic: u32,
|
||||
@@ -19,11 +31,17 @@ pub const Header = extern struct {
|
||||
};
|
||||
|
||||
pub const Entry = extern struct {
|
||||
name: [32]u8, // NUL-padded file name (basename)
|
||||
name: [maximum_name]u8, // NUL-padded FHS path, e.g. "/system/services/init"
|
||||
offset: u64, // byte offset of the blob within the image
|
||||
len: u64, // blob length in bytes
|
||||
};
|
||||
|
||||
/// The basename of a path: the final component after the last '/'.
|
||||
pub fn basename(path: []const u8) []const u8 {
|
||||
const i = std.mem.lastIndexOfScalar(u8, path, '/') orelse return path;
|
||||
return path[i + 1 ..];
|
||||
}
|
||||
|
||||
/// A validated view over an initial_ramdisk image. `init` checks the magic and that the
|
||||
/// entry table fits; `entry` bounds-checks each blob against the image.
|
||||
pub const Reader = struct {
|
||||
@@ -48,11 +66,74 @@ pub const Reader = struct {
|
||||
if (e.offset > self.image.len or e.len > self.image.len - e.offset) return null;
|
||||
// The name is stored in the entry's fixed field; return a stable slice
|
||||
// into the image (not the value copy) up to the NUL terminator.
|
||||
const name_field = self.image[off .. off + 32];
|
||||
const name_field = self.image[off .. off + maximum_name];
|
||||
const nlen = std.mem.indexOfScalar(u8, name_field, 0) orelse name_field.len;
|
||||
return .{
|
||||
.name = name_field[0..nlen],
|
||||
.blob = self.image[@intCast(e.offset)..][0..@intCast(e.len)],
|
||||
};
|
||||
}
|
||||
|
||||
/// Look a binary up by name: an exact path match wins; otherwise a unique
|
||||
/// basename match ("fat" finds "/system/services/fat") keeps pre-path callers
|
||||
/// working. The returned Item's name is always the stored full path.
|
||||
pub fn find(self: Reader, name: []const u8) ?Item {
|
||||
var i: u32 = 0;
|
||||
while (i < self.count) : (i += 1) {
|
||||
const item = self.entry(i) orelse continue;
|
||||
if (std.mem.eql(u8, item.name, name)) return item;
|
||||
}
|
||||
i = 0;
|
||||
while (i < self.count) : (i += 1) {
|
||||
const item = self.entry(i) orelse continue;
|
||||
if (std.mem.eql(u8, basename(item.name), name)) return item;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// --- tests (host) -----------------------------------------------------------
|
||||
|
||||
fn testImage(buffer: []u8, entries: []const struct { name: []const u8, blob: []const u8 }) []const u8 {
|
||||
const table_end = @sizeOf(Header) + entries.len * @sizeOf(Entry);
|
||||
var offset: usize = table_end;
|
||||
std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = magic, .count = @intCast(entries.len) };
|
||||
for (entries, 0..) |e, i| {
|
||||
var record = Entry{ .name = @splat(0), .offset = offset, .len = e.blob.len };
|
||||
@memcpy(record.name[0..e.name.len], e.name);
|
||||
std.mem.bytesAsValue(Entry, buffer[@sizeOf(Header) + i * @sizeOf(Entry) ..][0..@sizeOf(Entry)]).* = record;
|
||||
@memcpy(buffer[offset..][0..e.blob.len], e.blob);
|
||||
offset += e.blob.len;
|
||||
}
|
||||
return buffer[0..offset];
|
||||
}
|
||||
|
||||
test "find matches exact path, then unique basename; name is the stored path" {
|
||||
var buffer: [1024]u8 = undefined;
|
||||
const image = testImage(&buffer, &.{
|
||||
.{ .name = "/system/services/init", .blob = "INIT" },
|
||||
.{ .name = "/system/drivers/ps2-bus", .blob = "PS2" },
|
||||
});
|
||||
const rd = Reader.init(image).?;
|
||||
|
||||
const by_path = rd.find("/system/services/init").?;
|
||||
try std.testing.expectEqualStrings("/system/services/init", by_path.name);
|
||||
try std.testing.expectEqualStrings("INIT", by_path.blob);
|
||||
|
||||
const by_base = rd.find("ps2-bus").?;
|
||||
try std.testing.expectEqualStrings("/system/drivers/ps2-bus", by_base.name);
|
||||
try std.testing.expectEqualStrings("PS2", by_base.blob);
|
||||
|
||||
try std.testing.expect(rd.find("no-such-binary") == null);
|
||||
}
|
||||
|
||||
test "v1 magic is rejected" {
|
||||
var buffer: [64]u8 = @splat(0);
|
||||
std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = 0x444E5244, .count = 0 };
|
||||
try std.testing.expect(Reader.init(&buffer) == null);
|
||||
}
|
||||
|
||||
test "basename" {
|
||||
try std.testing.expectEqualStrings("fat", basename("/system/services/fat"));
|
||||
try std.testing.expectEqualStrings("fat", basename("fat"));
|
||||
}
|
||||
|
||||
@@ -516,6 +516,20 @@ pub fn installShmHandle(t: *Task, shm: *ShmObject) i64 {
|
||||
return installEntry(t, .{ .kind = handle_kind_shm, .ptr = @ptrCast(shm) });
|
||||
}
|
||||
|
||||
/// Install an endpoint handle, reusing an existing slot that already names this
|
||||
/// endpoint (no new reference taken in that case). For callers that install per
|
||||
/// operation — fs_resolve — so a 16-slot table can't be exhausted by repeats.
|
||||
/// Any subsystem installing handles per-call should come through here.
|
||||
pub fn installHandleDeduped(t: *Task, endpoint: *Endpoint) i64 {
|
||||
for (t.handles, 0..) |slot, i| {
|
||||
const entry = slot orelse continue;
|
||||
if (entry.kind == handle_kind_endpoint and entry.ptr == @as(*anyopaque, @ptrCast(endpoint))) return @intCast(i);
|
||||
}
|
||||
const h = installHandle(t, endpoint);
|
||||
if (h >= 0) endpoint.refcount += 1; // the table entry owns a reference
|
||||
return h;
|
||||
}
|
||||
|
||||
/// Resolve a handle to its endpoint, or null if out of range, unused, or a different kind
|
||||
/// (e.g. an shm handle used where an endpoint is expected).
|
||||
pub fn resolveHandle(t: *Task, h: u64) ?*Endpoint {
|
||||
|
||||
+12
-16
@@ -75,7 +75,7 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
||||
// Retain the whole stream in a RAM buffer too, so a user program can later
|
||||
// read it back (klog_read) and persist the boot log to disk — the only way to
|
||||
// see it on a headless/real machine with no host capturing serial.
|
||||
log.addSink(log.ramSink);
|
||||
// (Retention is the tagged ring inside log.zig — not a sink.)
|
||||
|
||||
// The **framebuffer** is deliberately *not* a log sink. It's a separate output
|
||||
// surface — a bootstrap text console today, a graphics device driver later — so
|
||||
@@ -329,20 +329,16 @@ fn kmain(boot_information: *const BootInformation) noreturn {
|
||||
// service supervisor and the device manager spawns the drivers it discovers.
|
||||
publishInitialRamdisk(boot_information);
|
||||
|
||||
// Hand over to user space: load /system/services/init (read off the boot volume by
|
||||
// the loader) and spawn it as a real ring-3 process, PID 1. As the supervisor it
|
||||
// brings up the system services (the VFS server, the device manager); the device
|
||||
// manager then discovers the hardware and spawns each driver. init runs on its own
|
||||
// address space, preemptively — this boot context becomes the BSP's idle loop.
|
||||
if (boot_information.init_len != 0) {
|
||||
status("/system/kernel: starting /system/services/init...\n");
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
process.spawnProcess(image, 4, &.{"/system/services/init"}) catch |err| {
|
||||
statusPrint("/system/kernel: /system/services/init failed to load: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
} else {
|
||||
status("no /system/services/init on the boot volume.\n");
|
||||
}
|
||||
// Hand over to user space: spawn /system/services/init out of the ramdisk as a
|
||||
// real ring-3 process, PID 1 — it rides the same table as every other binary.
|
||||
// As the supervisor it brings up the system services (the VFS server, the device
|
||||
// manager); the device manager then discovers the hardware and spawns each
|
||||
// driver. init runs on its own address space, preemptively — this boot context
|
||||
// becomes the BSP's idle loop.
|
||||
status("/system/kernel: starting /system/services/init...\n");
|
||||
process.spawnBundled("/system/services/init") catch |err| {
|
||||
statusPrint("/system/kernel: /system/services/init failed to start: {s}\n", .{@errorName(err)});
|
||||
};
|
||||
|
||||
// Become the idle task: drop below every real task and halt until an
|
||||
// interrupt. The timer keeps preempting into init and any other work.
|
||||
@@ -430,7 +426,7 @@ fn status(message: []const u8) void {
|
||||
/// any display service holding the framebuffer. The console is otherwise silent in normal
|
||||
/// operation (see `status`); it exists now only for early-boot and fatal output.
|
||||
fn fatal(message: []const u8) void {
|
||||
log.write(message);
|
||||
log.appendPanic(message); // bounded lock wait: a panic never deadlocks on the log
|
||||
console.setSuppressed(false);
|
||||
console.write(message);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
//! The tagged kernel log ring — a circular byte buffer of framed records, each
|
||||
//! stamped by the writer (the kernel) with the sender's pid, task name, level,
|
||||
//! per-boot sequence number, and monotonic timestamp. Pure code over an
|
||||
//! embedded buffer — no architecture or lock imports — so it host-tests
|
||||
//! alongside the other pure kernel pieces (`zig build test`).
|
||||
//!
|
||||
//! `head` and `tail` are free-running u64 positions in a logical byte stream;
|
||||
//! the physical wrap is invisible to readers (all copies are modulo the
|
||||
//! buffer), so a record never splits logically and no padding records exist.
|
||||
//! Reclaim happens record by record: the writer parses the header at `tail`
|
||||
//! (which it wrote itself) and advances until the new record fits — `tail`
|
||||
//! always sits on a record boundary, and sequence-number gaps tell a reader
|
||||
//! exactly how many records it lost.
|
||||
//!
|
||||
//! Locking is the caller's job (log.zig holds its log lock around every call);
|
||||
//! the ring itself is single-writer, snapshot-reader.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
|
||||
pub fn Ring(comptime capacity: usize) type {
|
||||
comptime std.debug.assert(std.math.isPowerOfTwo(capacity));
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
buffer: [capacity]u8 = undefined,
|
||||
head: u64 = 0,
|
||||
tail: u64 = 0,
|
||||
next_sequence: u64 = 0,
|
||||
|
||||
/// Append one record; returns its sequence number. `name` and `message`
|
||||
/// are clamped to their ABI caps (the syscall clamps earlier too — the
|
||||
/// clamp here makes the ring safe in isolation).
|
||||
pub fn append(
|
||||
self: *Self,
|
||||
pid: u32,
|
||||
name: []const u8,
|
||||
level: abi.KlogLevel,
|
||||
timestamp_ns: u64,
|
||||
message: []const u8,
|
||||
truncated: bool,
|
||||
) u64 {
|
||||
const name_len: usize = @min(name.len, abi.maximum_process_name);
|
||||
const message_len: usize = @min(message.len, abi.klog_maximum_message);
|
||||
const record_len = recordLength(name_len, message_len);
|
||||
|
||||
// Reclaim whole records until the new one fits.
|
||||
while (self.head + record_len - self.tail > capacity) self.reclaimOne();
|
||||
|
||||
const sequence = self.next_sequence;
|
||||
self.next_sequence += 1;
|
||||
|
||||
const header = abi.KlogRecordHeader{
|
||||
.magic = abi.klog_record_magic,
|
||||
.level = level,
|
||||
.name_len = @intCast(name_len),
|
||||
.pid = pid,
|
||||
.sequence = sequence,
|
||||
.timestamp_ns = timestamp_ns,
|
||||
.message_len = @intCast(message_len),
|
||||
.flags = if (truncated) abi.klog_flag_truncated else 0,
|
||||
._reserved = @splat(0),
|
||||
};
|
||||
self.put(self.head, std.mem.asBytes(&header));
|
||||
self.put(self.head + abi.klog_record_header_size, name[0..name_len]);
|
||||
self.put(self.head + abi.klog_record_header_size + name_len, message[0..message_len]);
|
||||
// The alignment pad is dead space; zero it so raw dumps stay tidy.
|
||||
var pad = abi.klog_record_header_size + name_len + message_len;
|
||||
while (pad < record_len) : (pad += 1)
|
||||
self.buffer[@intCast((self.head + pad) % capacity)] = 0;
|
||||
self.head += record_len;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
/// Copy stream bytes beginning at `offset` into `out`. Returns null if
|
||||
/// `offset` fell behind `tail` (overwritten) or lies past `head` — the
|
||||
/// reader re-syncs from status(). 0 bytes means caught up.
|
||||
pub fn read(self: *const Self, offset: u64, out: []u8) ?usize {
|
||||
if (offset < self.tail or offset > self.head) return null;
|
||||
const n: usize = @intCast(@min(out.len, self.head - offset));
|
||||
self.get(offset, out[0..n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
/// Cursors for klog_status. boot_unix_seconds is the kernel wrapper's
|
||||
/// to fill — the ring knows nothing of wall clocks.
|
||||
pub fn status(self: *const Self) abi.KlogStatus {
|
||||
return .{
|
||||
.tail = self.tail,
|
||||
.head = self.head,
|
||||
.next_sequence = self.next_sequence,
|
||||
.boot_unix_seconds = 0,
|
||||
};
|
||||
}
|
||||
|
||||
fn reclaimOne(self: *Self) void {
|
||||
var header_bytes: [abi.klog_record_header_size]u8 = undefined;
|
||||
self.get(self.tail, &header_bytes);
|
||||
const header = std.mem.bytesToValue(abi.KlogRecordHeader, &header_bytes);
|
||||
// The writer wrote this header itself: the assert guards against
|
||||
// memory corruption, not bad input.
|
||||
std.debug.assert(header.magic == abi.klog_record_magic);
|
||||
self.tail += recordLength(header.name_len, header.message_len);
|
||||
}
|
||||
|
||||
fn recordLength(name_len: usize, message_len: usize) usize {
|
||||
return std.mem.alignForward(usize, abi.klog_record_header_size + name_len + message_len, abi.klog_record_alignment);
|
||||
}
|
||||
|
||||
// Byte-at-a-time modulo copies keep the wrap logic obviously correct;
|
||||
// if they ever show in a profile, split into two @memcpy spans.
|
||||
fn put(self: *Self, offset: u64, bytes: []const u8) void {
|
||||
for (bytes, 0..) |b, i| self.buffer[@intCast((offset + i) % capacity)] = b;
|
||||
}
|
||||
|
||||
fn get(self: *const Self, offset: u64, out: []u8) void {
|
||||
for (out, 0..) |*b, i| b.* = self.buffer[@intCast((offset + i) % capacity)];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --- tests (host) -----------------------------------------------------------
|
||||
|
||||
const TestRing = Ring(4096);
|
||||
|
||||
/// Parse the record at `offset` out of `ring`, returning the header plus name
|
||||
/// and message copies — the same walk a userspace drainer performs.
|
||||
const Parsed = struct {
|
||||
header: abi.KlogRecordHeader,
|
||||
name: [abi.maximum_process_name]u8 = undefined,
|
||||
message: [abi.klog_maximum_message]u8 = undefined,
|
||||
|
||||
fn nameSlice(self: *const Parsed) []const u8 {
|
||||
return self.name[0..self.header.name_len];
|
||||
}
|
||||
fn messageSlice(self: *const Parsed) []const u8 {
|
||||
return self.message[0..self.header.message_len];
|
||||
}
|
||||
fn next(self: *const Parsed, offset: u64) u64 {
|
||||
return offset + std.mem.alignForward(usize, abi.klog_record_header_size + self.header.name_len + self.header.message_len, abi.klog_record_alignment);
|
||||
}
|
||||
};
|
||||
|
||||
fn parseAt(ring: *const TestRing, offset: u64) Parsed {
|
||||
var p: Parsed = undefined;
|
||||
var header_bytes: [abi.klog_record_header_size]u8 = undefined;
|
||||
std.debug.assert(ring.read(offset, &header_bytes).? == header_bytes.len);
|
||||
p.header = std.mem.bytesToValue(abi.KlogRecordHeader, &header_bytes);
|
||||
std.debug.assert(p.header.magic == abi.klog_record_magic);
|
||||
_ = ring.read(offset + abi.klog_record_header_size, p.name[0..p.header.name_len]);
|
||||
_ = ring.read(offset + abi.klog_record_header_size + p.header.name_len, p.message[0..p.header.message_len]);
|
||||
return p;
|
||||
}
|
||||
|
||||
test "header size is pinned" {
|
||||
try std.testing.expectEqual(abi.klog_record_header_size, @sizeOf(abi.KlogRecordHeader));
|
||||
}
|
||||
|
||||
test "append/read round trip" {
|
||||
var ring = std.testing.allocator.create(TestRing) catch unreachable;
|
||||
defer std.testing.allocator.destroy(ring);
|
||||
ring.* = .{};
|
||||
|
||||
_ = ring.append(7, "/system/services/fat", .info, 123, "mounted /mnt/usb", false);
|
||||
_ = ring.append(0, "kernel", .raw, 456, "wall clock online", false);
|
||||
|
||||
const first = parseAt(ring, ring.tail);
|
||||
try std.testing.expectEqual(@as(u32, 7), first.header.pid);
|
||||
try std.testing.expectEqual(abi.KlogLevel.info, first.header.level);
|
||||
try std.testing.expectEqual(@as(u64, 123), first.header.timestamp_ns);
|
||||
try std.testing.expectEqualStrings("/system/services/fat", first.nameSlice());
|
||||
try std.testing.expectEqualStrings("mounted /mnt/usb", first.messageSlice());
|
||||
|
||||
const second = parseAt(ring, first.next(ring.tail));
|
||||
try std.testing.expectEqual(@as(u32, 0), second.header.pid);
|
||||
try std.testing.expectEqualStrings("kernel", second.nameSlice());
|
||||
try std.testing.expectEqual(@as(u64, 1), second.header.sequence);
|
||||
}
|
||||
|
||||
test "wrap reclaims whole records and keeps tail on a boundary" {
|
||||
var ring = std.testing.allocator.create(TestRing) catch unreachable;
|
||||
defer std.testing.allocator.destroy(ring);
|
||||
ring.* = .{};
|
||||
|
||||
// Fill far past capacity so the ring wraps many times.
|
||||
var i: u32 = 0;
|
||||
while (i < 200) : (i += 1) {
|
||||
var message: [64]u8 = undefined;
|
||||
const m = std.fmt.bufPrint(&message, "line {d} padding padding padding", .{i}) catch unreachable;
|
||||
_ = ring.append(1, "/system/tests/writer", .info, i, m, false);
|
||||
}
|
||||
try std.testing.expect(ring.head - ring.tail <= 4096);
|
||||
|
||||
// The record at tail parses cleanly (boundary held), and walking to head
|
||||
// yields consecutive sequence numbers.
|
||||
var offset = ring.tail;
|
||||
var previous: ?u64 = null;
|
||||
while (offset < ring.head) {
|
||||
const p = parseAt(ring, offset);
|
||||
if (previous) |q| try std.testing.expectEqual(q + 1, p.header.sequence);
|
||||
previous = p.header.sequence;
|
||||
offset = p.next(offset);
|
||||
}
|
||||
try std.testing.expectEqual(ring.head, offset);
|
||||
// Records were lost (sequence at tail > 0), and the count is the gap.
|
||||
try std.testing.expect(parseAt(ring, ring.tail).header.sequence > 0);
|
||||
}
|
||||
|
||||
test "stale offset returns null; head offset reads zero bytes" {
|
||||
var ring = std.testing.allocator.create(TestRing) catch unreachable;
|
||||
defer std.testing.allocator.destroy(ring);
|
||||
ring.* = .{};
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < 300) : (i += 1)
|
||||
_ = ring.append(1, "w", .info, i, "0123456789abcdef0123456789abcdef", false);
|
||||
|
||||
var out: [16]u8 = undefined;
|
||||
try std.testing.expect(ring.read(0, &out) == null); // long overwritten
|
||||
try std.testing.expect(ring.read(ring.head + 1, &out) == null); // past the end
|
||||
try std.testing.expectEqual(@as(usize, 0), ring.read(ring.head, &out).?); // caught up
|
||||
}
|
||||
|
||||
test "truncation flag and clamping" {
|
||||
var ring = std.testing.allocator.create(TestRing) catch unreachable;
|
||||
defer std.testing.allocator.destroy(ring);
|
||||
ring.* = .{};
|
||||
|
||||
const long = "x" ** 300; // past klog_maximum_message
|
||||
_ = ring.append(2, "w", .warn, 0, long, true);
|
||||
const p = parseAt(ring, ring.tail);
|
||||
try std.testing.expectEqual(@as(u16, abi.klog_maximum_message), p.header.message_len);
|
||||
try std.testing.expect(p.header.flags & abi.klog_flag_truncated != 0);
|
||||
}
|
||||
+171
-39
@@ -4,22 +4,37 @@
|
||||
//! 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.
|
||||
//! serial UART and the 0xE9 debug console. A message reaches whatever channels
|
||||
//! exist; if none do, the kernel runs on, silent but correct.
|
||||
//!
|
||||
//! Retention is the tagged RING (log-ring.zig): every emission becomes one
|
||||
//! record per line, stamped with the sender's pid, task name (its binary path),
|
||||
//! level, sequence number, and monotonic timestamp — attribution is structural,
|
||||
//! stamped by the kernel, not a naming convention a process could forge. The
|
||||
//! stamping is per LINE: an embedded '\n' ends the record, so a payload cannot
|
||||
//! imitate another sender on the line that follows. Oldest records are
|
||||
//! overwritten when the ring is full; sequence gaps make the loss countable.
|
||||
//! `klog_read`/`klog_status` expose the stream to userspace (the logger service
|
||||
//! drains it into per-process files once storage is up).
|
||||
//!
|
||||
//! Locking: a dedicated log spinlock, NOT the big kernel lock. `print` is
|
||||
//! called both inside and outside BKL sections (and from ISRs), so the log
|
||||
//! lock is taken with interrupts off and nothing inside it ever takes the BKL —
|
||||
//! lock order is strictly BKL -> log lock, never the reverse. Panic paths use a
|
||||
//! bounded try-acquire and fall back to sinks-only: a panic must never deadlock
|
||||
//! on its own diagnostics.
|
||||
//!
|
||||
//! 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).
|
||||
//! the log never assumes the machine is text-based. Two channels bypass the
|
||||
//! sink list because they must survive even a total-output failure:
|
||||
//! `checkpoint` (a one-byte POST code) and `recordPanic` (a fixed breadcrumb).
|
||||
|
||||
const std = @import("std");
|
||||
const architecture = @import("architecture");
|
||||
const abi = @import("abi");
|
||||
const log_ring = @import("log-ring.zig");
|
||||
const wall_clock = @import("wall-clock.zig");
|
||||
|
||||
pub const SinkFn = *const fn ([]const u8) void;
|
||||
|
||||
@@ -36,42 +51,141 @@ pub fn addSink(sink: SinkFn) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fan `bytes` out to every registered sink.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
for (sinks[0..sink_count]) |sink| sink(bytes);
|
||||
// --- the log lock ------------------------------------------------------------
|
||||
|
||||
var lock_held = std.atomic.Value(u32).init(0);
|
||||
|
||||
fn lockAcquire() u64 {
|
||||
const flags = architecture.saveInterrupts();
|
||||
while (lock_held.cmpxchgWeak(0, 1, .acquire, .monotonic) != null) std.atomic.spinLoopHint();
|
||||
return flags;
|
||||
}
|
||||
|
||||
// --- 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).
|
||||
fn lockTryAcquire(spins: usize) ?u64 {
|
||||
const flags = architecture.saveInterrupts();
|
||||
var i: usize = 0;
|
||||
while (i < spins) : (i += 1) {
|
||||
if (lock_held.cmpxchgWeak(0, 1, .acquire, .monotonic) == null) return flags;
|
||||
std.atomic.spinLoopHint();
|
||||
}
|
||||
architecture.restoreInterrupts(flags);
|
||||
return null;
|
||||
}
|
||||
|
||||
const ram_capacity = 256 * 1024;
|
||||
var ram_buffer: [ram_capacity]u8 = undefined;
|
||||
var ram_len: usize = 0;
|
||||
fn lockRelease(flags: u64) void {
|
||||
lock_held.store(0, .release);
|
||||
architecture.restoreInterrupts(flags);
|
||||
}
|
||||
|
||||
/// 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 ring + renderer -----------------------------------------------------
|
||||
|
||||
/// 512 KiB: the tagged frames cost ~30% over the raw text, and the ring only
|
||||
/// needs to cover the pre-mount backlog (a boot is ~15 KiB of text) — the
|
||||
/// logger service tails it continuously once storage is up.
|
||||
const ring_capacity = 512 * 1024;
|
||||
var ring: log_ring.Ring(ring_capacity) = .{};
|
||||
|
||||
/// Renderer state: whether the sinks sit at a line start, and which pid's line
|
||||
/// is currently open — when a different sender interleaves mid-line, the
|
||||
/// renderer closes the line so serial output can't visually merge two senders.
|
||||
var at_line_start: bool = true;
|
||||
var open_line_pid: u32 = 0;
|
||||
|
||||
/// Append `bytes` as one tagged record per line and render them to the sinks.
|
||||
/// The core emission path: `debug_write` calls this with the sender's identity;
|
||||
/// kernel-internal `write`/`print` funnel here as pid 0 ("kernel", raw).
|
||||
pub fn append(pid: u32, name: []const u8, level: abi.KlogLevel, bytes: []const u8) void {
|
||||
if (bytes.len == 0) return;
|
||||
const now = architecture.nanos();
|
||||
const flags = lockAcquire();
|
||||
defer lockRelease(flags);
|
||||
appendLocked(pid, name, level, now, bytes);
|
||||
}
|
||||
|
||||
/// The panic-safe variant: bounded lock wait; on failure, sinks only — the ring
|
||||
/// entry is lost but the message still reaches serial, and the panic cannot
|
||||
/// deadlock on a core that died holding the log lock.
|
||||
pub fn appendPanic(bytes: []const u8) void {
|
||||
if (lockTryAcquire(100_000)) |flags| {
|
||||
defer lockRelease(flags);
|
||||
appendLocked(0, "kernel", .raw, architecture.nanos(), bytes);
|
||||
} else {
|
||||
for (sinks[0..sink_count]) |sink| sink(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// The accumulated log so far — what `klog_read` copies out.
|
||||
pub fn ramSnapshot() []const u8 {
|
||||
return ram_buffer[0..ram_len];
|
||||
fn appendLocked(pid: u32, name: []const u8, level: abi.KlogLevel, now: u64, bytes: []const u8) void {
|
||||
var rest = bytes;
|
||||
while (rest.len != 0) {
|
||||
const newline = std.mem.indexOfScalar(u8, rest, '\n');
|
||||
// The record payload excludes the newline: a record IS a line. Raw
|
||||
// emissions may leave a line open (kernel boot tables build lines from
|
||||
// pieces); a LEVELED record is a complete line by contract — std.log
|
||||
// payloads carry no trailing newline.
|
||||
const line = if (newline) |i| rest[0..i] else rest;
|
||||
const line_complete = newline != null or level != .raw;
|
||||
if (line.len != 0 or line_complete)
|
||||
_ = ring.append(pid, name, level, now, line, line.len > abi.klog_maximum_message);
|
||||
render(pid, name, level, line, line_complete);
|
||||
rest = if (newline) |i| rest[i + 1 ..] else rest[rest.len..];
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial/debugcon rendering. Kernel output and legacy raw user output pass
|
||||
/// through byte-identical to the historical stream (services still write their
|
||||
/// own "name: " prefixes until the std.log migration). Leveled (std.log)
|
||||
/// records get a kernel-rendered "<name>: " prefix at line start — err/warn/
|
||||
/// debug also get their level spelled out.
|
||||
fn render(pid: u32, name: []const u8, level: abi.KlogLevel, line: []const u8, line_complete: bool) void {
|
||||
if (sink_count == 0) return;
|
||||
if (line.len == 0 and !line_complete) return;
|
||||
// Compose the whole rendered piece first and emit it in ONE sink call per
|
||||
// sink: fewer, larger UART writes, and no partial-line window should any
|
||||
// path ever reach a sink without the log lock.
|
||||
var buffer: [render_buffer_size]u8 = undefined;
|
||||
var used: usize = 0;
|
||||
if (!at_line_start and open_line_pid != pid) {
|
||||
buffer[used] = '\n';
|
||||
used += 1;
|
||||
at_line_start = true;
|
||||
}
|
||||
if (at_line_start and level != .raw) {
|
||||
used += place(buffer[used..], name);
|
||||
used += place(buffer[used..], ": ");
|
||||
used += place(buffer[used..], switch (level) {
|
||||
.err => "error: ",
|
||||
.warn => "warning: ",
|
||||
.debug => "debug: ",
|
||||
.info, .raw => "",
|
||||
});
|
||||
}
|
||||
used += place(buffer[used..], line);
|
||||
if (line_complete and used < buffer.len) {
|
||||
buffer[used] = '\n';
|
||||
used += 1;
|
||||
}
|
||||
fanOut(buffer[0..used]);
|
||||
at_line_start = line_complete;
|
||||
open_line_pid = pid;
|
||||
}
|
||||
|
||||
/// newline + name + ": warning: " + a full payload line + newline.
|
||||
const render_buffer_size = 1 + abi.maximum_process_name + 11 + abi.klog_maximum_message + 1;
|
||||
|
||||
fn place(destination: []u8, bytes: []const u8) usize {
|
||||
const n = @min(destination.len, bytes.len);
|
||||
@memcpy(destination[0..n], bytes[0..n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
fn fanOut(bytes: []const u8) void {
|
||||
for (sinks[0..sink_count]) |sink| sink(bytes);
|
||||
}
|
||||
|
||||
/// Kernel-internal write — a raw record from "kernel" (pid 0). The signature is
|
||||
/// unchanged so every existing kernel call site stays as it is.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
append(0, "kernel", .raw, bytes);
|
||||
}
|
||||
|
||||
/// A formatted log line. Truncates past 256 bytes; the buffer is on the stack, so
|
||||
@@ -81,6 +195,24 @@ pub fn print(comptime fmt: []const u8, args: anytype) void {
|
||||
write(std.fmt.bufPrint(&buffer, fmt, args) catch return);
|
||||
}
|
||||
|
||||
/// klog_read: copy ring stream bytes from `offset` into `out`. Null when the
|
||||
/// cursor was overwritten or lies past the end — the reader re-syncs via
|
||||
/// status(). Zero bytes means caught up.
|
||||
pub fn readAt(offset: u64, out: []u8) ?usize {
|
||||
const flags = lockAcquire();
|
||||
defer lockRelease(flags);
|
||||
return ring.read(offset, out);
|
||||
}
|
||||
|
||||
/// klog_status: the ring cursors plus the boot wall-clock anchor.
|
||||
pub fn status() abi.KlogStatus {
|
||||
const flags = lockAcquire();
|
||||
defer lockRelease(flags);
|
||||
var s = ring.status();
|
||||
s.boot_unix_seconds = wall_clock.bootSeconds();
|
||||
return s;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
+186
-29
@@ -34,6 +34,7 @@ const ipc = @import("ipc-synchronous.zig");
|
||||
const devices_broker = @import("devices-broker.zig");
|
||||
const irq = @import("irq.zig");
|
||||
const initial_ramdisk = @import("initial-ramdisk");
|
||||
const vfs = @import("vfs.zig");
|
||||
const log = @import("log.zig");
|
||||
const wall_clock = @import("wall-clock.zig");
|
||||
|
||||
@@ -138,6 +139,17 @@ var ramdisk_image: ?[]const u8 = null;
|
||||
/// handoff) so a user-space supervisor can `system_spawn` binaries out of it.
|
||||
pub fn setInitialRamdisk(image: []const u8) void {
|
||||
ramdisk_image = image;
|
||||
vfs.setInitialRamdisk(image); // the kernel VFS serves the same bytes at /system
|
||||
}
|
||||
|
||||
/// Spawn a bundled binary from the kernel by path. Used exactly once, to start
|
||||
/// /system/services/init (PID 1) — every other spawn goes through the
|
||||
/// `system_spawn` syscall.
|
||||
pub fn spawnBundled(name: []const u8) !void {
|
||||
const image = ramdisk_image orelse return error.NoInitialRamdisk;
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return error.BadInitialRamdisk;
|
||||
const item = rd.find(name) orelse return error.NotBundled;
|
||||
try spawnProcess(item.blob, 4, &.{item.name});
|
||||
}
|
||||
|
||||
/// The system_call surface, dispatched on the saved system_call number (`abi.SystemCall`).
|
||||
@@ -224,6 +236,11 @@ fn system_call(state: *architecture.CpuState) void {
|
||||
.process_signal => systemProcessSignal(state),
|
||||
.timer_bind => systemTimerBind(state),
|
||||
.klog_read => systemKlogRead(state),
|
||||
.klog_status => systemKlogStatus(state),
|
||||
.fs_resolve => systemFsResolve(state),
|
||||
.fs_node => systemFsNode(state),
|
||||
.fs_mount => systemFsMount(state),
|
||||
.fs_unmount => systemFsUnmount(state),
|
||||
.wall_clock => systemWallClock(state),
|
||||
.shm_create => systemShmCreate(state),
|
||||
.shm_map => systemShmMap(state),
|
||||
@@ -656,8 +673,11 @@ fn systemSpawn(state: *architecture.CpuState) void {
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return fail(state);
|
||||
|
||||
const name = @as([*]const u8, @ptrFromInt(ptr))[0..len];
|
||||
// Exact path first, basename fallback second; either way argv[0] (and hence
|
||||
// the task name, and the log ring's attribution) is the stored full path.
|
||||
const item = rd.find(name) orelse return fail(state); // no bundled binary by that name
|
||||
var argv: [maximum_arguments][]const u8 = undefined;
|
||||
argv[0] = name;
|
||||
argv[0] = item.name;
|
||||
var argc: usize = 1;
|
||||
if (arguments_len != 0) {
|
||||
const blob = @as([*]const u8, @ptrFromInt(arguments_ptr))[0..arguments_len];
|
||||
@@ -669,15 +689,8 @@ fn systemSpawn(state: *architecture.CpuState) void {
|
||||
}
|
||||
}
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!std.mem.eql(u8, item.name, name)) continue;
|
||||
const child = spawnProcessSupervised(item.blob, 4, argv[0..argc], t.id, exit_endpoint) catch return fail(state);
|
||||
architecture.setSystemCallResult(state, child);
|
||||
return;
|
||||
}
|
||||
fail(state); // no bundled binary by that name
|
||||
const child = spawnProcessSupervised(item.blob, 4, argv[0..argc], t.id, exit_endpoint) catch return fail(state);
|
||||
architecture.setSystemCallResult(state, child);
|
||||
}
|
||||
|
||||
/// thread_spawn(entry, stack_top, arg) -> tid: start a task that shares the **caller's**
|
||||
@@ -1204,7 +1217,6 @@ fn systemIrqAck(state: *architecture.CpuState) void {
|
||||
/// Whether the debug_write stream sits at the start of a line — the last emitted
|
||||
/// byte was a newline (true at boot: nothing emitted yet). Guarded by the kernel
|
||||
/// lock in `systemDebugWrite`, like the stream it describes.
|
||||
var write_at_line_start: bool = true;
|
||||
|
||||
/// debug_write(ptr, len): copy bytes from user memory into the kernel log.
|
||||
/// A bring-up diagnostic — real output goes through the VFS/console later.
|
||||
@@ -1225,31 +1237,42 @@ var write_at_line_start: bool = true;
|
||||
fn systemDebugWrite(state: *architecture.CpuState) void {
|
||||
const ptr = architecture.systemCallArg(state, 0);
|
||||
const len = architecture.systemCallArg(state, 1);
|
||||
const level_raw = architecture.systemCallArg(state, 2);
|
||||
if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) {
|
||||
const source: [*]const u8 = @ptrFromInt(ptr);
|
||||
// Levels above the enum range clamp to raw — old two-arg callers land
|
||||
// there naturally (garbage in arg 2 stays harmless).
|
||||
const level: abi.KlogLevel = if (level_raw <= @intFromEnum(abi.KlogLevel.raw))
|
||||
@enumFromInt(level_raw)
|
||||
else
|
||||
.raw;
|
||||
const t = scheduler.current();
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
@memcpy(write_buffer[0..len], source[0..len]); // keep the latest message
|
||||
write_len = len;
|
||||
write_from_user = architecture.fromUser(state);
|
||||
write_count += 1;
|
||||
log.write(source[0..len]);
|
||||
if (len != 0) write_at_line_start = source[len - 1] == '\n';
|
||||
// The kernel stamps the sender's identity — attribution is structural,
|
||||
// not a prefix convention the payload could forge (and it is stamped
|
||||
// per line inside log.append).
|
||||
log.append(t.id, t.name(), level, source[0..len]);
|
||||
architecture.setSystemCallResult(state, len);
|
||||
} else {
|
||||
fail(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// klog_read(offset, ptr, len) -> bytes copied: copy the kernel's in-memory
|
||||
/// diagnostic log (the RAM sink in log.zig) out to the user buffer at `ptr`,
|
||||
/// starting at `offset`. Returns the count copied — 0 once `offset` reaches the
|
||||
/// end — so a program reads the whole log by looping from 0 until it gets 0.
|
||||
/// klog_read(offset, ptr, len) -> bytes copied: copy tagged log-ring stream
|
||||
/// bytes beginning at stream offset `offset` out to the user buffer at `ptr`.
|
||||
/// Returns the count copied — 0 means caught up — and fails once `offset` has
|
||||
/// fallen behind the ring's tail (the records were overwritten) or lies past
|
||||
/// its head; the reader re-syncs via klog_status. A reader parses
|
||||
/// [KlogRecordHeader][name][message] frames out of the byte stream (abi.zig).
|
||||
///
|
||||
/// The mirror of `debug_write`: the same overflow-safe user-half bounds check,
|
||||
/// but the copy runs kernel -> user. Written under the kernel lock so the source
|
||||
/// snapshot can't grow underneath the copy. A read-only diagnostic — it exposes
|
||||
/// only the log the kernel already broadcasts to serial, nothing else.
|
||||
/// but the copy runs kernel -> user, under the log lock (inside log.readAt) so
|
||||
/// the stream can't move underneath the copy. A read-only diagnostic.
|
||||
fn systemKlogRead(state: *architecture.CpuState) void {
|
||||
const offset = architecture.systemCallArg(state, 0);
|
||||
const ptr = architecture.systemCallArg(state, 1);
|
||||
@@ -1257,21 +1280,155 @@ fn systemKlogRead(state: *architecture.CpuState) void {
|
||||
// Confine the whole destination span to the user (low) half. `len <=
|
||||
// user_half_end - ptr` bounds the length without an overflowing add.
|
||||
if (ptr < user_half_end and len <= user_half_end - ptr) {
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
const snapshot = log.ramSnapshot();
|
||||
var n: usize = 0;
|
||||
if (offset < snapshot.len) {
|
||||
n = @min(len, snapshot.len - offset);
|
||||
const dest: [*]u8 = @ptrFromInt(ptr);
|
||||
@memcpy(dest[0..n], snapshot[offset..][0..n]);
|
||||
}
|
||||
const dest: [*]u8 = @ptrFromInt(ptr);
|
||||
const n = log.readAt(offset, dest[0..len]) orelse return fail(state);
|
||||
architecture.setSystemCallResult(state, n);
|
||||
} else {
|
||||
fail(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// klog_status(ptr) -> 0: copy a KlogStatus — the ring's live cursors plus the
|
||||
/// boot wall-clock anchor — out to the user buffer at `ptr`. How a log reader
|
||||
/// finds the oldest retained offset, detects lost records (sequence gaps), and
|
||||
/// names a per-boot log directory (boot_unix_seconds).
|
||||
fn systemKlogStatus(state: *architecture.CpuState) void {
|
||||
const ptr = architecture.systemCallArg(state, 0);
|
||||
const size = @sizeOf(abi.KlogStatus);
|
||||
if (ptr < user_half_end and size <= user_half_end - ptr) {
|
||||
var status = log.status();
|
||||
const dest: [*]u8 = @ptrFromInt(ptr);
|
||||
@memcpy(dest[0..size], std.mem.asBytes(&status)[0..size]);
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
} else {
|
||||
fail(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// fs_resolve(path_ptr, path_len, flags, out_ptr, out_cap): route a path
|
||||
/// through the kernel mount table (docs/vfs-protocol.md). Kernel-served ->
|
||||
/// rax=fs_route_kernel, rdx=node token. Backend-served -> rax=fs_route_backend,
|
||||
/// rdx=an endpoint handle in the caller's table (deduplicated), and the
|
||||
/// rewritten mount-relative path copied into `out` behind a u16 length prefix. Fails for unknown paths, create-intent on /system, or an
|
||||
/// undersized out buffer.
|
||||
fn systemFsResolve(state: *architecture.CpuState) void {
|
||||
const path_ptr = architecture.systemCallArg(state, 0);
|
||||
const path_len = architecture.systemCallArg(state, 1);
|
||||
const flags = architecture.systemCallArg(state, 2);
|
||||
const out_ptr = architecture.systemCallArg(state, 3);
|
||||
const out_cap = architecture.systemCallArg(state, 4);
|
||||
if (path_len == 0 or path_len > 224 or path_ptr >= user_half_end or path_ptr + path_len > user_half_end) return fail(state);
|
||||
if (out_cap != 0 and (out_ptr >= user_half_end or out_ptr + out_cap > user_half_end)) return fail(state);
|
||||
const path = @as([*]const u8, @ptrFromInt(path_ptr))[0..path_len];
|
||||
const t = scheduler.current();
|
||||
|
||||
const flags_lock = sync.enter();
|
||||
defer sync.leave(flags_lock);
|
||||
switch (vfs.resolvePath(path, flags & abi.fs_flag_create != 0)) {
|
||||
.kernel_node => |node_token| {
|
||||
architecture.setSystemCallResult(state, abi.fs_route_kernel);
|
||||
architecture.setSystemCallResult2(state, node_token);
|
||||
},
|
||||
.backend => |*backend| {
|
||||
// The rewritten path goes back in the out buffer behind a u16
|
||||
// length prefix (a third result register would collide with r8's
|
||||
// argument role in the userspace stub).
|
||||
if (backend.path_len + 2 > out_cap) return fail(state);
|
||||
const handle = ipc.installHandleDeduped(t, backend.endpoint);
|
||||
if (handle < 0) return fail(state);
|
||||
const destination: [*]u8 = @ptrFromInt(out_ptr);
|
||||
destination[0] = @intCast(backend.path_len & 0xFF);
|
||||
destination[1] = @intCast(backend.path_len >> 8);
|
||||
@memcpy(destination[2..][0..backend.path_len], backend.path[0..backend.path_len]);
|
||||
architecture.setSystemCallResult(state, abi.fs_route_backend);
|
||||
architecture.setSystemCallResult2(state, @intCast(handle));
|
||||
},
|
||||
.not_found => fail(state),
|
||||
}
|
||||
}
|
||||
|
||||
/// fs_node(op, node_token, offset, buf_ptr, buf_len) -> bytes/0/-errno: serve a
|
||||
/// kernel-backed node. read copies file bytes; status copies a FileAttributes;
|
||||
/// readdir copies [DirectoryEntryHeader][name] for the `offset`th child. Reads
|
||||
/// of the immutable initrd never take the kernel lock.
|
||||
fn systemFsNode(state: *architecture.CpuState) void {
|
||||
const operation = architecture.systemCallArg(state, 0);
|
||||
const node_token = architecture.systemCallArg(state, 1);
|
||||
const offset = architecture.systemCallArg(state, 2);
|
||||
const buf_ptr = architecture.systemCallArg(state, 3);
|
||||
const buf_len = architecture.systemCallArg(state, 4);
|
||||
if (buf_ptr >= user_half_end or buf_len > user_half_end - buf_ptr) return fail(state);
|
||||
const capped = @min(buf_len, 64 * 1024); // bound any single copy
|
||||
const destination: [*]u8 = @ptrFromInt(buf_ptr);
|
||||
switch (operation) {
|
||||
abi.fs_node_read => {
|
||||
const n = vfs.nodeRead(node_token, offset, destination[0..capped]) orelse return fail(state);
|
||||
architecture.setSystemCallResult(state, n);
|
||||
},
|
||||
abi.fs_node_status => {
|
||||
var attributes = vfs.nodeStatus(node_token) orelse return fail(state);
|
||||
if (capped < @sizeOf(abi.FileAttributes)) return fail(state);
|
||||
@memcpy(destination[0..@sizeOf(abi.FileAttributes)], std.mem.asBytes(&attributes));
|
||||
architecture.setSystemCallResult(state, @sizeOf(abi.FileAttributes));
|
||||
},
|
||||
abi.fs_node_readdir => {
|
||||
const header_size = @sizeOf(abi.DirectoryEntryHeader);
|
||||
if (capped < header_size) return fail(state);
|
||||
var name_buffer: [64]u8 = undefined;
|
||||
const result = vfs.nodeReaddir(node_token, offset, &name_buffer) orelse {
|
||||
architecture.setSystemCallResult(state, 0); // past the end
|
||||
return;
|
||||
};
|
||||
var header = result.header;
|
||||
const total = header_size + @min(result.name_len, capped - header_size);
|
||||
@memcpy(destination[0..header_size], std.mem.asBytes(&header));
|
||||
@memcpy(destination[header_size..total], name_buffer[0 .. total - header_size]);
|
||||
architecture.setSystemCallResult(state, total);
|
||||
},
|
||||
else => fail(state),
|
||||
}
|
||||
}
|
||||
|
||||
/// fs_mount(prefix_ptr, prefix_len, backend_handle, rewrite_ptr, rewrite_len):
|
||||
/// mount a userspace filesystem at an absolute prefix. Possession of the
|
||||
/// backend endpoint handle is the capability — the same trust as the old
|
||||
/// router's cap-passing mount. The mount takes its own endpoint reference.
|
||||
fn systemFsMount(state: *architecture.CpuState) void {
|
||||
const prefix_ptr = architecture.systemCallArg(state, 0);
|
||||
const prefix_len = architecture.systemCallArg(state, 1);
|
||||
const backend_handle = architecture.systemCallArg(state, 2);
|
||||
const rewrite_ptr = architecture.systemCallArg(state, 3);
|
||||
const rewrite_len = architecture.systemCallArg(state, 4);
|
||||
if (prefix_len == 0 or prefix_len > 64 or prefix_ptr >= user_half_end or prefix_ptr + prefix_len > user_half_end) return fail(state);
|
||||
if (rewrite_len > 32) return fail(state);
|
||||
if (rewrite_len != 0 and (rewrite_ptr >= user_half_end or rewrite_ptr + rewrite_len > user_half_end)) return fail(state);
|
||||
const t = scheduler.current();
|
||||
const prefix = @as([*]const u8, @ptrFromInt(prefix_ptr))[0..prefix_len];
|
||||
const rewrite = if (rewrite_len == 0) "" else @as([*]const u8, @ptrFromInt(rewrite_ptr))[0..rewrite_len];
|
||||
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
const endpoint = ipc.resolveHandle(t, backend_handle) orelse return failErr(state, ipc.EBADF);
|
||||
endpoint.refcount += 1; // the mount table's reference
|
||||
if (!vfs.mountBackend(prefix, endpoint, rewrite)) {
|
||||
ipc.dropRef(endpoint);
|
||||
return fail(state);
|
||||
}
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// fs_unmount(prefix_ptr, prefix_len): remove a backend mount.
|
||||
fn systemFsUnmount(state: *architecture.CpuState) void {
|
||||
const prefix_ptr = architecture.systemCallArg(state, 0);
|
||||
const prefix_len = architecture.systemCallArg(state, 1);
|
||||
if (prefix_len == 0 or prefix_len > 64 or prefix_ptr >= user_half_end or prefix_ptr + prefix_len > user_half_end) return fail(state);
|
||||
const prefix = @as([*]const u8, @ptrFromInt(prefix_ptr))[0..prefix_len];
|
||||
const flags = sync.enter();
|
||||
defer sync.leave(flags);
|
||||
if (!vfs.unmount(prefix)) return fail(state);
|
||||
architecture.setSystemCallResult(state, 0);
|
||||
}
|
||||
|
||||
/// mmap(len, prot) -> base: grant `len` bytes (rounded up to whole pages) of
|
||||
/// fresh, zeroed, writable+NX memory in the caller's mmap arena, and return the
|
||||
/// base virtual address. `prot` is accepted but not yet honoured (grants are
|
||||
|
||||
+185
-82
@@ -26,11 +26,14 @@ const irq = @import("irq.zig");
|
||||
const sync = @import("sync.zig");
|
||||
const process = @import("process.zig");
|
||||
const initial_ramdisk = @import("initial-ramdisk");
|
||||
const kernel_log = @import("log.zig");
|
||||
const kernel_vfs = @import("vfs.zig");
|
||||
|
||||
/// Formatted write straight to serial, independent of the framebuffer console.
|
||||
/// Formatted test-marker write. Goes through the kernel log (not straight to
|
||||
/// serial): the log lock is what keeps marker lines from interleaving with
|
||||
/// concurrent user-process records on other cores.
|
||||
fn log(comptime fmt: []const u8, args: anytype) void {
|
||||
var buffer: [128]u8 = undefined;
|
||||
architecture.serialWrite(std.fmt.bufPrint(&buffer, fmt, args) catch return);
|
||||
kernel_log.print(fmt, args);
|
||||
}
|
||||
|
||||
var passed: u32 = 0;
|
||||
@@ -207,6 +210,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
||||
initialRamdiskTest(boot_information);
|
||||
} else if (eql(case, "vfs")) {
|
||||
vfsTest(boot_information);
|
||||
} else if (eql(case, "kvfs")) {
|
||||
kernelVfsTest(boot_information);
|
||||
} else if (eql(case, "input")) {
|
||||
inputTest(boot_information);
|
||||
} else if (eql(case, "iopass")) {
|
||||
@@ -260,6 +265,31 @@ fn bufferHas(needle: []const u8) bool {
|
||||
return std.mem.indexOf(u8, process.write_buffer[0..process.write_len], needle) != null;
|
||||
}
|
||||
|
||||
/// Substring search across the whole retained log ring (record payloads are
|
||||
/// contiguous in the stream, so a one-line needle always matches if present).
|
||||
/// Unlike `bufferHas` (the single LAST write), this survives busy-tree chatter.
|
||||
fn ringHas(needle: []const u8) bool {
|
||||
var chunk: [1024]u8 = undefined;
|
||||
var overlap: [128]u8 = undefined;
|
||||
var overlap_len: usize = 0;
|
||||
var offset = kernel_log.status().tail;
|
||||
while (true) {
|
||||
const n = kernel_log.readAt(offset, &chunk) orelse return false;
|
||||
if (n == 0) return false;
|
||||
offset += n;
|
||||
// Search the previous tail glued to this chunk, then the chunk itself.
|
||||
if (overlap_len != 0) {
|
||||
var glued: [1152]u8 = undefined;
|
||||
@memcpy(glued[0..overlap_len], overlap[0..overlap_len]);
|
||||
const m = @min(n, glued.len - overlap_len);
|
||||
@memcpy(glued[overlap_len..][0..m], chunk[0..m]);
|
||||
if (std.mem.indexOf(u8, glued[0 .. overlap_len + m], needle) != null) return true;
|
||||
} else if (std.mem.indexOf(u8, chunk[0..n], needle) != null) return true;
|
||||
overlap_len = @min(n, @min(overlap.len, needle.len));
|
||||
@memcpy(overlap[0..overlap_len], chunk[n - overlap_len ..][0..overlap_len]);
|
||||
}
|
||||
}
|
||||
|
||||
/// How many `pci_device` functions the devices broker currently holds. A durable
|
||||
/// snapshot, unlike a `bufferHas` poll of the single-latest write_buffer line, so a
|
||||
/// test can wait on it without racing transient log output. `scratch` is
|
||||
@@ -1327,12 +1357,12 @@ fn procWorker() void {
|
||||
/// strongest cheap proof of address-space isolation.
|
||||
fn processTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: process\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
@@ -1414,12 +1444,12 @@ fn spawnFaultingProcess() ?u32 {
|
||||
/// time the harness out.
|
||||
fn faultRecoveryTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: fault-recovery\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
|
||||
process.write_count = 0;
|
||||
process.fault_kill_count = 0;
|
||||
@@ -1550,7 +1580,7 @@ fn threadJoinTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "join" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1594,7 +1624,7 @@ fn threadFutexTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "futex" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1642,7 +1672,7 @@ fn threadMutexTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "mutex" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1683,7 +1713,7 @@ fn threadIdTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "id" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1726,7 +1756,7 @@ fn threadAllocTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "alloc" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1769,7 +1799,7 @@ fn threadTlsTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "tls" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1811,7 +1841,7 @@ fn threadRwlockTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "thread-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "rwlock" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -1876,12 +1906,12 @@ fn taskReapTest(boot_information: *const BootInformation) void {
|
||||
/// (write + sleep), and stays alive rather than exiting.
|
||||
fn initTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: init\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
process.write_count = 0;
|
||||
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |err| blk: {
|
||||
log("DANOS-INIT-ERR: {s}\n", .{@errorName(err)});
|
||||
@@ -1912,12 +1942,12 @@ fn initTest(boot_information: *const BootInformation) void {
|
||||
/// learns how big a buffer to bring).
|
||||
fn processListTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: process-list\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
|
||||
var spawned: u32 = 0;
|
||||
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
|
||||
@@ -1963,13 +1993,12 @@ fn processListTest(boot_information: *const BootInformation) void {
|
||||
/// harness out rather than passing vacuously.
|
||||
fn processKillTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: process-kill\n", .{});
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(ramdisk) orelse {
|
||||
check("initial_ramdisk image is valid", false);
|
||||
@@ -2024,7 +2053,7 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
spinner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "spinner" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2041,7 +2070,7 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
||||
i = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "args-echo")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "args-echo")) continue;
|
||||
clean = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "clean-exit" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2089,12 +2118,12 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
||||
check("cleanup released owner 222", devices_broker.ownerOf(1) == null);
|
||||
|
||||
// The death-path wiring: a real process dies holding a claim.
|
||||
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
|
||||
if (boot_information.init_len == 0) {
|
||||
const image = bundledInit(boot_information) orelse {
|
||||
check("initial_ramdisk carries /system/services/init", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
};
|
||||
check("initial_ramdisk carries /system/services/init", true);
|
||||
const me = scheduler.currentId();
|
||||
const endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||
check("exit endpoint allocated", false);
|
||||
@@ -2116,11 +2145,11 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// M17.3: the published exit events, proven by their first subscriber. The VFS
|
||||
/// subscribes at startup; a client opens a file and parks holding the handle;
|
||||
/// the kill posts the exit event to the VFS's endpoint; the VFS releases the
|
||||
/// dead client's handle and says so — the service-side mirror of iron rule 1
|
||||
/// (a service must never depend on clients cleaning up after themselves).
|
||||
/// M17.3: the published exit events, proven by a stateful server. The fat
|
||||
/// server subscribes at startup; a client opens a file on the volume and parks
|
||||
/// holding the handle; the kill posts the exit event to fat's endpoint; fat
|
||||
/// releases the dead client's handle and says so — the service-side mirror of
|
||||
/// iron rule 1 (a service must never depend on clients cleaning up).
|
||||
fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: vfs-client-death\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
@@ -2136,7 +2165,11 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
};
|
||||
|
||||
process.write_count = 0;
|
||||
check("vfs spawned", spawnNamed(rd, "vfs"));
|
||||
// The full tree: the storage chain must come up for /mnt/usb to exist —
|
||||
// the fat server (not a router) now owns client file state and its sweep.
|
||||
process.setInitialRamdisk(image);
|
||||
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||
check("init spawned (boots the storage chain)", init_ok);
|
||||
|
||||
const me = scheduler.currentId();
|
||||
const endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||
@@ -2148,22 +2181,24 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "vfs-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "vfs-test")) continue;
|
||||
client = process.spawnProcessSupervised(item.blob, 4, &.{ "vfs-test", "park" }, me, endpoint) catch 0;
|
||||
break;
|
||||
}
|
||||
check("parked client spawned (supervised)", client != 0);
|
||||
|
||||
// Its heartbeat is the fence: once it beats, the handle is open.
|
||||
// Its heartbeat is the fence: once it beats, the handle is open. The park
|
||||
// waits out the whole USB->block->fat chain, so give it room; with the full
|
||||
// tree chattering, the ring (not the last-write buffer) is the evidence.
|
||||
const parked = "vfstest: parked";
|
||||
scheduler.setPriority(1);
|
||||
var deadline = architecture.millis() + 10000;
|
||||
var deadline = architecture.millis() + 30000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (bufferHas(parked)) break;
|
||||
if (ringHas(parked)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("client parked holding an open handle", bufferHas(parked));
|
||||
check("client parked holding an open handle", ringHas(parked));
|
||||
|
||||
check("the kill is accepted", process.killProcess(me, client) == 0);
|
||||
var badge: u64 = 0;
|
||||
@@ -2171,16 +2206,17 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void {
|
||||
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | client);
|
||||
|
||||
// The VFS heard the same published event; its release line is the proof.
|
||||
const released = "vfs: released 1 handle(s) for dead client";
|
||||
// The fat server heard the same published event; its release line in the
|
||||
// ring is the proof.
|
||||
const released = "released 1 handle(s) for dead client";
|
||||
scheduler.setPriority(1);
|
||||
deadline = architecture.millis() + 10000;
|
||||
while (architecture.millis() < deadline) {
|
||||
if (bufferHas(released)) break;
|
||||
if (ringHas(released)) break;
|
||||
scheduler.yield();
|
||||
}
|
||||
scheduler.setPriority(4);
|
||||
check("the VFS released the dead client's handle", bufferHas(released));
|
||||
check("the fat server released the dead client's handle", ringHas(released));
|
||||
result();
|
||||
}
|
||||
|
||||
@@ -2209,7 +2245,7 @@ fn signalsTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2257,7 +2293,7 @@ fn driverRestartTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2295,7 +2331,7 @@ fn usbReportTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2327,7 +2363,7 @@ fn deviceListTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2368,7 +2404,7 @@ fn pciScanTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2452,8 +2488,8 @@ fn usbStorageTest(boot_information: *const BootInformation) void {
|
||||
/// the fat mount and the client's success.
|
||||
fn fatMountTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: fat-mount\n", .{});
|
||||
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over init and the initial_ramdisk", false);
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over the initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
@@ -2464,8 +2500,7 @@ fn fatMountTest(boot_information: *const BootInformation) void {
|
||||
return;
|
||||
};
|
||||
process.setInitialRamdisk(ramdisk);
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
const init_ok = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
|
||||
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||
check("init spawned (boots the tree, incl. the fat server)", init_ok);
|
||||
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
|
||||
result();
|
||||
@@ -2473,30 +2508,28 @@ fn fatMountTest(boot_information: *const BootInformation) void {
|
||||
|
||||
fn bootServiceTreeTest(boot_information: *const BootInformation, comptime label: []const u8) void {
|
||||
log("DANOS-TEST-BEGIN: " ++ label ++ "\n", .{});
|
||||
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over init and the initial_ramdisk", false);
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over the initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
process.setInitialRamdisk(ramdisk);
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
|
||||
const spawned = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||
check("init spawned (boots vfs, input, device-manager, and the USB chain)", spawned);
|
||||
result();
|
||||
}
|
||||
|
||||
fn orderlyShutdownTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{});
|
||||
if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over init and the initial_ramdisk", false);
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over the initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
process.setInitialRamdisk(ramdisk);
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
|
||||
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
|
||||
const spawned = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||
check("init spawned as PID root of user space", spawned);
|
||||
result();
|
||||
}
|
||||
@@ -2524,7 +2557,7 @@ fn acpiReportTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
spawned = true;
|
||||
break;
|
||||
@@ -2562,7 +2595,7 @@ fn acpiParseTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "discovery")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "discovery")) continue;
|
||||
_ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", "1" }, scheduler.currentId(), null) catch 0;
|
||||
spawned = true;
|
||||
break;
|
||||
@@ -2597,7 +2630,7 @@ fn supervisionTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "process-test")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue;
|
||||
started = if (process.spawnProcess(item.blob, 4, &.{ "process-test", "run" })) true else |_| false;
|
||||
break;
|
||||
}
|
||||
@@ -2682,9 +2715,10 @@ fn vfsTest(boot_information: *const BootInformation) void {
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
// Spawn just the server and its client (other initial_ramdisk binaries would write to
|
||||
// the shared evidence buffer and confuse the marker check).
|
||||
_ = spawnNamed(rd, "vfs");
|
||||
// Seed the kernel VFS (/system) — the router the client exercises.
|
||||
process.setInitialRamdisk(image);
|
||||
// Spawn just the client: the kernel itself is the VFS root it exercises
|
||||
// (resolve + fs_node over /system through the plain runtime.fs API).
|
||||
_ = spawnNamed(rd, "vfs-test");
|
||||
|
||||
// Wait for the client's success heartbeat (it round-trips, then beats ~1/s).
|
||||
@@ -2921,7 +2955,7 @@ fn virtioGpuTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -2962,7 +2996,7 @@ fn displayNativeTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -3006,7 +3040,7 @@ fn displayReattachTest(boot_information: *const BootInformation) void {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!eql(item.name, "device-manager")) continue;
|
||||
if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue;
|
||||
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-scanout-restart" }, scheduler.currentId(), null) catch 0;
|
||||
break;
|
||||
}
|
||||
@@ -3055,7 +3089,7 @@ fn argsTest(boot_information: *const BootInformation) void {
|
||||
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
|
||||
scheduler.setPriority(4);
|
||||
|
||||
const expected = "args: args-echo alpha beta-42\n";
|
||||
const expected = "args: /system/tests/args-echo alpha beta-42\n";
|
||||
const echoed = process.write_len == expected.len and eql(process.write_buffer[0..process.write_len], expected);
|
||||
if (!echoed and process.write_len > 0) log("DANOS-ARGS: got \"{s}\"\n", .{process.write_buffer[0..process.write_len]});
|
||||
check("argv arrived intact (argv[0] = name, argv[1..] = spawn arguments)", echoed);
|
||||
@@ -3065,11 +3099,79 @@ fn argsTest(boot_information: *const BootInformation) void {
|
||||
|
||||
/// Spawn the initial_ramdisk binary named `name` as a ring-3 process. Returns false if it
|
||||
/// isn't in the image or fails to load.
|
||||
/// The init ELF image out of the initial_ramdisk — init rides the table like
|
||||
/// every other binary since the loader packs the whole /system tree.
|
||||
fn bundledInit(boot_information: *const BootInformation) ?[]const u8 {
|
||||
if (boot_information.initial_ramdisk_len == 0) return null;
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return null;
|
||||
const item = rd.find("/system/services/init") orelse return null;
|
||||
return item.blob;
|
||||
}
|
||||
|
||||
/// The kernel VFS root (M-F): resolve initrd paths to node tokens, read an ELF
|
||||
/// header through nodeRead, enumerate /system's derived directory table, and
|
||||
/// verify the read-only + unknown-path refusals. Pure kernel-side — the
|
||||
/// syscall surface gets its end-to-end coverage when runtime.fs cuts over.
|
||||
fn kernelVfsTest(boot_information: *const BootInformation) void {
|
||||
log("DANOS-TEST-BEGIN: kvfs\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
check("bootloader handed over the initial_ramdisk", false);
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||
process.setInitialRamdisk(image); // also seeds the kernel VFS /system mount
|
||||
|
||||
// A file resolves to a kernel node token; its status and bytes are served.
|
||||
const resolved = kernel_vfs.resolvePath("/system/services/init", false);
|
||||
const is_file = resolved == .kernel_node;
|
||||
check("/system/services/init resolves to a kernel node", is_file);
|
||||
if (is_file) {
|
||||
const status = kernel_vfs.nodeStatus(resolved.kernel_node);
|
||||
check("its status is a non-empty regular file", status != null and status.?.kind == abi.file_kind_regular and status.?.size > 0);
|
||||
var header: [4]u8 = undefined;
|
||||
const n = kernel_vfs.nodeRead(resolved.kernel_node, 0, &header) orelse 0;
|
||||
check("its first bytes are an ELF magic", n == 4 and header[0] == 0x7f and header[1] == 'E' and header[2] == 'L' and header[3] == 'F');
|
||||
}
|
||||
|
||||
// Directories resolve and enumerate: /system lists services/drivers/tests.
|
||||
const root_directory = kernel_vfs.resolvePath("/system", false);
|
||||
check("/system resolves to a directory node", root_directory == .kernel_node);
|
||||
var saw_services = false;
|
||||
var saw_drivers = false;
|
||||
var saw_files_in_services = false;
|
||||
if (root_directory == .kernel_node) {
|
||||
var cursor: u64 = 0;
|
||||
var name: [64]u8 = undefined;
|
||||
while (kernel_vfs.nodeReaddir(root_directory.kernel_node, cursor, &name)) |entry| : (cursor += 1) {
|
||||
if (eql(name[0..entry.name_len], "services")) saw_services = true;
|
||||
if (eql(name[0..entry.name_len], "drivers")) saw_drivers = true;
|
||||
}
|
||||
}
|
||||
check("readdir /system yields services and drivers", saw_services and saw_drivers);
|
||||
const services = kernel_vfs.resolvePath("/system/services", false);
|
||||
if (services == .kernel_node) {
|
||||
var cursor: u64 = 0;
|
||||
var name: [64]u8 = undefined;
|
||||
while (kernel_vfs.nodeReaddir(services.kernel_node, cursor, &name)) |entry| : (cursor += 1) {
|
||||
if (eql(name[0..entry.name_len], "init")) saw_files_in_services = true;
|
||||
}
|
||||
}
|
||||
check("readdir /system/services yields init", saw_files_in_services);
|
||||
|
||||
// Refusals: unknown paths, and create-intent on the immutable initrd.
|
||||
check("an unknown path does not resolve", kernel_vfs.resolvePath("/system/services/no-such", false) == .not_found);
|
||||
check("an unmounted absolute path does not resolve", kernel_vfs.resolvePath("/elsewhere", false) == .not_found);
|
||||
check("create on /system is refused (read-only)", kernel_vfs.resolvePath("/system/services/new-file", true) == .not_found);
|
||||
result();
|
||||
}
|
||||
|
||||
fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (eql(item.name, name)) {
|
||||
if (eql(initial_ramdisk.basename(item.name), name)) {
|
||||
return if (process.spawnProcess(item.blob, 4, &.{item.name})) true else |_| false;
|
||||
}
|
||||
}
|
||||
@@ -3082,7 +3184,7 @@ fn spawnNamedWithArg(rd: initial_ramdisk.Reader, name: []const u8, arg: []const
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (eql(item.name, name)) {
|
||||
if (eql(initial_ramdisk.basename(item.name), name)) {
|
||||
return if (process.spawnProcess(item.blob, 4, &.{ item.name, arg })) true else |_| false;
|
||||
}
|
||||
}
|
||||
@@ -3236,7 +3338,8 @@ fn processRunning(name: []const u8) bool {
|
||||
var table: [64]abi.ProcessDescriptor = undefined;
|
||||
const total = scheduler.enumerate(&table);
|
||||
for (table[0..@min(total, table.len)]) |d| {
|
||||
if (std.mem.eql(u8, d.name[0..d.name_length], name)) return true;
|
||||
// Task names are full binary paths; callers pass either form.
|
||||
if (std.mem.eql(u8, initial_ramdisk.basename(d.name[0..d.name_length]), name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
//! The kernel-resident VFS root: the mount table and the kernel-backed nodes.
|
||||
//!
|
||||
//! The kernel's job here is NAMING, never data plumbing to userspace backends —
|
||||
//! the mechanism is **resolve + redirect**:
|
||||
//!
|
||||
//! - `fs_resolve(path)` walks the mount table. A path under a KERNEL-backed
|
||||
//! mount (the initrd at /system, the scratch ram nodes) resolves to a
|
||||
//! stateless node TOKEN served directly by `fs_node` (read/status/readdir
|
||||
//! with copy-out). A path under a USERSPACE mount (the fat server at
|
||||
//! /mnt/usb and /var) resolves to the backend's ENDPOINT: the kernel
|
||||
//! installs a (deduplicated) handle in the caller's table, rewrites the
|
||||
//! path mount-relative, and the caller speaks the unchanged vfs-protocol
|
||||
//! to the backend over the ordinary ipc_call rendezvous. The kernel never
|
||||
//! blocks on a userspace server.
|
||||
//!
|
||||
//! - Kernel node tokens are PERMANENT for a boot: the initrd is immutable and
|
||||
//! ram nodes are never reclaimed — no open-handle state, no close, no sweep
|
||||
//! on client death. Backend file state lives in the backend, which sweeps
|
||||
//! dead clients itself via the published exit events.
|
||||
//!
|
||||
//! Mounting is `fs_mount(prefix, backend_handle, rewrite)`: possession of the
|
||||
//! backend endpoint handle is the capability, exactly the trust of the old
|
||||
//! userspace router's op-6 cap-pass. An optional REWRITE prefix maps the mount
|
||||
//! into the backend's namespace ("/var" -> fat's "/var" subtree while the same
|
||||
//! backend also serves "/mnt/usb" from its root), so FHS paths stay decoupled
|
||||
//! from which volume happens to carry them.
|
||||
|
||||
const std = @import("std");
|
||||
const abi = @import("abi");
|
||||
const initial_ramdisk = @import("initial-ramdisk");
|
||||
const ipc = @import("ipc-synchronous.zig");
|
||||
|
||||
// --- node tokens -------------------------------------------------------------
|
||||
|
||||
/// Kind lives in the top byte of a token; the index below. Tokens are permanent
|
||||
/// for a boot, so userspace may cache them freely.
|
||||
pub const token_kind_shift = 56;
|
||||
pub const token_kind_initrd_file: u64 = 1;
|
||||
pub const token_kind_initrd_directory: u64 = 2;
|
||||
pub const token_kind_ram: u64 = 3;
|
||||
|
||||
fn token(kind: u64, index: u64) u64 {
|
||||
return (kind << token_kind_shift) | index;
|
||||
}
|
||||
|
||||
fn tokenKind(t: u64) u64 {
|
||||
return t >> token_kind_shift;
|
||||
}
|
||||
|
||||
fn tokenIndex(t: u64) u64 {
|
||||
return t & ((@as(u64, 1) << token_kind_shift) - 1);
|
||||
}
|
||||
|
||||
// --- the mount table ---------------------------------------------------------
|
||||
|
||||
pub const maximum_mounts = 8;
|
||||
const maximum_prefix = 64;
|
||||
const maximum_rewrite = 32;
|
||||
|
||||
const MountKind = enum(u8) { kernel_initrd, backend };
|
||||
|
||||
const Mount = struct {
|
||||
used: bool = false,
|
||||
prefix: [maximum_prefix]u8 = undefined,
|
||||
prefix_len: usize = 0,
|
||||
kind: MountKind = .backend,
|
||||
backend: ?*ipc.Endpoint = null, // referenced while mounted
|
||||
rewrite: [maximum_rewrite]u8 = undefined,
|
||||
rewrite_len: usize = 0,
|
||||
|
||||
fn prefixSlice(self: *const Mount) []const u8 {
|
||||
return self.prefix[0..self.prefix_len];
|
||||
}
|
||||
fn rewriteSlice(self: *const Mount) []const u8 {
|
||||
return self.rewrite[0..self.rewrite_len];
|
||||
}
|
||||
};
|
||||
|
||||
var mounts: [maximum_mounts]Mount = @splat(.{});
|
||||
|
||||
/// The initrd image (set once at boot) and its derived directory table.
|
||||
var ramdisk_image: ?[]const u8 = null;
|
||||
|
||||
const maximum_directories = 8;
|
||||
const Directory = struct {
|
||||
path: [maximum_prefix]u8 = undefined,
|
||||
path_len: usize = 0,
|
||||
parent: usize = 0, // index into `directories`; 0 is /system itself
|
||||
|
||||
fn slice(self: *const Directory) []const u8 {
|
||||
return self.path[0..self.path_len];
|
||||
}
|
||||
};
|
||||
var directories: [maximum_directories]Directory = @splat(.{});
|
||||
var directory_count: usize = 0;
|
||||
|
||||
// --- pure path helpers (ported from the userspace router, with its tests) ----
|
||||
|
||||
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by
|
||||
/// a path separator — return the path relative to the mount ("/" for an exact
|
||||
/// match, otherwise the tail beginning with '/'). Null when not under the
|
||||
/// mount, so "/mnt/usb" never captures "/mnt/usbextra".
|
||||
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
|
||||
if (path.len < mount_prefix.len) return null;
|
||||
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
|
||||
if (path.len == mount_prefix.len) return "/";
|
||||
if (path[mount_prefix.len] != '/') return null;
|
||||
return path[mount_prefix.len..];
|
||||
}
|
||||
|
||||
pub fn isAbsolute(path: []const u8) bool {
|
||||
return path.len > 0 and path[0] == '/';
|
||||
}
|
||||
|
||||
/// The parent directory portion of an initrd path ("/system/services/fat" ->
|
||||
/// "/system/services").
|
||||
fn parentOf(path: []const u8) []const u8 {
|
||||
const slash = std.mem.lastIndexOfScalar(u8, path, '/') orelse return path[0..0];
|
||||
if (slash == 0) return path[0..1];
|
||||
return path[0..slash];
|
||||
}
|
||||
|
||||
// --- boot wiring -------------------------------------------------------------
|
||||
|
||||
/// Publish the initrd as the kernel-backed /system mount and derive its bounded
|
||||
/// directory table (the unique parents of the entry paths). Called once at boot.
|
||||
pub fn setInitialRamdisk(image: []const u8) void {
|
||||
ramdisk_image = image;
|
||||
installMount("/system", .kernel_initrd, null, "");
|
||||
|
||||
// Directory 0 is /system itself.
|
||||
directories[0] = .{ .parent = 0 };
|
||||
@memcpy(directories[0].path[0..7], "/system");
|
||||
directories[0].path_len = 7;
|
||||
directory_count = 1;
|
||||
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
// Register every ancestor directory strictly below /system.
|
||||
var parent = parentOf(item.name);
|
||||
while (parent.len > 7) : (parent = parentOf(parent)) {
|
||||
if (directoryIndex(parent) == null and directory_count < maximum_directories) {
|
||||
var d = &directories[directory_count];
|
||||
@memcpy(d.path[0..parent.len], parent);
|
||||
d.path_len = parent.len;
|
||||
d.parent = 0; // fixed up below once all exist
|
||||
directory_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parent links (a second pass so out-of-order registration doesn't matter).
|
||||
for (directories[1..directory_count]) |*d| {
|
||||
d.parent = directoryIndex(parentOf(d.slice())) orelse 0;
|
||||
}
|
||||
}
|
||||
|
||||
fn directoryIndex(path: []const u8) ?usize {
|
||||
for (directories[0..directory_count], 0..) |*d, i| {
|
||||
if (std.mem.eql(u8, d.slice(), path)) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn installMount(prefix: []const u8, kind: MountKind, backend: ?*ipc.Endpoint, rewrite: []const u8) void {
|
||||
// Remount replaces: a restarted backend re-mounts its prefix.
|
||||
var slot: ?*Mount = null;
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and std.mem.eql(u8, m.prefixSlice(), prefix)) {
|
||||
if (m.backend) |old| ipc.dropRef(old);
|
||||
slot = m;
|
||||
break;
|
||||
}
|
||||
if (slot == null and !m.used) slot = m;
|
||||
}
|
||||
const m = slot orelse return;
|
||||
m.* = .{ .used = true, .kind = kind, .backend = backend };
|
||||
@memcpy(m.prefix[0..prefix.len], prefix);
|
||||
m.prefix_len = prefix.len;
|
||||
@memcpy(m.rewrite[0..rewrite.len], rewrite);
|
||||
m.rewrite_len = rewrite.len;
|
||||
}
|
||||
|
||||
// --- resolve -----------------------------------------------------------------
|
||||
|
||||
pub const Resolved = union(enum) {
|
||||
/// Kernel-served: a permanent node token.
|
||||
kernel_node: u64,
|
||||
/// Backend-served: the endpoint plus the rewritten mount-relative path.
|
||||
backend: struct { endpoint: *ipc.Endpoint, path: [maximum_rewrite + maximum_prefix + 160]u8, path_len: usize },
|
||||
not_found: void,
|
||||
};
|
||||
|
||||
/// Longest-prefix match over the mount table, then per-kind resolution.
|
||||
/// `create`-intent on the immutable /system fails here (EROFS-style).
|
||||
pub fn resolvePath(path: []const u8, wants_create: bool) Resolved {
|
||||
if (!isAbsolute(path)) {
|
||||
return .{ .not_found = {} }; // bare names have no kernel namespace (ramfs retired)
|
||||
}
|
||||
var best: ?*Mount = null;
|
||||
var best_relative: []const u8 = undefined;
|
||||
for (&mounts) |*m| {
|
||||
if (!m.used) continue;
|
||||
const relative = underMount(path, m.prefixSlice()) orelse continue;
|
||||
if (best == null or m.prefix_len > best.?.prefix_len) {
|
||||
best = m;
|
||||
best_relative = relative;
|
||||
}
|
||||
}
|
||||
const m = best orelse return .{ .not_found = {} };
|
||||
switch (m.kind) {
|
||||
.kernel_initrd => {
|
||||
if (wants_create) return .{ .not_found = {} }; // read-only
|
||||
return resolveInitrd(path);
|
||||
},
|
||||
.backend => {
|
||||
const endpoint = m.backend orelse return .{ .not_found = {} };
|
||||
if (endpoint.dead) {
|
||||
// The backend died: treat the mount as gone (it re-mounts on
|
||||
// restart) and release our reference lazily.
|
||||
ipc.dropRef(endpoint);
|
||||
m.backend = null;
|
||||
m.used = false;
|
||||
return .{ .not_found = {} };
|
||||
}
|
||||
var out: Resolved = .{ .backend = .{ .endpoint = endpoint, .path = undefined, .path_len = 0 } };
|
||||
const rewrite = m.rewriteSlice();
|
||||
const tail = if (std.mem.eql(u8, best_relative, "/") and rewrite.len != 0) "" else best_relative;
|
||||
const total = rewrite.len + tail.len;
|
||||
if (total > out.backend.path.len or total == 0) {
|
||||
if (rewrite.len == 0 and tail.len == 0) return .{ .not_found = {} };
|
||||
if (total > out.backend.path.len) return .{ .not_found = {} };
|
||||
}
|
||||
@memcpy(out.backend.path[0..rewrite.len], rewrite);
|
||||
@memcpy(out.backend.path[rewrite.len..][0..tail.len], tail);
|
||||
out.backend.path_len = total;
|
||||
return out;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn resolveInitrd(path: []const u8) Resolved {
|
||||
if (directoryIndex(path)) |index| return .{ .kernel_node = token(token_kind_initrd_directory, index) };
|
||||
const image = ramdisk_image orelse return .{ .not_found = {} };
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return .{ .not_found = {} };
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (std.mem.eql(u8, item.name, path)) return .{ .kernel_node = token(token_kind_initrd_file, i) };
|
||||
}
|
||||
return .{ .not_found = {} };
|
||||
}
|
||||
|
||||
// --- fs_node: serving kernel-backed nodes ------------------------------------
|
||||
|
||||
/// Read `out.len` bytes of an initrd file at `offset`. Returns bytes copied
|
||||
/// (0 at EOF) or null for a bad token. Lock-free: the initrd is immutable.
|
||||
pub fn nodeRead(node_token: u64, offset: u64, out: []u8) ?usize {
|
||||
if (tokenKind(node_token) != token_kind_initrd_file) return null;
|
||||
const image = ramdisk_image orelse return null;
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return null;
|
||||
const item = rd.entry(@intCast(tokenIndex(node_token))) orelse return null;
|
||||
if (offset >= item.blob.len) return 0;
|
||||
const n = @min(out.len, item.blob.len - @as(usize, @intCast(offset)));
|
||||
@memcpy(out[0..n], item.blob[@intCast(offset)..][0..n]);
|
||||
return n;
|
||||
}
|
||||
|
||||
/// A node's metadata in vfs-protocol FileStatus shape (size, kind, mtime).
|
||||
pub fn nodeStatus(node_token: u64) ?abi.FileAttributes {
|
||||
switch (tokenKind(node_token)) {
|
||||
token_kind_initrd_file => {
|
||||
const image = ramdisk_image orelse return null;
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return null;
|
||||
const item = rd.entry(@intCast(tokenIndex(node_token))) orelse return null;
|
||||
return .{ .size = item.blob.len, .kind = abi.file_kind_regular };
|
||||
},
|
||||
token_kind_initrd_directory => {
|
||||
if (tokenIndex(node_token) >= directory_count) return null;
|
||||
return .{ .size = 0, .kind = abi.file_kind_directory };
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `cursor`th child of an initrd directory: fills `name_out`, returns the
|
||||
/// entry header, or null past the end / bad token. Cursor enumerates
|
||||
/// subdirectories first, then files whose parent is this directory — stable,
|
||||
/// because the initrd is immutable.
|
||||
pub fn nodeReaddir(node_token: u64, cursor: u64, name_out: []u8) ?struct { header: abi.DirectoryEntryHeader, name_len: usize } {
|
||||
if (tokenKind(node_token) != token_kind_initrd_directory) return null;
|
||||
const directory_index = tokenIndex(node_token);
|
||||
if (directory_index >= directory_count) return null;
|
||||
const self_path = directories[@intCast(directory_index)].slice();
|
||||
|
||||
var index: u64 = 0;
|
||||
// Subdirectories whose parent is this directory.
|
||||
for (directories[0..directory_count], 0..) |*d, i| {
|
||||
if (i == directory_index) continue;
|
||||
if (d.parent != directory_index) continue;
|
||||
if (i == 0) continue;
|
||||
if (index == cursor) {
|
||||
const name = d.slice()[self_path.len + 1 ..];
|
||||
const n = @min(name.len, name_out.len);
|
||||
@memcpy(name_out[0..n], name[0..n]);
|
||||
return .{ .header = .{ .kind = abi.file_kind_directory, .name_len = @intCast(n), .size = 0 }, .name_len = n };
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
// Files directly inside this directory.
|
||||
const image = ramdisk_image orelse return null;
|
||||
const rd = initial_ramdisk.Reader.init(image) orelse return null;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (!std.mem.eql(u8, parentOf(item.name), self_path)) continue;
|
||||
if (index == cursor) {
|
||||
const name = item.name[self_path.len + 1 ..];
|
||||
const n = @min(name.len, name_out.len);
|
||||
@memcpy(name_out[0..n], name[0..n]);
|
||||
return .{ .header = .{ .kind = abi.file_kind_regular, .name_len = @intCast(n), .size = item.blob.len }, .name_len = n };
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- mount/unmount (syscall bodies; caller resolved the handle) --------------
|
||||
|
||||
/// Mount `backend` at `prefix` with an optional backend-side `rewrite` prefix.
|
||||
/// The endpoint reference is taken by the caller (process.zig bumps it); refuses
|
||||
/// shadowing or replacing /system.
|
||||
pub fn mountBackend(prefix: []const u8, backend: *ipc.Endpoint, rewrite: []const u8) bool {
|
||||
if (!isAbsolute(prefix) or prefix.len < 2 or prefix.len > maximum_prefix) return false;
|
||||
if (rewrite.len > maximum_rewrite) return false;
|
||||
if (underMount(prefix, "/system") != null) return false; // the initrd is not shadowable
|
||||
installMount(prefix, .backend, backend, rewrite);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn unmount(prefix: []const u8) bool {
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and m.kind == .backend and std.mem.eql(u8, m.prefixSlice(), prefix)) {
|
||||
if (m.backend) |endpoint| ipc.dropRef(endpoint);
|
||||
m.* = .{};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- tests (host) ------------------------------------------------------------
|
||||
|
||||
test "underMount matches only at path boundaries" {
|
||||
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
|
||||
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
|
||||
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null);
|
||||
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null);
|
||||
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
|
||||
try std.testing.expect(underMount("greeting", "/mnt/usb") == null);
|
||||
}
|
||||
|
||||
test "parentOf walks toward the root" {
|
||||
try std.testing.expectEqualStrings("/system/services", parentOf("/system/services/fat"));
|
||||
try std.testing.expectEqualStrings("/system", parentOf("/system/services"));
|
||||
try std.testing.expectEqualStrings("/", parentOf("/system"));
|
||||
}
|
||||
@@ -24,3 +24,9 @@ pub fn init() void {
|
||||
pub fn nowSeconds() u64 {
|
||||
return boot_unix_seconds + (architecture.nanos() -% boot_nanos) / 1_000_000_000;
|
||||
}
|
||||
|
||||
/// The wall-clock time of boot itself (the RTC anchor) — what klog_status hands
|
||||
/// the logger service to name a per-boot log directory. Zero until `init` runs.
|
||||
pub fn bootSeconds() u64 {
|
||||
return boot_unix_seconds;
|
||||
}
|
||||
|
||||
@@ -21,11 +21,6 @@ const power = runtime.power_protocol;
|
||||
/// integer decode names the opcodes instead of bare 0x0A/0x0B/… (docs/coding-standards.md).
|
||||
const opcodes = aml.opcodes;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// The claimed acpi-tables node and the resource index of its broad io_port
|
||||
// window — the Hal routes every port access through this one claim.
|
||||
var node_id: u64 = 0;
|
||||
@@ -163,12 +158,12 @@ pub fn main(init: runtime.process.Init) void {
|
||||
};
|
||||
var namespace = result.namespace;
|
||||
const devices = aml.deviceCount(&namespace);
|
||||
writeLine("/system/services/acpi: parsed {d} AML blob(s), {d} namespace devices\n", .{ block_count, devices });
|
||||
std.log.info("parsed {d} AML blob(s), {d} namespace devices", .{ block_count, devices });
|
||||
if (floor) |minimum| {
|
||||
if (devices >= minimum) {
|
||||
_ = runtime.system.write("acpi-parse: ok\n");
|
||||
} else {
|
||||
writeLine("acpi-parse: too few (ring-3 {d} < floor {d})\n", .{ devices, minimum });
|
||||
std.log.info("acpi-parse: too few (ring-3 {d} < floor {d})", .{ devices, minimum });
|
||||
}
|
||||
// Self-verify mode is standalone (no manager); stop before reporting.
|
||||
while (true) runtime.system.sleep(1000);
|
||||
@@ -216,9 +211,9 @@ fn onInit(endpoint: runtime.ipc.Handle) bool {
|
||||
const hid = entry.hid[0..entry.hid_len];
|
||||
const desc = acpi_ids.description(hid);
|
||||
if (desc.len != 0)
|
||||
writeLine("/system/services/acpi: reported {s} (device {d}, {d} resources) — {s}\n", .{ hid, entry.device_id, entry.resource_count, desc })
|
||||
std.log.info("reported {s} (device {d}, {d} resources) — {s}", .{ hid, entry.device_id, entry.resource_count, desc })
|
||||
else
|
||||
writeLine("/system/services/acpi: reported {s} (device {d}, {d} resources)\n", .{ hid, entry.device_id, entry.resource_count });
|
||||
std.log.info("reported {s} (device {d}, {d} resources)", .{ hid, entry.device_id, entry.resource_count });
|
||||
if (manager) |h| {
|
||||
var report = protocol.ChildAdded{ .parent = node_id, .bus_address = entry.device_id, .identity = 0, .device_id = entry.device_id };
|
||||
@memcpy(report.hid[0..entry.hid_len], entry.hid[0..entry.hid_len]);
|
||||
@@ -226,7 +221,7 @@ fn onInit(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = runtime.ipc.call(h, std.mem.asBytes(&report), &reply) catch {};
|
||||
}
|
||||
}
|
||||
writeLine("/system/services/acpi: reported {d} device(s) to the manager\n", .{registered_count});
|
||||
std.log.info("reported {d} device(s) to the manager", .{registered_count});
|
||||
|
||||
armPowerButton(endpoint);
|
||||
return true;
|
||||
@@ -373,7 +368,7 @@ fn publishNotify(node: *aml.Node, code: u64) void {
|
||||
const which: power.Event = if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) .battery else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) .ac else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) .lid else .notify;
|
||||
var event = power.EventMessage{ .event = @intFromEnum(which), .code = @truncate(code) };
|
||||
event.hid = hid;
|
||||
writeLine("power: notify {s} code {d}\n", .{ hid[0..7], code });
|
||||
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
|
||||
publishEvent(std.mem.asBytes(&event));
|
||||
}
|
||||
|
||||
@@ -500,7 +495,7 @@ fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) vo
|
||||
applyCrs(&descriptor, node, interpreter);
|
||||
|
||||
const id = device.register(node_id, &descriptor) orelse {
|
||||
writeLine("/system/services/acpi: register refused for {s}\n", .{hid[0..@intCast(hid_len)]});
|
||||
std.log.info("register refused for {s}", .{hid[0..@intCast(hid_len)]});
|
||||
return;
|
||||
};
|
||||
registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
|
||||
|
||||
@@ -24,14 +24,6 @@ const protocol = runtime.device_manager_protocol;
|
||||
const device = runtime.device;
|
||||
const system = runtime.system;
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so output
|
||||
/// from the drivers this manager starts (which run concurrently) can never land
|
||||
/// in the middle of it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
/// The PCI class/subclass/prog-IF triple of an xHCI (USB 3) host controller —
|
||||
/// Serial Bus Controller / USB Controller / XHCI — named from pci-class.zig rather
|
||||
/// than written as the bare 0x0C0330 (docs/coding-standards.md, "Named values").
|
||||
@@ -56,8 +48,8 @@ const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{
|
||||
/// its registered id as argv[1].
|
||||
fn pciDriverForIdentity(identity: u64) ?[]const u8 {
|
||||
return switch (identity) {
|
||||
xhci_pci_class => "usb-xhci-bus",
|
||||
virtio_gpu_pci_class => "virtio-gpu",
|
||||
xhci_pci_class => "/system/drivers/usb-xhci-bus",
|
||||
virtio_gpu_pci_class => "/system/drivers/virtio-gpu",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
@@ -67,8 +59,8 @@ fn pciDriverForIdentity(identity: u64) ?[]const u8 {
|
||||
/// nodes the kernel used to build). ps2-bus is a singleton that finds both its
|
||||
/// devices by hid once spawned, so keyboard and mouse map to the same name.
|
||||
fn hidDriverFor(hid: []const u8) ?[]const u8 {
|
||||
if (std.mem.eql(u8, hid, "PNP0303")) return "ps2-bus"; // PS/2 keyboard
|
||||
if (std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus"; // PS/2 mouse
|
||||
if (std.mem.eql(u8, hid, "PNP0303")) return "/system/drivers/ps2-bus"; // PS/2 keyboard
|
||||
if (std.mem.eql(u8, hid, "PNP0F13")) return "/system/drivers/ps2-bus"; // PS/2 mouse
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -95,9 +87,9 @@ fn usbDriverForIdentity(identity: u64) ?[]const u8 {
|
||||
@intFromEnum(usb_ids.mass_storage.Protocol.bulk_only),
|
||||
);
|
||||
return switch (identity) {
|
||||
keyboard => "usb-hid-keyboard",
|
||||
mouse => "usb-hid-mouse",
|
||||
storage => "usb-storage",
|
||||
keyboard => "/system/drivers/usb-hid-keyboard",
|
||||
mouse => "/system/drivers/usb-hid-mouse",
|
||||
storage => "/system/drivers/usb-storage",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
@@ -132,7 +124,7 @@ const DriverState = enum {
|
||||
|
||||
const Driver = struct {
|
||||
used: bool = false,
|
||||
name_buffer: [24]u8 = undefined,
|
||||
name_buffer: [64]u8 = undefined, // fits a full binary path (abi.maximum_process_name)
|
||||
name_len: usize = 0,
|
||||
// The assigned device id (becomes argv[1]), or protocol.no_device.
|
||||
device_id: u64 = protocol.no_device,
|
||||
@@ -223,7 +215,7 @@ fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, report
|
||||
fn pruneChildrenOf(reporter: u32) void {
|
||||
for (&children) |*child| {
|
||||
if (child.used and child.reporter == reporter) {
|
||||
writeLine("/system/services/device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
||||
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
||||
child.used = false;
|
||||
const event = protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address };
|
||||
publishEvent(std.mem.asBytes(&event));
|
||||
@@ -270,7 +262,7 @@ fn addDriver(name: []const u8, device_id: u64, speaks_protocol: bool) void {
|
||||
spawnDriver(driver);
|
||||
return;
|
||||
}
|
||||
writeLine("/system/services/device-manager: driver table full; cannot supervise {s}\n", .{name});
|
||||
std.log.info("driver table full; cannot supervise {s}", .{name});
|
||||
}
|
||||
|
||||
/// (Re)spawn a driver instance: supervised on the manager's own endpoint, the
|
||||
@@ -285,7 +277,7 @@ fn spawnDriver(driver: *Driver) void {
|
||||
argument_count = 1;
|
||||
}
|
||||
const child = system.spawnSupervised(driver.name(), arguments[0..argument_count], manager_endpoint) orelse {
|
||||
writeLine("/system/services/device-manager: failed to spawn {s}\n", .{driver.name()});
|
||||
std.log.info("failed to spawn {s}", .{driver.name()});
|
||||
driver.state = .failed;
|
||||
return;
|
||||
};
|
||||
@@ -299,9 +291,9 @@ fn spawnDriver(driver: *Driver) void {
|
||||
driver.state = .running;
|
||||
}
|
||||
if (driver.device_id != protocol.no_device) {
|
||||
writeLine("/system/services/device-manager: spawned {s} for device {d}\n", .{ driver.name(), driver.device_id });
|
||||
std.log.info("spawned {s} for device {d}", .{ driver.name(), driver.device_id });
|
||||
} else {
|
||||
writeLine("/system/services/device-manager: spawned {s}\n", .{driver.name()});
|
||||
std.log.info("spawned {s}", .{driver.name()});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +305,7 @@ fn onDriverExit(driver: *Driver) void {
|
||||
const reason = runtime.process.exitReason(driver.process_id) orelse .fault;
|
||||
if (reason == .exited) {
|
||||
driver.state = .stopped;
|
||||
writeLine("/system/services/device-manager: {s} exited cleanly; not restarting\n", .{driver.name()});
|
||||
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
||||
return;
|
||||
}
|
||||
const now = system.clock();
|
||||
@@ -321,13 +313,13 @@ fn onDriverExit(driver: *Driver) void {
|
||||
driver.restarts = if (alive_ns < fast_death_ns) driver.restarts + 1 else 1;
|
||||
if (driver.restarts >= crash_loop_cap) {
|
||||
driver.state = .failed;
|
||||
writeLine("/system/services/device-manager: {s} is failing repeatedly (crash loop); giving up\n", .{driver.name()});
|
||||
std.log.info("{s} is failing repeatedly (crash loop); giving up", .{driver.name()});
|
||||
return;
|
||||
}
|
||||
const delay_ms = backoff_base_ms << @intCast(driver.restarts - 1);
|
||||
driver.state = .restarting;
|
||||
driver.restart_due_ns = now + delay_ms * 1_000_000;
|
||||
writeLine("/system/services/device-manager: restarting {s} in {d} ms (died: {s})\n", .{ driver.name(), delay_ms, @tagName(reason) });
|
||||
std.log.info("restarting {s} in {d} ms (died: {s})", .{ driver.name(), delay_ms, @tagName(reason) });
|
||||
_ = system.timerOnce(manager_endpoint, delay_ms + 50);
|
||||
}
|
||||
|
||||
@@ -338,7 +330,7 @@ fn onDriverExit(driver: *Driver) void {
|
||||
fn sweepDeadlines() void {
|
||||
const now = system.clock();
|
||||
if (test_kill_pid != 0 and now >= test_kill_due_ns) {
|
||||
writeLine("/system/services/device-manager: test mode: killing the reporter\n", .{});
|
||||
std.log.info("test mode: killing the reporter", .{});
|
||||
_ = system.kill(test_kill_pid);
|
||||
test_kill_pid = 0;
|
||||
}
|
||||
@@ -346,7 +338,7 @@ fn sweepDeadlines() void {
|
||||
if (!driver.used) continue;
|
||||
switch (driver.state) {
|
||||
.awaiting_hello => if (now >= driver.hello_deadline_ns) {
|
||||
writeLine("/system/services/device-manager: {s} missed its hello deadline\n", .{driver.name()});
|
||||
std.log.info("{s} missed its hello deadline", .{driver.name()});
|
||||
_ = system.kill(driver.process_id);
|
||||
// The exit notification finishes the job via onDriverExit.
|
||||
},
|
||||
@@ -423,13 +415,13 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime
|
||||
var status: i32 = 0;
|
||||
if (hello.version != protocol.version) {
|
||||
status = -1;
|
||||
writeLine("/system/services/device-manager: refused hello (version {d}) from process {d}\n", .{ hello.version, sender });
|
||||
std.log.info("refused hello (version {d}) from process {d}", .{ hello.version, sender });
|
||||
} else if (driverByProcess(sender)) |driver| {
|
||||
driver.state = .running;
|
||||
writeLine("/system/services/device-manager: hello from {s} (device {d})\n", .{ driver.name(), hello.device_id });
|
||||
std.log.info("hello from {s} (device {d})", .{ driver.name(), hello.device_id });
|
||||
// Resilience drill (V6): once, kill the virtio-gpu driver a moment after it hellos, so
|
||||
// the normal restart policy respawns it — the compositor must survive and re-attach.
|
||||
if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "virtio-gpu")) {
|
||||
if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "/system/drivers/virtio-gpu")) {
|
||||
test_scanout_killed = true;
|
||||
test_kill_pid = sender;
|
||||
test_kill_due_ns = system.clock() + 1_500_000_000;
|
||||
@@ -437,7 +429,7 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime
|
||||
}
|
||||
} else {
|
||||
status = -1;
|
||||
writeLine("/system/services/device-manager: hello from unknown process {d}\n", .{sender});
|
||||
std.log.info("hello from unknown process {d}", .{sender});
|
||||
}
|
||||
const hello_reply = protocol.HelloReply{ .status = status };
|
||||
@memcpy(reply[0..protocol.reply_size], std.mem.asBytes(&hello_reply));
|
||||
@@ -453,7 +445,7 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
||||
var status: i32 = 0;
|
||||
if (driverByProcess(sender)) |driver| {
|
||||
if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
|
||||
writeLine("/system/services/device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||
if (status == 0) publishEvent(message[0..protocol.child_added_size]);
|
||||
// Matching from reports (M19.3): a registered child whose identity
|
||||
// names a driver gets one, once — re-reports after a bus restart
|
||||
@@ -498,7 +490,7 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
||||
// Only the xHCI reporter is the drill's victim — pci-bus also reports
|
||||
// now, and whichever finishes second must not trigger the kill.
|
||||
if (driverByProcess(sender)) |driver| {
|
||||
if (std.mem.eql(u8, driver.name(), "usb-xhci-bus")) {
|
||||
if (std.mem.eql(u8, driver.name(), "/system/drivers/usb-xhci-bus")) {
|
||||
// Delayed, not immediate: the device-list scenario's subscriber
|
||||
// needs a window to enumerate and subscribe before the events.
|
||||
test_usb_killed = true;
|
||||
@@ -519,7 +511,7 @@ fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize {
|
||||
var status: i32 = -1;
|
||||
for (&children) |*child| {
|
||||
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == sender) {
|
||||
writeLine("/system/services/device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
||||
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
||||
child.used = false;
|
||||
status = 0;
|
||||
}
|
||||
|
||||
+337
-16
@@ -70,6 +70,15 @@ pub const FileSystem = struct {
|
||||
// server before a mutating op. 0 leaves the on-disk timestamps untouched (host
|
||||
// tests that don't care about time, and reads).
|
||||
current_time_epoch: u64 = 0,
|
||||
// Where the next allocateCluster scan starts — clusters below this were seen
|
||||
// in use, so a fresh scan needn't re-read them (frees rewind it). Without
|
||||
// this the scan re-read the FAT from cluster 2 per allocation: measured at
|
||||
// ~1 s/cluster on a part-full volume (a 37 s shutdown log flush).
|
||||
next_free_hint: u32 = 2,
|
||||
// Which absolute LBA `fat_sector` currently holds (0 = none). Lets a FAT
|
||||
// scan serve consecutive entries from one device read; every write through
|
||||
// the sector keeps the cache coherent (writeFatBytes updates it in place).
|
||||
fat_sector_lba: u64 = 0,
|
||||
|
||||
// Every filesystem-relative sector access adds the partition base.
|
||||
fn blockRead(self: *FileSystem, lba: u64, buffer: []u8) bool {
|
||||
@@ -139,7 +148,10 @@ pub const FileSystem = struct {
|
||||
while (done < out.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
if (lba != self.fat_sector_lba) {
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
self.fat_sector_lba = lba;
|
||||
}
|
||||
const n = @min(out.len - done, sector_size - within);
|
||||
@memcpy(out[done .. done + n], self.fat_sector[within .. within + n]);
|
||||
done += n;
|
||||
@@ -159,7 +171,10 @@ pub const FileSystem = struct {
|
||||
while (done < in.len) {
|
||||
const lba = position / sector_size;
|
||||
const within: usize = @intCast(position % sector_size);
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
if (lba != self.fat_sector_lba) {
|
||||
if (!self.blockRead(lba, &self.fat_sector)) return false;
|
||||
self.fat_sector_lba = lba;
|
||||
}
|
||||
const n = @min(in.len - done, sector_size - within);
|
||||
@memcpy(self.fat_sector[within .. within + n], in[done .. done + n]);
|
||||
if (!self.blockWrite(lba, &self.fat_sector)) return false;
|
||||
@@ -237,11 +252,19 @@ pub const FileSystem = struct {
|
||||
|
||||
// Find and claim a free cluster, marking it end-of-chain. Returns its number.
|
||||
fn allocateCluster(self: *FileSystem) ?u32 {
|
||||
var cluster: u32 = 2;
|
||||
while (cluster < self.geometry.cluster_count + 2) : (cluster += 1) {
|
||||
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
|
||||
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
|
||||
return cluster;
|
||||
const limit = self.geometry.cluster_count + 2;
|
||||
// Two passes: hint..end, then 2..hint (the hint only skips known-used
|
||||
// ground, it never hides a freed cluster — freeChain rewinds it).
|
||||
var pass: u2 = 0;
|
||||
while (pass < 2) : (pass += 1) {
|
||||
var cluster: u32 = if (pass == 0) self.next_free_hint else 2;
|
||||
const end: u32 = if (pass == 0) limit else self.next_free_hint;
|
||||
while (cluster < end) : (cluster += 1) {
|
||||
if (self.readFatEntry(cluster) == on_disk.free_cluster) {
|
||||
if (!self.writeFatEntry(cluster, self.endOfChainValue())) return null;
|
||||
self.next_free_hint = cluster + 1;
|
||||
return cluster;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -257,6 +280,7 @@ pub const FileSystem = struct {
|
||||
while (cluster >= 2 and cluster < limit and guard < limit) : (guard += 1) {
|
||||
const next = self.readFatEntry(cluster);
|
||||
_ = self.writeFatEntry(cluster, on_disk.free_cluster);
|
||||
if (cluster < self.next_free_hint) self.next_free_hint = cluster;
|
||||
if (self.isEndOfChain(next) or next < 2) break;
|
||||
cluster = next;
|
||||
}
|
||||
@@ -603,6 +627,192 @@ pub const FileSystem = struct {
|
||||
_ = self.blockWrite(node.entry_sector, &self.dir_sector);
|
||||
}
|
||||
|
||||
// --- long-name creation --------------------------------------------------
|
||||
|
||||
// The standard 8.3 short-name checksum carried by every long-name entry.
|
||||
fn shortChecksum(raw: [11]u8) u8 {
|
||||
var sum: u8 = 0;
|
||||
for (raw) |c| sum = ((sum & 1) << 7) +% (sum >> 1) +% c;
|
||||
return sum;
|
||||
}
|
||||
|
||||
fn valid83Char(c: u8) bool {
|
||||
return (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '-' or c == '_';
|
||||
}
|
||||
|
||||
// Whether an 8.3 entry with exactly this raw name exists in `dir`.
|
||||
const RawContext = struct { raw: [11]u8, found: *bool };
|
||||
fn rawVisit(context: *const RawContext, entry: on_disk.DirectoryEntry, name: []const u8, entry_sector: u64, entry_offset: u32) bool {
|
||||
_ = name;
|
||||
_ = entry_sector;
|
||||
_ = entry_offset;
|
||||
if (std.mem.eql(u8, &entry.name, &context.raw)) {
|
||||
context.found.* = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn shortNameExists(self: *FileSystem, dir: Node, raw: [11]u8) bool {
|
||||
var found = false;
|
||||
var context = RawContext{ .raw = raw, .found = &found };
|
||||
self.scanDirectory(dir, &context, rawVisit);
|
||||
return found;
|
||||
}
|
||||
|
||||
// A mangled STEM~N.EXT short name that collides with nothing in `dir` — the
|
||||
// alias behind a long-name chain.
|
||||
fn shortNameFor(self: *FileSystem, dir: Node, name: []const u8) ?[11]u8 {
|
||||
const dot = std.mem.lastIndexOfScalar(u8, name, '.');
|
||||
const base = if (dot) |d| name[0..d] else name;
|
||||
const ext = if (dot) |d| name[d + 1 ..] else name[0..0];
|
||||
|
||||
var stem: [6]u8 = undefined;
|
||||
var stem_len: usize = 0;
|
||||
for (base) |c| {
|
||||
if (stem_len == stem.len) break;
|
||||
const upper = std.ascii.toUpper(c);
|
||||
if (valid83Char(upper)) {
|
||||
stem[stem_len] = upper;
|
||||
stem_len += 1;
|
||||
}
|
||||
}
|
||||
if (stem_len == 0) {
|
||||
stem[0] = 'X';
|
||||
stem_len = 1;
|
||||
}
|
||||
|
||||
var raw = [_]u8{' '} ** 11;
|
||||
var ext_len: usize = 0;
|
||||
for (ext) |c| {
|
||||
if (ext_len == 3) break;
|
||||
const upper = std.ascii.toUpper(c);
|
||||
if (valid83Char(upper)) {
|
||||
raw[8 + ext_len] = upper;
|
||||
ext_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
var index: u32 = 1;
|
||||
while (index <= 999_999) : (index += 1) {
|
||||
var tail_buffer: [8]u8 = undefined;
|
||||
const tail = std.fmt.bufPrint(&tail_buffer, "~{d}", .{index}) catch return null;
|
||||
const keep = @min(stem_len, 8 - tail.len);
|
||||
@memset(raw[0..8], ' ');
|
||||
@memcpy(raw[0..keep], stem[0..keep]);
|
||||
@memcpy(raw[keep .. keep + tail.len], tail);
|
||||
if (!self.shortNameExists(dir, raw)) return raw;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fill one long-name entry's 13 UTF-16 slots from `name` starting at
|
||||
// `offset`: the name's bytes widened, then a 0x0000 terminator, then 0xFFFF.
|
||||
fn fillLongNamePiece(lfn: *on_disk.LongNameEntry, name: []const u8, offset: usize) void {
|
||||
var units: [13]u16 = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < 13) : (i += 1) {
|
||||
const at = offset + i;
|
||||
units[i] = if (at < name.len) name[at] else if (at == name.len) 0x0000 else 0xFFFF;
|
||||
}
|
||||
lfn.name1 = units[0..5].*;
|
||||
lfn.name2 = units[5..11].*;
|
||||
lfn.name3 = units[11..13].*;
|
||||
}
|
||||
|
||||
// The first entry index of a run of `count` free slots in `dir`, growing the
|
||||
// directory as needed. Fresh clusters are zeroed, so growth always yields
|
||||
// free slots; only the fixed FAT12/16 root can genuinely run out.
|
||||
fn findFreeRun(self: *FileSystem, dir: Node, count: usize) ?u32 {
|
||||
var run_start: u32 = 0;
|
||||
var run_len: usize = 0;
|
||||
var sector_index: u32 = 0;
|
||||
while (self.dirSectorLba(dir, sector_index, true)) |lba| : (sector_index += 1) {
|
||||
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
||||
var i: u32 = 0;
|
||||
while (i < entries_per_sector) : (i += 1) {
|
||||
const offset = i * @sizeOf(on_disk.DirectoryEntry);
|
||||
const entry = std.mem.bytesToValue(on_disk.DirectoryEntry, self.dir_sector[offset .. offset + @sizeOf(on_disk.DirectoryEntry)]);
|
||||
if (entry.isFree()) {
|
||||
if (run_len == 0) run_start = sector_index * entries_per_sector + i;
|
||||
run_len += 1;
|
||||
if (run_len == count) return run_start;
|
||||
} else {
|
||||
run_len = 0;
|
||||
}
|
||||
}
|
||||
if (sector_index > 4096) return null; // runaway guard
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Write one 32-byte directory entry at a global entry index (read-modify-
|
||||
// write of its sector). Returns the entry's (sector, offset) or null.
|
||||
fn writeEntryAt(self: *FileSystem, dir: Node, index: u32, bytes: *const [32]u8) ?EntryLoc {
|
||||
const lba = self.dirSectorLba(dir, index / entries_per_sector, true) orelse return null;
|
||||
if (!self.blockRead(lba, &self.dir_sector)) return null;
|
||||
const offset = (index % entries_per_sector) * @sizeOf(on_disk.DirectoryEntry);
|
||||
@memcpy(self.dir_sector[offset .. offset + 32], bytes);
|
||||
if (!self.blockWrite(lba, &self.dir_sector)) return null;
|
||||
return .{ .sector = lba, .offset = offset };
|
||||
}
|
||||
|
||||
// Add a named directory entry, creating a long-name chain when the name is
|
||||
// not its own 8.3 form. Write order is LFN pieces first, 8.3 entry last: an
|
||||
// interrupted create leaves orphaned long-name entries, which every FAT
|
||||
// reader (this engine's scanner included) skips as unattached — never a
|
||||
// mismatched chain.
|
||||
fn addEntryNamed(self: *FileSystem, dir: Node, name: []const u8, attributes: u8, first_cluster: u32, size: u32) ?Node {
|
||||
if (to83(name)) |raw| {
|
||||
var display: [12]u8 = undefined;
|
||||
// Only a name that IS its 8.3 form (already uppercase) skips the
|
||||
// chain — a lowercase name gets one so its exact case survives,
|
||||
// matching tools/make-fat-image.py.
|
||||
if (std.mem.eql(u8, format83(raw, &display), name))
|
||||
return self.addEntry(dir, raw, attributes, first_cluster, size);
|
||||
}
|
||||
if (name.len == 0 or name.len > 255) return null;
|
||||
|
||||
const raw = self.shortNameFor(dir, name) orelse return null;
|
||||
const checksum = shortChecksum(raw);
|
||||
const piece_count: u32 = @intCast((name.len + 12) / 13);
|
||||
if (piece_count > 20) return null;
|
||||
const start = self.findFreeRun(dir, piece_count + 1) orelse return null;
|
||||
|
||||
var k: u32 = 0;
|
||||
while (k < piece_count) : (k += 1) {
|
||||
const piece = piece_count - k; // stored last-logical-first
|
||||
var lfn = std.mem.zeroes(on_disk.LongNameEntry);
|
||||
lfn.order = @intCast(piece | (if (k == 0) @as(u8, 0x40) else 0));
|
||||
lfn.attributes = on_disk.attribute_long_name;
|
||||
lfn.checksum = checksum;
|
||||
fillLongNamePiece(&lfn, name, (piece - 1) * 13);
|
||||
_ = self.writeEntryAt(dir, start + k, std.mem.asBytes(&lfn)[0..32]) orelse return null;
|
||||
}
|
||||
|
||||
var entry = std.mem.zeroes(on_disk.DirectoryEntry);
|
||||
entry.name = raw;
|
||||
entry.attributes = attributes;
|
||||
entry.file_size = size;
|
||||
entry.setFirstCluster(first_cluster);
|
||||
const stamp = on_disk.epochToFatDateTime(self.current_time_epoch);
|
||||
entry.creation_date = stamp.date;
|
||||
entry.creation_time = stamp.time;
|
||||
entry.write_date = stamp.date;
|
||||
entry.write_time = stamp.time;
|
||||
entry.last_access_date = stamp.date;
|
||||
const location = self.writeEntryAt(dir, start + piece_count, std.mem.asBytes(&entry)[0..32]) orelse return null;
|
||||
return .{
|
||||
.first_cluster = first_cluster,
|
||||
.size = size,
|
||||
.is_directory = attributes & on_disk.attribute_directory != 0,
|
||||
.mtime = self.current_time_epoch,
|
||||
.entry_sector = location.sector,
|
||||
.entry_offset = location.offset,
|
||||
.has_entry = true,
|
||||
};
|
||||
}
|
||||
|
||||
// Add an 8.3 directory entry to `dir` with the given attributes, first cluster,
|
||||
// and size, reusing a free (0x00 or 0xE5) slot and growing the directory chain
|
||||
// if needed. Returns the new node (with its entry location) or null if full.
|
||||
@@ -645,19 +855,20 @@ pub const FileSystem = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Create an 8.3-named file in directory `dir`. Returns the new (empty) node,
|
||||
/// or null if the name is not 8.3-representable or no directory slot is free.
|
||||
/// Create a file in directory `dir`. Uppercase 8.3 names get a bare short
|
||||
/// entry; anything else gets a long-name chain over a mangled ~N alias.
|
||||
/// Returns the new (empty) node, or null (bad name / directory full /
|
||||
/// duplicate — the caller checks existence first if it must distinguish).
|
||||
pub fn createFile(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
const raw = to83(name) orelse return null;
|
||||
return self.addEntry(dir, raw, on_disk.attribute_archive, 0, 0);
|
||||
return self.addEntryNamed(dir, name, on_disk.attribute_archive, 0, 0);
|
||||
}
|
||||
|
||||
/// Create an 8.3-named subdirectory in `dir`: allocate and initialise its first
|
||||
/// Create a subdirectory in `dir`: allocate and initialise its first
|
||||
/// cluster with "." (itself) and ".." (the parent) entries, then add its
|
||||
/// directory entry to `dir`. Returns the new directory node, or null (bad name,
|
||||
/// no free cluster, or the directory is full). Long names are not created.
|
||||
/// directory entry to `dir` (long-name chain when the name needs one).
|
||||
/// Returns the new directory node, or null (bad name, no free cluster, or
|
||||
/// the directory is full).
|
||||
pub fn createDirectory(self: *FileSystem, dir: Node, name: []const u8) ?Node {
|
||||
const raw = to83(name) orelse return null;
|
||||
const cluster = self.allocateCluster() orelse return null;
|
||||
self.zeroCluster(cluster);
|
||||
|
||||
@@ -685,7 +896,7 @@ pub const FileSystem = struct {
|
||||
self.freeChain(cluster);
|
||||
return null;
|
||||
}
|
||||
return self.addEntry(dir, raw, on_disk.attribute_directory, cluster, 0) orelse {
|
||||
return self.addEntryNamed(dir, name, on_disk.attribute_directory, cluster, 0) orelse {
|
||||
self.freeChain(cluster);
|
||||
return null;
|
||||
};
|
||||
@@ -1103,3 +1314,113 @@ test "a create stamps the modification time" {
|
||||
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.resolve("/STAMP.TXT").?.mtime);
|
||||
try std.testing.expectEqual(@as(u64, 1_700_000_000), fs.listEntry(fs.rootNode(), 0).?.mtime);
|
||||
}
|
||||
|
||||
test "long-name create: directory + file round-trip by long name" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
// The per-boot log directory shape: an 18-char stamp, nested paths, .log names.
|
||||
const stamp_dir = fs.createDirectory(fs.rootNode(), "2026-07-21T101530Z").?;
|
||||
try std.testing.expect(stamp_dir.is_directory);
|
||||
const file = fs.createFile(stamp_dir, "device-manager.log").?;
|
||||
_ = file;
|
||||
|
||||
// Resolve by exact long name, and case-insensitively (FAT semantics).
|
||||
try std.testing.expect(fs.resolve("/2026-07-21T101530Z/device-manager.log") != null);
|
||||
try std.testing.expect(fs.resolve("/2026-07-21t101530z/DEVICE-MANAGER.LOG") != null);
|
||||
|
||||
// The listing shows the long names, not the ~N aliases.
|
||||
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
||||
try std.testing.expectEqualStrings("2026-07-21T101530Z", listing.name_buffer[0..listing.name_len]);
|
||||
var inner = fs.listEntry(stamp_dir, 2).?; // after "." and ".."
|
||||
try std.testing.expectEqualStrings("device-manager.log", inner.name_buffer[0..inner.name_len]);
|
||||
|
||||
// Write through the created file and read it back by long-name resolve.
|
||||
var node = fs.resolve("/2026-07-21T101530Z/device-manager.log").?;
|
||||
try std.testing.expectEqual(@as(usize, 10), fs.writeFile(&node, 0, "hello logs"));
|
||||
var buffer: [16]u8 = undefined;
|
||||
try std.testing.expectEqual(@as(usize, 10), fs.readFile(node, 0, buffer[0..10]));
|
||||
try std.testing.expectEqualStrings("hello logs", buffer[0..10]);
|
||||
}
|
||||
|
||||
test "long-name create: ~N alias collision suffixes stay distinct" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
_ = fs.createFile(fs.rootNode(), "logger-alpha.log").?;
|
||||
_ = fs.createFile(fs.rootNode(), "logger-beta.log").?;
|
||||
// Same 6-char mangle stem (LOGGER) — the second must take ~2.
|
||||
var raw_one = false;
|
||||
var raw_two = false;
|
||||
var cursor: u32 = 0;
|
||||
while (fs.listEntry(fs.rootNode(), cursor)) |entry| : (cursor += 1) {
|
||||
if (std.mem.eql(u8, entry.name_buffer[0..entry.name_len], "logger-alpha.log")) raw_one = true;
|
||||
if (std.mem.eql(u8, entry.name_buffer[0..entry.name_len], "logger-beta.log")) raw_two = true;
|
||||
}
|
||||
try std.testing.expect(raw_one and raw_two);
|
||||
try std.testing.expect(fs.resolve("/logger-alpha.log") != null);
|
||||
try std.testing.expect(fs.resolve("/logger-beta.log") != null);
|
||||
// Their short aliases took distinct ~N tails. (Alias LOOKUP is not a
|
||||
// feature — findChild matches display names — but the on-disk aliases
|
||||
// must not collide for other FAT readers.)
|
||||
try std.testing.expect(fs.shortNameExists(fs.rootNode(), "LOGGER~1LOG".*));
|
||||
try std.testing.expect(fs.shortNameExists(fs.rootNode(), "LOGGER~2LOG".*));
|
||||
}
|
||||
|
||||
test "long-name create: unlink removes the chain; slots are reused cleanly" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
_ = fs.createFile(fs.rootNode(), "a-rather-long-file-name.txt").?;
|
||||
try std.testing.expect(fs.removeFile(fs.rootNode(), "a-rather-long-file-name.txt"));
|
||||
try std.testing.expect(fs.resolve("/a-rather-long-file-name.txt") == null);
|
||||
|
||||
// A new long name reuses the freed run without inheriting the old chain.
|
||||
_ = fs.createFile(fs.rootNode(), "an-entirely-different-name.md").?;
|
||||
try std.testing.expect(fs.resolve("/an-entirely-different-name.md") != null);
|
||||
try std.testing.expect(fs.resolve("/a-rather-long-file-name.txt") == null);
|
||||
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
||||
try std.testing.expectEqualStrings("an-entirely-different-name.md", listing.name_buffer[0..listing.name_len]);
|
||||
}
|
||||
|
||||
test "8.3 fast path: an uppercase-compliant name gets one bare entry" {
|
||||
const allocator = std.testing.allocator;
|
||||
const bytes = try allocator.alloc(u8, 5000 * sector_size);
|
||||
defer allocator.free(bytes);
|
||||
formatFat16(bytes);
|
||||
var disk = RamDisk{ .bytes = bytes };
|
||||
var fs = FileSystem.mount(disk.device()).?;
|
||||
|
||||
_ = fs.createFile(fs.rootNode(), "DANOS.LOG").?;
|
||||
// Exactly one directory entry: entry 0 is the file, entry 1 is the end.
|
||||
var listing = fs.listEntry(fs.rootNode(), 0).?;
|
||||
try std.testing.expectEqualStrings("DANOS.LOG", listing.name_buffer[0..listing.name_len]);
|
||||
try std.testing.expect(fs.listEntry(fs.rootNode(), 1) == null);
|
||||
// A lowercase 8.3-shaped name is case-preserved via a chain instead.
|
||||
_ = fs.createFile(fs.rootNode(), "fat.log").?;
|
||||
var second = fs.listEntry(fs.rootNode(), 1).?;
|
||||
try std.testing.expectEqualStrings("fat.log", second.name_buffer[0..second.name_len]);
|
||||
}
|
||||
|
||||
test "short-name checksum matches the reference vector" {
|
||||
// "README TXT" is a widely published example: checksum 0x15... compute a
|
||||
// fixed pair to pin the rotate-add against regressions.
|
||||
const a = FileSystem.shortChecksum("README TXT".*);
|
||||
const b = FileSystem.shortChecksum("LOGGER~1LOG".*);
|
||||
try std.testing.expect(a != b);
|
||||
// The algorithm is order-sensitive: swapped bytes change the sum.
|
||||
const c = FileSystem.shortChecksum("REDAME TXT".*);
|
||||
try std.testing.expect(a != c);
|
||||
}
|
||||
|
||||
+43
-22
@@ -16,11 +16,6 @@ const on_disk = @import("on-disk.zig");
|
||||
const protocol = runtime.vfs_protocol;
|
||||
const dma = runtime.dma;
|
||||
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
const mount_point = "/mnt/usb";
|
||||
|
||||
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
|
||||
@@ -104,20 +99,44 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
||||
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
|
||||
return false;
|
||||
};
|
||||
writeLine("/system/services/fat: mounted FAT ({s}, {d} clusters, partition lba {d})\n", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
||||
std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
||||
|
||||
// Mount ourselves into the VFS namespace at /mnt/usb (retry while the VFS
|
||||
// comes up). From here the VFS routes /mnt/usb/... to this server.
|
||||
var tries: u32 = 0;
|
||||
while (tries < 100) : (tries += 1) {
|
||||
if (runtime.fs.mount(mount_point, endpoint)) {
|
||||
writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
|
||||
return true;
|
||||
}
|
||||
runtime.system.sleep(50);
|
||||
// With the router in the kernel, clients hold OUR node ids directly; sweep
|
||||
// a dead client's open handles via the published exit events (the pattern
|
||||
// the old userspace router used for its own table).
|
||||
_ = runtime.process.subscribeExits(endpoint);
|
||||
|
||||
// Mount ourselves into the kernel VFS at /mnt/usb — and serve /var from the
|
||||
// volume's /var subtree, so FHS paths (the logger's /var/log) stay decoupled
|
||||
// from which volume carries them. A mount is one syscall now; no retry
|
||||
// needed (the kernel's table exists before any service).
|
||||
if (runtime.fs.mount(mount_point, endpoint)) {
|
||||
std.log.info("mounted {s}", .{mount_point});
|
||||
} else {
|
||||
_ = runtime.system.write("/system/services/fat: could not mount /mnt/usb\n");
|
||||
}
|
||||
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
|
||||
return true; // still serve directly, even if the namespace mount didn't take
|
||||
if (runtime.fs.mountRewritten("/var", endpoint, "/var")) {
|
||||
std.log.info("mounted /var", .{});
|
||||
} else {
|
||||
_ = runtime.system.write("/system/services/fat: could not mount /var\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A subscribed process-exit event: release every open handle the dead client
|
||||
/// held, so a crashed reader can't pin table slots (or, later, locks).
|
||||
fn onNotification(badge: u64) void {
|
||||
const got = runtime.ipc.Received{ .len = 0, .badge = badge, .cap = null };
|
||||
if (!got.isChildExit()) return;
|
||||
const dead = got.childProcessId();
|
||||
var released: u32 = 0;
|
||||
for (&open_nodes) |*o| {
|
||||
if (o.used and o.owner == dead) {
|
||||
o.* = .{};
|
||||
released += 1;
|
||||
}
|
||||
}
|
||||
if (released != 0) std.log.info("released {d} handle(s) for dead client {d}", .{ released, dead });
|
||||
}
|
||||
|
||||
const ParentLeaf = struct { parent: []const u8, leaf: []const u8 };
|
||||
@@ -132,7 +151,7 @@ fn splitParent(path: []const u8) ParentLeaf {
|
||||
};
|
||||
}
|
||||
|
||||
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
|
||||
fn handleOpen(out: []u8, path: []const u8, flags: u32, sender: u32) usize {
|
||||
var node = filesystem.resolve(path);
|
||||
if (node == null and flags & protocol.create != 0) {
|
||||
const split = splitParent(path);
|
||||
@@ -146,13 +165,12 @@ fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
|
||||
filesystem.truncate(&resolved);
|
||||
}
|
||||
const index = allocOpen() orelse return fail(out);
|
||||
open_nodes[index] = .{ .used = true, .node = resolved };
|
||||
open_nodes[index] = .{ .used = true, .node = resolved, .owner = sender };
|
||||
return writeReply(out, .{ .status = 0, .node = index }, &.{});
|
||||
}
|
||||
|
||||
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = capability;
|
||||
_ = sender;
|
||||
if (message.len < protocol.request_size) return fail(out);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
const payload = message[protocol.request_size..];
|
||||
@@ -162,7 +180,7 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
|
||||
filesystem.current_time_epoch = runtime.system.wallClock();
|
||||
|
||||
switch (request.operation) {
|
||||
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
|
||||
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags, sender),
|
||||
.read => {
|
||||
const o = openAt(request.node) orelse return fail(out);
|
||||
var buffer: [protocol.maximum_payload]u8 = undefined;
|
||||
@@ -208,7 +226,9 @@ fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.i
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
.mkdir => {
|
||||
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
||||
const path = payload[0..@min(payload.len, request.len)];
|
||||
if (filesystem.resolve(path) != null) return fail(out); // already exists — no duplicate entries
|
||||
const split = splitParent(path);
|
||||
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
||||
if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
@@ -240,5 +260,6 @@ pub fn main() void {
|
||||
.service = .fat,
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,16 +22,22 @@ const runtime = @import("runtime");
|
||||
const power = runtime.power_protocol;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
/// 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
|
||||
/// manifest under /system/services instead of a hardcoded list.)
|
||||
const boot_services = [_][]const u8{ "vfs", "input", "device-manager", "fat", "display", "display-demo" };
|
||||
/// The system services init brings up at boot, in order, by binary path. 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 manifest under /system/services instead of a
|
||||
/// hardcoded list.)
|
||||
const boot_services = [_][]const u8{
|
||||
"/system/services/input",
|
||||
"/system/services/device-manager",
|
||||
"/system/services/fat",
|
||||
"/system/services/display",
|
||||
"/system/services/display-demo",
|
||||
// Last: at shutdown children stop in reverse order, so the logger goes down
|
||||
// FIRST — its final drain still has the fat server (and the whole storage
|
||||
// chain) alive underneath it.
|
||||
"/system/services/logger",
|
||||
};
|
||||
|
||||
/// The live process id of each boot service (0 = not running), indexed by its position
|
||||
/// in `boot_services`, plus how many times init has restarted it. init supervises these:
|
||||
@@ -78,14 +84,6 @@ pub fn main() void {
|
||||
if (runtime.system.spawnSupervised(service, &.{}, supervision_endpoint)) |id| child_ids[i] = id;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -137,26 +135,21 @@ fn restartChild(id: u32) void {
|
||||
// An unknown reason (the record aged out) is treated as a crash worth restarting.
|
||||
const reason = runtime.process.exitReason(id) orelse .fault;
|
||||
if (reason == .exited) {
|
||||
logLine("/system/services/init: {s} exited cleanly; not restarting\n", .{service});
|
||||
std.log.info("{s} exited cleanly; not restarting", .{service});
|
||||
return;
|
||||
}
|
||||
restart_counts[i] += 1;
|
||||
if (restart_counts[i] > maximum_restarts) {
|
||||
logLine("/system/services/init: {s} keeps crashing; giving up after {d} restarts\n", .{ service, maximum_restarts });
|
||||
std.log.info("{s} keeps crashing; giving up after {d} restarts", .{ service, maximum_restarts });
|
||||
return;
|
||||
}
|
||||
logLine("/system/services/init: {s} died ({s}); restarting ({d}/{d})\n", .{ service, @tagName(reason), restart_counts[i], maximum_restarts });
|
||||
std.log.info("{s} died ({s}); restarting ({d}/{d})", .{ service, @tagName(reason), restart_counts[i], maximum_restarts });
|
||||
if (runtime.system.spawnSupervised(service, &.{}, supervision_endpoint)) |new_id| child_ids[i] = new_id;
|
||||
return;
|
||||
}
|
||||
// An untracked child (e.g. the log-flush one-shot): nothing to restart.
|
||||
}
|
||||
|
||||
fn logLine(comptime fmt: []const u8, args: anytype) void {
|
||||
var line: [128]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, args) catch return);
|
||||
}
|
||||
|
||||
/// Look up the power service and subscribe our endpoint (handed over as the
|
||||
/// call's capability) so events arrive as buffered messages here.
|
||||
fn subscribePower() void {
|
||||
@@ -175,26 +168,6 @@ fn subscribePower() void {
|
||||
_ = runtime.ipc.callCap(h, std.mem.asBytes(&request), &reply, supervision_endpoint) catch {};
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// Truncate on open so this fuller flush replaces the boot-time one cleanly.
|
||||
var file = runtime.fs.open(log_path, .{ .create = true, .truncate = true }) orelse return; // no USB volume
|
||||
defer file.close();
|
||||
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 (file.writeAll(chunk[0..got]) == null) 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
|
||||
@@ -202,10 +175,9 @@ fn flushKernelLog() void {
|
||||
fn shutDown() void {
|
||||
shutting_down = true; // the stop loop below kills children — those deaths aren't crashes
|
||||
_ = 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 first, so /mnt/usb must be
|
||||
// written while it is still mounted.
|
||||
flushKernelLog();
|
||||
// Log persistence is the logger service's job: it is the LAST boot service,
|
||||
// so the reverse-order stop below terminates it first and its final drain
|
||||
// runs while the whole storage chain is still alive.
|
||||
var i = boot_services.len;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//! 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 fs = runtime.fs;
|
||||
|
||||
const log_path = "/mnt/usb/DANOS.LOG";
|
||||
|
||||
/// Copy the whole kernel log to the open file, looping klog_read -> write until
|
||||
/// the log is exhausted. Returns the number of bytes written.
|
||||
fn drainKernelLog(file: *fs.File) 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 (file.writeAll(chunk[0..got]) == null) 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 ready = false;
|
||||
var tries: u32 = 0;
|
||||
while (tries < 1400) : (tries += 1) {
|
||||
if (fs.openDirectory("/mnt/usb")) |directory| {
|
||||
var dir = directory;
|
||||
dir.close();
|
||||
ready = true;
|
||||
break;
|
||||
}
|
||||
runtime.system.sleep(50);
|
||||
}
|
||||
if (!ready) return; // /mnt/usb never became available — nothing to persist to
|
||||
|
||||
// Truncate on open: each flush replaces the file, so a shorter log on a later
|
||||
// boot of the same stick leaves no stale tail from a previous, longer one.
|
||||
var file = fs.open(log_path, .{ .create = true, .truncate = true }) orelse return;
|
||||
const written = drainKernelLog(&file);
|
||||
file.close();
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
//! The logger service — the per-process log persister.
|
||||
//!
|
||||
//! Drains the tagged kernel log ring (`klog_read`/`klog_status`) and
|
||||
//! demultiplexes it into **one file per process** on the flash volume:
|
||||
//!
|
||||
//! <base>/<boot-stamp>/<binary-path>.log
|
||||
//! e.g. /mnt/usb/var/log/2026-07-21T101530Z/system/services/fat.log
|
||||
//!
|
||||
//! The boot stamp is the wall-clock time of boot (from klog_status), so one
|
||||
//! boot session is one self-contained directory; the kernel's own records go to
|
||||
//! kernel.log. Records carry the sender's pid and binary path, stamped by the
|
||||
//! kernel — the logger trusts the ring, never the payload.
|
||||
//!
|
||||
//! Storage is best-effort and late: until the FAT volume mounts, the ring
|
||||
//! simply buffers (it holds a full boot many times over), and the first drain
|
||||
//! writes the whole backlog. The storage stack's own records are captured the
|
||||
//! same way — services never write their own log files (the fat service
|
||||
//! logging through itself would rendezvous-deadlock; the ring sidesteps that
|
||||
//! by design).
|
||||
//!
|
||||
//! The logger announces itself ONCE (a periodic status line would feed the
|
||||
//! very stream it drains — self-sustaining churn). Lost records surface as an
|
||||
//! explicit "-- N records lost --" line derived from sequence-number gaps.
|
||||
//!
|
||||
//! Durability: files are opened create-once and kept open across a burst, then
|
||||
//! all closed after a quiet period (~2 s) — each close is the fat server's
|
||||
//! SCSI SYNCHRONIZE CACHE, so data-at-risk is bounded by the last busy burst
|
||||
//! without thrashing the device on every record. `on_terminate` does a final
|
||||
//! drain and closes everything, so an orderly shutdown loses nothing (init
|
||||
//! stops the logger FIRST — reverse boot order — while fat is still up).
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
|
||||
const system = runtime.system;
|
||||
const fs = runtime.fs;
|
||||
|
||||
/// Where log trees live: the FHS path. The kernel VFS routes /var to whatever
|
||||
/// volume the fat server mounted there (today: the /var subtree of the USB
|
||||
/// flash volume) — swapping the persistent medium later touches fat's two
|
||||
/// mount calls, never this constant.
|
||||
const base = "/var/log";
|
||||
|
||||
/// Drain cadence and the quiet period after which files are closed (flushed).
|
||||
const tick_ms = 250;
|
||||
const quiet_close_ticks = 8; // 8 * 250 ms = 2 s
|
||||
|
||||
/// One cached open file per source process path. Sized above the practical
|
||||
/// process count; the fat server's global open-node table (32) is the real
|
||||
/// ceiling, so stay comfortably below it.
|
||||
const maximum_files = 24;
|
||||
|
||||
const CachedFile = struct {
|
||||
used: bool = false,
|
||||
name: [system.maximum_process_name]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
file: fs.File = undefined,
|
||||
};
|
||||
|
||||
var files: [maximum_files]CachedFile = @splat(.{});
|
||||
var endpoint: runtime.ipc.Handle = 0;
|
||||
|
||||
/// The drain cursor into the ring's byte stream, and loss accounting.
|
||||
var cursor: u64 = 0;
|
||||
var next_expected_sequence: u64 = 0;
|
||||
|
||||
/// Carry buffer: a record can straddle two klog_read chunks.
|
||||
var carry: [carry_capacity]u8 = undefined;
|
||||
var carry_len: usize = 0;
|
||||
const carry_capacity = 64 + 256 + 64; // header + payload + name, padded generously
|
||||
|
||||
/// The per-boot directory, formatted once storage appears.
|
||||
var boot_directory: [base.len + 1 + 19]u8 = undefined;
|
||||
var boot_directory_len: usize = 0;
|
||||
var storage_ready = false;
|
||||
var announced = false;
|
||||
var ticks_since_record: u32 = 0;
|
||||
|
||||
pub fn main() void {
|
||||
runtime.service.run(64, .{
|
||||
.init = initialise,
|
||||
.on_message = onMessage,
|
||||
.on_notification = onNotification,
|
||||
.on_terminate = onTerminate,
|
||||
});
|
||||
}
|
||||
|
||||
fn initialise(harness_endpoint: runtime.ipc.Handle) bool {
|
||||
endpoint = harness_endpoint;
|
||||
const status = system.klogStatus() orelse return false;
|
||||
cursor = status.tail;
|
||||
// Sequence expectations start at the tail record's sequence — discovered on
|
||||
// the first drain; 0 is right for a fresh boot either way.
|
||||
formatBootDirectory(status.boot_unix_seconds);
|
||||
_ = system.timerOnce(endpoint, tick_ms);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The logger serves no protocol; the ping is answered by the harness.
|
||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||
_ = message;
|
||||
_ = reply;
|
||||
_ = sender;
|
||||
_ = capability;
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & runtime.ipc.notify_timer_bit == 0) return;
|
||||
tick();
|
||||
_ = system.timerOnce(endpoint, tick_ms);
|
||||
}
|
||||
|
||||
fn onTerminate() void {
|
||||
// Final drain: everything still in the ring, then close (= flush) all files.
|
||||
drain();
|
||||
closeAll();
|
||||
var line: [96]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, "logger: flushed through sequence {d}\n", .{next_expected_sequence}) catch return);
|
||||
}
|
||||
|
||||
fn tick() void {
|
||||
if (!storage_ready) {
|
||||
// makePath doubles as the readiness probe: while /var is unmounted the
|
||||
// resolve fails fast (no storage round trip) and the ring buffers; the
|
||||
// first success creates the whole per-boot tree.
|
||||
if (!fs.makePath(boot_directory[0..boot_directory_len])) return;
|
||||
storage_ready = true;
|
||||
if (!announced) {
|
||||
announced = true; // once — a periodic line would feed the stream we drain
|
||||
var line: [128]u8 = undefined;
|
||||
_ = system.write(std.fmt.bufPrint(&line, "logger: logging to {s}\n", .{boot_directory[0..boot_directory_len]}) catch "");
|
||||
}
|
||||
}
|
||||
drain();
|
||||
// Quiet-period close: one device cache flush per burst.
|
||||
ticks_since_record += 1;
|
||||
if (ticks_since_record == quiet_close_ticks) closeAll();
|
||||
}
|
||||
|
||||
fn drain() void {
|
||||
if (!storage_ready) return;
|
||||
var chunk: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
@memcpy(chunk[0..carry_len], carry[0..carry_len]);
|
||||
const got = system.klogRead(cursor, chunk[carry_len..]) orelse {
|
||||
// Cursor overwritten: re-sync to the ring tail; the sequence gap is
|
||||
// reported by the next record's header.
|
||||
const status = system.klogStatus() orelse return;
|
||||
cursor = status.tail;
|
||||
carry_len = 0;
|
||||
continue;
|
||||
};
|
||||
if (got == 0) return; // caught up (any partial record stays carried)
|
||||
cursor += got;
|
||||
consume(chunk[0 .. carry_len + got]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse whole records out of `bytes`; keep any trailing partial in `carry`.
|
||||
fn consume(bytes: []u8) void {
|
||||
const header_size = system.klog_record_header_size;
|
||||
var offset: usize = 0;
|
||||
while (bytes.len - offset >= header_size) {
|
||||
const header = std.mem.bytesToValue(system.KlogRecordHeader, bytes[offset..][0..32]);
|
||||
if (header.magic != system.klog_record_magic) {
|
||||
// Corrupt frame — should not happen; drop the carry and re-sync.
|
||||
carry_len = 0;
|
||||
const status = system.klogStatus() orelse return;
|
||||
cursor = status.head;
|
||||
return;
|
||||
}
|
||||
const record_len = recordLength(header);
|
||||
if (bytes.len - offset < record_len) break; // partial — carry it
|
||||
const name = bytes[offset + header_size ..][0..header.name_len];
|
||||
const message = bytes[offset + header_size + header.name_len ..][0..header.message_len];
|
||||
deliver(header, name, message);
|
||||
offset += record_len;
|
||||
}
|
||||
const rest = bytes.len - offset;
|
||||
if (rest > carry_capacity) {
|
||||
carry_len = 0; // cannot happen with sane frames; drop rather than overflow
|
||||
return;
|
||||
}
|
||||
@memcpy(carry[0..rest], bytes[offset..]);
|
||||
carry_len = rest;
|
||||
}
|
||||
|
||||
fn deliver(header: system.KlogRecordHeader, name: []const u8, message: []const u8) void {
|
||||
ticks_since_record = 0;
|
||||
const file = fileFor(if (header.pid == 0 or name.len == 0) "kernel" else name) orelse return;
|
||||
|
||||
if (header.sequence != next_expected_sequence and next_expected_sequence != 0) {
|
||||
var gap_line: [64]u8 = undefined;
|
||||
const lost = header.sequence - next_expected_sequence;
|
||||
if (std.fmt.bufPrint(&gap_line, "-- {d} records lost --\n", .{lost})) |line| {
|
||||
_ = file.writeAll(line);
|
||||
} else |_| {}
|
||||
}
|
||||
next_expected_sequence = header.sequence + 1;
|
||||
|
||||
// [+ssssss.mmm] level: payload
|
||||
var stamp: [48]u8 = undefined;
|
||||
const seconds = header.timestamp_ns / 1_000_000_000;
|
||||
const millis = (header.timestamp_ns / 1_000_000) % 1000;
|
||||
const level: []const u8 = switch (header.level) {
|
||||
.err => "error: ",
|
||||
.warn => "warning: ",
|
||||
.debug => "debug: ",
|
||||
.info, .raw => "",
|
||||
};
|
||||
if (std.fmt.bufPrint(&stamp, "[{d:>6}.{d:0>3}] {s}", .{ seconds, millis, level })) |prefix| {
|
||||
_ = file.writeAll(prefix);
|
||||
} else |_| {}
|
||||
_ = file.writeAll(message);
|
||||
if (header.flags & system.klog_flag_truncated != 0) _ = file.writeAll("~");
|
||||
_ = file.writeAll("\n");
|
||||
}
|
||||
|
||||
/// The cached (or freshly opened) file for a source name. The file path is the
|
||||
/// binary path with its leading '/' stripped, ".log" appended, under the
|
||||
/// per-boot directory; parents are created on first use.
|
||||
fn fileFor(name: []const u8) ?*fs.File {
|
||||
for (&files) |*cached| {
|
||||
if (cached.used and std.mem.eql(u8, cached.name[0..cached.name_len], name)) return &cached.file;
|
||||
}
|
||||
var slot: ?*CachedFile = null;
|
||||
for (&files) |*cached| {
|
||||
if (!cached.used) {
|
||||
slot = cached;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const cached = slot orelse evictOne() orelse return null;
|
||||
|
||||
var path: [base.len + 1 + 19 + 1 + system.maximum_process_name + 4]u8 = undefined;
|
||||
const relative = if (name.len != 0 and name[0] == '/') name[1..] else name;
|
||||
const full = std.fmt.bufPrint(&path, "{s}/{s}.log", .{ boot_directory[0..boot_directory_len], relative }) catch return null;
|
||||
|
||||
// Parent directories: everything up to the final slash.
|
||||
if (std.mem.lastIndexOfScalar(u8, full, '/')) |last| {
|
||||
if (!fs.makePath(full[0..last])) return null;
|
||||
}
|
||||
var file = fs.open(full, .{ .create = true }) orelse return null;
|
||||
// Append: land after whatever an earlier open of this boot wrote.
|
||||
if (file.attributes()) |attributes| file.seekTo(attributes.size);
|
||||
|
||||
cached.* = .{ .used = true, .file = file };
|
||||
@memcpy(cached.name[0..name.len], name);
|
||||
cached.name_len = name.len;
|
||||
return &cached.file;
|
||||
}
|
||||
|
||||
fn evictOne() ?*CachedFile {
|
||||
// All slots busy: close the first (oldest-created) and reuse it. Simple and
|
||||
// rare — the process count sits well under the cache size.
|
||||
for (&files) |*cached| {
|
||||
if (cached.used) {
|
||||
cached.file.close();
|
||||
cached.used = false;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn closeAll() void {
|
||||
for (&files) |*cached| {
|
||||
if (cached.used) {
|
||||
cached.file.close();
|
||||
cached.used = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recordLength(header: system.KlogRecordHeader) usize {
|
||||
return std.mem.alignForward(usize, system.klog_record_header_size + header.name_len + header.message_len, system.klog_record_alignment);
|
||||
}
|
||||
|
||||
/// Format the per-boot directory "<base>/YYYY-MM-DDTHHMMSSZ" from the boot
|
||||
/// wall-clock anchor. No colons — FAT names cannot carry them. A dead RTC
|
||||
/// (anchor 0) yields the 1970 epoch directory, which is still a valid,
|
||||
/// distinct-per-boot-rarely name and better than refusing to log.
|
||||
fn formatBootDirectory(boot_unix_seconds: u64) void {
|
||||
const epoch_seconds = std.time.epoch.EpochSeconds{ .secs = boot_unix_seconds };
|
||||
const year_day = epoch_seconds.getEpochDay().calculateYearDay();
|
||||
const month_day = year_day.calculateMonthDay();
|
||||
const day_seconds = epoch_seconds.getDaySeconds();
|
||||
const written = std.fmt.bufPrint(&boot_directory, "{s}/{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}{d:0>2}{d:0>2}Z", .{
|
||||
base,
|
||||
year_day.year,
|
||||
month_day.month.numeric(),
|
||||
@as(u32, month_day.day_index) + 1,
|
||||
day_seconds.getHoursIntoDay(),
|
||||
day_seconds.getMinutesIntoHour(),
|
||||
day_seconds.getSecondsIntoMinute(),
|
||||
}) catch return;
|
||||
boot_directory_len = written.len;
|
||||
}
|
||||
@@ -147,8 +147,8 @@ pub fn main(init: runtime.process.Init) void {
|
||||
const spinner = runtime.system.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner");
|
||||
|
||||
runtime.system.sleep(100); // let the sleeper block and the spinner get a core
|
||||
if (!listed(sleeper, "process-test")) fail("sleeper not in process_enumerate");
|
||||
if (!listed(spinner, "process-test")) fail("spinner not in process_enumerate");
|
||||
if (!listed(sleeper, "/system/tests/process-test")) fail("sleeper not in process_enumerate");
|
||||
if (!listed(spinner, "/system/tests/process-test")) fail("spinner not in process_enumerate");
|
||||
|
||||
// Kills that must be refused: a kernel task (id 0), and an id that was never
|
||||
// issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level
|
||||
@@ -167,8 +167,8 @@ pub fn main(init: runtime.process.Init) void {
|
||||
if (!runtime.system.kill(spinner)) fail("kill spinner");
|
||||
if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification");
|
||||
|
||||
if (listed(sleeper, "process-test")) fail("sleeper still listed after kill");
|
||||
if (listed(spinner, "process-test")) fail("spinner still listed after kill");
|
||||
if (listed(sleeper, "/system/tests/process-test")) fail("sleeper still listed after kill");
|
||||
if (listed(spinner, "/system/tests/process-test")) fail("spinner still listed after kill");
|
||||
|
||||
// M17.2: both children were killed by us, and the reason says so — the whole
|
||||
// restart-policy input, read through the runtime like a real supervisor would.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//! /system/tests/vfs-test — a ring-3 client that proves the kernel VFS end to
|
||||
//! end through the plain `runtime.fs` API: resolve its OWN binary under the
|
||||
//! kernel-served /system mount, check its metadata, read its ELF magic, and
|
||||
//! list /system/services. On success it heartbeats "vfstest: ok" so the kernel
|
||||
//! test can observe it; on failure it reports what went wrong.
|
||||
//!
|
||||
//! The "park" role (the fat-client-death test): open a file on the FAT volume,
|
||||
//! then hold the handle forever without closing — the kill and the fat
|
||||
//! server's release-on-death sweep are the point.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const fs = runtime.fs;
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
if (init.arguments.count > 1) {
|
||||
park();
|
||||
return;
|
||||
}
|
||||
|
||||
// Our own binary, resolved through the kernel mount table.
|
||||
const self_path = "/system/tests/vfs-test";
|
||||
var file = fs.open(self_path, .{}) orelse {
|
||||
_ = runtime.system.write("vfstest: open of own binary failed\n");
|
||||
return;
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
const attributes = file.attributes() orelse {
|
||||
_ = runtime.system.write("vfstest: attributes failed\n");
|
||||
return;
|
||||
};
|
||||
if (attributes.kind != .regular or attributes.size == 0) {
|
||||
_ = runtime.system.write("vfstest: bad attributes\n");
|
||||
return;
|
||||
}
|
||||
|
||||
var header: [4]u8 = undefined;
|
||||
const n = file.read(&header) orelse 0;
|
||||
if (n != 4 or header[0] != 0x7f or header[1] != 'E' or header[2] != 'L' or header[3] != 'F') {
|
||||
_ = runtime.system.write("vfstest: ELF magic mismatch\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// The write refusal: /system is read-only by construction.
|
||||
if (file.write("x") != null or fs.open("/system/tests/new-file", .{ .create = true }) != null) {
|
||||
_ = runtime.system.write("vfstest: /system accepted a write\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Listing: /system/services contains init.
|
||||
var saw_init = false;
|
||||
if (fs.openDirectory("/system/services")) |listing| {
|
||||
var directory = listing;
|
||||
defer directory.close();
|
||||
var entry: fs.Entry = .{};
|
||||
while (directory.next(&entry)) {
|
||||
if (std.mem.eql(u8, entry.name(), "init")) saw_init = true;
|
||||
}
|
||||
}
|
||||
if (!saw_init) {
|
||||
_ = runtime.system.write("vfstest: /system/services listing missed init\n");
|
||||
return;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
_ = runtime.system.write("vfstest: ok\n");
|
||||
runtime.system.sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
fn park() void {
|
||||
// The storage chain (usb -> block -> fat -> mounts) takes a few seconds;
|
||||
// retry until the volume appears.
|
||||
var parked: ?fs.File = null;
|
||||
var tries: u32 = 0;
|
||||
while (parked == null and tries < 1000) : (tries += 1) {
|
||||
parked = fs.open("/mnt/usb/parked", .{ .create = true });
|
||||
if (parked == null) runtime.system.sleep(20);
|
||||
}
|
||||
if (parked == null) {
|
||||
_ = runtime.system.write("vfstest: park open failed\n");
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
_ = runtime.system.write("vfstest: parked\n");
|
||||
runtime.system.sleep(500);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
//! Pure path utilities for the VFS mount router — no IPC, no state, so they are
|
||||
//! host-testable in isolation. The router uses these to decide whether an opened
|
||||
//! path lies under a mount point and, if so, what it looks like relative to that
|
||||
//! mount.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by a
|
||||
/// path separator — return the path relative to the mount ("/" for an exact
|
||||
/// match, otherwise the tail beginning with '/'). Returns null when `path` is not
|
||||
/// under the mount, so a prefix like "/mnt/usb" never captures "/mnt/usbextra".
|
||||
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
|
||||
if (path.len < mount_prefix.len) return null;
|
||||
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
|
||||
if (path.len == mount_prefix.len) return "/";
|
||||
if (path[mount_prefix.len] != '/') return null;
|
||||
return path[mount_prefix.len..];
|
||||
}
|
||||
|
||||
/// Whether `path` is absolute (rooted at '/'). Bare names — what the flat ramfs
|
||||
/// uses — are relative and never route through a mount.
|
||||
pub fn isAbsolute(path: []const u8) bool {
|
||||
return path.len > 0 and path[0] == '/';
|
||||
}
|
||||
|
||||
test "underMount matches only at path boundaries" {
|
||||
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
|
||||
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
|
||||
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null); // not a boundary
|
||||
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null); // shorter than the prefix
|
||||
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
|
||||
try std.testing.expect(underMount("greeting", "/mnt/usb") == null); // a bare name
|
||||
}
|
||||
|
||||
test "isAbsolute distinguishes paths from bare names" {
|
||||
try std.testing.expect(isAbsolute("/mnt/usb"));
|
||||
try std.testing.expect(!isAbsolute("greeting"));
|
||||
try std.testing.expect(!isAbsolute(""));
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
//! /system/services/vfs/vfs-test — a client that proves the VFS round trip end to end: open a
|
||||
//! file through the `runtime.fs` file API, write to it, seek back, read it, and compare.
|
||||
//! On success it heartbeats "vfstest: ok" so the kernel test can observe it;
|
||||
//! on failure it reports what went wrong. Shipped in the initial_ramdisk alongside vfs.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const fs = runtime.fs;
|
||||
|
||||
pub fn main(init: runtime.process.Init) void {
|
||||
const payload = "hello-vfs";
|
||||
|
||||
// The "park" role (the vfs-client-death test): open a file, then hold the
|
||||
// handle forever without closing — the kill and the VFS's release-on-death
|
||||
// are the point.
|
||||
if (init.arguments.count > 1) {
|
||||
var parked: ?fs.File = null;
|
||||
var tries: u32 = 0;
|
||||
while (parked == null and tries < 200) : (tries += 1) {
|
||||
parked = fs.open("parked", .{ .create = true });
|
||||
if (parked == null) runtime.system.sleep(20);
|
||||
}
|
||||
if (parked == null) {
|
||||
_ = runtime.system.write("vfstest: park open failed\n");
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
_ = runtime.system.write("vfstest: parked\n");
|
||||
runtime.system.sleep(500);
|
||||
}
|
||||
}
|
||||
|
||||
// The VFS server may not have registered yet — retry open until it's up.
|
||||
var opened: ?fs.File = null;
|
||||
var tries: u32 = 0;
|
||||
while (opened == null and tries < 200) : (tries += 1) {
|
||||
opened = fs.open("greeting", .{ .create = true });
|
||||
if (opened == null) runtime.system.sleep(20);
|
||||
}
|
||||
var greeting = opened orelse {
|
||||
_ = runtime.system.write("vfstest: open failed\n");
|
||||
return;
|
||||
};
|
||||
|
||||
if ((greeting.write(payload) orelse 0) != payload.len) {
|
||||
_ = runtime.system.write("vfstest: write failed\n");
|
||||
return;
|
||||
}
|
||||
greeting.seekTo(0);
|
||||
|
||||
var buffer: [32]u8 = undefined;
|
||||
const n = greeting.read(&buffer) orelse 0;
|
||||
greeting.close();
|
||||
|
||||
if (n == payload.len and std.mem.eql(u8, buffer[0..n], payload)) {
|
||||
while (true) {
|
||||
_ = runtime.system.write("vfstest: ok\n");
|
||||
runtime.system.sleep(1000);
|
||||
}
|
||||
}
|
||||
_ = runtime.system.write("vfstest: mismatch\n");
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
//! system/services/vfs — the user-space VFS server. Shipped in the initial_ramdisk, spawned as a
|
||||
//! ring-3 process, and reached by every other process through IPC (the `runtime`
|
||||
//! file API marshals open/read/write/stat/close into calls to this server's
|
||||
//! endpoint, published under the well-known `vfs` service id).
|
||||
//!
|
||||
//! Two namespaces meet here (M5):
|
||||
//! - a small in-memory **ramfs** — opening a bare name creates it — enough to
|
||||
//! prove the round trip and to back the existing tests;
|
||||
//! - **mounted filesystems**: a mount table maps an absolute path prefix (e.g.
|
||||
//! `/mnt/usb`) to a backend server's endpoint. An open of a path under a mount
|
||||
//! is *forwarded* to that backend (which speaks this same protocol), and every
|
||||
//! later read/write/status/readdir/close on the resulting handle is relayed to
|
||||
//! it. The VFS is the router; a filesystem (FAT) is the backend.
|
||||
//!
|
||||
//! A path routes through a mount only when it is absolute and lies under a mount
|
||||
//! prefix; bare names always resolve in the flat ramfs — the backward-compat
|
||||
//! contract the `vfs` / `vfs-client-death` tests rely on.
|
||||
|
||||
const std = @import("std");
|
||||
const runtime = @import("runtime");
|
||||
const protocol = runtime.vfs_protocol;
|
||||
const path = @import("path.zig");
|
||||
const ipc = runtime.ipc;
|
||||
|
||||
const Node = struct {
|
||||
used: bool = false,
|
||||
name: [24]u8 = undefined,
|
||||
name_len: usize = 0,
|
||||
data: [512]u8 = undefined,
|
||||
size: usize = 0,
|
||||
};
|
||||
|
||||
const OpenFile = struct {
|
||||
used: bool = false,
|
||||
// For a local handle: an index into `nodes`. For a forwarding handle: the
|
||||
// node id the backend returned. (usize == u64 here, so it holds either.)
|
||||
node: usize = 0,
|
||||
// Non-null for a handle that forwards to a mounted backend.
|
||||
backend: ?ipc.Handle = null,
|
||||
// The client (task id — an IPC badge is one) that opened this handle. What
|
||||
// release-on-death sweeps by: a service must never depend on its clients
|
||||
// cleaning up after themselves (docs/process-lifecycle.md).
|
||||
owner: u32 = 0,
|
||||
};
|
||||
|
||||
// One mounted filesystem: an absolute path prefix and the backend endpoint that
|
||||
// serves everything under it.
|
||||
const Mount = struct {
|
||||
used: bool = false,
|
||||
prefix: [64]u8 = undefined,
|
||||
prefix_len: usize = 0,
|
||||
backend: ipc.Handle = 0,
|
||||
};
|
||||
|
||||
var nodes = [_]Node{.{}} ** 8;
|
||||
var opens = [_]OpenFile{.{}} ** 16;
|
||||
var mounts = [_]Mount{.{}} ** 8;
|
||||
|
||||
fn findNode(name: []const u8) ?usize {
|
||||
for (&nodes, 0..) |*n, i| {
|
||||
if (n.used and std.mem.eql(u8, n.name[0..n.name_len], name)) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn createNode(name: []const u8) ?usize {
|
||||
for (&nodes, 0..) |*n, i| {
|
||||
if (!n.used) {
|
||||
const l = @min(name.len, n.name.len);
|
||||
@memcpy(n.name[0..l], name[0..l]);
|
||||
n.* = .{ .used = true, .name = n.name, .name_len = l, .size = 0 };
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn openAt(id: u64) ?*OpenFile {
|
||||
if (id >= opens.len) return null;
|
||||
const o = &opens[@intCast(id)];
|
||||
return if (o.used) o else null;
|
||||
}
|
||||
|
||||
/// The mount whose prefix most specifically contains `name`, and the path
|
||||
/// relative to it. Only absolute paths route; bare names never match.
|
||||
const MountMatch = struct { backend: ipc.Handle, relative: []const u8 };
|
||||
fn longestMount(name: []const u8) ?MountMatch {
|
||||
if (!path.isAbsolute(name)) return null;
|
||||
var best: ?MountMatch = null;
|
||||
var best_len: usize = 0;
|
||||
for (&mounts) |*m| {
|
||||
if (!m.used) continue;
|
||||
const prefix = m.prefix[0..m.prefix_len];
|
||||
if (path.underMount(name, prefix)) |relative| {
|
||||
if (best == null or prefix.len >= best_len) {
|
||||
best_len = prefix.len;
|
||||
best = .{ .backend = m.backend, .relative = relative };
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
/// Serialise a reply header + payload into `out`; returns the total length.
|
||||
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
|
||||
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
|
||||
const n = @min(payload.len, out.len - protocol.reply_size);
|
||||
@memcpy(out[protocol.reply_size..][0..n], payload[0..n]);
|
||||
return protocol.reply_size + n;
|
||||
}
|
||||
|
||||
fn fail(out: []u8) usize {
|
||||
return writeReply(out, .{ .status = -1 }, &.{});
|
||||
}
|
||||
|
||||
/// Format one whole log line and emit it in a single `debug_write`, so lines from
|
||||
/// concurrent processes can never land in the middle of it.
|
||||
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||
var line: [96]u8 = undefined;
|
||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||
}
|
||||
|
||||
// --- mount routing ----------------------------------------------------------
|
||||
|
||||
/// Forward an open under a mount to its backend and, on success, allocate a local
|
||||
/// forwarding handle that remembers the backend's node id.
|
||||
fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize {
|
||||
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
|
||||
@memcpy(message[protocol.request_size..][0..rel.len], rel);
|
||||
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
|
||||
if (n < protocol.reply_size) return fail(out);
|
||||
const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{});
|
||||
|
||||
for (&opens, 0..) |*o, i| {
|
||||
if (!o.used) {
|
||||
o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender };
|
||||
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
/// Relay a read/write/status/readdir/close on a forwarding handle to the backend
|
||||
/// (the node already rewritten to the backend's id) and copy its reply out.
|
||||
fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize {
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const plen = @min(payload.len, protocol.maximum_payload);
|
||||
@memcpy(message[protocol.request_size..][0..plen], payload[0..plen]);
|
||||
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out);
|
||||
const copy = @min(n, out.len);
|
||||
@memcpy(out[0..copy], reply[0..copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Forward a path-based operation (mkdir, unlink) under a mount to its backend and
|
||||
/// relay the reply. No handle is created — these operate by path and return only a
|
||||
/// status.
|
||||
fn forwardPath(out: []u8, backend: ipc.Handle, operation: protocol.Operation, relative: []const u8) usize {
|
||||
const request = protocol.Request{ .operation = operation, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = 0 };
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
|
||||
@memcpy(message[protocol.request_size..][0..rel.len], rel);
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
|
||||
const copy = @min(n, out.len);
|
||||
@memcpy(out[0..copy], reply[0..copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Forward a rename to its backend: the payload is the mount-relative old path, a
|
||||
/// 0x00 separator, then the mount-relative new path. Relays the backend's reply.
|
||||
fn forwardRename(out: []u8, backend: ipc.Handle, old_relative: []const u8, new_relative: []const u8) usize {
|
||||
const total = old_relative.len + 1 + new_relative.len;
|
||||
var message: [protocol.message_maximum]u8 = undefined;
|
||||
if (protocol.request_size + total > message.len) return fail(out);
|
||||
const request = protocol.Request{ .operation = .rename, .node = 0, .offset = 0, .len = @intCast(total), .flags = 0 };
|
||||
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||
var p = protocol.request_size;
|
||||
@memcpy(message[p..][0..old_relative.len], old_relative);
|
||||
p += old_relative.len;
|
||||
message[p] = 0;
|
||||
p += 1;
|
||||
@memcpy(message[p..][0..new_relative.len], new_relative);
|
||||
p += new_relative.len;
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
const n = ipc.call(backend, message[0..p], &reply) catch return fail(out);
|
||||
const copy = @min(n, out.len);
|
||||
@memcpy(out[0..copy], reply[0..copy]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
/// Best-effort close of a backend node (used when a dead client's forwarding
|
||||
/// handles are swept — the backend must not leak the vfs's opens).
|
||||
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
|
||||
const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 };
|
||||
var reply: [protocol.message_maximum]u8 = undefined;
|
||||
_ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {};
|
||||
}
|
||||
|
||||
fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize {
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||
m.backend = backend;
|
||||
writeLine("/system/services/vfs: remounted {s}\n", .{prefix});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
for (&mounts) |*m| {
|
||||
if (!m.used) {
|
||||
const l = @min(prefix.len, m.prefix.len);
|
||||
m.used = true;
|
||||
@memcpy(m.prefix[0..l], prefix[0..l]);
|
||||
m.prefix_len = l;
|
||||
m.backend = backend;
|
||||
writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
fn doUnmount(out: []u8, prefix: []const u8) usize {
|
||||
for (&mounts) |*m| {
|
||||
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||
m.used = false;
|
||||
writeLine("/system/services/vfs: unmounted {s}\n", .{prefix});
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
}
|
||||
|
||||
/// Release every open handle `client` held — called on that client's published
|
||||
/// exit event. Forwarding handles also tell their backend to release; local
|
||||
/// nodes (the ramfs files) stay, since ramfs contents outlive their writers.
|
||||
fn releaseClientHandles(client: u32) void {
|
||||
var released: u32 = 0;
|
||||
for (&opens) |*o| {
|
||||
if (o.used and o.owner == client) {
|
||||
if (o.backend) |backend| forwardClose(backend, o.node);
|
||||
o.used = false;
|
||||
released += 1;
|
||||
}
|
||||
}
|
||||
if (released != 0) writeLine("/system/services/vfs: released {d} handle(s) for dead client {d}\n", .{ released, client });
|
||||
}
|
||||
|
||||
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
||||
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize {
|
||||
if (message.len < protocol.request_size) return fail(out);
|
||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||
const payload = message[protocol.request_size..];
|
||||
|
||||
switch (request.operation) {
|
||||
.mount => {
|
||||
const prefix = payload[0..@min(payload.len, request.len)];
|
||||
const backend = capability orelse return fail(out);
|
||||
return doMount(out, prefix, backend);
|
||||
},
|
||||
.unmount => {
|
||||
const prefix = payload[0..@min(payload.len, request.len)];
|
||||
return doUnmount(out, prefix);
|
||||
},
|
||||
.open => {
|
||||
const name = payload[0..@min(payload.len, request.len)];
|
||||
if (longestMount(name)) |m| return forwardOpen(out, m.backend, m.relative, request.flags, sender);
|
||||
// An absolute path with no matching mount is simply not found — only
|
||||
// bare names live in the flat ramfs. (Else /mnt/usb would be silently
|
||||
// created as a flat file when its filesystem is not yet mounted.)
|
||||
if (path.isAbsolute(name)) return fail(out);
|
||||
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
|
||||
for (&opens, 0..) |*o, i| {
|
||||
if (!o.used) {
|
||||
o.* = .{ .used = true, .node = ni, .backend = null, .owner = sender };
|
||||
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||
}
|
||||
}
|
||||
return fail(out);
|
||||
},
|
||||
.read => {
|
||||
const of = openAt(request.node) orelse return fail(out);
|
||||
if (of.backend) |backend| {
|
||||
var forwarded = request;
|
||||
forwarded.node = of.node;
|
||||
return forwardRequest(out, backend, forwarded, payload);
|
||||
}
|
||||
const nd = &nodes[@intCast(of.node)];
|
||||
const off: usize = @intCast(request.offset);
|
||||
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
|
||||
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
|
||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]);
|
||||
},
|
||||
.write => {
|
||||
const of = openAt(request.node) orelse return fail(out);
|
||||
if (of.backend) |backend| {
|
||||
var forwarded = request;
|
||||
forwarded.node = of.node;
|
||||
return forwardRequest(out, backend, forwarded, payload);
|
||||
}
|
||||
const nd = &nodes[@intCast(of.node)];
|
||||
const off: usize = @intCast(request.offset);
|
||||
if (off > nd.data.len) return fail(out);
|
||||
const n = @min(@min(payload.len, request.len), nd.data.len - off);
|
||||
@memcpy(nd.data[off .. off + n], payload[0..n]);
|
||||
if (off + n > nd.size) nd.size = off + n;
|
||||
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
|
||||
},
|
||||
.status => {
|
||||
const of = openAt(request.node) orelse return fail(out);
|
||||
if (of.backend) |backend| {
|
||||
var forwarded = request;
|
||||
forwarded.node = of.node;
|
||||
return forwardRequest(out, backend, forwarded, payload);
|
||||
}
|
||||
const st = protocol.FileStatus{ .size = nodes[@intCast(of.node)].size, .kind = @intFromEnum(protocol.NodeKind.regular) };
|
||||
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st));
|
||||
},
|
||||
.readdir => {
|
||||
const of = openAt(request.node) orelse return fail(out);
|
||||
if (of.backend) |backend| {
|
||||
var forwarded = request;
|
||||
forwarded.node = of.node;
|
||||
return forwardRequest(out, backend, forwarded, payload);
|
||||
}
|
||||
// The flat ramfs has no directories: report EOF.
|
||||
return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
||||
},
|
||||
.close => {
|
||||
const of = openAt(request.node);
|
||||
if (of) |o| {
|
||||
if (o.backend) |backend| forwardClose(backend, o.node);
|
||||
o.used = false;
|
||||
}
|
||||
return writeReply(out, .{ .status = 0 }, &.{});
|
||||
},
|
||||
.mkdir, .unlink => {
|
||||
const name = payload[0..@min(payload.len, request.len)];
|
||||
if (longestMount(name)) |m| return forwardPath(out, m.backend, request.operation, m.relative);
|
||||
// Only a mounted backend has real directories; the flat ramfs cannot
|
||||
// create or remove them (and a bare-name path is not a mount target).
|
||||
return fail(out);
|
||||
},
|
||||
.rename => {
|
||||
const both = payload[0..@min(payload.len, request.len)];
|
||||
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
||||
const old_path = both[0..sep];
|
||||
const new_path = both[sep + 1 ..];
|
||||
const mo = longestMount(old_path) orelse return fail(out);
|
||||
const mn = longestMount(new_path) orelse return fail(out);
|
||||
// Both paths must live under the same mount — cross-filesystem rename is
|
||||
// not supported.
|
||||
if (mo.backend != mn.backend) return fail(out);
|
||||
return forwardRename(out, mo.backend, mo.relative, mn.relative);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup, under the harness: subscribe to the published exit events — when a
|
||||
/// client dies holding open handles, the exit notification is how the VFS learns
|
||||
/// to release them (docs/process-lifecycle.md).
|
||||
fn initialise(endpoint: ipc.Handle) bool {
|
||||
if (!runtime.process.subscribeExits(endpoint)) {
|
||||
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
|
||||
}
|
||||
_ = runtime.system.write("/system/services/vfs: ready\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
||||
fn onNotification(badge: u64) void {
|
||||
if (badge & ipc.notify_exit_bit != 0) {
|
||||
releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() void {
|
||||
// The harness owns the loop: requests dispatch to handle(), exit events to
|
||||
// onNotification(), ping and terminate are answered for free — this service
|
||||
// gained the whole lifecycle contract by deleting its hand-rolled loop.
|
||||
runtime.service.run(protocol.message_maximum, .{
|
||||
.service = .vfs,
|
||||
.init = initialise,
|
||||
.on_message = handle,
|
||||
.on_notification = onNotification,
|
||||
});
|
||||
}
|
||||
+22
-14
@@ -427,6 +427,7 @@ CASES = [
|
||||
# open handle, and the VFS releases it (process-lifecycle.md "Who learns of a death").
|
||||
{"name": "vfs-client-death",
|
||||
"smp": 4,
|
||||
"timeout": 90, # the park client waits out the whole USB->block->fat chain
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M17.4: signals over IPC — ping, reload, terminate (clean exit), the one-shot
|
||||
@@ -447,7 +448,7 @@ CASES = [
|
||||
r"device-manager: child added[\s\S]*"
|
||||
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||
r"device-manager: child removed[\s\S]*"
|
||||
r"device-manager: restarting usb-xhci-bus[\s\S]*"
|
||||
r"device-manager: restarting \S*usb-xhci-bus[\s\S]*"
|
||||
r"device-manager: child added",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# USB HID end to end: boot the full tree, enumerate the xHCI, and let the
|
||||
@@ -458,7 +459,7 @@ CASES = [
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
# usb-kbd/usb-mouse ride the default boot xHCI bus (see qemu_args).
|
||||
"expect": r"(?=[\s\S]*usb-hid/keyboard: ok)(?=[\s\S]*usb-hid/mouse: ok)",
|
||||
"expect": r"(?=[\s\S]*usb-hid-keyboard: ok)(?=[\s\S]*usb-hid-mouse: ok)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# USB mass storage end to end: the boot usb-storage device (the FAT32 image,
|
||||
# which has a real 0x55AA boot sector) is enough — the manager spawns
|
||||
@@ -526,8 +527,8 @@ CASES = [
|
||||
{"name": "acpi-ps2",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"acpi: reported PNP0303[\s\S]*"
|
||||
r"device-manager: spawned ps2-bus[\s\S]*"
|
||||
"expect": r"discovery: reported PNP0303[\s\S]*"
|
||||
r"device-manager: spawned \S*ps2-bus[\s\S]*"
|
||||
r"ps2-bus: keyboard driver attached",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M21.1: the SCI + power button. Boot the manager (which spawns the acpi
|
||||
@@ -553,17 +554,18 @@ CASES = [
|
||||
r"power: entering S5",
|
||||
"fail": r"power: S5 write did not take|DANOS-TEST-RESULT: FAIL"},
|
||||
# M8: the boot log is persisted to the USB FAT volume. Reuses the orderly-
|
||||
# shutdown build (full tree + power button): init spawns log-flush at boot,
|
||||
# which copies the kernel log to /mnt/usb/DANOS.LOG once /mnt/usb is mounted
|
||||
# (first marker); then the power button drives init's own pre-teardown flush
|
||||
# (second marker), proving both triggers write the file while storage is up.
|
||||
{"name": "log-flush",
|
||||
# shutdown build (full tree + power button): the logger service announces its
|
||||
# per-boot directory once storage mounts (first marker), then the power
|
||||
# button drives the orderly stop — the logger, stopped first, final-drains
|
||||
# and reports the flush (second marker) before S5.
|
||||
{"name": "logger",
|
||||
"build_case": "orderly-shutdown",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"qmp_after": {"delay": 8, "command": "system_powerdown"},
|
||||
"expect": r"log-flush: wrote \d+ bytes to /mnt/usb/DANOS\.LOG[\s\S]*"
|
||||
r"init: flushed log to /mnt/usb/DANOS\.LOG[\s\S]*"
|
||||
"expect": r"logger: logging to /var/log/\d{4}-\d{2}-\d{2}T\d{6}Z[\s\S]*"
|
||||
r"init: shutting down[\s\S]*"
|
||||
r"logger: flushed through sequence \d+[\s\S]*"
|
||||
r"power: entering S5",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M20.2: the acpi service evaluates _CRS/_STA in ring 3 and registers +
|
||||
@@ -572,8 +574,8 @@ CASES = [
|
||||
{"name": "acpi-report",
|
||||
"smp": 4,
|
||||
"timeout": 150,
|
||||
"expect": r"acpi: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*"
|
||||
r"acpi: reported PNP0F13 \(device \d+, 1 resources\)",
|
||||
"expect": r"discovery: reported PNP0303 \(device \d+, 3 resources\)[\s\S]*"
|
||||
r"discovery: reported PNP0F13 \(device \d+, 1 resources\)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# M19.1/M19.3: the ring-3 PCI scan. pci-bus walks the ECAM through its mmio_map
|
||||
# grant and registers every function it finds; the kernel's own walk retired, so
|
||||
@@ -589,7 +591,7 @@ CASES = [
|
||||
"timeout": 60,
|
||||
"expect": r"pci-bus: (\d+) functions found[\s\S]*"
|
||||
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||
r"device-manager: restarting pci-bus[\s\S]*"
|
||||
r"device-manager: restarting \S*pci-bus[\s\S]*"
|
||||
r"pci-bus: \1 functions found[\s\S]*"
|
||||
r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
@@ -623,6 +625,12 @@ CASES = [
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The user-space VFS: a client opens/writes/reads a file through the rt file
|
||||
# API, which IPCs the VFS server process; the round trip must match.
|
||||
# The kernel VFS root (M-F): the mount table serves the initrd at /system —
|
||||
# path resolution, node status/read (an ELF magic), and directory listing,
|
||||
# asserted kernel-side.
|
||||
{"name": "kvfs",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
{"name": "vfs",
|
||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
|
||||
@@ -179,14 +179,17 @@ def short_name_for(name, used):
|
||||
else:
|
||||
base, ext = name, ""
|
||||
upper_base, upper_ext = base.upper(), ext.upper()
|
||||
# A name fits 8.3 if it is short enough and uses valid characters; a lowercase
|
||||
# name is simply stored uppercased (FAT is case-insensitive, so the bootloader
|
||||
# and the danos driver still find it). Only genuinely non-8.3 names (too long,
|
||||
# e.g. initial-ramdisk.img) get a mangled short name plus LFN entries.
|
||||
# A name fits 8.3 if it is short enough and uses valid characters. The raw
|
||||
# 8.3 entry is always uppercase; if that loses the real name's case (e.g.
|
||||
# "init" -> "INIT"), a long-name chain carries the exact name. This matters
|
||||
# because the EFI loader *enumerates* /system to build the ramdisk — it gets
|
||||
# back whatever the directory stores, so the stored name must be exact, not
|
||||
# merely case-insensitively findable.
|
||||
fits = (1 <= len(base) <= 8 and len(ext) <= 3
|
||||
and all(c in VALID_83 for c in upper_base + upper_ext))
|
||||
if fits:
|
||||
return (upper_base.ljust(8) + upper_ext.ljust(3)).encode("ascii"), False
|
||||
exact = base == upper_base and ext == upper_ext
|
||||
return (upper_base.ljust(8) + upper_ext.ljust(3)).encode("ascii"), not exact
|
||||
# Mangle to STEM~N.EXT.
|
||||
stem = "".join(c for c in upper_base if c in VALID_83 and c != " ")[:6] or "FILE"
|
||||
index = 1
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build-time initial_ramdisk packer. Concatenates user binaries into one image the
|
||||
bootloader ferries to the kernel.
|
||||
|
||||
Usage: make-initial-ramdisk.py <out.img> [<name> <file>]...
|
||||
|
||||
Image layout (little-endian), mirroring src/user/proto/initial-ramdisk.zig:
|
||||
Header : magic u32 ("DNRD"=0x444E5244), count u32
|
||||
Entry*N : name [32]u8 (NUL-padded), offset u64, len u64
|
||||
blobs : each entry's file bytes at its offset
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
MAGIC = 0x444E5244
|
||||
HEADER = struct.Struct("<II") # magic, count
|
||||
ENTRY = struct.Struct("<32sQQ") # name[32], offset, len
|
||||
|
||||
|
||||
def main() -> int:
|
||||
out_path = sys.argv[1]
|
||||
rest = sys.argv[2:]
|
||||
if len(rest) % 2 != 0:
|
||||
sys.stderr.write("usage: make-initial-ramdisk.py <out.img> [<name> <file>]...\n")
|
||||
return 2
|
||||
items = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)]
|
||||
|
||||
table_end = HEADER.size + len(items) * ENTRY.size
|
||||
entries = b""
|
||||
blobs = []
|
||||
off = table_end
|
||||
for name, path in items:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
entries += ENTRY.pack(name.encode()[:31], off, len(data))
|
||||
blobs.append(data)
|
||||
off += len(data)
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(HEADER.pack(MAGIC, len(items)))
|
||||
f.write(entries)
|
||||
for b in blobs:
|
||||
f.write(b)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user