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:
@@ -57,9 +57,27 @@ pub const DefineRange = extern struct {
|
|||||||
block_count: u64,
|
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(.{
|
pub const Protocol = envelope.Define(.{
|
||||||
.name = "block",
|
.name = "block",
|
||||||
.version = 1,
|
.version = 1,
|
||||||
|
.events = &.{
|
||||||
|
.{ .name = "medium_changed", .payload = MediumChanged },
|
||||||
|
},
|
||||||
.operations = &.{
|
.operations = &.{
|
||||||
.{ .name = "geometry", .reply = Geometry },
|
.{ .name = "geometry", .reply = Geometry },
|
||||||
.{ .name = "read", .request = Transfer, .reply = Transferred },
|
.{ .name = "read", .request = Transfer, .reply = Transferred },
|
||||||
@@ -88,4 +106,5 @@ pub const Protocol = envelope.Define(.{
|
|||||||
});
|
});
|
||||||
|
|
||||||
pub const Operation = Protocol.Operation;
|
pub const Operation = Protocol.Operation;
|
||||||
|
pub const Event = Protocol.Event;
|
||||||
pub const message_maximum: usize = Protocol.message_maximum;
|
pub const message_maximum: usize = Protocol.message_maximum;
|
||||||
|
|||||||
@@ -25,9 +25,11 @@ const bot = @import("bulk-only-transport.zig");
|
|||||||
const envelope = @import("envelope");
|
const envelope = @import("envelope");
|
||||||
const block_protocol = @import("block-protocol");
|
const block_protocol = @import("block-protocol");
|
||||||
|
|
||||||
/// The generated block dispatch. One device per process, so the handler context
|
/// The generated block dispatch plus the subscriber machinery the harness owns
|
||||||
/// is empty and the geometry stays in this file's globals.
|
/// (subscribe/unsubscribe, the exit sweep, the fan-out) — usb-storage publishes
|
||||||
const Serve = block_protocol.Protocol.Provider(void);
|
/// `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 Invocation = envelope.Invocation;
|
||||||
const Answer = envelope.Answer;
|
const Answer = envelope.Answer;
|
||||||
@@ -47,6 +49,21 @@ var next_tag: u32 = 1;
|
|||||||
var block_size: u32 = 512;
|
var block_size: u32 = 512;
|
||||||
var block_count: u64 = 0;
|
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
|
/// 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.
|
/// `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 {
|
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;
|
var bring_up_failed = false;
|
||||||
|
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
|
service_endpoint = endpoint;
|
||||||
// One hello, both directions: the block-serving endpoint goes UP (the
|
// One hello, both directions: the block-serving endpoint goes UP (the
|
||||||
// manager routes fat's consumer hello here — this driver serves one
|
// manager routes fat's consumer hello here — this driver serves one
|
||||||
// volume, one process per stick, so `block` is never a registry name),
|
// 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);
|
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] });
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,9 +303,34 @@ const handlers = Serve.Handlers{
|
|||||||
};
|
};
|
||||||
|
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
// Peeked, never taken: `attach` forwards the capability and the controller's
|
// The harness peeks the arrival and takes it only if a handler (subscribe)
|
||||||
// binding takes its own reference, so this copy stays the turn's to close.
|
// claimed it; attach/detach forward the capability without claiming, so the
|
||||||
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
// 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 {
|
pub fn main(init: process.Init) void {
|
||||||
@@ -303,6 +348,8 @@ pub fn main(init: process.Init) void {
|
|||||||
service.run(block_protocol.message_maximum, .{
|
service.run(block_protocol.message_maximum, .{
|
||||||
.init = initialise,
|
.init = initialise,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
|
.on_notification = onNotification,
|
||||||
|
.subscribers = Serve.hooks,
|
||||||
});
|
});
|
||||||
// A failure exit (nonzero -> .aborted) tells the device manager to restart
|
// A failure exit (nonzero -> .aborted) tells the device manager to restart
|
||||||
// us with backoff; a clean return means there was nothing to serve.
|
// us with backoff; a clean return means there was nothing to serve.
|
||||||
|
|||||||
Reference in New Issue
Block a user