establishment: usb-transfer stops being a name

P1 of docs/establishment-planes-plan.md. The bus no longer binds
/protocol/usb-transfer — the bind race made whichever instance came second
unreachable, which on a real three-controller Ryzen meant a mouse no class
driver could reach ("could not open device 50"). Each instance hands its
serving endpoint up in the hello that already delegates its controller, and
class drivers receive their OWN controller's channel from helloForChannel —
routed by the manager's lineage, retried while a provider is mid-restart,
on one manager handle so retries never spend handle-table slots.

usb.open(bus, id) now takes the channel it used to look up; the name rows
leave protocol.csv with the code (bind 67, opens 119/120/122); the
conformance fixture's prose stops claiming the bus binds; and the stale
input-client import leaves the bus with the channel one.

Gate: 19 QEMU cases green (usb family, hubs, both IOMMU variants, fat chain,
boot-from-USB, orderly shutdown, conformance).
This commit is contained in:
Daniel Samson
2026-08-09 11:48:31 +01:00
parent 77fe4d220e
commit d603d40b5c
10 changed files with 84 additions and 67 deletions
+24
View File
@@ -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,