Repository navigation
Add USB hot-plug for sticks, hubs and keyboards - #3256
Merged
Merged
Conversation
A hot-plug thread, started once the scheduler's timer ticks, waits for port changes: the xHCI Port Status Change Event on a root port, the status change endpoint on a hub port, or a 250 ms poll when the controller has no MSI-X. It clears the change bits, drops whatever sat on a port that disconnected, and after a 100 ms debounce enumerates what was plugged in. A device that leaves is marked disconnected from the interrupt handler already, so transfers waiting on it return the new Disconnected status instead of timing out and skip recovery. Its subtree is then released children first: each class driver's Disconnect, the device leaving the manager's list, and the controller disabling its slot once no control or bulk transfer still holds its memory. Hub port handling moves into UsbHub, which also follows the hub's status change endpoint. The mass storage and keyboard drivers keep copy-on-write lists and report units that come and go through attach/detach callbacks. I/O on a removed stick throws IOException at once, and names are reused from the lowest free usbN. Without the scheduler's timer (x64 with ACPI off) hot-plug stays off and the devices are the ones found at boot.
Kernel.Start starts the USB hot-plug thread once interrupts are on. A stick that shows up is registered and its partitions scanned like one found at boot; one that goes away is unregistered with its partitions, the primary device moves on, and every filesystem mounted from one of its partitions is detached without a flush, since the disk is gone. USB keyboards join and leave KeyboardManager the same way. The device, partition, mount and keyboard tables are replaced whole on every change instead of changed in place, so a reader on another thread always sees a consistent list. Partition scans run outside the lock and are published only if their device is still registered. TryUnmount drops the mount outside the lock, and TryFormat refuses a partition that is mounted.
`umount <mountpoint>` unmounts and moves the shell out of it. An I/O error, such as a stick pulled out under a running command, is reported and the shell keeps going. The boot stick is auto-mounted through its partition, so it is detached if pulled out, and an empty disk list now says a USB disk can be plugged in.
USB sticks and keyboards can be plugged in and pulled out while the kernel runs. Explain what happens to mounts (partition mounts are detached, source-string mounts stay), to unmount before pulling a stick, how to try it from the QEMU monitor, and where Kernel.Start starts the hot-plug thread.
TR.RequestHost sends a new HostRequest frame (109), and the engine acts on it as soon as it shows up on the UART: `usb-unplug [n]` removes stick n from the xHCI controller with device_del, and `usb-plug [n]` puts it back on the same image with drive_add and device_add. It gets there through QMP: a profile with a USB disk launches QEMU with its monitor connected to a port the engine listens on (QemuLaunchOptions.MonitorPort), and USB sticks get stable QEMU ids. Nothing replies; the test waits for the change, within the 10 s the engine gives it before taking the guest for hung.
Five tests in the usb cells pull the stick out and plug it back in: the stick leaves StorageManager and the driver, I/O on it throws IOException, it comes back under its old name with the sector written before the unplug, and a filesystem mounted from its partition is detached with it, then mounts again with its file intact. They skip on other cells and where the hot-plug thread cannot run (x64 with ACPI off).
🧪 Graphic Tests
Skipped
📎 Artifacts
|
🧪 Interrupts Tests
Skipped
📎 Artifacts
|
🧪 Storage Tests
Skipped
📎 Artifacts
|
Bring the files this branch adds or changes in line with docs/articles/dev/coding-guidelines.md. No behavior changes. - Move constructors after fields and properties. - Split UsbEndpointType into its own file. - Replace pass-through fields with get-only auto-properties. - Use explicit types instead of var, is null / is not null instead of == null / != null, interpolation instead of concatenation, and collection expressions. - Add license headers and a <summary> to QemuHotPlug.DisposeAsync. - Rename HostRequestScanner.Magic to s_magic. - Remove comments that only restate the code. - Explain why the USB drivers' lazily filled statics stay null.
Track .claude/agents/ so every contributor gets the code-cleaner agent, which reads docs/articles/dev/coding-guidelines.md and cleans source files to it without changing behavior. The rest of .claude, such as settings.local.json, stays ignored.
The parser's range check stopped at TestDestructiveReached (108), so HostRequest (109) frames were skipped as noise and their case never ran.
DisposeAsync waited for the monitor connection before it stopped the listener. If QEMU exited before connecting, for example because it rejected an argument, the accept never completed: its token belongs to a source the host disposes without cancelling. The engine then hung at await using. Stop the listener first, which ends the pending accept.
IndexOf and LastIndexOf both return -1 when qemu-xhci is missing, so the one-controller check passed with no controller at all. Assert that the controller is there too.
Move USB support from the future releases to the Gen3 features, with what it covers now and what it doesn't yet (USB mouse, EHCI). Drop the first-release percentage badge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #3255 (USB mass storage): the base is
feature/usb-mass-storage, to be retargeted togen3once #3254 and #3255 merge.Problem
USB devices were enumerated once, at boot, and nothing listened for port changes after that. A stick plugged in later never showed up, and one pulled out stayed registered.
StorageManager, its partitions and any filesystem mounted from them kept pointing at a device whose xHCI slot no longer answered. The same held for keyboards and hubs. Switching sticks meant rebooting.Fix
Hot-plug thread.
UsbManager.StartHotPlug, called fromKernel.Startonce interrupts are on, starts a thread that waits for port changes.Interrupt handlers signal it:
A controller without MSI-X (arm64 with GICv2) is polled every 250 ms instead.
Root ports (
XhciController.HandlePortChanges): the change bits are cleared first (RW1C). On a connect change, or an enable change that left the port disabled, the thread:Hub ports (the new
UsbHub, split out ofUsbHubDriver): GET_STATUS, then every change feature is cleared (the USB 2.0 set or the SuperSpeed set), including the hub-level local power and over-current changes. The port is handled the same way as a root port.Thread.Startreturns only once the new thread has run, and only a scheduler tick can run it. On x64 with ACPI off the LAPIC timer is never calibrated, so the Storage suite'susb+acpi-offcell hung there.StartHotPlugnow waits up to 50 ms for a tick first. Without one, it logsScheduler timer not ticking, hot-plug disabledand the devices stay the ones found at boot.Disconnect.
UsbDevice.IsDisconnected). Control and bulk transfers waiting on such a device return the newUsbTransferStatus.Disconnectedinstead of running into their timeout, and endpoint recovery is skipped.UsbDriver.Disconnect(device, interface), which is new and abstract;UsbManager.Devices;ReleaseDeviceruns.ReleaseDevicetakes the control mutex and every bulk pipe's mutex, so no transfer still uses the device's memory. It then runs Disable Slot, clears the slot under the event and ring locks, and frees the memory.Class drivers.
DiskAttached/DiskDetachedandKeyboardAttached/KeyboardDetached.UsbMassStoragethrowsIOException("USB mass storage device usbN was removed.") fromReadBlock,WriteBlockandFlush, and no longer retries a transport error.usbN, so one plugged back in gets its name back.System.
StorageManagerregisters a stick that arrives like one found at boot, partition scan included.StorageManager.UnregisterDevicedrops a stick that leaves together with its partitions and moves the primary device on. It then callsVfsManager.DetachMounts, which drops every mount made from one of those partitions without flushing, since the disk is gone.TryUnmountdrops the mount outside the lock.TryFormatrefuses a mounted partition.DevKernel.
umount <mountpoint>command, which also moves the shell out of the mount.IOExceptionfrom a command, such as a stick pulled out undercat, is reported and the shell keeps running.Partition, so it is detached if pulled out.Tests. A test can now ask the engine to change the machine under the running guest.
TR.RequestHost("usb-unplug")sends a newHostRequestframe (109). The engine's UART monitor picks it up as it arrives and carries it out over QMP:device_del;drive_addplusdevice_addon the same image.-chardev socket,host=127.0.0.1,port=N -mon chardev=qmp0,mode=control), so there is no port race. Only profiles with a USB disk get a monitor.blockdev-addfor the replug. A node added that way outlives its device and keeps the image open and locked, so plugging the same image in again fails. Adrive_adddrive is deleted along with its device, like the command-line-drive.usbcells (68 → 73 per cell):UsbHotPlug_UnplugUnregistersDiskStorageManagerand the driver, and is marked removedUsbHotPlug_RemovedDiskFailsIoIOExceptionUsbHotPlug_ReplugRegistersDiskUsbHotPlug_ReplugKeepsDataUsbHotPlug_UnplugDetachesMountThey skip on the other cells, and where the hot-plug thread cannot run (x64 with ACPI off).
Verification
Real hardware: not tested yet.
DevKernel, QEMU x64 (KVM). Sticks and hubs were added and removed from the QEMU monitor with
drive_add/device_add/device_del:usb0registered;diskinfo,mount,cat,writeworkusb0back with its dataformatcat(256 KiB/s)I/O error: USB mass storage device usb1 was removed.usb-kbdin and outKeyboardManagerumount/if it was insideDevKernel, QEMU arm64:
write,cat, unplug. The host reads the written file back from the image.cat, unplug.Storage suite, x64 (
make test KERNEL=Storage): 6 cells, 0 failed.usbcell passes all 5 hot-plug tests. Its first boot logs:[HotPlug] usb-unplug: doneand[HotPlug] usb-plug: donetwice per boot.usb+acpi-offnow runs to the end instead of timing out. It logs[USB] Scheduler timer not ticking, hot-plug disabledand skips the hot-plug tests.Storage suite, arm64 (
TIMEOUT=90): 18 cells, 0 failed, 0 timed out.usb,usb+gicv2(polled, no ITS),usb+gicv3(MSI-X)usb+acpi-off,usb+gicv2+acpi-off,usb+gicv3+acpi-offOther checks:
dotnet test tests/Cosmos.Tests.Patcher --filter QemuLauncher: 44 passed.dotnet format whitespace --verify-no-changesis clean on the changed files.Known gaps:
TryMount("fat", "3", ...)) is not tied to aPartition, so it stays mounted after its disk leaves. Its I/O then throwsIOException. Only mounts made through thePartitionoverload are detached.TryMount(Partition)does not hold the storage lock while it mounts. A stick pulled out at that exact moment can leave that one mount behind.