Files
danos/library/device/block/block.zig
T
Daniel Samson 700452dc4e volume-manager: consume medium_changed — the second removal trigger (S5)
The block client gains subscribeMedium / unsubscribeMedium /
decodeMediumChanged (the reserved subscribe/unsubscribe verbs plus the
MediumChanged decode), so a consumer never hand-rolls the wire format.
The volume manager subscribes to each device it adopts and consumes the
event through the new on_buffered_message seam — never the protocol
dispatch, whose op numbers collide with the manager's own hello.

A medium leaving while its device stays in the tree (a card reader, an
eject) now runs the SAME kill-retire-remount path as a pulled stick:
absent retires the volume, present re-probes it. That closes the "two
triggers, one lifecycle" the architecture specifies — device-presence
polling alone could never see a medium leave under a present device.
dropDevice unsubscribes before closing so the driver's bounded
subscriber table frees the slot; on a dead channel (a real pull) the
call fails fast, proven by volume-removal still passing.

New volume-medium-change case: eject the medium (not the device) ->
"medium absent" -> the manager unmounts. Fails against a pre-S5 manager
that never subscribed. Suite 132/132 (device-authority is the known
child-cleanup flake, green on rerun).
2026-08-10 04:58:54 +01:00

134 lines
7.0 KiB
Zig

//! 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 envelope = @import("envelope");
const ipc = @import("ipc");
const block_protocol = @import("block-protocol");
const Protocol = block_protocol.Protocol;
/// The medium_changed event payload, re-exported so a consumer decodes it without
/// reaching into the wire-format module.
pub const MediumChanged = block_protocol.MediumChanged;
/// Decode a medium_changed event from a buffered-message payload a subscriber
/// received (a `Received.isMessage` wake). Null if the bytes are too short to be
/// one — a caller ignores anything that is not a well-formed event.
pub fn decodeMediumChanged(payload: []const u8) ?MediumChanged {
if (payload.len < envelope.prefix_size + @sizeOf(MediumChanged)) return null;
return std.mem.bytesToValue(MediumChanged, payload[envelope.prefix_size..][0..@sizeOf(MediumChanged)]);
}
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 reply: [block_protocol.message_maximum]u8 = undefined;
const answered = self.call(.geometry, {}, null, &reply) orelse return null;
const result = Protocol.decodeReply(.geometry, answered) orelse 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 reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.attach, {}, handle, &reply) != null;
}
/// The reverse of `attach`: the buffer leaves the device's reach. The same
/// region capability rides again (the kernel matches the region). Do not name
/// the buffer's physical address in `read`/`write` after this.
pub fn detach(self: Device, handle: ipc.Handle) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.detach, {}, handle, &reply) != null;
}
/// Read `count` blocks starting at `lba` into the DMA buffer at `physical`.
pub fn read(self: Device, lba: u64, count: u32, physical: u64) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.read, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
}
/// Write `count` blocks starting at `lba` from the DMA buffer at `physical`.
pub fn write(self: Device, lba: u64, count: u32, physical: u64) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.write, .{ .lba = lba, .count = count, .physical = physical }, null, &reply) != null;
}
/// 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 {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.flush, {}, null, &reply) != null;
}
/// Confine the process `badge` to blocks `[base_lba, base_lba + block_count)`
/// on this device — the volume manager's per-volume grant to a filesystem.
/// The caller must itself be unconfined (whole-device); a confined caller is
/// refused, so a filesystem cannot widen its own range. See `DefineRange`.
pub fn defineRange(self: Device, badge: u32, base_lba: u64, block_count: u64) bool {
var reply: [block_protocol.message_maximum]u8 = undefined;
return self.call(.define_range, .{ .badge = badge, .base_lba = base_lba, .block_count = block_count }, null, &reply) != null;
}
/// One request at the driver. `target` is always 0: one endpoint per device, so
/// there is no object within the peer to address.
fn call(
self: Device,
comptime operation: Protocol.Operation,
request: Protocol.RequestOf(operation),
capability: ?ipc.Handle,
reply: []u8,
) ?[]u8 {
var packet: [block_protocol.message_maximum]u8 = undefined;
const framed = Protocol.encodeRequest(operation, 0, request, &.{}, &packet) orelse return null;
const answer = ipc.callCap(self.endpoint, framed, reply, capability) catch return null;
const status = envelope.statusOf(reply[0..answer.len]) orelse return null;
if (status.status != 0) return null;
return reply[0..answer.len];
}
/// Subscribe `subscriber` (an endpoint) to this device's medium_changed
/// events: the reserved `subscribe` verb carries the subscriber's endpoint as
/// the capability, and the driver then ipc.sends each medium transition to it.
pub fn subscribeMedium(self: Device, subscriber: ipc.Handle) bool {
var packet: [block_protocol.message_maximum]u8 = undefined;
const framed = envelope.encodeSubscribe(0, &packet) orelse return false; // interest 0: every event (block has one)
var reply: [block_protocol.message_maximum]u8 = undefined;
const answer = ipc.callCap(self.endpoint, framed, &reply, subscriber) catch return false;
const status = envelope.statusOf(reply[0..answer.len]) orelse return false;
return status.status == 0;
}
/// Unsubscribe from this device's medium_changed events. Call before closing
/// the channel so the driver's bounded subscriber table frees the slot rather
/// than holding a dead endpoint until an exit sweep notices.
pub fn unsubscribeMedium(self: Device) bool {
var packet: [block_protocol.message_maximum]u8 = undefined;
const framed = envelope.encodeUnsubscribe(&packet) orelse return false;
var reply: [block_protocol.message_maximum]u8 = undefined;
const answer = ipc.callCap(self.endpoint, framed, &reply, null) catch return false;
const status = envelope.statusOf(reply[0..answer.len]) orelse return false;
return status.status == 0;
}
};
// There is deliberately no open-by-name here: `block` is not a registry name.
// One storage process serves each volume, and a consumer receives its volume's
// channel from the device manager (establishment by lineage, communication.md
// "Establishment: two planes"), then wraps it: `block.Device{ .endpoint = c }`.