diff --git a/library/protocol/block/block-protocol.zig b/library/protocol/block/block-protocol.zig index b1fca3b..43e3e2c 100644 --- a/library/protocol/block/block-protocol.zig +++ b/library/protocol/block/block-protocol.zig @@ -57,9 +57,27 @@ pub const DefineRange = extern struct { block_count: u64, }; +/// The `medium_changed` event payload: whether a medium is now present, and a +/// monotonic counter so a subscriber that missed an edge still sees that +/// SOMETHING changed. Pushed by a driver whose transport can tell medium from +/// device (a card reader, an ATAPI tray): the device stays, the medium comes and +/// goes. The volume manager consumes it into the same unmount/remount path it +/// runs on device death — one lifecycle, two triggers +/// (docs/file-system-development/storage-architecture.md). Presence only, never +/// content: the driver reports that the medium changed, not what is on it. +pub const MediumChanged = extern struct { + present: u8, // 1 present, 0 absent + _padding: u8 = 0, + _padding2: u16 = 0, + change_count: u32, +}; + pub const Protocol = envelope.Define(.{ .name = "block", .version = 1, + .events = &.{ + .{ .name = "medium_changed", .payload = MediumChanged }, + }, .operations = &.{ .{ .name = "geometry", .reply = Geometry }, .{ .name = "read", .request = Transfer, .reply = Transferred }, @@ -88,4 +106,5 @@ pub const Protocol = envelope.Define(.{ }); pub const Operation = Protocol.Operation; +pub const Event = Protocol.Event; pub const message_maximum: usize = Protocol.message_maximum; diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig index 4c3e53d..99c1b20 100644 --- a/system/drivers/usb-storage/usb-storage.zig +++ b/system/drivers/usb-storage/usb-storage.zig @@ -25,9 +25,11 @@ const bot = @import("bulk-only-transport.zig"); const envelope = @import("envelope"); const block_protocol = @import("block-protocol"); -/// The generated block dispatch. One device per process, so the handler context -/// is empty and the geometry stays in this file's globals. -const Serve = block_protocol.Protocol.Provider(void); +/// The generated block dispatch plus the subscriber machinery the harness owns +/// (subscribe/unsubscribe, the exit sweep, the fan-out) — usb-storage publishes +/// `medium_changed`, so it is a Subscribers provider, not a bare Provider. One +/// device per process, so the handler context is empty. +const Serve = service.Subscribers(block_protocol.Protocol, void); const Invocation = envelope.Invocation; const Answer = envelope.Answer; @@ -47,6 +49,21 @@ var next_tag: u32 = 1; var block_size: u32 = 512; var block_count: u64 = 0; +// --- medium presence -------------------------------------------------------- +// +// A slow TEST UNIT READY poll tracks whether the medium is present; on a +// transition the driver publishes `medium_changed` to its subscribers (the +// volume manager). This is the second removal trigger — the DEVICE stays while +// the MEDIUM leaves (a card reader, an ATAPI tray) — which channel death cannot +// see (docs/file-system-development/storage-architecture.md). Presence only, +// never content. A device that is genuinely unplugged is reaped by the device +// manager instead; a poll failure just before that death publishes absent +// harmlessly. +var service_endpoint: ipc.Handle = 0; +var medium_present: bool = true; // a successful bring-up means the medium is here +var medium_change_count: u32 = 0; +const presence_poll_ms = 1000; + /// One Bulk-Only-Transport command: send the CBW, run the data stage (to/from /// `data_physical`), read and validate the CSW. Returns true on a passed status. fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length: u32) bool { @@ -82,6 +99,7 @@ fn transact(cdb: []const u8, direction_in: bool, data_physical: u64, data_length var bring_up_failed = false; fn initialise(endpoint: ipc.Handle) bool { + service_endpoint = endpoint; // One hello, both directions: the block-serving endpoint goes UP (the // manager routes fat's consumer hello here — this driver serves one // volume, one process per stick, so `block` is never a registry name), @@ -154,6 +172,8 @@ fn initialise(endpoint: ipc.Handle) bool { const sector: [*]const u8 = @ptrFromInt(command_data.virtual); std.log.info("block 0 signature 0x{x:0>2}{x:0>2}", .{ sector[510], sector[511] }); } + // Bring-up succeeded, so the medium is present; start the presence poll. + _ = time.timerOnce(endpoint, presence_poll_ms); return true; } @@ -283,9 +303,34 @@ const handlers = Serve.Handlers{ }; fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize { - // Peeked, never taken: `attach` forwards the capability and the controller's - // binding takes its own reference, so this copy stays the turn's to close. - return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply); + // The harness peeks the arrival and takes it only if a handler (subscribe) + // claimed it; attach/detach forward the capability without claiming, so the + // turn still closes their copy after the controller took its own reference. + return Serve.dispatch({}, handlers, message, sender, arrived, reply); +} + +/// A slow TEST UNIT READY poll: success means the medium is present, failure +/// means it is not. On a transition, bump the counter and publish. (Sense-key +/// inspection to tell "medium absent" from other transport errors is a +/// refinement; a clean eject — what QEMU and a card reader produce — makes +/// TEST UNIT READY report not-ready, which this reads correctly.) +fn pollPresence() void { + const ready = scsi.testUnitReady(); + const now = transact(&ready, false, 0, 0); + if (now == medium_present) return; + medium_present = now; + medium_change_count +%= 1; + std.log.info("medium {s}", .{if (now) "present" else "absent"}); + Serve.publish(.medium_changed, 0, .{ .present = @intFromBool(now), .change_count = medium_change_count }); +} + +fn onNotification(badge: u64) void { + const got = ipc.Received{ .len = 0, .badge = badge, .cap = null }; + if (got.isTimer()) { + pollPresence(); + _ = time.timerOnce(service_endpoint, presence_poll_ms); + } + // Subscriber deaths are swept by the harness (Serve.hooks); nothing else here. } pub fn main(init: process.Init) void { @@ -303,6 +348,8 @@ pub fn main(init: process.Init) void { service.run(block_protocol.message_maximum, .{ .init = initialise, .on_message = onMessage, + .on_notification = onNotification, + .subscribers = Serve.hooks, }); // A failure exit (nonzero -> .aborted) tells the device manager to restart // us with backoff; a clean return means there was nothing to serve.