Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4f8dc51b9 | ||
|
|
36ca3f98b3 | ||
|
|
c3604429d4 | ||
|
|
77e7001878 | ||
|
|
a6a3402d92 | ||
|
|
d565a6b845 |
@@ -0,0 +1,54 @@
|
|||||||
|
# The hot-plug matrix: unplug anything, replug anywhere
|
||||||
|
|
||||||
|
*Status: COMPLETE, 2026-08-09, same day — H1..H5 all green, full suite 124/124.
|
||||||
|
The predicted cascade bug was real and found by H2: `tearDownPort` did not recurse
|
||||||
|
into a hub yanked from a root port, so its subtree stayed live against vanished
|
||||||
|
hardware and the replugged hub found its port occupied; fixed by mirroring
|
||||||
|
`tearDownHubDevice`'s children-first recursion (77e7001). Every other cell passed
|
||||||
|
against the establishment-track machinery unchanged.*
|
||||||
|
|
||||||
|
*2026-08-09. The requirement, verbatim: "i want to be able to unplug devices and
|
||||||
|
plug them back in, in any order and on any hub." The machinery exists end to end —
|
||||||
|
per-device driver processes, cascading teardown (`tearDownHubDevice` recurses a
|
||||||
|
departing hub's subtree, children first), reap-on-removal in the manager, idempotent
|
||||||
|
per-port registration identity, lineage rebinding — but only one cell of the matrix
|
||||||
|
is verified: a leaf keyboard behind a hub, unplugged and replugged on the same port
|
||||||
|
(`usb-hub-unplug`). This plan verifies the rest and fixes what verification flushes
|
||||||
|
out. The cascade path has never executed; expect it to carry at least one bug.*
|
||||||
|
|
||||||
|
**Out of scope, stated up front:** unplugging the BOOT stick (the drivers handle it;
|
||||||
|
fat holds a dead block channel until M21 remount gives it re-acquisition), and
|
||||||
|
real-hardware root-port timing (the Ryzen bench is the acceptance run for that, as
|
||||||
|
always — QEMU proves the logic, not the silicon).
|
||||||
|
|
||||||
|
## The discrimination lever
|
||||||
|
|
||||||
|
Every case's rebind tail (— reaping → delegated → second-generation `ok`) is
|
||||||
|
impossible without the manager's reap: stashing the reap out of
|
||||||
|
`onChildRemoved`/`pruneChildrenOf` makes every matrix case fail at the dedupe wall.
|
||||||
|
That is the standing discrimination check for the whole matrix — run it once per
|
||||||
|
case shape, not per commit.
|
||||||
|
|
||||||
|
## The matrix
|
||||||
|
|
||||||
|
| Case | Topology | Event sequence | What it proves |
|
||||||
|
|---|---|---|---|
|
||||||
|
| H1 `usb-root-replug` | keyboard on a 2nd controller's ROOT port | del @8s, add @14s (same port) | the `tearDownPort` path + reap + rebind; root-port changes arrive via the 250 ms reconcile tick (QEMU raises no root-port events) |
|
||||||
|
| H2 `usb-hub-yank` | hub on 2nd controller; keyboard AND mouse behind it | del the HUB @8s; re-add hub @14s, kbd @17s, mouse @18s | the recursive cascade: one event removes the subtree, EVERY bound driver is reaped, and the rebuilt hub re-binds both |
|
||||||
|
| H3 `usb-hub-nested-yank` | hub → hub → keyboard | del the OUTER hub @8s; re-add all three @14–18s | the cascade recursion depth ≥ 2, and a nested rebuild |
|
||||||
|
| H4 `usb-replug-moved` | keyboard behind hub port 1.1 | del @8s; add on port 1.2 @14s | a replug on a DIFFERENT port is simply a new device: new port identity, new id, fresh match — no stale state ties a driver to the old port |
|
||||||
|
| H5 `usb-replug-cycles` | keyboard behind hub | del/add three times (6 hooks) | no state leaks across generations: slots, the bus's open table, driver entries (reaped entries must be reusable) |
|
||||||
|
|
||||||
|
Ordered expects follow the `usb-hub-unplug` shape: the boot devices' `ok` lines all
|
||||||
|
precede the first unplug, so a tail of `removed → reaping → delegated → ok` can only
|
||||||
|
be satisfied by the generation the sequence created. H2/H3 require one `reaping` per
|
||||||
|
bound child; H5 requires three.
|
||||||
|
|
||||||
|
## Order and discipline
|
||||||
|
|
||||||
|
H1 → H2 → H3 → H4 → H5, one commit per case (plus its fix if it finds a bug — the
|
||||||
|
case and the fix land together, the case having failed first). The standing
|
||||||
|
discrimination run once for H1 and once for H2's shape. Full suite green at the end,
|
||||||
|
docs touched where behavior was corrected, memory updated. Fixes stay within the
|
||||||
|
settled design: teardown order (children first), reap-on-removal, identity per port
|
||||||
|
— anything design-shaped that surfaces stops the loop and asks.
|
||||||
@@ -490,6 +490,14 @@ fn deviceIsHub(usb_device: *const library.Device) bool {
|
|||||||
fn tearDownPort(manager: ipc.Handle, engine: *library.Controller, port: u32) void {
|
fn tearDownPort(manager: ipc.Handle, engine: *library.Controller, port: u32) void {
|
||||||
const usb_device = engine.deviceOnPort(port) orelse return;
|
const usb_device = engine.deviceOnPort(port) orelse return;
|
||||||
std.log.info("port {d} disconnected", .{port});
|
std.log.info("port {d} disconnected", .{port});
|
||||||
|
// A hub yanked from a root port takes its whole subtree with it — children
|
||||||
|
// first, recursively, exactly as tearDownHubDevice does for a hub leaving
|
||||||
|
// one level down. Without this, the downstream slots stayed live against
|
||||||
|
// vanished hardware, their class drivers were never reaped, and the
|
||||||
|
// replugged hub found its port still occupied — nothing re-enumerated.
|
||||||
|
if (usb_device.is_hub) {
|
||||||
|
while (engine.nextChildOf(usb_device.slot_id, 0)) |child| tearDownHubDevice(manager, engine, child);
|
||||||
|
}
|
||||||
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
|
||||||
if (interface.registered_device_id == 0) continue;
|
if (interface.registered_device_id == 0) continue;
|
||||||
reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number });
|
reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number });
|
||||||
|
|||||||
@@ -984,6 +984,135 @@ CASES = [
|
|||||||
r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard"
|
r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard"
|
||||||
r"[\s\S]*usb-hid-keyboard: ok)",
|
r"[\s\S]*usb-hid-keyboard: ok)",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Hot-plug matrix H1 (docs/hot-plug-matrix-plan.md): ROOT-port unplug and
|
||||||
|
# replug on the same port. Different teardown path than the hub case
|
||||||
|
# (tearDownPort, not tearDownHubDevice), and QEMU raises no port-change
|
||||||
|
# events for root ports — the bus's 250 ms reconcile tick must notice the
|
||||||
|
# PORTSC change on its own. The ordered tail (removed → reaping → delegated
|
||||||
|
# → ok) can only be the second generation: every boot device's ok precedes
|
||||||
|
# the unplug.
|
||||||
|
{"name": "usb-root-replug",
|
||||||
|
"build_case": "usb-hid",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||||
|
"-device", "usb-kbd,bus=xhci2.0,port=2,id=rkbd"],
|
||||||
|
"qmp_sequence": [
|
||||||
|
{"delay": 8, "command": "device_del", "arguments": {"id": "rkbd"}},
|
||||||
|
{"delay": 14, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "2", "id": "rkbd2"}},
|
||||||
|
],
|
||||||
|
"expect": r"(?s)(?=.*usb-hid-keyboard: ok \(device (\d+)\b"
|
||||||
|
r"[\s\S]*port \d+ disconnected"
|
||||||
|
r"[\s\S]*device-manager: child removed"
|
||||||
|
r"[\s\S]*device-manager: reaping \S*usb-hid-keyboard"
|
||||||
|
r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard"
|
||||||
|
r"[\s\S]*usb-hid-keyboard: ok)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Hot-plug matrix H2 (docs/hot-plug-matrix-plan.md): yank a POPULATED hub.
|
||||||
|
# One device_del removes the hub with a keyboard and a mouse behind it — the
|
||||||
|
# bus's recursive teardown (tearDownHubDevice, children first) must report
|
||||||
|
# every downstream interface removed, the manager must reap BOTH bound
|
||||||
|
# drivers, and re-adding the hub and both devices must rebuild the subtree.
|
||||||
|
# The reaping lines only exist post-yank, so `reaping X ... X: ok` chains
|
||||||
|
# are unambiguously second-generation.
|
||||||
|
{"name": "usb-hub-yank",
|
||||||
|
"build_case": "usb-hid",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||||
|
"-device", "usb-hub,bus=xhci2.0,port=1,id=yhub",
|
||||||
|
"-device", "usb-kbd,bus=xhci2.0,port=1.1,id=ykbd",
|
||||||
|
"-device", "usb-mouse,bus=xhci2.0,port=1.2,id=ymouse"],
|
||||||
|
"qmp_sequence": [
|
||||||
|
{"delay": 8, "command": "device_del", "arguments": {"id": "yhub"}},
|
||||||
|
{"delay": 14, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-hub", "bus": "xhci2.0", "port": "1", "id": "yhub2"}},
|
||||||
|
{"delay": 17, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "ykbd2"}},
|
||||||
|
{"delay": 18, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-mouse", "bus": "xhci2.0", "port": "1.2", "id": "ymouse2"}},
|
||||||
|
],
|
||||||
|
"expect": r"(?s)(?=.*hub device slot \d+ disconnected)"
|
||||||
|
r"(?=.*device-manager: reaping \S*usb-hid-keyboard[\s\S]*usb-hid-keyboard: ok)"
|
||||||
|
r"(?=.*device-manager: reaping \S*usb-hid-mouse[\s\S]*usb-hid-mouse: ok)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Hot-plug matrix H3 (docs/hot-plug-matrix-plan.md): yank a NESTED hub tree.
|
||||||
|
# hub → hub → keyboard, one device_del of the outer hub — the recursion must
|
||||||
|
# go two levels (inner hub torn down as a child, ITS keyboard first), the
|
||||||
|
# keyboard's driver reaped, and re-adding all three rebuilds and rebinds.
|
||||||
|
{"name": "usb-hub-nested-yank",
|
||||||
|
"build_case": "usb-hid",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||||
|
"-device", "usb-hub,bus=xhci2.0,port=1,id=ohub",
|
||||||
|
"-device", "usb-hub,bus=xhci2.0,port=1.1,id=ihub",
|
||||||
|
"-device", "usb-kbd,bus=xhci2.0,port=1.1.1,id=nkbd"],
|
||||||
|
"qmp_sequence": [
|
||||||
|
{"delay": 8, "command": "device_del", "arguments": {"id": "ohub"}},
|
||||||
|
{"delay": 14, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-hub", "bus": "xhci2.0", "port": "1", "id": "ohub2"}},
|
||||||
|
{"delay": 17, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-hub", "bus": "xhci2.0", "port": "1.1", "id": "ihub2"}},
|
||||||
|
{"delay": 20, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1.1", "id": "nkbd2"}},
|
||||||
|
],
|
||||||
|
"expect": r"(?s)(?=.*hub device slot \d+ disconnected)"
|
||||||
|
r"(?=.*device-manager: reaping \S*usb-hid-keyboard[\s\S]*usb-hid-keyboard: ok)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Hot-plug matrix H4 (docs/hot-plug-matrix-plan.md): replug on a DIFFERENT
|
||||||
|
# port. Registration identity is per-port, so a moved device is simply a new
|
||||||
|
# device: the old child is removed and its driver reaped; the new port's
|
||||||
|
# child gets a fresh id and a fresh driver. Nothing may tie a driver to the
|
||||||
|
# old port. The tail (reaping → hub slot N port 2 device → delegated → ok)
|
||||||
|
# is unambiguous: port 2 of the hub had nothing before the move.
|
||||||
|
{"name": "usb-replug-moved",
|
||||||
|
"build_case": "usb-hid",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||||
|
"-device", "usb-hub,bus=xhci2.0,port=1,id=mhub",
|
||||||
|
"-device", "usb-kbd,bus=xhci2.0,port=1.1,id=mkbd"],
|
||||||
|
"qmp_sequence": [
|
||||||
|
{"delay": 8, "command": "device_del", "arguments": {"id": "mkbd"}},
|
||||||
|
{"delay": 14, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.2", "id": "mkbd2"}},
|
||||||
|
],
|
||||||
|
"expect": r"(?s)(?=.*device-manager: reaping \S*usb-hid-keyboard"
|
||||||
|
r"[\s\S]*hub slot \d+ port 2 device:"
|
||||||
|
r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard"
|
||||||
|
r"[\s\S]*usb-hid-keyboard: ok)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# Hot-plug matrix H5 (docs/hot-plug-matrix-plan.md): three unplug/replug
|
||||||
|
# cycles of the same keyboard on the same hub port. Each cycle must reap the
|
||||||
|
# old generation and bind a new one — three reaping lines, and a bind after
|
||||||
|
# the last — proving no state leaks across generations: xHCI slots, the
|
||||||
|
# bus's open table, and the manager's driver entries must all be reusable.
|
||||||
|
{"name": "usb-replug-cycles",
|
||||||
|
"build_case": "usb-hid",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci2",
|
||||||
|
"-device", "usb-hub,bus=xhci2.0,port=1,id=chub",
|
||||||
|
"-device", "usb-kbd,bus=xhci2.0,port=1.1,id=ckbd0"],
|
||||||
|
"qmp_sequence": [
|
||||||
|
{"delay": 8, "command": "device_del", "arguments": {"id": "ckbd0"}},
|
||||||
|
{"delay": 12, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "ckbd1"}},
|
||||||
|
{"delay": 18, "command": "device_del", "arguments": {"id": "ckbd1"}},
|
||||||
|
{"delay": 22, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "ckbd2"}},
|
||||||
|
{"delay": 28, "command": "device_del", "arguments": {"id": "ckbd2"}},
|
||||||
|
{"delay": 32, "command": "device_add",
|
||||||
|
"arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "ckbd3"}},
|
||||||
|
],
|
||||||
|
"expect": r"(?s)(?=.*device-manager: reaping \S*usb-hid-keyboard"
|
||||||
|
r"[\s\S]*device-manager: reaping \S*usb-hid-keyboard"
|
||||||
|
r"[\s\S]*device-manager: reaping \S*usb-hid-keyboard"
|
||||||
|
r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard"
|
||||||
|
r"[\s\S]*usb-hid-keyboard: ok)",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# The kernel VFS root (M-F): the mount table serves the initrd at /system —
|
# The kernel VFS root (M-F): the mount table serves the initrd at /system —
|
||||||
# path resolution, node status/read (an ELF magic), and directory listing,
|
# path resolution, node status/read (an ELF magic), and directory listing,
|
||||||
# asserted kernel-side.
|
# asserted kernel-side.
|
||||||
|
|||||||
Reference in New Issue
Block a user