diff --git a/library/kernel/file-system-harness.zig b/library/kernel/file-system-harness.zig index dce1706..52c3a23 100644 --- a/library/kernel/file-system-harness.zig +++ b/library/kernel/file-system-harness.zig @@ -61,10 +61,14 @@ pub fn Server(comptime Engine: type) type { /// caller does the filesystem-specific bring-up (find the block /// device, set up DMA, mount the engine) and returns a `Volume`. bringUp: *const fn (endpoint: ipc.Handle) ?Volume, - /// The vfs contract name to bind. A filesystem serving one volume - /// binds "vfs" today; the volume-manager era hands each per-volume - /// process its own establishment and this fades. - service_name: ?[]const u8 = "vfs", + /// A contract name to bind under /protocol, or null to bind none. In + /// the volume-manager era every filesystem is a per-volume process and + /// clients reach it through the kernel mount table — fs_resolve routes + /// a path to its backing endpoint by prefix — so no filesystem binds a + /// shared name. Two volumes would collide on one: the second's bind is + /// refused and service.run would exit, so its volume never mounts. The + /// endpoint still serves as the mount backend without a name. + service_name: ?[]const u8 = null, }; // --- the harness's own state, one set per instantiation --------------- diff --git a/system/services/fat/fat.zig b/system/services/fat/fat.zig index fea3bc2..13b18ac 100644 --- a/system/services/fat/fat.zig +++ b/system/services/fat/fat.zig @@ -188,6 +188,8 @@ fn fatBringUp(endpoint: ipc.Handle) ?Harness.Volume { mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" }; mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" }; mount_count = 3; + } else { + std.log.info("volume {d} is a data volume; mounted at {s}", .{ my_volume_id, volume_mount_prefix }); } return .{ .engine = &filesystem, .mounts = mount_specs[0..mount_count], .flush = flushIfDirty }; } diff --git a/test/qemu_test.py b/test/qemu_test.py index 0d32956..7db229c 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -816,6 +816,27 @@ CASES = [ "expect": r"volume-manager: volume 0x0*12345678 -> \S+ \(pid \d+\), lba \d+, \d+ blocks" r"[\s\S]*volume-manager: handed volume \d+ to pid \d+", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # S3 multi-volume: a SECOND usb-storage device (a generated data volume, serial + # da7a0001, an empty FAT with no /system) plugged in beside the boot volume. + # Proves the volume manager adopts BOTH devices and spawns a confined fat per + # volume, each mounted at its own CONTENT id-path (/volumes/fat-); and + # that boot-volume detection is by content — only the volume that carries + # /system backs /system/configuration, while the data volume mounts at its + # id-path alone. Against the pre-S3 one-device/one-volume manager the data + # volume never mounts, so the da7a0001 lookaheads fail (toggle-demonstrated by + # checking out the step-2 volume-manager.zig). + {"name": "two-volumes", + "build_case": "fat-mount", + "smp": 4, + "timeout": 150, + "data_volume": {"serial": "DA7A0001", "label": "DATAVOL", "size_mib": 64}, + "expect": r"(?s)(?=.*volume-manager: volume 0x0*12345678 -> )" + r"(?=.*volume-manager: volume 0x0*da7a0001 -> )" + r"(?=.*fat: mounted /volumes/fat-12345678)" + r"(?=.*fat: mounted /volumes/fat-da7a0001)" + r"(?=.*carries the system tree)" + r"(?=.*data volume; mounted at /volumes/fat-da7a0001)", + "fail": r"data volume; mounted at /volumes/fat-12345678|DANOS-TEST-RESULT: FAIL"}, # Phase 2b: mkdir/unlink through the mount. Reuses the fat-mount build — the # fat-test client, after listing, makes a directory, writes+reads a file inside # it, then removes the file, exercising the whole VFS -> fat mutation path. @@ -1422,6 +1443,23 @@ def run_case(arch, case): cmd[cmd.index("-m") + 1] = case["mem"] if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case cmd += case["qemu_extra"] + # A multi-volume case attaches a second usb-storage device backed by a freshly + # GENERATED data volume: a distinct-serial FAT32 with no /system tree, so the + # volume manager mounts it at its own id-path and the fat process marks it a + # data volume (never a system volume). Regenerated per run — no image is + # committed to the tree (the user keeps the boot files copyable, not baked in). + if case.get("data_volume"): + dv = case["data_volume"] + data_img = os.path.join(WORK, "data-volume.img") + subprocess.run( + [sys.executable, os.path.join(REPO, "tools", "make-fat-image.py"), + "--serial", dv["serial"], "--label", dv.get("label", "DATAVOL"), + data_img, str(dv.get("size_mib", 64))], + check=True, stdout=subprocess.DEVNULL) + cmd += [ + "-drive", f"if=none,id=datausb,format=raw,file={data_img}", + "-device", "usb-storage,bus=xhci.0,port=4,drive=datausb,removable=on,id=datastorage", + ] # A QMP control socket, always present (additive): how a case's `qmp_after` # hook injects host-side events into the guest mid-run. Kept under a short temp # dir, not WORK: a unix socket path is capped at ~104 bytes (sun_path), and a diff --git a/tools/make-fat-image.py b/tools/make-fat-image.py index 7e85bf1..b3f05eb 100644 --- a/tools/make-fat-image.py +++ b/tools/make-fat-image.py @@ -47,8 +47,14 @@ def fat32_geometry(total_sectors): class Fat32Image: - def __init__(self, total_sectors): + def __init__(self, total_sectors, volume_id=0x12345678, label="DANOS"): self.total_sectors = total_sectors + # The FAT volume serial (its content identity — the /volumes/fat- + # mount path danos derives from it) and the display label. A second image + # needs a distinct serial so its id-path does not collide with the boot + # volume's. + self.volume_id = volume_id & 0xFFFFFFFF + self.label = label self.fat_size, self.cluster_count = fat32_geometry(total_sectors) if self.cluster_count < 65525: sys.exit(f"error: image too small for FAT32 ({self.cluster_count} clusters " @@ -146,8 +152,8 @@ class Fat32Image: 0x80, # drive number 0, # reserved 0x29, # extended boot signature - 0x12345678, # volume id - b"DANOS ", # volume label + self.volume_id, # volume id + self.label.encode("ascii", "replace")[:11].ljust(11, b" "), # volume label b"FAT32 ", # filesystem type ) sector[510] = 0x55 @@ -276,9 +282,9 @@ def build_tree(pairs): return root -def build(out_path, size_mib, pairs): +def build(out_path, size_mib, pairs, volume_id=0x12345678, label="DANOS"): total_sectors = size_mib * 1024 * 1024 // SECTOR - image = Fat32Image(total_sectors) + image = Fat32Image(total_sectors, volume_id, label) tree = build_tree(pairs) write_directory(image, 2, tree, 0, True) with open(out_path, "wb") as handle: @@ -350,14 +356,32 @@ def main(argv): if len(argv) == 3 and argv[1] == "--verify": verify(argv[2]) return 0 + # Optional flags ahead of the positionals: --serial sets the FAT volume + # id (the /volumes/fat- content identity), --label its display + # label. A second FAT image passes a distinct --serial so its id-path cannot + # collide with the boot volume's. + argv = list(argv) + volume_id = 0x12345678 + label = "DANOS" + i = 1 + while i < len(argv): + if argv[i] == "--serial" and i + 1 < len(argv): + volume_id = int(argv[i + 1], 16) + del argv[i:i + 2] + elif argv[i] == "--label" and i + 1 < len(argv): + label = argv[i + 1] + del argv[i:i + 2] + else: + i += 1 if len(argv) < 3 or (len(argv) - 3) % 2 != 0: - sys.exit("usage: make-fat-image.py [ ]...\n" + sys.exit("usage: make-fat-image.py [--serial ] [--label ] " + " [ ]...\n" " make-fat-image.py --verify ") out_path = argv[1] size_mib = int(argv[2]) rest = argv[3:] pairs = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)] - build(out_path, size_mib, pairs) + build(out_path, size_mib, pairs, volume_id, label) return 0