reorg: move test fixtures to test/system/services (source + boot volume)
The 11 QEMU-suite fixtures lived mixed into system/services/ with their binaries bundled at /system/tests/<name>. Now the repo path is the boot path, like every real service: test/system/services/<name>. fat-test moves out of the fat server's directory into its own; display-demo stays a boot service. - kernel VFS: setInitialRamdisk derives one read-only initrd mount per top-level tree named by the ramdisk entry paths (/system, /test), registers ancestors generically with self-parented roots, and refuses backend shadowing of any initrd tree - EFI loader: the fallback walk also enumerates \test (optional — a volume without fixtures still boots); manifest and capsule unchanged - path literals: vfs-test self-open + create probe, process-test process_enumerate matches, the args-echo argv[0] expectation; the kvfs case now covers the /test root end to end - docs: DFHS /test rows, tree diagrams, loader prose, and the location convention gain the third home; fixed the input-source link 100/100 QEMU cases pass.
This commit is contained in:
parent
b9cec7d1be
commit
f023f1cfd6
|
|
@ -52,7 +52,8 @@ zig build
|
||||||
Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume:
|
Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume:
|
||||||
the UEFI bootloader at `zig-out/EFI/BOOT/BOOTX64.efi`, the kernel at
|
the UEFI bootloader at `zig-out/EFI/BOOT/BOOTX64.efi`, the kernel at
|
||||||
`zig-out/system/kernel`, init at `zig-out/system/services/init`, drivers under
|
`zig-out/system/kernel`, init at `zig-out/system/services/init`, drivers under
|
||||||
`zig-out/system/drivers/`, and the initial-ramdisk at `zig-out/boot/`.
|
`zig-out/system/drivers/`, the test fixtures under `zig-out/test/system/services/`,
|
||||||
|
and the initial-ramdisk at `zig-out/boot/`.
|
||||||
|
|
||||||
## Release media
|
## Release media
|
||||||
|
|
||||||
|
|
|
||||||
36
boot/efi.zig
36
boot/efi.zig
|
|
@ -16,11 +16,12 @@ const MemoryMapSlice = uefi.tables.MemoryMapSlice;
|
||||||
/// The kernel image: /system/kernel.
|
/// The kernel image: /system/kernel.
|
||||||
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
|
const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel");
|
||||||
|
|
||||||
/// The user binaries: everything under /system except the kernel itself. The
|
/// The user binaries: everything under /system except the kernel itself, plus
|
||||||
/// loader walks this tree and packs it into the in-RAM initial_ramdisk image —
|
/// the test fixtures under /test. The loader walks both trees and packs them
|
||||||
/// the volume's file structure is the single source of truth (no packed image
|
/// into the in-RAM initial_ramdisk image — the volume's file structure is the
|
||||||
/// artifact on disk).
|
/// single source of truth (no packed image artifact on disk).
|
||||||
const system_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("system");
|
const system_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("system");
|
||||||
|
const test_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("test");
|
||||||
|
|
||||||
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
||||||
const page_size = 4096;
|
const page_size = 4096;
|
||||||
|
|
@ -364,16 +365,17 @@ fn handoff(cr3: u64, entry: usize, boot_information: *const BootInformation) nor
|
||||||
unreachable;
|
unreachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- the /system tree -> initial_ramdisk ------------------------------------
|
// --- the /system and /test trees -> initial_ramdisk --------------------------
|
||||||
|
|
||||||
/// Cap on bundled binaries. Generous: the tree carries ~30 today.
|
/// Cap on bundled binaries. Generous: the tree carries ~30 today.
|
||||||
const maximum_bundled = 64;
|
const maximum_bundled = 64;
|
||||||
|
|
||||||
/// How deep the walk goes below /system ("/system/services/x" is depth 1).
|
/// How deep the walk goes below a tree root ("/system/services/x" is depth 1,
|
||||||
|
/// "/test/system/services/x" is depth 2).
|
||||||
const maximum_tree_depth = 3;
|
const maximum_tree_depth = 3;
|
||||||
|
|
||||||
/// One binary discovered under /system: its FHS path (UTF-8, '/'-separated,
|
/// One binary discovered under a walked tree: its FHS path (UTF-8,
|
||||||
/// NUL-free) and its contents in a transient pool buffer.
|
/// '/'-separated, NUL-free) and its contents in a transient pool buffer.
|
||||||
const Bundled = struct {
|
const Bundled = struct {
|
||||||
path: [initial_ramdisk.maximum_name]u8,
|
path: [initial_ramdisk.maximum_name]u8,
|
||||||
path_len: usize,
|
path_len: usize,
|
||||||
|
|
@ -389,10 +391,10 @@ const Bundled = struct {
|
||||||
/// 1. /system/manifest (written by the build): each listed path is opened BY
|
/// 1. /system/manifest (written by the build): each listed path is opened BY
|
||||||
/// NAME — the case-insensitive lookup every firmware FAT driver gets
|
/// NAME — the case-insensitive lookup every firmware FAT driver gets
|
||||||
/// right, and the only file access the pre-tree loader ever used.
|
/// right, and the only file access the pre-tree loader ever used.
|
||||||
/// 2. No manifest: ENUMERATE the /system tree. Portable in principle, but
|
/// 2. No manifest: ENUMERATE the /system and /test trees. Portable in
|
||||||
/// firmware differs in what names enumeration returns (bare 8.3 entries
|
/// principle, but firmware differs in what names enumeration returns
|
||||||
/// come back uppercase on some drivers), so this is the fallback for
|
/// (bare 8.3 entries come back uppercase on some drivers), so this is
|
||||||
/// hand-assembled sticks, not the primary path.
|
/// the fallback for hand-assembled sticks, not the primary path.
|
||||||
fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||||
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
|
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
|
||||||
return error.NoLoadedImage;
|
return error.NoLoadedImage;
|
||||||
|
|
@ -426,6 +428,11 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat
|
||||||
const system_directory = try root.open(system_directory_name, .read, .{});
|
const system_directory = try root.open(system_directory_name, .read, .{});
|
||||||
defer _ = system_directory.close() catch {};
|
defer _ = system_directory.close() catch {};
|
||||||
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
|
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
|
||||||
|
// The /test tree is optional: a stick without fixtures still boots.
|
||||||
|
if (root.open(test_directory_name, .read, .{})) |test_directory| {
|
||||||
|
defer _ = test_directory.close() catch {};
|
||||||
|
try walkDirectory(bs, test_directory, "/test", 0, &list, &count);
|
||||||
|
} else |_| {}
|
||||||
}
|
}
|
||||||
if (count == 0) return error.NoBinaries;
|
if (count == 0) return error.NoBinaries;
|
||||||
|
|
||||||
|
|
@ -452,7 +459,7 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat
|
||||||
|
|
||||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||||
boot_information.initial_ramdisk_len = total;
|
boot_information.initial_ramdisk_len = total;
|
||||||
log("EFI: /system tree loaded, starting the kernel\r\n");
|
log("EFI: boot tree loaded, starting the kernel\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The boot capsule: the bundled binaries as one v2 initial_ramdisk image.
|
/// The boot capsule: the bundled binaries as one v2 initial_ramdisk image.
|
||||||
|
|
@ -522,7 +529,8 @@ fn loadByManifest(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, list
|
||||||
|
|
||||||
/// Recursively collect the regular files below `directory` into `list`. Top-level
|
/// Recursively collect the regular files below `directory` into `list`. Top-level
|
||||||
/// files (depth 0) are skipped: the only one is /system/kernel, which loadKernel
|
/// files (depth 0) are skipped: the only one is /system/kernel, which loadKernel
|
||||||
/// has already consumed and which is not a spawnable user binary.
|
/// has already consumed and which is not a spawnable user binary (/test has no
|
||||||
|
/// top-level files, so the skip is a no-op there).
|
||||||
fn walkDirectory(
|
fn walkDirectory(
|
||||||
bs: *uefi.tables.BootServices,
|
bs: *uefi.tables.BootServices,
|
||||||
directory: *uefi.protocol.File,
|
directory: *uefi.protocol.File,
|
||||||
|
|
|
||||||
55
build.zig
55
build.zig
|
|
@ -648,11 +648,11 @@ pub fn build(b: *std.Build) void {
|
||||||
init_options.addOption(bool, "diagnose", diagnose);
|
init_options.addOption(bool, "diagnose", diagnose);
|
||||||
programModule(init_exe).addImport("build_options", init_options.createModule());
|
programModule(init_exe).addImport("build_options", init_options.createModule());
|
||||||
|
|
||||||
// --- the rest of the /system tree: services, drivers, test fixtures ---
|
// --- the rest of the boot tree: /system services and drivers, /test fixtures ---
|
||||||
// Each is built by the same user-binary recipe and laid out at its FHS path on
|
// 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
|
// 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).
|
// and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig).
|
||||||
const vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "system/services/vfs-test/vfs-test.zig");
|
const vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "test/system/services/vfs-test/vfs-test.zig");
|
||||||
const ps2_bus_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
const ps2_bus_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig");
|
||||||
const ps2_keyboard_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
const ps2_keyboard_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig");
|
||||||
programModule(ps2_keyboard_exe).addImport("input-protocol", input_protocol_module);
|
programModule(ps2_keyboard_exe).addImport("input-protocol", input_protocol_module);
|
||||||
|
|
@ -696,9 +696,9 @@ pub fn build(b: *std.Build) void {
|
||||||
programModule(virtio_gpu_exe).addImport("pci", pci_module); // library/device/pci — the claimed-function view
|
programModule(virtio_gpu_exe).addImport("pci", pci_module); // library/device/pci — the claimed-function view
|
||||||
programModule(virtio_gpu_exe).addImport("display-protocol", display_protocol_module);
|
programModule(virtio_gpu_exe).addImport("display-protocol", display_protocol_module);
|
||||||
programModule(virtio_gpu_exe).addImport("scanout-protocol", scanout_protocol_module);
|
programModule(virtio_gpu_exe).addImport("scanout-protocol", scanout_protocol_module);
|
||||||
const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "system/services/shared-memory-server/shared-memory-server.zig");
|
const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "test/system/services/shared-memory-server/shared-memory-server.zig");
|
||||||
const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "system/services/shared-memory-client/shared-memory-client.zig");
|
const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "test/system/services/shared-memory-client/shared-memory-client.zig");
|
||||||
const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "system/services/fat/fat-test.zig");
|
const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "test/system/services/fat-test/fat-test.zig");
|
||||||
const pci_bus_exe = addUserBinary(b, kernel_target, &default_imports, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
const pci_bus_exe = addUserBinary(b, kernel_target, &default_imports, "pci-bus", "system/drivers/pci-bus/pci-bus.zig");
|
||||||
programModule(pci_bus_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
programModule(pci_bus_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
||||||
// The PCI bus driver decodes each function's class triple to human names in its
|
// The PCI bus driver decodes each function's class triple to human names in its
|
||||||
|
|
@ -706,9 +706,9 @@ pub fn build(b: *std.Build) void {
|
||||||
programModule(pci_bus_exe).addImport("pci-class", pci_class_module);
|
programModule(pci_bus_exe).addImport("pci-class", pci_class_module);
|
||||||
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||||
// what the driver-restart scenario drives the crash-loop cap with.
|
// what the driver-restart scenario drives the crash-loop cap with.
|
||||||
const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "system/services/crash-test/crash-test.zig");
|
const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "test/system/services/crash-test/crash-test.zig");
|
||||||
programModule(crash_test_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
programModule(crash_test_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
||||||
const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "system/services/device-list/device-list.zig");
|
const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "test/system/services/device-list/device-list.zig");
|
||||||
programModule(device_list_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
programModule(device_list_exe).addImport("device-manager-protocol", device_manager_protocol_module);
|
||||||
// The discovery service: one swappable process per firmware
|
// The discovery service: one swappable process per firmware
|
||||||
// (docs/discovery.md), bundled under the neutral ramdisk name
|
// (docs/discovery.md), bundled under the neutral ramdisk name
|
||||||
|
|
@ -738,21 +738,22 @@ pub fn build(b: *std.Build) void {
|
||||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||||
const input_exe = addUserBinary(b, kernel_target, &default_imports, "input", "system/services/input/input.zig");
|
const input_exe = addUserBinary(b, kernel_target, &default_imports, "input", "system/services/input/input.zig");
|
||||||
programModule(input_exe).addImport("input-protocol", input_protocol_module);
|
programModule(input_exe).addImport("input-protocol", input_protocol_module);
|
||||||
const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "system/services/input-source/input-source.zig");
|
const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "test/system/services/input-source/input-source.zig");
|
||||||
const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "system/services/input-test/input-test.zig");
|
const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "test/system/services/input-test/input-test.zig");
|
||||||
const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "system/services/args-echo/args-echo.zig");
|
const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "test/system/services/args-echo/args-echo.zig");
|
||||||
const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "system/services/process-test/process-test.zig");
|
const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "test/system/services/process-test/process-test.zig");
|
||||||
const logger_exe = addUserBinary(b, kernel_target, &default_imports, "logger", "system/services/logger/logger.zig");
|
const logger_exe = addUserBinary(b, kernel_target, &default_imports, "logger", "system/services/logger/logger.zig");
|
||||||
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI
|
// The first multi-threaded binary: exercises runtime.Thread over the thread ABI
|
||||||
// (docs/threading.md). Built threaded so its shared-memory poll is real.
|
// (docs/threading.md). Built threaded so its shared-memory poll is real.
|
||||||
const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "system/services/thread-test/thread-test.zig");
|
const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "test/system/services/thread-test/thread-test.zig");
|
||||||
|
|
||||||
// Every user binary and its FHS home on the boot volume. There is no packed
|
// 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
|
// 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
|
// path on the image, and the EFI loader walks /system and /test at boot and
|
||||||
// in-RAM initial_ramdisk table from the tree — the volume's file structure is
|
// builds the in-RAM initial_ramdisk table from the trees — the volume's file
|
||||||
// the single source of truth. Entry names (and hence argv[0] and task names)
|
// structure is the single source of truth. Entry names (and hence argv[0] and
|
||||||
// are these paths with a leading slash.
|
// task names) are these paths with a leading slash. Test fixtures mirror their
|
||||||
|
// repo home: test/system/services/<name> in the source tree IS the boot path.
|
||||||
const bundled = [_]BundledBinary{
|
const bundled = [_]BundledBinary{
|
||||||
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
||||||
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
||||||
|
|
@ -771,17 +772,17 @@ pub fn build(b: *std.Build) void {
|
||||||
.{ .path = "system/drivers/usb-storage", .binary = usb_storage_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/virtio-gpu", .binary = virtio_gpu_exe.getEmittedBin() },
|
||||||
.{ .path = "system/drivers/pci-bus", .binary = pci_bus_exe.getEmittedBin() },
|
.{ .path = "system/drivers/pci-bus", .binary = pci_bus_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/vfs-test", .binary = vfstest_exe.getEmittedBin() },
|
.{ .path = "test/system/services/vfs-test", .binary = vfstest_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/fat-test", .binary = fat_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/fat-test", .binary = fat_test_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() },
|
.{ .path = "test/system/services/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/shared-memory-client", .binary = shared_memory_client_exe.getEmittedBin() },
|
.{ .path = "test/system/services/shared-memory-client", .binary = shared_memory_client_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/crash-test", .binary = crash_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/crash-test", .binary = crash_test_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/device-list", .binary = device_list_exe.getEmittedBin() },
|
.{ .path = "test/system/services/device-list", .binary = device_list_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/input-source", .binary = input_source_exe.getEmittedBin() },
|
.{ .path = "test/system/services/input-source", .binary = input_source_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/input-test", .binary = input_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/input-test", .binary = input_test_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/args-echo", .binary = args_echo_exe.getEmittedBin() },
|
.{ .path = "test/system/services/args-echo", .binary = args_echo_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/process-test", .binary = process_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/process-test", .binary = process_test_exe.getEmittedBin() },
|
||||||
.{ .path = "system/tests/thread-test", .binary = thread_test_exe.getEmittedBin() },
|
.{ .path = "test/system/services/thread-test", .binary = thread_test_exe.getEmittedBin() },
|
||||||
};
|
};
|
||||||
|
|
||||||
// The boot manifest: the FHS path of every bundled binary, one per line. The
|
// The boot manifest: the FHS path of every bundled binary, one per line. The
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,7 @@ addressed as **`system/services/init`** — the repeated leaf resolves away:
|
||||||
|----------------------------------------|--------------------------------------------|
|
|----------------------------------------|--------------------------------------------|
|
||||||
| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` |
|
| `system/services/init/init.zig` | `system/services/init` → `/system/services/init` |
|
||||||
| `system/drivers/ps2-bus/ps2-bus.zig` | `system/drivers/ps2-bus` → `/system/drivers/ps2-bus` |
|
| `system/drivers/ps2-bus/ps2-bus.zig` | `system/drivers/ps2-bus` → `/system/drivers/ps2-bus` |
|
||||||
|
| `test/system/services/vfs-test/vfs-test.zig` | `test/system/services/vfs-test` → `/test/system/services/vfs-test` |
|
||||||
| `library/device/pci/pci.zig` | `library/device/pci` (the `pci` module) |
|
| `library/device/pci/pci.zig` | `library/device/pci` (the `pci` module) |
|
||||||
|
|
||||||
In **source**, a sub-project is a directory so it can hold many files — the entry is
|
In **source**, a sub-project is a directory so it can hold many files — the entry is
|
||||||
|
|
@ -258,7 +259,11 @@ library/ → /lib libraries, one sub-directory each
|
||||||
protocol/ driver↔service wire contracts (vfs block display scanout input
|
protocol/ driver↔service wire contracts (vfs block display scanout input
|
||||||
power device-manager usb-transfer), one module per directory
|
power device-manager usb-transfer), one module per directory
|
||||||
boot/ → /boot the loaders
|
boot/ → /boot the loaders
|
||||||
tools/ test/ host-side build + QEMU test harness
|
test/ → /test the test tree: the QEMU harness (qemu_test.py, host-side)
|
||||||
|
system/services/ beside the on-image test fixtures — vfs-test/ thread-test/
|
||||||
|
crash-test/ … — whose repo path IS their boot-volume path
|
||||||
|
(/test/system/services/<name>)
|
||||||
|
tools/ host-side build scripts
|
||||||
```
|
```
|
||||||
|
|
||||||
**Wire protocols live in `library/protocol/`**, one module per directory
|
**Wire protocols live in `library/protocol/`**, one module per directory
|
||||||
|
|
@ -314,5 +319,6 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam.
|
||||||
| Service clients (a program's view of a service) and device clients | `library/client/` (display, input), `library/device/driver` |
|
| Service clients (a program's view of a service) and device clients | `library/client/` (display, input), `library/device/driver` |
|
||||||
| System services (init, the `fat` filesystem, the device-manager) | `system/services/` |
|
| System services (init, the `fat` filesystem, the device-manager) | `system/services/` |
|
||||||
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
| Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` |
|
||||||
|
| On-image test fixtures for the QEMU cases (`vfs-test`, `crash-test`, `thread-test`, …) → `/test/system/services` | `test/system/services/` |
|
||||||
| Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` |
|
| Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` |
|
||||||
| QEMU integration test harness | `test/qemu_test.py` |
|
| QEMU integration test harness | `test/qemu_test.py` |
|
||||||
|
|
|
||||||
|
|
@ -97,8 +97,9 @@ Three, and only three.
|
||||||
|
|
||||||
That's all — no Unix-abbreviation exception. The source directories are full words
|
That's all — no Unix-abbreviation exception. The source directories are full words
|
||||||
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
(`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver
|
||||||
lives in `system/drivers/` and a service in `system/services/`, so the *location*
|
lives in `system/drivers/`, a service in `system/services/`, and a test fixture in
|
||||||
already says what it is. Encoding the role in the name too (`busd`, `fatd`) is
|
`test/system/services/` (the repo path *is* its path on the boot volume), so the
|
||||||
|
*location* already says what it is. Encoding the role in the name too (`busd`, `fatd`) is
|
||||||
redundant — the program is just `ps2-bus`, `fat`. Don't put in a name what its directory
|
redundant — the program is just `ps2-bus`, `fat`. Don't put in a name what its directory
|
||||||
already tells you.
|
already tells you.
|
||||||
|
|
||||||
|
|
@ -142,8 +143,9 @@ conventions above — `snake_case` — because it's an identifier, not a filenam
|
||||||
|
|
||||||
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
|
**A sub-project's entry point repeats its directory's name** — `init/init.zig`,
|
||||||
`pci/pci.zig`, `ps2-bus/ps2-bus.zig` — and the sub-project is addressed by the
|
`pci/pci.zig`, `ps2-bus/ps2-bus.zig` — and the sub-project is addressed by the
|
||||||
*directory* (`system/services/init`, `library/device/pci`), with the repeated leaf
|
*directory* (`system/services/init`, `library/device/pci`,
|
||||||
resolving away. See the repository-layout section of [README.md](README.md).
|
`test/system/services/vfs-test`), with the repeated leaf resolving away. See the
|
||||||
|
repository-layout section of [README.md](README.md).
|
||||||
|
|
||||||
## Named values, not magic numbers
|
## Named values, not magic numbers
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o
|
||||||
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) |
|
| /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) |
|
||||||
| /system/services | system-service binaries — init, the FAT server, and other user-mode servers (e.g. /system/services/init, /system/services/fat) |
|
| /system/services | system-service binaries — init, the FAT server, and other user-mode servers (e.g. /system/services/init, /system/services/fat) |
|
||||||
| /system/kernel | the kernel image |
|
| /system/kernel | the kernel image |
|
||||||
|
| /test | Test fixtures for the QEMU integration suite. Read-only and initrd-backed like /system, and its layout likewise mirrors the source tree (the repo's test/ directory). Present on development and test images; a volume without it still boots. |
|
||||||
|
| /test/system/services | test-fixture binaries (e.g. /test/system/services/vfs-test, /test/system/services/thread-test) — the same path in the repo source tree and on the boot volume |
|
||||||
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
| /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. |
|
||||||
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
|
| /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. |
|
||||||
| /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. |
|
| /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. |
|
||||||
|
|
@ -52,8 +54,9 @@ IPC endpoint, and subsequent reads and writes are calls against it. Resolve-to-e
|
||||||
is exactly what the kernel's `fs_resolve` already does for any mounted backend, and
|
is exactly what the kernel's `fs_resolve` already does for any mounted backend, and
|
||||||
`FileStatus.kind` is the field that marks a device node; **what is not implemented today
|
`FileStatus.kind` is the field that marks a device node; **what is not implemented today
|
||||||
is `/dev` itself** — no service mounts it. (The flat eight-node ramfs this section once
|
is `/dev` itself** — no service mounts it. (The flat eight-node ramfs this section once
|
||||||
described is retired: the kernel-resident VFS root in `system/kernel/vfs.zig` serves the
|
described is retired: the kernel-resident VFS root in `system/kernel/vfs.zig` serves a
|
||||||
read-only `/system` initrd mount with real directories and node kinds, and filesystem
|
read-only initrd mount per top-level tree — `/system`, and `/test` on images that carry
|
||||||
|
the fixtures — with real directories and node kinds, and filesystem
|
||||||
backends such as the FAT server mount the rest.) The three sections below describe the
|
backends such as the FAT server mount the rest.) The three sections below describe the
|
||||||
intended shape, and are honest about which parts the kernel can already support.
|
intended shape, and are honest about which parts the kernel can already support.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ test/qemu_test.py <case>`), each layering on the last:
|
||||||
layer — logging `display: compositor self-check ok`.
|
layer — logging `display: compositor self-check ok`.
|
||||||
- **`display-demo`** — the full pipeline from a separate process: the hardware-free
|
- **`display-demo`** — the full pipeline from a separate process: the hardware-free
|
||||||
[`display-demo`](../system/services/display-demo/) client (the
|
[`display-demo`](../system/services/display-demo/) client (the
|
||||||
[`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper and
|
[`input-source`](../test/system/services/input-source/) analog) drives layers — a wallpaper and
|
||||||
a sliding rectangle — through the layer client API and heartbeats
|
a sliding rectangle — through the layer client API and heartbeats
|
||||||
`display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as
|
`display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as
|
||||||
the [input test](input.md) proves an event travels source → service → subscriber. It draws
|
the [input test](input.md) proves an event travels source → service → subscriber. It draws
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,8 @@ captures the **ACPI RSDP** from the UEFI configuration table (while boot
|
||||||
services are still up), loads the system binaries into an in-RAM
|
services are still up), loads the system binaries into an in-RAM
|
||||||
**initial ramdisk** (`loadSystemTree` — normally a single read of the pre-packed
|
**initial ramdisk** (`loadSystemTree` — normally a single read of the pre-packed
|
||||||
`boot\system.img` capsule, which already *is* the ramdisk wire format; it falls
|
`boot\system.img` capsule, which already *is* the ramdisk wire format; it falls
|
||||||
back to opening each manifest-listed path, and walks the `/system` tree only as
|
back to opening each manifest-listed path, and walks the `/system` and `/test`
|
||||||
a last resort for hand-assembled sticks — the capsule's format, builder, and
|
trees only as a last resort for hand-assembled sticks — the capsule's format, builder, and
|
||||||
fallback chain are documented in [system-image.md](system-image.md). Best-effort either way — a kernel-only
|
fallback chain are documented in [system-image.md](system-image.md). Best-effort either way — a kernel-only
|
||||||
volume still boots), and builds the **bootstrap page tables** the kernel starts
|
volume still boots), and builds the **bootstrap page tables** the kernel starts
|
||||||
life on (`buildBootstrapTables`), all before the jump:
|
life on (`buildBootstrapTables`), all before the jump:
|
||||||
|
|
@ -196,7 +196,7 @@ power on
|
||||||
-> queryFramebuffer (via GOP: EDID native res, setMode, describe fb)
|
-> queryFramebuffer (via GOP: EDID native res, setMode, describe fb)
|
||||||
-> loadKernel (read system/kernel ELF, load PT_LOAD segments low, .text at 0x100000)
|
-> loadKernel (read system/kernel ELF, load PT_LOAD segments low, .text at 0x100000)
|
||||||
-> loadSystemTree (read the boot\system.img capsule as the in-RAM initial ramdisk;
|
-> loadSystemTree (read the boot\system.img capsule as the in-RAM initial ramdisk;
|
||||||
fallbacks: manifest-listed paths, then a /system tree walk)
|
fallbacks: manifest-listed paths, then a /system + /test tree walk)
|
||||||
-> buildBootstrapTables (identity + physmap + higher-half kernel mappings)
|
-> buildBootstrapTables (identity + physmap + higher-half kernel mappings)
|
||||||
-> exitBootServices (retry until the memory-map key holds)
|
-> exitBootServices (retry until the memory-map key holds)
|
||||||
-> handoff: load bootstrap CR3, jump to e_entry, boot_information pointer in RDI
|
-> handoff: load bootstrap CR3, jump to e_entry, boot_information pointer in RDI
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ v2), written to disk ahead of time. The EFI loader reads it in a single
|
||||||
sequential pass and hands the bytes to the kernel unmodified.
|
sequential pass and hands the bytes to the kernel unmodified.
|
||||||
|
|
||||||
The capsule is a *performance artifact*, not a source of truth. The boot
|
The capsule is a *performance artifact*, not a source of truth. The boot
|
||||||
volume's `/system` file tree remains the canonical layout (see
|
volume's `/system` and `/test` file trees remain the canonical layout (see
|
||||||
[danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md));
|
[danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md));
|
||||||
the capsule is a pre-baked snapshot of the same binaries, derived from the same
|
the capsule is a pre-baked snapshot of the same binaries, derived from the same
|
||||||
build graph, so the running system is identical whether the loader read the
|
build graph, so the running system is identical whether the loader read the
|
||||||
|
|
@ -58,8 +58,9 @@ blobs... each entry's file bytes, at its offset within the image
|
||||||
Three artifacts are derived from that same list, in the same build graph, so
|
Three artifacts are derived from that same list, in the same build graph, so
|
||||||
they cannot drift apart:
|
they cannot drift apart:
|
||||||
|
|
||||||
1. **The tree**: each binary installed at its FHS path (`zig-out/system/...`,
|
1. **The tree**: each binary installed at its FHS path (`zig-out/system/...`
|
||||||
mirrored onto the FAT boot volume by `tools/make-fat-image.py`).
|
and `zig-out/test/...`, mirrored onto the FAT boot volume by
|
||||||
|
`tools/make-fat-image.py`).
|
||||||
2. **The manifest** (`system/manifest`): the FHS path of every bundled binary,
|
2. **The manifest** (`system/manifest`): the FHS path of every bundled binary,
|
||||||
one per line — the loader's per-file fallback input.
|
one per line — the loader's per-file fallback input.
|
||||||
3. **The capsule**: `tools/pack-system-image.py` packs the same binaries into
|
3. **The capsule**: `tools/pack-system-image.py` packs the same binaries into
|
||||||
|
|
@ -81,7 +82,8 @@ same in-RAM ramdisk image:
|
||||||
2. **The manifest** — read `system\manifest` and open each listed path *by
|
2. **The manifest** — read `system\manifest` and open each listed path *by
|
||||||
name*. FAT name lookup is case-insensitive and firmware-portable, unlike
|
name*. FAT name lookup is case-insensitive and firmware-portable, unlike
|
||||||
directory enumeration. The loader assembles the v2 image in RAM itself.
|
directory enumeration. The loader assembles the v2 image in RAM itself.
|
||||||
3. **The tree walk** — enumerate `/system` recursively. Last resort for
|
3. **The tree walk** — enumerate `/system` and `/test` recursively (`/test`
|
||||||
|
is optional: a stick without fixtures still boots). Last resort for
|
||||||
hand-assembled sticks with neither file: some firmware FAT drivers return
|
hand-assembled sticks with neither file: some firmware FAT drivers return
|
||||||
bare 8.3 names uppercase from enumeration, which is why this is the
|
bare 8.3 names uppercase from enumeration, which is why this is the
|
||||||
fallback and not the primary path.
|
fallback and not the primary path.
|
||||||
|
|
@ -105,10 +107,11 @@ consumers:
|
||||||
pre-path callers — and loads them as fresh ring-3 processes. The stored path
|
pre-path callers — and loads them as fresh ring-3 processes. The stored path
|
||||||
becomes the task's name.
|
becomes the task's name.
|
||||||
- **The VFS root** (`vfs.zig`, `setInitialRamdisk`): the image is mounted as
|
- **The VFS root** (`vfs.zig`, `setInitialRamdisk`): the image is mounted as
|
||||||
the kernel-backed, read-only `/system` mount. Directory nodes are derived
|
kernel-backed, read-only mounts — one per top-level tree named by the entry
|
||||||
from the entry paths (the unique parents), so `/system` is listable and its
|
paths, so `/system` and, when the fixtures are bundled, `/test`. Directory
|
||||||
files readable over the normal VFS protocol — the FHS boot tree every
|
nodes are derived from the entry paths (the unique parents), so the trees
|
||||||
process sees comes straight out of the capsule bytes.
|
are listable and their files readable over the normal VFS protocol — the
|
||||||
|
FHS boot tree every process sees comes straight out of the capsule bytes.
|
||||||
|
|
||||||
The image is never copied after the handoff and never mutated: the initrd is
|
The image is never copied after the handoff and never mutated: the initrd is
|
||||||
immutable, which is what makes the VFS's node serving lock-free.
|
immutable, which is what makes the VFS's node serving lock-free.
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,9 @@ hypervisor configured for UEFI firmware and an xHCI USB controller.
|
||||||
- The loader reads `/system/kernel` off the FAT boot volume, then loads user
|
- The loader reads `/system/kernel` off the FAT boot volume, then loads user
|
||||||
space: a prebuilt `boot\system.img` capsule
|
space: a prebuilt `boot\system.img` capsule
|
||||||
([system-image.md](system-image.md)) when present, otherwise it walks
|
([system-image.md](system-image.md)) when present, otherwise it walks
|
||||||
the volume's `/system` tree (init included) into the initial ramdisk. The
|
the volume's `/system` and optional `/test` trees (init included) into the
|
||||||
kernel can boot "kernel-only" without either. (`efi.zig:16`, `efi.zig:68`)
|
initial ramdisk. The kernel can boot "kernel-only" without either.
|
||||||
|
(`efi.zig:16`, `efi.zig:68`)
|
||||||
|
|
||||||
## Interrupt controller
|
## Interrupt controller
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ test "wrap reclaims whole records and keeps tail on a boundary" {
|
||||||
while (i < 200) : (i += 1) {
|
while (i < 200) : (i += 1) {
|
||||||
var message: [64]u8 = undefined;
|
var message: [64]u8 = undefined;
|
||||||
const m = std.fmt.bufPrint(&message, "line {d} padding padding padding", .{i}) catch unreachable;
|
const m = std.fmt.bufPrint(&message, "line {d} padding padding padding", .{i}) catch unreachable;
|
||||||
_ = ring.append(1, "/system/tests/writer", .info, i, m, false);
|
_ = ring.append(1, "/test/system/services/writer", .info, i, m, false);
|
||||||
}
|
}
|
||||||
try std.testing.expect(ring.head - ring.tail <= 4096);
|
try std.testing.expect(ring.head - ring.tail <= 4096);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3105,7 +3105,7 @@ fn argsTest(boot_information: *const BootInformation) void {
|
||||||
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
|
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
|
||||||
scheduler.setPriority(4);
|
scheduler.setPriority(4);
|
||||||
|
|
||||||
const expected = "args: /system/tests/args-echo alpha beta-42\n";
|
const expected = "args: /test/system/services/args-echo alpha beta-42\n";
|
||||||
const echoed = process.write_len == expected.len and eql(process.write_buffer[0..process.write_len], expected);
|
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]});
|
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);
|
check("argv arrived intact (argv[0] = name, argv[1..] = spawn arguments)", echoed);
|
||||||
|
|
@ -3116,7 +3116,7 @@ fn argsTest(boot_information: *const BootInformation) void {
|
||||||
/// Spawn the initial_ramdisk binary named `name` as a ring-3 process. Returns false if it
|
/// 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.
|
/// isn't in the image or fails to load.
|
||||||
/// The init ELF image out of the initial_ramdisk — init rides the table like
|
/// 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.
|
/// every other binary since the loader packs the whole boot tree.
|
||||||
fn bundledInit(boot_information: *const BootInformation) ?[]const u8 {
|
fn bundledInit(boot_information: *const BootInformation) ?[]const u8 {
|
||||||
if (boot_information.initial_ramdisk_len == 0) return null;
|
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 image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
|
@ -3137,7 +3137,7 @@ fn kernelVfsTest(boot_information: *const BootInformation) void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
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
|
process.setInitialRamdisk(image); // also seeds the kernel VFS /system and /test mounts
|
||||||
|
|
||||||
// A file resolves to a kernel node token; its status and bytes are served.
|
// A file resolves to a kernel node token; its status and bytes are served.
|
||||||
const resolved = kernel_vfs.resolvePath("/system/services/init", false);
|
const resolved = kernel_vfs.resolvePath("/system/services/init", false);
|
||||||
|
|
@ -3151,21 +3151,22 @@ fn kernelVfsTest(boot_information: *const BootInformation) void {
|
||||||
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');
|
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.
|
// Directories resolve and enumerate: /system lists services/drivers.
|
||||||
const root_directory = kernel_vfs.resolvePath("/system", false);
|
const root_directory = kernel_vfs.resolvePath("/system", false);
|
||||||
check("/system resolves to a directory node", root_directory == .kernel_node);
|
check("/system resolves to a directory node", root_directory == .kernel_node);
|
||||||
var saw_services = false;
|
var saw_services = false;
|
||||||
var saw_drivers = false;
|
var saw_drivers = false;
|
||||||
|
var saw_stray_in_root = false;
|
||||||
var saw_files_in_services = false;
|
var saw_files_in_services = false;
|
||||||
if (root_directory == .kernel_node) {
|
if (root_directory == .kernel_node) {
|
||||||
var cursor: u64 = 0;
|
var cursor: u64 = 0;
|
||||||
var name: [64]u8 = undefined;
|
var name: [64]u8 = undefined;
|
||||||
while (kernel_vfs.nodeReaddir(root_directory.kernel_node, cursor, &name)) |entry| : (cursor += 1) {
|
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], "services")) saw_services = true else if (eql(name[0..entry.name_len], "drivers")) saw_drivers = true else saw_stray_in_root = true;
|
||||||
if (eql(name[0..entry.name_len], "drivers")) saw_drivers = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check("readdir /system yields services and drivers", saw_services and saw_drivers);
|
check("readdir /system yields services and drivers", saw_services and saw_drivers);
|
||||||
|
check("readdir /system yields nothing else (no /test leakage)", !saw_stray_in_root);
|
||||||
const services = kernel_vfs.resolvePath("/system/services", false);
|
const services = kernel_vfs.resolvePath("/system/services", false);
|
||||||
if (services == .kernel_node) {
|
if (services == .kernel_node) {
|
||||||
var cursor: u64 = 0;
|
var cursor: u64 = 0;
|
||||||
|
|
@ -3176,10 +3177,26 @@ fn kernelVfsTest(boot_information: *const BootInformation) void {
|
||||||
}
|
}
|
||||||
check("readdir /system/services yields init", saw_files_in_services);
|
check("readdir /system/services yields init", saw_files_in_services);
|
||||||
|
|
||||||
// Refusals: unknown paths, and create-intent on the immutable initrd.
|
// The /test tree is a second initrd root, resolvable and enumerable like
|
||||||
|
// /system (the fixtures live at /test/system/services, mirroring the repo).
|
||||||
|
const test_root = kernel_vfs.resolvePath("/test", false);
|
||||||
|
check("/test resolves to a directory node", test_root == .kernel_node);
|
||||||
|
check("/test/system/services/args-echo resolves to a kernel node", kernel_vfs.resolvePath("/test/system/services/args-echo", false) == .kernel_node);
|
||||||
|
var saw_test_subtree = false;
|
||||||
|
if (test_root == .kernel_node) {
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
var name: [64]u8 = undefined;
|
||||||
|
while (kernel_vfs.nodeReaddir(test_root.kernel_node, cursor, &name)) |entry| : (cursor += 1) {
|
||||||
|
if (eql(name[0..entry.name_len], "system")) saw_test_subtree = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("readdir /test yields system", saw_test_subtree);
|
||||||
|
|
||||||
|
// Refusals: unknown paths, and create-intent on the immutable initrd trees.
|
||||||
check("an unknown path does not resolve", kernel_vfs.resolvePath("/system/services/no-such", false) == .not_found);
|
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("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);
|
check("create on /system is refused (read-only)", kernel_vfs.resolvePath("/system/services/new-file", true) == .not_found);
|
||||||
|
check("create on /test is refused (read-only)", kernel_vfs.resolvePath("/test/system/services/new-file", true) == .not_found);
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! the mechanism is **resolve + redirect**:
|
//! the mechanism is **resolve + redirect**:
|
||||||
//!
|
//!
|
||||||
//! - `fs_resolve(path)` walks the mount table. A path under a KERNEL-backed
|
//! - `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
|
//! mount (the initrd trees at /system and /test, the scratch ram nodes) resolves to a
|
||||||
//! stateless node TOKEN served directly by `fs_node` (read/status/readdir
|
//! stateless node TOKEN served directly by `fs_node` (read/status/readdir
|
||||||
//! with copy-out). A path under a USERSPACE mount (the fat server at
|
//! 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
|
//! /mnt/usb and /var) resolves to the backend's ENDPOINT: the kernel
|
||||||
|
|
@ -85,7 +85,7 @@ const maximum_directories = 8;
|
||||||
const Directory = struct {
|
const Directory = struct {
|
||||||
path: [maximum_prefix]u8 = undefined,
|
path: [maximum_prefix]u8 = undefined,
|
||||||
path_len: usize = 0,
|
path_len: usize = 0,
|
||||||
parent: usize = 0, // index into `directories`; 0 is /system itself
|
parent: usize = 0, // index into `directories`; a top-level tree root is its own parent
|
||||||
|
|
||||||
fn slice(self: *const Directory) []const u8 {
|
fn slice(self: *const Directory) []const u8 {
|
||||||
return self.path[0..self.path_len];
|
return self.path[0..self.path_len];
|
||||||
|
|
@ -122,37 +122,35 @@ fn parentOf(path: []const u8) []const u8 {
|
||||||
|
|
||||||
// --- boot wiring -------------------------------------------------------------
|
// --- boot wiring -------------------------------------------------------------
|
||||||
|
|
||||||
/// Publish the initrd as the kernel-backed /system mount and derive its bounded
|
/// Publish the initrd as kernel-backed mounts — one per top-level tree its
|
||||||
/// directory table (the unique parents of the entry paths). Called once at boot.
|
/// entry paths name (/system, /test) — and derive the bounded directory table
|
||||||
|
/// (every ancestor directory of the entry paths). Called once at boot.
|
||||||
pub fn setInitialRamdisk(image: []const u8) void {
|
pub fn setInitialRamdisk(image: []const u8) void {
|
||||||
ramdisk_image = image;
|
ramdisk_image = image;
|
||||||
installMount("/system", .kernel_initrd, null, "");
|
directory_count = 0;
|
||||||
|
|
||||||
// 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;
|
const rd = initial_ramdisk.Reader.init(image) orelse return;
|
||||||
var i: u32 = 0;
|
var i: u32 = 0;
|
||||||
while (i < rd.count) : (i += 1) {
|
while (i < rd.count) : (i += 1) {
|
||||||
const item = rd.entry(i) orelse continue;
|
const item = rd.entry(i) orelse continue;
|
||||||
// Register every ancestor directory strictly below /system.
|
// Register every ancestor directory, top-level tree roots included.
|
||||||
var parent = parentOf(item.name);
|
var parent = parentOf(item.name);
|
||||||
while (parent.len > 7) : (parent = parentOf(parent)) {
|
while (parent.len > 1) : (parent = parentOf(parent)) {
|
||||||
if (directoryIndex(parent) == null and directory_count < maximum_directories) {
|
if (directoryIndex(parent) == null and directory_count < maximum_directories) {
|
||||||
var d = &directories[directory_count];
|
var d = &directories[directory_count];
|
||||||
@memcpy(d.path[0..parent.len], parent);
|
@memcpy(d.path[0..parent.len], parent);
|
||||||
d.path_len = parent.len;
|
d.path_len = parent.len;
|
||||||
d.parent = 0; // fixed up below once all exist
|
d.parent = directory_count; // fixed up below once all exist
|
||||||
directory_count += 1;
|
directory_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Parent links (a second pass so out-of-order registration doesn't matter).
|
// Parent links (a second pass so out-of-order registration doesn't matter).
|
||||||
for (directories[1..directory_count]) |*d| {
|
// A top-level root keeps itself as parent and becomes an initrd mount.
|
||||||
d.parent = directoryIndex(parentOf(d.slice())) orelse 0;
|
for (directories[0..directory_count], 0..) |*d, index| {
|
||||||
|
const parent = parentOf(d.slice());
|
||||||
|
d.parent = directoryIndex(parent) orelse index;
|
||||||
|
if (parent.len == 1) installMount(d.slice(), .kernel_initrd, null, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +191,7 @@ pub const Resolved = union(enum) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Longest-prefix match over the mount table, then per-kind resolution.
|
/// Longest-prefix match over the mount table, then per-kind resolution.
|
||||||
/// `create`-intent on the immutable /system fails here (EROFS-style).
|
/// `create`-intent on the immutable initrd trees fails here (EROFS-style).
|
||||||
pub fn resolvePath(path: []const u8, wants_create: bool) Resolved {
|
pub fn resolvePath(path: []const u8, wants_create: bool) Resolved {
|
||||||
if (!isAbsolute(path)) {
|
if (!isAbsolute(path)) {
|
||||||
return .{ .not_found = {} }; // bare names have no kernel namespace (ramfs retired)
|
return .{ .not_found = {} }; // bare names have no kernel namespace (ramfs retired)
|
||||||
|
|
@ -295,11 +293,11 @@ pub fn nodeReaddir(node_token: u64, cursor: u64, name_out: []u8) ?struct { heade
|
||||||
const self_path = directories[@intCast(directory_index)].slice();
|
const self_path = directories[@intCast(directory_index)].slice();
|
||||||
|
|
||||||
var index: u64 = 0;
|
var index: u64 = 0;
|
||||||
// Subdirectories whose parent is this directory.
|
// Subdirectories whose parent is this directory (roots are self-parented,
|
||||||
|
// so they never appear as anyone's child).
|
||||||
for (directories[0..directory_count], 0..) |*d, i| {
|
for (directories[0..directory_count], 0..) |*d, i| {
|
||||||
if (i == directory_index) continue;
|
if (i == directory_index) continue;
|
||||||
if (d.parent != directory_index) continue;
|
if (d.parent != directory_index) continue;
|
||||||
if (i == 0) continue;
|
|
||||||
if (index == cursor) {
|
if (index == cursor) {
|
||||||
const name = d.slice()[self_path.len + 1 ..];
|
const name = d.slice()[self_path.len + 1 ..];
|
||||||
const n = @min(name.len, name_out.len);
|
const n = @min(name.len, name_out.len);
|
||||||
|
|
@ -330,11 +328,13 @@ pub fn nodeReaddir(node_token: u64, cursor: u64, name_out: []u8) ?struct { heade
|
||||||
|
|
||||||
/// Mount `backend` at `prefix` with an optional backend-side `rewrite` prefix.
|
/// Mount `backend` at `prefix` with an optional backend-side `rewrite` prefix.
|
||||||
/// The endpoint reference is taken by the caller (process.zig bumps it); refuses
|
/// The endpoint reference is taken by the caller (process.zig bumps it); refuses
|
||||||
/// shadowing or replacing /system.
|
/// shadowing or replacing the initrd trees (/system, /test).
|
||||||
pub fn mountBackend(prefix: []const u8, backend: *ipc.Endpoint, rewrite: []const u8) bool {
|
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 (!isAbsolute(prefix) or prefix.len < 2 or prefix.len > maximum_prefix) return false;
|
||||||
if (rewrite.len > maximum_rewrite) return false;
|
if (rewrite.len > maximum_rewrite) return false;
|
||||||
if (underMount(prefix, "/system") != null) return false; // the initrd is not shadowable
|
for (&mounts) |*m| { // the initrd trees are not shadowable
|
||||||
|
if (m.used and m.kind == .kernel_initrd and underMount(prefix, m.prefixSlice()) != null) return false;
|
||||||
|
}
|
||||||
installMount(prefix, .backend, backend, rewrite);
|
installMount(prefix, .backend, backend, rewrite);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! system/services/fat/fat-test — a client that proves the FAT mount end to end:
|
//! test/system/services/fat-test — a client that proves the FAT mount end to end:
|
||||||
//! it waits for the fat server to mount the USB volume at /mnt/usb, lists the
|
//! it waits for the fat server to mount the USB volume at /mnt/usb, lists the
|
||||||
//! root directory through the VFS (which routes /mnt/usb to the fat backend), and
|
//! root directory through the VFS (which routes /mnt/usb to the fat backend), and
|
||||||
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
|
//! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount`
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! system/services/input-source — a hardware-free synthetic input source, used to exercise
|
//! test/system/services/input-source — a hardware-free synthetic input source, used to exercise
|
||||||
//! the input service end to end without a real PS/2 controller (the `input` test case, and
|
//! the input service end to end without a real PS/2 controller (the `input` test case, and
|
||||||
//! any bring-up where there is no hardware). It stands in for a driver: it connects to the
|
//! any bring-up where there is no hardware). It stands in for a driver: it connects to the
|
||||||
//! input service and publishes a rolling stream that cycles through all device classes —
|
//! input service and publishes a rolling stream that cycles through all device classes —
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! system/services/input-test — the input service's client and test oracle, the input
|
//! test/system/services/input-test — the input service's client and test oracle, the input
|
||||||
//! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the
|
//! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the
|
||||||
//! events a source broadcasts, logging each with its class. It emits the success marker
|
//! events a source broadcasts, logging each with its class. It emits the success marker
|
||||||
//! `"input-test: ok"` only **after it has received at least one of each class** (keyboard,
|
//! `"input-test: ok"` only **after it has received at least one of each class** (keyboard,
|
||||||
|
|
@ -151,8 +151,8 @@ pub fn main(init: process.Init) void {
|
||||||
const spinner = process.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner");
|
const spinner = process.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner");
|
||||||
|
|
||||||
time.sleepMillis(100); // let the sleeper block and the spinner get a core
|
time.sleepMillis(100); // let the sleeper block and the spinner get a core
|
||||||
if (!listed(sleeper, "/system/tests/process-test")) fail("sleeper not in process_enumerate");
|
if (!listed(sleeper, "/test/system/services/process-test")) fail("sleeper not in process_enumerate");
|
||||||
if (!listed(spinner, "/system/tests/process-test")) fail("spinner not in process_enumerate");
|
if (!listed(spinner, "/test/system/services/process-test")) fail("spinner not in process_enumerate");
|
||||||
|
|
||||||
// Kills that must be refused: a kernel task (id 0), and an id that was never
|
// 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
|
// issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level
|
||||||
|
|
@ -171,8 +171,8 @@ pub fn main(init: process.Init) void {
|
||||||
if (!process.kill(spinner)) fail("kill spinner");
|
if (!process.kill(spinner)) fail("kill spinner");
|
||||||
if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification");
|
if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification");
|
||||||
|
|
||||||
if (listed(sleeper, "/system/tests/process-test")) fail("sleeper still listed after kill");
|
if (listed(sleeper, "/test/system/services/process-test")) fail("sleeper still listed after kill");
|
||||||
if (listed(spinner, "/system/tests/process-test")) fail("spinner still listed after kill");
|
if (listed(spinner, "/test/system/services/process-test")) fail("spinner still listed after kill");
|
||||||
|
|
||||||
// M17.2: both children were killed by us, and the reason says so — the whole
|
// 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.
|
// restart-policy input, read through the runtime like a real supervisor would.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! system/services/shared-memory-client — the creating half of the shared-memory test (docs/display-v2.md V2).
|
//! test/system/services/shared-memory-client — the creating half of the shared-memory test (docs/display-v2.md V2).
|
||||||
//! It `shared_memory_create`s a shared region, writes a known pattern into it, and hands the region's
|
//! It `shared_memory_create`s a shared region, writes a known pattern into it, and hands the region's
|
||||||
//! capability to `shared-memory-server` as an `ipc_call` send_cap. The server maps that capability and
|
//! capability to `shared-memory-server` as an `ipc_call` send_cap. The server maps that capability and
|
||||||
//! confirms the pattern is visible — proving cross-process shared memory over the extended
|
//! confirms the pattern is visible — proving cross-process shared memory over the extended
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! system/services/shared-memory-server — the receiving half of the shared-memory test (docs/display-v2.md V2).
|
//! test/system/services/shared-memory-server — the receiving half of the shared-memory test (docs/display-v2.md V2).
|
||||||
//! It registers under `ServiceId.shared_memory_test`; when `shared-memory-client` calls it carrying a
|
//! It registers under `ServiceId.shared_memory_test`; when `shared-memory-client` calls it carrying a
|
||||||
//! shared-memory capability, it `shared_memory_map`s that capability and checks the client's pattern
|
//! shared-memory capability, it `shared_memory_map`s that capability and checks the client's pattern
|
||||||
//! is visible through the mapping — proving the two processes share the same physical pages
|
//! is visible through the mapping — proving the two processes share the same physical pages
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! /system/tests/vfs-test — a ring-3 client that proves the kernel VFS end to
|
//! /test/system/services/vfs-test — a ring-3 client that proves the kernel VFS
|
||||||
//! end through the plain `file_system` API: resolve its OWN binary under the
|
//! end to end through the plain `file_system` API: resolve its OWN binary under
|
||||||
//! kernel-served /system mount, check its metadata, read its ELF magic, and
|
//! the kernel-served /test mount, check its metadata, read its ELF magic, and
|
||||||
//! list /system/services. On success it heartbeats "vfstest: ok" so the kernel
|
//! list /system/services. On success it heartbeats "vfstest: ok" so the kernel
|
||||||
//! test can observe it; on failure it reports what went wrong.
|
//! test can observe it; on failure it reports what went wrong.
|
||||||
//!
|
//!
|
||||||
|
|
@ -21,7 +21,7 @@ pub fn main(init: process.Init) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Our own binary, resolved through the kernel mount table.
|
// Our own binary, resolved through the kernel mount table.
|
||||||
const self_path = "/system/tests/vfs-test";
|
const self_path = "/test/system/services/vfs-test";
|
||||||
var file = fs.open(self_path, .{}) orelse {
|
var file = fs.open(self_path, .{}) orelse {
|
||||||
_ = logging.write("vfstest: open of own binary failed\n");
|
_ = logging.write("vfstest: open of own binary failed\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -44,9 +44,9 @@ pub fn main(init: process.Init) void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The write refusal: /system is read-only by construction.
|
// The write refusal: the initrd trees are read-only by construction.
|
||||||
if (file.write("x") != null or fs.open("/system/tests/new-file", .{ .create = true }) != null) {
|
if (file.write("x") != null or fs.open("/test/system/services/new-file", .{ .create = true }) != null) {
|
||||||
_ = logging.write("vfstest: /system accepted a write\n");
|
_ = logging.write("vfstest: the initrd tree accepted a write\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue