diff --git a/library/device/driver/driver.zig b/library/device/driver/driver.zig index a2ac57b..45c6c37 100644 --- a/library/device/driver/driver.zig +++ b/library/device/driver/driver.zig @@ -269,6 +269,25 @@ pub const Exchange = struct { channel: ?ipc.Handle, }; +/// A consumer's whole establishment step: hello until the channel to this +/// device's provider arrives. The manager acks a hello whose provider is not +/// there yet (mid-restart, re-report on the way) with no channel — retryable +/// by design — so this re-hellos on the ONE manager handle, on the same +/// cadence the old name lookup used, and gives up on a refusal or a vanished +/// manager. Re-hello is benign: the manager just re-marks the entry running. +pub fn helloForChannel(role: Role, device_id: u64) ?ipc.Handle { + const first = helloExchange(role, device_id, null, true) orelse return null; + if (first.channel) |bus| return bus; + var attempts: u32 = 0; + while (attempts < lookup_attempts) : (attempts += 1) { + time.sleepMillis(lookup_pause_ms); + const again = exchangeOn(first.manager, role, device_id, null, true) orelse return null; + if (again.channel) |bus| return bus; + } + std.log.info("no provider channel for device {d}", .{device_id}); + return null; +} + /// The full handshake (communication.md "Establishment: two planes, one /// namespace"): a provider hands `serving` — the endpoint its consumers will /// be routed to — up with the request; a consumer sets `want_channel` and @@ -284,7 +303,12 @@ pub fn helloExchange(role: Role, device_id: u64, serving: ?ipc.Handle, want_chan std.log.info("no device manager to hello", .{}); return null; }; + return exchangeOn(manager, role, device_id, serving, want_channel); +} +/// One hello on an already-open manager handle — the exchange without the +/// lookup, so a retry loop never spends a handle-table slot per attempt. +fn exchangeOn(manager: ipc.Handle, role: Role, device_id: u64, serving: ?ipc.Handle, want_channel: bool) ?Exchange { var packet: [device_manager_protocol.message_maximum]u8 = undefined; const framed = device_manager_protocol.Protocol.encodeRequest( .hello, diff --git a/library/device/usb/usb.zig b/library/device/usb/usb.zig index d3aa34a..b620d07 100644 --- a/library/device/usb/usb.zig +++ b/library/device/usb/usb.zig @@ -5,12 +5,16 @@ //! service and `device.zig` over the raw device calls. //! //! A class driver, spawned with its interface's assigned device id as argv[1]: -//! if (device_manager.hello(.device, id) == null) return; // meet the spawn deadline -//! var device = usb.open(id) orelse return; // open + get its endpoints +//! const bus = device_manager.helloForChannel(.device, id) orelse return; +//! var device = usb.open(bus, id) orelse return; // open + get its endpoints //! _ = device.controlOut(usb_abi.setProtocol(...));// class requests, descriptors //! _ = device.subscribeInterrupt(address, length); // reports arrive asynchronously //! while (true) { ... ipc.replyWait(device.endpoint, ...) ... } // its own loop //! +//! The bus channel arrives from the device manager's hello — routed to THIS +//! device's controller by lineage — never from a registry name; several bus +//! processes provide this contract on a multi-controller machine. +//! //! Reports are delivered to `device.endpoint` as asynchronous `interrupt_report` //! event packets, decoded with `reportOf` (the class driver runs a bare `replyWait` //! loop to read them, because the service harness drops buffered-message payloads @@ -21,10 +25,8 @@ //! a control transfer's data stage in the tail. const std = @import("std"); -const channel = @import("channel"); const envelope = @import("envelope"); const ipc = @import("ipc"); -const time = @import("time"); const usb_transfer_protocol = @import("usb-transfer-protocol"); const Protocol = usb_transfer_protocol.Protocol; @@ -157,18 +159,14 @@ pub fn reportOf(packet: []const u8) ?InterruptReport { return Protocol.decodeEvent(.interrupt_report, packet); } -/// Open `/protocol/usb-transfer` and, on that channel, open the device with the -/// assigned id, handing over a freshly created endpoint for asynchronous interrupt -/// reports. Retries while the bus is still coming up (a class driver races the bus -/// driver at boot). Two opens, deliberately: the first names the contract, the -/// second names an object within it. -pub fn open(device_id: u64) ?Device { - var attempts: usize = 0; - const bus = while (attempts < 100) : (attempts += 1) { - if (channel.openEndpoint("usb-transfer")) |handle| break handle; - time.sleepMillis(20); - } else return null; - +/// On `bus` — the channel to this device's own controller, handed to the class +/// driver by the device manager's hello (establishment by lineage, +/// communication.md "Establishment: two planes") — open the device with the +/// assigned id, handing over a freshly created endpoint for asynchronous +/// interrupt reports. The channel is never found by name: one machine carries +/// several controllers, several processes provide this contract, and only the +/// manager knows which one reported this device. +pub fn open(bus: ipc.Handle, device_id: u64) ?Device { const endpoint = ipc.createIpcEndpoint() orelse return null; // The assigned device id is the target: it is what the caller has before a // token exists, and the token the reply hands back addresses every packet diff --git a/system/configuration/protocol.csv b/system/configuration/protocol.csv index 9ba60e7..13123e5 100644 --- a/system/configuration/protocol.csv +++ b/system/configuration/protocol.csv @@ -63,8 +63,11 @@ /system/services/discovery, /system/services/device-manager, bind, power # --- the drivers, which the device manager spawns --------------------------- +# usb-transfer has NO bind row: several bus processes provide it (one per +# controller), so it is never a registry name — consumers get their controller's +# channel from the device manager's hello, routed by lineage (communication.md +# "Establishment: two planes, one namespace"). /system/drivers/ps2-bus, /system/services/device-manager, bind, ps2-bus -/system/drivers/usb-xhci-bus, /system/services/device-manager, bind, usb-transfer /system/drivers/usb-storage, /system/services/device-manager, bind, block /system/drivers/virtio-gpu, /system/services/device-manager, bind, scanout @@ -116,10 +119,7 @@ # to the compositor. /system/drivers/*, /system/services/device-manager, open, device-manager /system/services/discovery, /system/services/device-manager, open, device-manager -/system/drivers/usb-storage, /system/services/device-manager, open, usb-transfer -/system/drivers/usb-hid-keyboard, /system/services/device-manager, open, usb-transfer /system/drivers/usb-hid-keyboard, /system/services/device-manager, open, input -/system/drivers/usb-hid-mouse, /system/services/device-manager, open, usb-transfer /system/drivers/usb-hid-mouse, /system/services/device-manager, open, input /system/drivers/virtio-gpu, /system/services/device-manager, open, display diff --git a/system/drivers/usb-hid/keyboard.zig b/system/drivers/usb-hid/keyboard.zig index 0416288..30b295e 100644 --- a/system/drivers/usb-hid/keyboard.zig +++ b/system/drivers/usb-hid/keyboard.zig @@ -75,9 +75,10 @@ pub fn main(init: process.Init) void { }; const layout = xkb.byName(init.arguments.get(2) orelse "us") orelse xkb.us; - // Hello the manager first (meet the spawn deadline), then open the device. - if (device_manager.hello(.device, device_id) == null) return; - var device = usb.open(device_id) orelse { + // The hello both meets the spawn deadline and brings back the channel to + // THIS device's controller — routed by lineage, never found by name. + const bus = device_manager.helloForChannel(.device, device_id) orelse return; + var device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return; }; diff --git a/system/drivers/usb-hid/mouse.zig b/system/drivers/usb-hid/mouse.zig index beb0a64..a214f76 100644 --- a/system/drivers/usb-hid/mouse.zig +++ b/system/drivers/usb-hid/mouse.zig @@ -41,8 +41,10 @@ pub fn main(init: process.Init) void { return; }; - if (device_manager.hello(.device, device_id) == null) return; - var device = usb.open(device_id) orelse { + // The hello both meets the spawn deadline and brings back the channel to + // THIS device's controller — routed by lineage, never found by name. + const bus = device_manager.helloForChannel(.device, device_id) orelse return; + var device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return; }; diff --git a/system/drivers/usb-storage/usb-storage.zig b/system/drivers/usb-storage/usb-storage.zig index 5db245e..2a1810f 100644 --- a/system/drivers/usb-storage/usb-storage.zig +++ b/system/drivers/usb-storage/usb-storage.zig @@ -83,8 +83,10 @@ var bring_up_failed = false; fn initialise(endpoint: ipc.Handle) bool { _ = endpoint; - if (device_manager.hello(.device, device_id) == null) return false; - device = usb.open(device_id) orelse { + // The hello both meets the spawn deadline and brings back the channel to + // THIS device's controller — routed by lineage, never found by name. + const bus = device_manager.helloForChannel(.device, device_id) orelse return false; + device = usb.open(bus, device_id) orelse { std.log.info("could not open device {d}", .{device_id}); return false; }; diff --git a/system/drivers/usb-xhci-bus/build.zig b/system/drivers/usb-xhci-bus/build.zig index 216dfa3..c3826e5 100644 --- a/system/drivers/usb-xhci-bus/build.zig +++ b/system/drivers/usb-xhci-bus/build.zig @@ -10,10 +10,10 @@ pub fn build(b: *std.Build) void { .name = "usb-xhci-bus", .root_source_file = b.path("usb-xhci-bus.zig"), .imports = &.{ - "channel", "device-manager-protocol", "driver", "envelope", - "input-client", "ipc", "logging", "memory", - "mmio", "pci", "process", "service", - "time", "usb-abi", "usb-ids", "usb-transfer-protocol", + "device-manager-protocol", "driver", "envelope", "ipc", + "logging", "memory", "mmio", "pci", + "process", "service", "time", "usb-abi", + "usb-ids", "usb-transfer-protocol", }, }); b.installArtifact(exe); diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index 8fcb5fb..c6bbcf2 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -15,12 +15,10 @@ const std = @import("std"); const device = @import("driver"); -const channel = @import("channel"); const ipc = @import("ipc"); const process = @import("process"); const service = @import("service"); const time = @import("time"); -const input = @import("input-client"); const device_manager = @import("driver"); const memory = @import("memory"); const logging = @import("logging"); @@ -158,37 +156,27 @@ fn initialise(endpoint: ipc.Handle) bool { // its device open, or its restarted instance could never claim it back. _ = process.subscribeExits(endpoint); - // The transfer contract, bound by hand rather than through the harness's - // `.service`, because **losing it is not fatal here**. One machine can carry - // several xHCI controllers and the driver model spawns one process per - // controller, so several processes provide the same contract for different - // hardware — and `/protocol` holds exactly one name, deliberately (addressing - // lives inside the protocol, never in the path). Whoever binds first is the - // one clients reach by name; a later instance still owns its controller, - // enumerates its bus, and reports its children to the device manager, so it - // keeps running. **Known gap:** a class driver behind a second controller - // cannot reach it — the transfer protocol has no controller field for - // `target`, and the fix is either one process multiplexing every controller - // or the spawner wiring the child's channel (P5), not a second name. - if (!channel.bindPatiently("usb-transfer", endpoint)) - _ = logging.write("/system/drivers/usb-xhci-bus: /protocol/usb-transfer is another controller's; serving mine unnamed\n"); - - // **The handshake comes first, because it is where the device arrives.** This - // driver used to claim `controller_id` here — first-come-first-served, so the - // manager's matching was advisory and any process could have claimed it by - // passing the same integer. Now the manager holds the controller and transfers - // it in `onHello`, so by the time this call returns the device is ours and - // nothing else could have taken it (docs/os-development/device-authority.md). + // **The handshake comes first, because it is where everything moves.** The + // manager holds the controller and transfers it in `onHello`, so by the time + // this call returns the device is ours and nothing else could have taken it + // (docs/os-development/device-authority.md). And the serving endpoint rides + // up with the same call: one machine can carry several xHCI controllers, one + // process per controller, so the transfer contract is never a registry name + // — `/protocol` holds no instances, deliberately. Class drivers reach *this* + // controller because the manager answers their hellos with this endpoint, + // routed by lineage (communication.md "Establishment: two planes"). The bind + // race this replaces left every device behind a losing controller + // unreachable — a real machine's mouse, "could not open device 50". // // `hello` is synchronous, so the transfer has completed before the reply lands — // there is no window between being told yes and holding the thing. // // Keep the handle: the tick's hot-plug dispatch reports through it. - const handle = device_manager.hello(.bus, controller_id) orelse { + const exchanged = device_manager.helloExchange(.bus, controller_id, endpoint, false) orelse { std.log.warn("no hello with the device manager; controller {d} not delegated", .{controller_id}); return false; }; - manager_handle = handle; + manager_handle = exchanged.manager; // Fetch our own descriptor back for the controller's resources. const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch { @@ -259,7 +247,7 @@ fn initialise(endpoint: ipc.Handle) bool { return false; } - scanPorts(handle); + scanPorts(exchanged.manager); // Arm the timer: in polling mode it drains the event ring; in MSI mode it is the // slower port-reconcile/safety-net tick. Re-armed on each tick in onNotification. diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index e58a085..5f5a05f 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -2778,7 +2778,7 @@ fn usbReportTest(boot_information: *const BootInformation) void { }; process.setInitialRamdisk(image); - _ = spawnRegistry(rd); // the xhci driver binds /protocol/usb-transfer + _ = spawnRegistry(rd); // drivers reach /protocol/device-manager through it var manager: u32 = 0; var i: u32 = 0; while (i < rd.count) : (i += 1) { diff --git a/test/system/services/protocol-conformance-test/protocol-conformance-test.zig b/test/system/services/protocol-conformance-test/protocol-conformance-test.zig index 879d013..b3af1c1 100644 --- a/test/system/services/protocol-conformance-test/protocol-conformance-test.zig +++ b/test/system/services/protocol-conformance-test/protocol-conformance-test.zig @@ -35,11 +35,14 @@ //! enumerating an xHCI bus (the `usb-storage` scenario); //! - `scanout` — the virtio-gpu driver, which needs an emulated virtio-gpu the //! default harness does not attach (the `virtio-gpu` scenario); -//! - `device-manager`, `power` and `usb-transfer` — the three P4b rebased. All -//! three come with the device manager: it *is* the first, it spawns the -//! discovery service that binds the second, and the xHCI driver it spawns -//! binds the third. So booting a provider for any one of them means booting -//! the whole driver tree here. +//! - `device-manager` and `power` — both come with the device manager: it *is* +//! the first and spawns the discovery service that binds the second, so +//! booting a provider for either means booting the whole driver tree here. +//! - `usb-transfer` — never a registry name at all: several bus processes +//! provide it (one per controller) and consumers get their controller's +//! channel from the device manager's hello, routed by lineage +//! (communication.md "Establishment: two planes"). Its row below checks the +//! contract's shape only. //! //! That is the reason this scenario stays at two providers rather than five or //! eight. It is not only the cost of booting half the system to send two @@ -120,12 +123,11 @@ const contracts = [_]Contract{ contractOf(vfs_protocol.Protocol, false), // the FAT server — needs a volume contractOf(block_protocol.Protocol, false), // usb-storage — needs the xHCI chain contractOf(scanout_protocol.Protocol, false), // virtio-gpu — needs the device - // The three P4b rebased. Each needs the device manager (and, for the last - // two, what the device manager starts), which is more than this scenario - // boots — see the header. + // device-manager and power need the device manager (and what it starts), + // which is more than this scenario boots — see the header. contractOf(device_manager_protocol.Protocol, false), contractOf(power_protocol.Protocol, false), // the discovery service - contractOf(usb_transfer_protocol.Protocol, false), // the xHCI bus driver + contractOf(usb_transfer_protocol.Protocol, false), // never bound: per-controller, routed by hello }; /// A verb number no protocol in the system defines, and none plausibly will: far