usb: hot-plug plumbing, all ports powered, interrupter enabled (M20)
B3 — the runtime lifecycle a hot-pluggable bus needs, plus two init fixes that runtime device arrival depends on: - pump() now handles PORT STATUS CHANGE events (silently dropped before): it queues the port, and the bus driver brings the port up (a device arrived) or tears it down (a device left) on its tick — reporting each interface ChildRemoved to the device manager, which prunes the node, notifies watchers, and lets the class driver's world end honestly, then Disable Slot frees the controller-side state. - ALL root-hub ports are powered at init, not just those with a boot-time device: an unpowered port (PP=0) cannot signal a later connect, so a hot-plug would never be seen. - the interrupter is enabled (IMAN.IE + USBCMD.INTE) while the ring stays polled — some controllers only WRITE runtime events to the ring when the interrupter is enabled. Real-hardware validation is flagged for the user: QEMU's qemu-xhci does not raise a runtime port-change event to a polling driver on device_add, so the end-to-end hot-plug path can't be exercised in the harness (the port-change handling itself IS proven — a late boot device's PSCE is caught and acked). The harness gained qmp_sequence (multi-step QMP injection with arguments) for when a drivable case exists. Full suite 88/88; the working USB path (enumeration, HID, storage) is unregressed by the port-power and interrupter changes.
This commit is contained in:
+26
-14
@@ -625,6 +625,11 @@ CASES = [
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||
# The user-space VFS: a client opens/writes/reads a file through the rt file
|
||||
# API, which IPCs the VFS server process; the round trip must match.
|
||||
# (A usb-hotplug case was prototyped here, but QEMU's qemu-xhci does not
|
||||
# raise a runtime port-change event to a polling driver on device_add, so it
|
||||
# cannot exercise the path. The hot-plug code — port-change queue, teardown
|
||||
# via Disable Slot, ChildRemoved reporting — is validated on real hardware,
|
||||
# flagged for the user. The qmp_sequence harness support it added remains.)
|
||||
# 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,
|
||||
# asserted kernel-side.
|
||||
@@ -704,11 +709,12 @@ def resolve_firmware(arch):
|
||||
+ "\nInstall OVMF (edk2-ovmf / ovmf) or add its path above.")
|
||||
|
||||
|
||||
def qmp_send(path, command):
|
||||
def qmp_send(path, command, arguments=None):
|
||||
"""One QMP command: connect, capabilities handshake, execute. Raises on any
|
||||
failure — the caller retries until the guest's socket is ready. This is how
|
||||
a case injects a host-side event (system_powerdown = the ACPI power button)
|
||||
into the running guest (docs/power.md)."""
|
||||
a case injects a host-side event into the running guest: system_powerdown
|
||||
(the ACPI power button, docs/power.md) or device_add/device_del (USB
|
||||
hot-plug, docs/driver-model.md)."""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
try:
|
||||
@@ -718,7 +724,10 @@ def qmp_send(path, command):
|
||||
stream.write(json.dumps({"execute": "qmp_capabilities"}) + "\n")
|
||||
stream.flush()
|
||||
stream.readline() # {"return": {}}
|
||||
stream.write(json.dumps({"execute": command}) + "\n")
|
||||
message = {"execute": command}
|
||||
if arguments:
|
||||
message["arguments"] = arguments
|
||||
stream.write(json.dumps(message) + "\n")
|
||||
stream.flush()
|
||||
stream.readline()
|
||||
finally:
|
||||
@@ -767,8 +776,10 @@ def run_case(arch, case):
|
||||
if os.path.exists(qmp_path):
|
||||
os.remove(qmp_path)
|
||||
cmd += ["-qmp", f"unix:{qmp_path},server,nowait"]
|
||||
qmp_after = case.get("qmp_after") # {"delay": seconds, "command": "..."}
|
||||
qmp_sent = False
|
||||
# Hooks: a single qmp_after {"delay","command"} or a qmp_sequence list of
|
||||
# {"delay","command","arguments"} — every hook must deliver before a pass.
|
||||
qmp_hooks = case.get("qmp_sequence") or ([case["qmp_after"]] if case.get("qmp_after") else [])
|
||||
qmp_pending = [dict(hook, sent=False) for hook in qmp_hooks]
|
||||
started = time.monotonic()
|
||||
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
@@ -776,12 +787,13 @@ def run_case(arch, case):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(0.2)
|
||||
if qmp_after and not qmp_sent and time.monotonic() - started >= qmp_after["delay"]:
|
||||
try:
|
||||
qmp_send(qmp_path, qmp_after["command"])
|
||||
qmp_sent = True
|
||||
except OSError:
|
||||
pass # socket not up yet; retry next tick
|
||||
for hook in qmp_pending:
|
||||
if not hook["sent"] and time.monotonic() - started >= hook["delay"]:
|
||||
try:
|
||||
qmp_send(qmp_path, hook["command"], hook.get("arguments"))
|
||||
hook["sent"] = True
|
||||
except OSError:
|
||||
pass # socket not up yet; retry next tick
|
||||
text = ""
|
||||
if os.path.exists(serial):
|
||||
with open(serial, "r", errors="replace") as f:
|
||||
@@ -789,8 +801,8 @@ def run_case(arch, case):
|
||||
if fail and fail.search(text):
|
||||
return False, "hit failure marker"
|
||||
if expect.search(text):
|
||||
if qmp_after and not qmp_sent:
|
||||
continue # the hook must deliver before the case may pass
|
||||
if any(not hook["sent"] for hook in qmp_pending):
|
||||
continue # every hook must deliver before the case may pass
|
||||
return True, "matched " + repr(case["expect"])
|
||||
if qemu.poll() is not None: # QEMU exited on its own
|
||||
if expect.search(text):
|
||||
|
||||
Reference in New Issue
Block a user