//! Block-device client: the helper a filesystem uses to read and write a block //! device (a USB stick, via usb-storage) without hand-rolling the block-protocol //! IPC. Layered over `ipc` and the shared `block-protocol` wire format, like //! `runtime.usb` over the transfer protocol. //! //! Transfers name a caller-owned DMA buffer by physical address (from //! `runtime.dma.alloc`), so whole sectors move without crossing the IPC size //! limit — the same handoff usb-storage uses toward the controller. const std = @import("std"); const ipc = @import("ipc"); const time = @import("time"); const block_protocol = @import("block-protocol"); pub const Geometry = struct { block_size: u32, block_count: u64 }; pub const Device = struct { endpoint: ipc.Handle, /// The device's block size and total block count. pub fn geometry(self: Device) ?Geometry { var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.geometry), .lba = 0, .count = 0, .physical = 0 }; var reply: [block_protocol.reply_size]u8 = undefined; const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return null; if (n < block_protocol.reply_size) return null; const result = std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]); if (result.status != 0) return null; return .{ .block_size = result.block_size, .block_count = result.block_count }; } /// Hand the block server a DMA-region capability (`handle` — from a `shareable` /// dma_alloc) so it forwards it to the controller and the buffer's physical /// addresses become reachable by the device. Call once per buffer before naming it /// in `read`/`write`. Harmless success when no IOMMU is enforcing. pub fn attach(self: Device, handle: ipc.Handle) bool { var request = block_protocol.Request{ .operation = @intFromEnum(block_protocol.Operation.attach), .lba = 0, .count = 0, .physical = 0 }; var reply: [block_protocol.reply_size]u8 = undefined; const result = ipc.callCap(self.endpoint, std.mem.asBytes(&request), &reply, handle) catch return false; if (result.len < block_protocol.reply_size) return false; return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0; } /// Read `count` blocks starting at `lba` into the DMA buffer at `physical`. pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool { return self.transfer(.read, lba, count, physical); } /// Write `count` blocks starting at `lba` from the DMA buffer at `physical`. pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool { return self.transfer(.write, lba, count, physical); } /// Commit any device write cache to stable media (SCSI SYNCHRONIZE CACHE), so /// prior writes survive a power-off. A filesystem calls this before the machine /// goes down; no data transfer, so the buffer arguments are unused. pub fn flush(self: Device) bool { return self.transfer(.flush, 0, 0, 0); } fn transfer(self: Device, operation: block_protocol.Operation, lba: u64, count: u32, physical: u64) bool { var request = block_protocol.Request{ .operation = @intFromEnum(operation), .lba = lba, .count = count, .physical = physical }; var reply: [block_protocol.reply_size]u8 = undefined; const n = ipc.call(self.endpoint, std.mem.asBytes(&request), &reply) catch return false; if (n < block_protocol.reply_size) return false; return std.mem.bytesToValue(block_protocol.Reply, reply[0..block_protocol.reply_size]).status == 0; } }; /// One lookup attempt, no waiting — for a server that retries on its own /// timer (the fat service) instead of blocking its harness in here. pub fn tryOpen() ?Device { if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle }; return null; } /// Look up the block device, retrying generously while the USB storage chain /// (controller reset, enumeration, mass-storage bring-up) comes up. pub fn open() ?Device { // Patient: the whole USB storage chain (firmware discovery, xHCI reset and // enumeration, mass-storage bring-up) must complete first, which can take // tens of seconds under emulation. var attempts: usize = 0; // 30 s covers the slowest observed healthy chain (a flaky QEMU enumeration // completed at ~24 s); a machine whose stick genuinely failed setup should // not sit a further minute pretending otherwise. while (attempts < 600) : (attempts += 1) { if (ipc.lookup(.block)) |handle| return .{ .endpoint = handle }; time.sleepMillis(50); } return null; }