block: medium presence — the medium_changed event and usb-storage as publisher (V2b)
The block protocol gains a pushed medium_changed event (present + a monotonic change counter; presence only, never content). usb-storage becomes a Subscribers provider and runs a slow TEST UNIT READY poll (1 s): success is present, failure absent, and a transition bumps the counter and publishes. This is the second removal trigger — the DEVICE stays while the MEDIUM leaves (card readers, ATAPI trays) — which channel death cannot see (storage-architecture.md, two triggers one lifecycle). The subscriber is the volume manager (V3); until it exists the publish is a no-op fan-out, so this commit is behaviour-neutral, and its end-to-end test (eject -> medium_changed -> unmount/remount) lands in V4 with the real consumer rather than a throwaway subscriber fixture (recorded sequencing). Sense-key inspection to tell medium-absent from other transport errors is a noted refinement; a clean eject reads correctly as not-ready. Neutral: 12/12 across the block-serving surface, restart, confinement, conformance, and logging.
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user