establishment: unplug reaps like death, so a replug rebinds

The hot-unplug path (onChildRemoved, a report from a live bus) cleared the
child but left the bound class driver: a process blocked on reports that
will never come, whose stale entry made the matcher's dedupe refuse the
respawn when the device was plugged back in — the same wall the restart
zombie hit, one path over. Unplug now reaps exactly like reporter death.

The usb-hub-unplug case grows the replug: device_del the hub keyboard, then
device_add it back (qmp_sequence); the ordered tail — child removed, reaping,
delegated, ok — can only be satisfied by the second generation, since every
boot keyboard's ok precedes the unplug. Discrimination: without the reap the
replug never rebinds and the case times out (verified by stash run). Hub
family, restart drill, and the two-controller proof all green (8/8).
This commit is contained in:
Daniel Samson
2026-08-09 13:51:07 +01:00
parent 3c9f454398
commit 4a2df587bb
2 changed files with 26 additions and 6 deletions
@@ -706,6 +706,12 @@ fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildR
for (&children) |*child| {
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) {
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
// Unplug reaps exactly like reporter death (pruneChildrenOf): the
// bound driver's device is gone and it cannot observe that — it
// blocks on reports that will never come — and its stale entry
// would make the dedupe refuse the respawn when the device is
// PLUGGED BACK IN. Reap now, and a replug's re-report rebinds.
reapDriverBoundTo(child.device_id);
child.used = false;
status = 0;
}