Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a53c2b0193 | ||
|
|
bf481c080c | ||
|
|
3a78dcab3f | ||
|
|
470f93a83d | ||
|
|
7798706b41 | ||
|
|
ad40de03c2 | ||
|
|
d8778b4b70 | ||
|
|
79d859a111 | ||
|
|
37fb09f75e | ||
|
|
34ebeb968d |
@@ -342,6 +342,21 @@ pub fn build(b: *std.Build) void {
|
|||||||
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
// A test fixture, not a real driver: hellos to the device manager, then faults —
|
||||||
// what the driver-restart scenario drives the crash-loop cap with.
|
// what the driver-restart scenario drives the crash-loop cap with.
|
||||||
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
const crash_test_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "crash-test", "system/services/crash-test/crash-test.zig");
|
||||||
|
const device_list_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-list", "system/services/device-list/device-list.zig");
|
||||||
|
// The discovery service: one swappable process per firmware
|
||||||
|
// (docs/m19-m20-plan.md decision 7), bundled under the neutral ramdisk name
|
||||||
|
// "discovery" so the device manager never learns which firmware it is on.
|
||||||
|
// x86 boots describe hardware with ACPI; the Raspberry Pis hand over a
|
||||||
|
// flattened device tree — the aarch64 target flips the default when it
|
||||||
|
// lands (docs/arm.md). Both are placeholders until M20.1 (acpi) and the
|
||||||
|
// ARM bring-up (fdt).
|
||||||
|
const Discovery = enum { acpi, fdt };
|
||||||
|
const discovery = b.option(Discovery, "discovery", "Which discovery service fills the ramdisk's 'discovery' slot (default: acpi)") orelse Discovery.acpi;
|
||||||
|
const discovery_source: []const u8 = switch (discovery) {
|
||||||
|
.acpi => "system/services/acpi/acpi.zig",
|
||||||
|
.fdt => "system/services/fdt/fdt.zig",
|
||||||
|
};
|
||||||
|
const discovery_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "discovery", discovery_source);
|
||||||
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
const device_manager_exe = addUserBinary(b, kernel_target, runtime_module, posix_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "device-manager", "system/services/device-manager/device-manager.zig");
|
||||||
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
// The input service and its exercisers: the fan-out server, a hardware-free synthetic
|
||||||
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
// source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md.
|
||||||
@@ -375,6 +390,10 @@ pub fn build(b: *std.Build) void {
|
|||||||
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin());
|
||||||
mk_run.addArg("crash-test");
|
mk_run.addArg("crash-test");
|
||||||
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
mk_run.addFileArg(crash_test_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("device-list");
|
||||||
|
mk_run.addFileArg(device_list_exe.getEmittedBin());
|
||||||
|
mk_run.addArg("discovery");
|
||||||
|
mk_run.addFileArg(discovery_exe.getEmittedBin());
|
||||||
mk_run.addArg("device-manager");
|
mk_run.addArg("device-manager");
|
||||||
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
mk_run.addFileArg(device_manager_exe.getEmittedBin());
|
||||||
mk_run.addArg("input");
|
mk_run.addArg("input");
|
||||||
|
|||||||
@@ -4,8 +4,13 @@
|
|||||||
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
with its deadline, supervised spawn, restart with backoff, and the crash-loop
|
||||||
cap are in — usb-xhci-bus is the first conforming driver, and the
|
cap are in — usb-xhci-bus is the first conforming driver, and the
|
||||||
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
`driver-restart` scenario proves fault → backoff → re-claim → cap end to end.
|
||||||
Tree reports (M18.2) and the application surface (M18.3) remain design. The
|
Tree reports are built too (M18.2, 2026-07-13): the xHCI driver scans its
|
||||||
primitives underneath are real ([process-management.md](process-management.md):
|
root-hub ports and reports each connected device (`child_added`); the manager
|
||||||
|
mirrors them and prunes a dead reporter's children, and the `usb-report`
|
||||||
|
scenario proves report → prune → respawn → re-report. The application surface is built (M18.3, 2026-07-13):
|
||||||
|
`enumerate` and `subscribe` over IPC, with `device-list` as the first client —
|
||||||
|
the manager is now the one answer to "what devices exist" for applications.
|
||||||
|
The primitives underneath are real ([process-management.md](process-management.md):
|
||||||
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
spawn/supervise/kill/exit-notification; [driver-model.md](driver-model.md): the device
|
||||||
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
table as a capability system; [drivers.md](drivers.md): claim/map/IRQ), and the first
|
||||||
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
per-device driver spawn works (the device manager matches the xHCI controller by PCI
|
||||||
|
|||||||
+13
-4
@@ -52,10 +52,19 @@ only when its definition of green holds.
|
|||||||
crash-loop cap; usb-xhci-bus first conforming driver; crash-test fixture
|
crash-loop cap; usb-xhci-bus first conforming driver; crash-test fixture
|
||||||
re-proving claim release each respawn; `driver-restart` scenario;
|
re-proving claim release each respawn; `driver-restart` scenario;
|
||||||
maximum_tasks 16→32 — the sweep was overflowing the pool; suite 52/52)
|
maximum_tasks 16→32 — the sweep was overflowing the pool; suite 52/52)
|
||||||
- [ ] **merge** `feat/device-manager` → main, push
|
- [x] **merge** `feat/device-manager` → main, push (merged 2026-07-13)
|
||||||
- [ ] **M18.2** — xHCI port scan + tree reports (branch `feat/usb-xhci-bus`)
|
- [x] **M18.2** — xHCI port scan + tree reports (child_added/child_removed in
|
||||||
- [ ] **M18.3** — app surface: enumerate/subscribe + device-list
|
the protocol; the manager's child mirror with death-pruning; xHCI maps the
|
||||||
- [ ] **merge** `feat/usb-xhci-bus` → main, push — **loop ends here**
|
register BAR — resource 0 is ECAM — reads CAPLENGTH/HCSPARAMS1, scans
|
||||||
|
PORTSC, reports connected ports with speed-class identity; `usb-report`
|
||||||
|
scenario proves report → prune → respawn → re-report; suite 53/53)
|
||||||
|
- [x] **M18.3** — app surface: enumerate/subscribe over IPC (subscriber
|
||||||
|
endpoint rides as the call's capability; events are the same structs the
|
||||||
|
buses send); device-list first client; protocol capped at the kernel's
|
||||||
|
IPC MESSAGE_MAXIMUM (256); the startUserTask debug print removed — it
|
||||||
|
sheared concurrent serial lines and was the scenario-flake root cause;
|
||||||
|
`device-list` scenario; suite 54/54)
|
||||||
|
- [x] **merge** `feat/usb-xhci-bus` → main, push (merged 2026-07-13) — **plan complete**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# M19–M20 execution plan: discovery migration
|
||||||
|
|
||||||
|
The operational plan for [device-manager.md](device-manager.md)'s increment 8:
|
||||||
|
discovery leaves the kernel — a **pci-bus driver** (M19) and an **acpi service**
|
||||||
|
(M20), with the kernel's device enumeration retired behind them. Same rules as
|
||||||
|
[m17-m18-plan.md](m17-m18-plan.md): one phase at a time, each green before the
|
||||||
|
next; this file is the build order and the checklist.
|
||||||
|
|
||||||
|
**Definition of green, every phase:** `zig build` clean, `zig build test` clean,
|
||||||
|
`python3 test/qemu_test.py` passes (existing scenarios plus the phase's new
|
||||||
|
one), and the relevant design doc updated. Commit per green phase (no co-author
|
||||||
|
trailers). The full suite is the regression net — the existing
|
||||||
|
`driver-restart` / `usb-report` / `device-list` / `input` scenarios must stay
|
||||||
|
green *through* the migration, which is the whole point: the system must not be
|
||||||
|
able to tell who enumerated it.
|
||||||
|
|
||||||
|
**Workflow:** dedicated worktree; branches off `main` — `feat/pci-bus`
|
||||||
|
(M19.0–19.3), `feat/acpi-service` (M20.1–20.3); auto-merge to main when a
|
||||||
|
branch is green; keep branches; push everything.
|
||||||
|
|
||||||
|
## Settled decisions (2026-07-13 — veto before the loop starts)
|
||||||
|
|
||||||
|
1. **What "retiring the kernel scan" means.** The kernel keeps, forever, the
|
||||||
|
parses it needs before user space exists: RSDP/XSDT location, MADT (SMP),
|
||||||
|
the HPET table (the tick), FADT + the AML `\_S5` evaluation (poweroff — the
|
||||||
|
power tests prove it), and MCFG (the host bridge node). What retires is
|
||||||
|
**device enumeration**: the ECAM function walk (M19.3) and the DSDT/SSDT
|
||||||
|
namespace walk that builds device nodes (M20.3). The AML module stays a
|
||||||
|
shared build module compiled into both the kernel (for `\_S5`) and the acpi
|
||||||
|
service (for everything else) — same source, two builds, no fork.
|
||||||
|
2. **Bridge apertures come from the firmware memory map, not AML.** Registered
|
||||||
|
PCI functions carry BAR resources, and containment demands the bridge own
|
||||||
|
windows that cover them. The apertures are derived kernel-side from the
|
||||||
|
boot memory map's MMIO holes (regions that are neither RAM nor tables) —
|
||||||
|
mechanical, AML-free, and available at boot regardless of what later moved
|
||||||
|
to user space. (The bridge today carries only ECAM + bus range; this is the
|
||||||
|
prerequisite M19.0 exists for.)
|
||||||
|
3. **`device_register` becomes idempotent on exact match.** A re-registration
|
||||||
|
with identical (parent, class, resources) returns the existing id instead
|
||||||
|
of appending. The kernel table has no unregister, so without this a
|
||||||
|
restarted registering bus would duplicate its children on every respawn —
|
||||||
|
idempotence makes restart-and-re-report safe for every future bus, not just
|
||||||
|
PCI.
|
||||||
|
4. **The manager matches from reports.** `ChildAdded` gains a `device_id`
|
||||||
|
field (the kernel-registered id, `no_device` for unregistered leaves like
|
||||||
|
USB ports). After the M19.3 flip, PCI driver matching keys off reported
|
||||||
|
identity (the class triple) instead of the manager's boot-time snapshot —
|
||||||
|
the snapshot match remains only for what the kernel still seeds. One flip
|
||||||
|
phase changes both sides at once so no device is ever matched twice.
|
||||||
|
5. **The acpi service's authority is one node.** The kernel publishes an
|
||||||
|
`acpi-tables` device: memory resources covering the table blobs plus a
|
||||||
|
broad `io_port` resource — the documented trust grant to exactly one
|
||||||
|
process (AML OperationRegions reach EC/PM ports; the claim-gated
|
||||||
|
io_read/io_write calls already exist). The service claims it, maps the
|
||||||
|
tables, and runs the shared AML module in ring 3 behind a `Hal` backed by
|
||||||
|
`mmio_map` + `io_read`/`io_write`.
|
||||||
|
6. **Both new processes are protocol drivers** under the manager: hello,
|
||||||
|
supervision, restart with backoff — all inherited from M18.1 for free.
|
||||||
|
Registration idempotence (decision 3) is what makes their restarts sound.
|
||||||
|
7. **Firmware neutrality is the contract** (2026-07-13). The generic layer is
|
||||||
|
everything at and above the device-manager protocol — descriptors,
|
||||||
|
containment, reports, matching, supervision — and none of it may become
|
||||||
|
x86-specific. Discovery is one swappable process per firmware: the acpi
|
||||||
|
service on x86; an **fdt service** on the Raspberry Pis (claims a
|
||||||
|
`devicetree-blob` node, reports children from the flattened device tree —
|
||||||
|
pure data, no bytecode, no port grant, strictly simpler than ACPI). The
|
||||||
|
manager owns the tree as *data* and touches no hardware, ever — AML runs in
|
||||||
|
a crashable, supervised discoverer precisely so a firmware-bytecode fault
|
||||||
|
can never take down the supervisor. Two consequences recorded now:
|
||||||
|
`DeviceDescriptor`'s 8-byte `hid` cannot hold an FDT `compatible` string
|
||||||
|
("brcm,bcm2835-aux-uart") — identity widens before the fdt service exists;
|
||||||
|
and cross-firmware surfaces are named by **domain, not firmware** (M21
|
||||||
|
defines a *power* protocol, not an "ACPI events" protocol — PSCI/mailbox
|
||||||
|
sources feed the same subscribers on ARM). **Landed early (2026-07-13):**
|
||||||
|
both services exist as placeholders (system/services/acpi, system/services/
|
||||||
|
fdt) and the build's `-Ddiscovery=acpi|fdt` option fills the ramdisk's
|
||||||
|
neutral `discovery` slot — the manager will spawn "discovery" by that name
|
||||||
|
in M20.3 and never learn which firmware it is on.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- [ ] **M19.0** — prerequisites on `feat/pci-bus`: bridge MMIO apertures from
|
||||||
|
the memory-map holes; `device_register` idempotence (+ kernel unit
|
||||||
|
checks); `ChildAdded.device_id`; archive note on m17-m18-plan.md.
|
||||||
|
- [ ] **M19.1** — pci-bus driver, scan only: claim the host bridge, map the
|
||||||
|
ECAM window, walk bus/device/function headers, log what it finds.
|
||||||
|
Scenario `pci-scan`: the kernel test compares the driver's reported count
|
||||||
|
against the broker table's `pci_device` count — equivalence, per class.
|
||||||
|
- [ ] **M19.2** — register + report: each function registered under the bridge
|
||||||
|
(config-space slice + BARs, `pci_class` in the descriptor), reported with
|
||||||
|
`child_added { device_id, identity = class triple }`. Manager mirrors;
|
||||||
|
spawn-from-reports stays **off**. Scenario extends `pci-scan`:
|
||||||
|
registered ids resolve, no duplicates after a forced driver restart
|
||||||
|
(idempotence proven end to end).
|
||||||
|
- [ ] **M19.3** — the flip: kernel `enumeratePci` call removed (bridge node
|
||||||
|
stays); manager matches PCI drivers from reports. One commit. The
|
||||||
|
existing xHCI scenarios (`driver-restart`, `usb-report`, `device-list`)
|
||||||
|
are the assertion — xhci must come up spawned off a pci-bus report, and
|
||||||
|
the suite must not be able to tell the difference. discovery.md updated.
|
||||||
|
- [ ] **merge** `feat/pci-bus` → main, push.
|
||||||
|
- [ ] **M20.1** — acpi service, parse only (fills the existing placeholder at
|
||||||
|
system/services/acpi/acpi.zig): kernel publishes `acpi-tables`
|
||||||
|
(decision 5) — memory over the table blobs, the broad io_port grant, and
|
||||||
|
**the SCI as an irq resource** (from the FADT; unused until M21 but free
|
||||||
|
to record now). The service claims it, maps the blobs, runs the shared
|
||||||
|
AML module in ring 3, logs the namespace device count and `_HID`s.
|
||||||
|
Scenario `acpi-parse`: user-space count equals the kernel walk's count.
|
||||||
|
- [ ] **M20.2** — register + report: namespace devices with `_HID` + `_CRS`
|
||||||
|
resources registered under `acpi-tables` (its io_port + the memory-map
|
||||||
|
holes give containment), reported to the manager. Spawn-from-reports for
|
||||||
|
ACPI matches stays off. Scenario: the reported set includes the PS/2
|
||||||
|
keyboard and mouse nodes with their IRQ resources.
|
||||||
|
- [ ] **M20.3** — the flip: kernel DSDT device-node building removed (static
|
||||||
|
tables + `\_S5` stay, decision 1); manager matches ACPI-hid drivers
|
||||||
|
(ps2-bus) from reports. The `input` and `device-manager` scenarios are
|
||||||
|
the assertion. discovery.md + acpi.md + device-manager.md updated;
|
||||||
|
device-manager.md increment 8 closed.
|
||||||
|
- [ ] **merge** `feat/acpi-service` → main, push — **loop ends here**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase notes
|
||||||
|
|
||||||
|
**M19.0 apertures:** the boot memory map already crosses the handoff
|
||||||
|
([boot-handoff]); the holes computation belongs where the bridge node is built
|
||||||
|
(`parseMcfg`). Sanity-check on QEMU q35: the xHCI BAR (`0xc0000000`-region
|
||||||
|
values seen in the M18 logs) must land inside a derived aperture, asserted in
|
||||||
|
the kernel unit test.
|
||||||
|
|
||||||
|
**M19.1 scanning without owning config access twice:** the driver reads config
|
||||||
|
space through its ECAM mmio_map grant of the *bridge* window — the same bytes
|
||||||
|
the kernel walk read. Vendor-id `0xFFFF` skip, header-type multifunction rule,
|
||||||
|
no bridge recursion (matches the kernel's current single-segment walk).
|
||||||
|
|
||||||
|
**M19.2 BAR sizing:** the classic size probe (write all-ones, read mask,
|
||||||
|
restore) is deferred — the BARs' current programmed values and types are
|
||||||
|
enough for containment-checked registration at bring-up; sizing lands with the
|
||||||
|
first driver that needs to *move* a BAR. Log what is registered so the
|
||||||
|
scenario can assert it.
|
||||||
|
|
||||||
|
**M19.3 what the manager still seeds from the snapshot:** everything the
|
||||||
|
kernel still enumerates (timers, ACPI nodes until M20.3). The PCI arm of
|
||||||
|
`pciDriverFor` switches source; `driverFor` doesn't move until M20.3.
|
||||||
|
|
||||||
|
**M20.1 Hal in ring 3:** `mapMmio` → `device.mmioMap` over the claimed
|
||||||
|
acpi-tables node (plus a table-offset map for blobs); `pioRead`/`pioWrite` →
|
||||||
|
`device.ioRead`/`ioWrite` against its io_port resource. The interpreter cannot
|
||||||
|
tell it moved — that is the assertion of `acpi-parse`.
|
||||||
|
|
||||||
|
**M20.2 containment for `_CRS`:** io ports fall inside the node's broad
|
||||||
|
io_port resource; MMIO windows (HPET, LAPIC ranges some firmwares list) fall
|
||||||
|
inside the memory-map holes added to the node in M20.1. Anything that doesn't
|
||||||
|
fit is logged and skipped, loudly — bring-up honesty over silent drops.
|
||||||
|
|
||||||
|
**M20.3 ps2 ordering:** ps2-bus binds nodes the acpi service now reports, so
|
||||||
|
its spawn moves behind the report (the manager's matching handles this once
|
||||||
|
the source flips); the `input` scenario proves the keyboard still types.
|
||||||
|
|
||||||
|
**Explicitly out of scope:** PCI bridge recursion (single segment, flat bus
|
||||||
|
walk stays); BAR reprogramming/sizing; disk/PCIe hotplug; interrupt routing
|
||||||
|
changes (`_PRT` stays wherever it is today); the USB descriptor track;
|
||||||
|
multi-segment ECAM; per-device power states (D-states, `_PSx`/`_PRx`,
|
||||||
|
suspend/resume — a future *lifecycle-vocabulary* extension, since "suspend"
|
||||||
|
has the shape of a signal every driver must answer, and it has no consumer
|
||||||
|
until laptop sleep); CPU P/C-states.
|
||||||
|
|
||||||
|
## M21 preview — ACPI events + system power (planned next, not in this loop)
|
||||||
|
|
||||||
|
The acpi service grows the event side (settled direction 2026-07-13; detailed
|
||||||
|
phases when M20 lands):
|
||||||
|
|
||||||
|
- **21.1 SCI + fixed events**: irq_bind the SCI (the resource M20.1 already
|
||||||
|
records), read/clear PM1 status, publish the power-button event to
|
||||||
|
subscribers (the same pub/sub shape the manager uses).
|
||||||
|
- **21.2 GPE + Notify**: Notify dispatch in the shared AML interpreter, GPE
|
||||||
|
block handling, `Notify(device, code)` published per reported node. The
|
||||||
|
acpi service is a **bus** here: battery (PNP0C0A), AC (ACPI0003), and lid
|
||||||
|
(PNP0C0D) nodes are reported children; small class drivers bind them and
|
||||||
|
speak an evaluate/subscribe protocol to the service — the xHCI split,
|
||||||
|
repeated. The embedded controller (`_Qxx` queries) rides this phase;
|
||||||
|
QEMU emulates no battery/EC, so those paths are interface-complete and
|
||||||
|
validated on real hardware (the laptop is the win condition), while the
|
||||||
|
plumbing is proven by the power button.
|
||||||
|
- **21.3 the capstone**: QEMU `system_powerdown` → acpi service event → init
|
||||||
|
runs the M17 stop sequence over its children → kernel `\_S5` — orderly
|
||||||
|
shutdown as the scenario that proves lifecycle + events compose. (The
|
||||||
|
harness grows a QMP poke to inject the event.)
|
||||||
@@ -22,8 +22,10 @@ pub const Callbacks = struct {
|
|||||||
/// Return false to abort startup (the process exits).
|
/// Return false to abort startup (the process exits).
|
||||||
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
init: ?*const fn (endpoint: ipc.Handle) bool = null,
|
||||||
/// One protocol request from `sender` (a task id): write the reply into
|
/// One protocol request from `sender` (a task id): write the reply into
|
||||||
/// `reply`, return its length. The zero-length ping never reaches this.
|
/// `reply`, return its length. `capability` is the handle the request
|
||||||
on_message: *const fn (message: []const u8, reply: []u8, sender: u32) usize,
|
/// carried, if any (M13 cap passing — how a subscriber hands over its
|
||||||
|
/// endpoint). The zero-length ping never reaches this.
|
||||||
|
on_message: *const fn (message: []const u8, reply: []u8, sender: u32, capability: ?ipc.Handle) usize,
|
||||||
/// A notification that is not a signal — a subscribed exit event, a bound
|
/// A notification that is not a signal — a subscribed exit event, a bound
|
||||||
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
/// IRQ, a timer landing. The raw badge; decode with the ipc helpers.
|
||||||
on_notification: ?*const fn (badge: u64) void = null,
|
on_notification: ?*const fn (badge: u64) void = null,
|
||||||
@@ -76,6 +78,6 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
|||||||
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
reply_len = 0; // the universal ping: a zero-length reply, from the harness
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId());
|
reply_len = callbacks.on_message(receive[0..got.len], &reply_buffer, got.senderTaskId(), got.cap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@
|
|||||||
//! argv[1]; this instance claims that device and no other, so multiple
|
//! argv[1]; this instance claims that device and no other, so multiple
|
||||||
//! instances never fight over hardware.
|
//! instances never fight over hardware.
|
||||||
//!
|
//!
|
||||||
//! M18.1 (this increment): a harness service and the first conforming driver of
|
//! M18.2 (this increment): after the hello, real hardware — map the xHC's
|
||||||
//! the device-manager protocol — claim the controller, `hello` the manager
|
//! register window (the first memory BAR; resource 0 is the ECAM config
|
||||||
//! (role, version, assignment) inside its deadline, then serve. Controller
|
//! space), read the capability registers, and walk the root-hub ports: one
|
||||||
//! bring-up (map the MMIO window, reset, port scan) and tree reports
|
//! `child_added` report to the manager per connected port, carrying the port
|
||||||
//! (`child_added` for each connected port) land in M18.2.
|
//! number and the PORTSC speed class as identity. No transfer rings yet —
|
||||||
|
//! descriptors and USB class matching are the USB track; the connect bit and
|
||||||
|
//! speed come straight from PORTSC, which reflects hardware state whether or
|
||||||
|
//! not the controller is running.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
@@ -47,12 +50,16 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The controller's operational registers live behind BAR0, enumerated as
|
// The xHC's registers live behind the first memory BAR. Resource 0 is the
|
||||||
// the device's first memory resource.
|
// function's ECAM configuration space (M15), so the walk starts at 1.
|
||||||
const register_window = for (descriptor.resources[0..@intCast(descriptor.resource_count)]) |resource| {
|
var register_index: u64 = 0;
|
||||||
if (resource.kind == @intFromEnum(device.ResourceKind.memory)) break resource;
|
const register_window = for (descriptor.resources[1..@intCast(descriptor.resource_count)], 1..) |resource, index| {
|
||||||
|
if (resource.kind == @intFromEnum(device.ResourceKind.memory)) {
|
||||||
|
register_index = index;
|
||||||
|
break resource;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
writeLine("usb-xhci-bus: controller device {d} has no MMIO window\n", .{controller_id});
|
writeLine("usb-xhci-bus: controller device {d} has no register BAR\n", .{controller_id});
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
writeLine("usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{
|
writeLine("usb-xhci-bus: claimed controller device {d} (registers at 0x{x}, {d} bytes)\n", .{
|
||||||
@@ -60,6 +67,10 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
register_window.start,
|
register_window.start,
|
||||||
register_window.len,
|
register_window.len,
|
||||||
});
|
});
|
||||||
|
register_base = device.mmioMap(controller_id, register_index) orelse {
|
||||||
|
_ = runtime.system.write("usb-xhci-bus: mmio_map failed\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
// The handshake: role, protocol version, assignment — inside the manager's
|
// The handshake: role, protocol version, assignment — inside the manager's
|
||||||
// deadline (the lookup retries cover the manager still registering).
|
// deadline (the lookup retries cover the manager still registering).
|
||||||
@@ -84,14 +95,62 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_ = runtime.system.write("usb-xhci-bus: hello acknowledged\n");
|
_ = runtime.system.write("usb-xhci-bus: hello acknowledged\n");
|
||||||
|
|
||||||
|
scanPorts(h);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var register_base: usize = 0;
|
||||||
|
|
||||||
|
/// One 32-bit volatile register read at `offset` from the mapped window.
|
||||||
|
fn readRegister(offset: usize) u32 {
|
||||||
|
const register: *volatile u32 = @ptrFromInt(register_base + offset);
|
||||||
|
return register.*;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The root-hub port scan: read the capability registers for the port count
|
||||||
|
/// and the operational-register offset, then one PORTSC per port. The connect
|
||||||
|
/// bit (CCS) and the speed field reflect hardware state directly — no
|
||||||
|
/// controller reset or run needed to *see* the devices; driving them needs the
|
||||||
|
/// rings (the USB track).
|
||||||
|
fn scanPorts(manager: runtime.ipc.Handle) void {
|
||||||
|
// Capability registers: CAPLENGTH is byte 0 of the first dword; HCSPARAMS1
|
||||||
|
// carries MaxPorts in bits 31:24.
|
||||||
|
const capability_length = readRegister(0) & 0xFF;
|
||||||
|
const structural = readRegister(0x04);
|
||||||
|
const maximum_ports: u32 = structural >> 24;
|
||||||
|
writeLine("usb-xhci-bus: {d} root-hub ports\n", .{maximum_ports});
|
||||||
|
|
||||||
|
// PORTSC registers: operational base + 0x400 + 0x10 per port (1-based).
|
||||||
|
var port: u32 = 1;
|
||||||
|
var connected: u32 = 0;
|
||||||
|
while (port <= maximum_ports) : (port += 1) {
|
||||||
|
const port_status = readRegister(capability_length + 0x400 + 0x10 * (port - 1));
|
||||||
|
if (port_status & 1 == 0) continue; // CCS: nothing connected
|
||||||
|
connected += 1;
|
||||||
|
const speed = (port_status >> 10) & 0xF; // the PORTSC port-speed class
|
||||||
|
writeLine("usb-xhci-bus: port {d} connected (speed class {d})\n", .{ port, speed });
|
||||||
|
|
||||||
|
const report = protocol.ChildAdded{
|
||||||
|
.parent = controller_id,
|
||||||
|
.bus_address = port,
|
||||||
|
.identity = speed,
|
||||||
|
};
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = runtime.ipc.call(manager, std.mem.asBytes(&report), &reply) catch {
|
||||||
|
writeLine("usb-xhci-bus: child report for port {d} failed\n", .{port});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (connected == 0) _ = runtime.system.write("usb-xhci-bus: no devices connected\n");
|
||||||
|
}
|
||||||
|
|
||||||
/// No bus protocol to serve yet — transfer requests arrive with the USB track.
|
/// No bus protocol to serve yet — transfer requests arrive with the USB track.
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
_ = message;
|
_ = message;
|
||||||
_ = reply;
|
_ = reply;
|
||||||
_ = sender;
|
_ = sender;
|
||||||
|
_ = capability;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -350,8 +350,9 @@ pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority
|
|||||||
/// context switch and lock release.
|
/// context switch and lock release.
|
||||||
fn startUserTask() void {
|
fn startUserTask() void {
|
||||||
const t = current();
|
const t = current();
|
||||||
var buffer: [96]u8 = undefined;
|
// No serial chatter here: this runs on every spawn, unserialized against
|
||||||
architecture.serialWrite(std.fmt.bufPrint(&buffer, "DBG startUserTask ip=0x{x} sp=0x{x} aspace=0x{x} kstack=0x{x}\n", .{ t.user_ip, t.user_sp, t.aspace, t.kstack_top }) catch "");
|
// user-space writes, and its output used to shear concurrent log lines in
|
||||||
|
// half — the largest source of corrupted markers in the QEMU scenarios.
|
||||||
architecture.jumpToUser(t.user_ip, t.user_sp); // noreturn
|
architecture.jumpToUser(t.user_ip, t.user_sp); // noreturn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,10 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
signalsTest(boot_information);
|
signalsTest(boot_information);
|
||||||
} else if (eql(case, "driver-restart")) {
|
} else if (eql(case, "driver-restart")) {
|
||||||
driverRestartTest(boot_information);
|
driverRestartTest(boot_information);
|
||||||
|
} else if (eql(case, "usb-report")) {
|
||||||
|
usbReportTest(boot_information);
|
||||||
|
} else if (eql(case, "device-list")) {
|
||||||
|
deviceListTest(boot_information);
|
||||||
} else if (eql(case, "initial-ramdisk")) {
|
} else if (eql(case, "initial-ramdisk")) {
|
||||||
initialRamdiskTest(boot_information);
|
initialRamdiskTest(boot_information);
|
||||||
} else if (eql(case, "vfs")) {
|
} else if (eql(case, "vfs")) {
|
||||||
@@ -1694,6 +1698,73 @@ fn driverRestartTest(boot_information: *const BootInformation) void {
|
|||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// M18.2: bus tree reports, end to end. The manager (test-usb-restart mode)
|
||||||
|
/// spawns the xHCI driver; the driver maps its BAR, scans the root-hub ports,
|
||||||
|
/// and reports the two QEMU devices; the manager mirrors them, kills the
|
||||||
|
/// reporter (the test trigger), prunes both children, restarts the driver with
|
||||||
|
/// backoff, and the respawned instance re-claims, re-scans, and re-reports.
|
||||||
|
/// The harness's ordered expect regex is the assertion; this test only
|
||||||
|
/// orchestrates the spawn.
|
||||||
|
fn usbReportTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: usb-report\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
var manager: u32 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "device-manager")) continue;
|
||||||
|
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("device-manager spawned in test-usb-restart mode", manager != 0);
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// M18.3: the application surface. device-list enumerates the manager's tree
|
||||||
|
/// over IPC, subscribes with its endpoint as a capability, and prints every
|
||||||
|
/// published event; the manager's delayed test-kill of the reporter produces a
|
||||||
|
/// removed/added storm the subscriber must observe. The harness's ordered
|
||||||
|
/// expect regex is the assertion.
|
||||||
|
fn deviceListTest(boot_information: *const BootInformation) void {
|
||||||
|
log("DANOS-TEST-BEGIN: device-list\n", .{});
|
||||||
|
if (boot_information.initial_ramdisk_len == 0) {
|
||||||
|
check("bootloader handed over an initial_ramdisk", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
|
||||||
|
const rd = initial_ramdisk.Reader.init(image) orelse {
|
||||||
|
check("initial_ramdisk image is valid", false);
|
||||||
|
result();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
process.setInitialRamdisk(image);
|
||||||
|
var manager: u32 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(item.name, "device-manager")) continue;
|
||||||
|
manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("device-manager spawned in test-usb-restart mode", manager != 0);
|
||||||
|
check("device-list spawned", spawnNamed(rd, "device-list"));
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
/// The whole user-side surface at once: spawn process-test's supervisor role,
|
||||||
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
|
||||||
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
/// children supervised, sees them in process_enumerate, kills them (one blocked,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! /system/services/acpi — the ACPI discovery service: the x86 firmware
|
||||||
|
//! interpreter, moved out of ring 0 (docs/m19-m20-plan.md, M20). **Placeholder:
|
||||||
|
//! not implemented until M20.1** — it exists so the build's `-Ddiscovery`
|
||||||
|
//! option has both of its values and the ramdisk's neutral `discovery` slot is
|
||||||
|
//! wired before the implementation lands.
|
||||||
|
//!
|
||||||
|
//! What it becomes (the plan's decisions 5 and 7): claim the `acpi-tables`
|
||||||
|
//! node the kernel publishes (table blobs + the broad io_port grant + the SCI),
|
||||||
|
//! map the tables, and run the **shared AML module** in ring 3 behind a `Hal`
|
||||||
|
//! backed by `mmio_map` and `io_read`/`io_write` — the interpreter cannot tell
|
||||||
|
//! it moved. Then the bus-driver shape: `device_register` the namespace
|
||||||
|
//! devices (`_HID`, `_CRS` resources, containment against the node's
|
||||||
|
//! apertures), report each to the device manager, stay resident under its
|
||||||
|
//! supervision. M21 grows the event side on the same claim: the SCI, PM1 fixed
|
||||||
|
//! events, GPEs, Notify — published through the domain-named power protocol,
|
||||||
|
//! never an "ACPI events" protocol.
|
||||||
|
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Not implemented: exit cleanly and silently (a bare spawn by the
|
||||||
|
// initial-ramdisk sweep must not derange other tests' markers). The
|
||||||
|
// supervisor reads a clean exit as "meant to stop" — correct for a
|
||||||
|
// placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//! device-list — the `ps` analog for the device tree (docs/device-manager.md
|
||||||
|
//! M18.3): asks the device manager for the tree over IPC, prints it, then
|
||||||
|
//! subscribes and prints every published add/remove event. The manager is the
|
||||||
|
//! one answer to "what devices exist" for user space; nothing here touches a
|
||||||
|
//! device_* system call.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
const protocol = runtime.device_manager_protocol;
|
||||||
|
|
||||||
|
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
||||||
|
var line: [96]u8 = undefined;
|
||||||
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
var manager: ?runtime.ipc.Handle = null;
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (manager == null and tries < 200) : (tries += 1) {
|
||||||
|
manager = runtime.ipc.lookup(.device_manager);
|
||||||
|
if (manager == null) runtime.system.sleep(20);
|
||||||
|
}
|
||||||
|
const h = manager orelse {
|
||||||
|
_ = runtime.system.write("device-list: no device manager\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The snapshot — polled briefly, because at boot the bus drivers may still
|
||||||
|
// be scanning: an empty first answer usually just means "too early".
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
var count: u32 = 0;
|
||||||
|
var length: usize = 0;
|
||||||
|
tries = 0;
|
||||||
|
while (tries < 20) : (tries += 1) {
|
||||||
|
const request = protocol.Enumerate{};
|
||||||
|
length = runtime.ipc.call(h, std.mem.asBytes(&request), &reply) catch 0;
|
||||||
|
if (length >= @sizeOf(protocol.EnumerateReply)) {
|
||||||
|
count = std.mem.bytesToValue(protocol.EnumerateReply, reply[0..@sizeOf(protocol.EnumerateReply)]).count;
|
||||||
|
if (count != 0) break;
|
||||||
|
}
|
||||||
|
runtime.system.sleep(100);
|
||||||
|
}
|
||||||
|
writeLine("device-list: {d} devices\n", .{count});
|
||||||
|
var offset: usize = @sizeOf(protocol.EnumerateReply);
|
||||||
|
var index: u32 = 0;
|
||||||
|
while (index < count and offset + @sizeOf(protocol.ChildEntry) <= length) : (index += 1) {
|
||||||
|
const entry = std.mem.bytesToValue(protocol.ChildEntry, reply[offset..][0..@sizeOf(protocol.ChildEntry)]);
|
||||||
|
writeLine("device-list: device {d} port {d} identity {d}\n", .{ entry.parent, entry.bus_address, entry.identity });
|
||||||
|
offset += @sizeOf(protocol.ChildEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The subscription: our endpoint rides as the call's capability; events
|
||||||
|
// arrive as buffered messages carrying the same structs the bus sends.
|
||||||
|
const endpoint = runtime.ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = runtime.system.write("device-list: no endpoint\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const subscribe = protocol.Subscribe{};
|
||||||
|
_ = runtime.ipc.callCap(h, std.mem.asBytes(&subscribe), &reply, endpoint) catch {
|
||||||
|
_ = runtime.system.write("device-list: subscribe failed\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = runtime.system.write("device-list: subscribed\n");
|
||||||
|
|
||||||
|
var receive: [protocol.message_maximum]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = runtime.ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
|
if (!got.isMessage() or got.len < 1) continue;
|
||||||
|
switch (receive[0]) {
|
||||||
|
@intFromEnum(protocol.Operation.child_added) => {
|
||||||
|
if (got.len < protocol.child_added_size) continue;
|
||||||
|
const event = std.mem.bytesToValue(protocol.ChildAdded, receive[0..protocol.child_added_size]);
|
||||||
|
writeLine("device-list: added (device {d} port {d})\n", .{ event.parent, event.bus_address });
|
||||||
|
},
|
||||||
|
@intFromEnum(protocol.Operation.child_removed) => {
|
||||||
|
if (got.len < protocol.child_removed_size) continue;
|
||||||
|
const event = std.mem.bytesToValue(protocol.ChildRemoved, receive[0..protocol.child_removed_size]);
|
||||||
|
writeLine("device-list: removed (device {d} port {d})\n", .{ event.parent, event.bus_address });
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -19,10 +19,13 @@ pub const Role = enum(u8) {
|
|||||||
device = 2,
|
device = 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The message kinds. `child_added`/`child_removed` land in M18.2;
|
/// The message kinds.
|
||||||
/// `enumerate`/`subscribe` in M18.3.
|
|
||||||
pub const Operation = enum(u8) {
|
pub const Operation = enum(u8) {
|
||||||
hello = 1,
|
hello = 1,
|
||||||
|
child_added = 2,
|
||||||
|
child_removed = 3,
|
||||||
|
enumerate = 4,
|
||||||
|
subscribe = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `Hello.device_id` for a driver that serves no enumerated device (a test
|
/// `Hello.device_id` for a driver that serves no enumerated device (a test
|
||||||
@@ -54,5 +57,83 @@ pub const HelloReply = extern struct {
|
|||||||
|
|
||||||
pub const reply_size = @sizeOf(HelloReply);
|
pub const reply_size = @sizeOf(HelloReply);
|
||||||
|
|
||||||
|
/// A bus driver reporting one device it discovered behind its controller
|
||||||
|
/// (docs/device-manager.md "the tree"). Identity is the bus's native language —
|
||||||
|
/// for USB a port-speed class; the (class, subclass, protocol) triple joins it
|
||||||
|
/// once control transfers exist (the USB track). The manager mirrors the child
|
||||||
|
/// into its tree; when the reporting driver dies, the manager prunes everything
|
||||||
|
/// it reported (the children describe protocol state that died with it) and the
|
||||||
|
/// restarted instance rediscovers and re-reports.
|
||||||
|
pub const ChildAdded = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.child_added),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
reserved1: u16 = 0,
|
||||||
|
reserved2: u32 = 0,
|
||||||
|
/// The reporting driver's own device (the controller) — the child's parent.
|
||||||
|
parent: u64,
|
||||||
|
/// Where on the bus (for USB: the root port number, 1-based).
|
||||||
|
bus_address: u64,
|
||||||
|
/// Bus-specific identity (for USB: the PORTSC port-speed class).
|
||||||
|
identity: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const child_added_size = @sizeOf(ChildAdded);
|
||||||
|
|
||||||
|
/// A bus driver reporting a device gone (hot-unplug). Not yet sent by any
|
||||||
|
/// driver — the port scan has no unplug interrupt — but the manager handles it;
|
||||||
|
/// death-pruning covers removal until hotplug lands.
|
||||||
|
pub const ChildRemoved = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.child_removed),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
reserved1: u16 = 0,
|
||||||
|
reserved2: u32 = 0,
|
||||||
|
parent: u64,
|
||||||
|
bus_address: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const child_removed_size = @sizeOf(ChildRemoved);
|
||||||
|
|
||||||
|
/// The manager's answer to a tree report.
|
||||||
|
pub const ReportReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
reserved: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An application asking for the tree (M18.3): the reply is an EnumerateReply
|
||||||
|
/// header followed by `count` ChildEntry records.
|
||||||
|
pub const Enumerate = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.enumerate),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
reserved1: u16 = 0,
|
||||||
|
reserved2: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const EnumerateReply = extern struct {
|
||||||
|
status: i32,
|
||||||
|
/// ChildEntry records following this header.
|
||||||
|
count: u32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const ChildEntry = extern struct {
|
||||||
|
parent: u64,
|
||||||
|
bus_address: u64,
|
||||||
|
identity: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// An application subscribing to published add/remove events (the input-service
|
||||||
|
/// pattern): the subscriber's endpoint rides as the call's **capability**, and
|
||||||
|
/// events arrive on it as buffered messages whose payload is the same
|
||||||
|
/// ChildAdded / ChildRemoved struct the bus drivers send — one encoding, both
|
||||||
|
/// directions.
|
||||||
|
pub const Subscribe = extern struct {
|
||||||
|
operation: u8 = @intFromEnum(Operation.subscribe),
|
||||||
|
reserved0: u8 = 0,
|
||||||
|
reserved1: u16 = 0,
|
||||||
|
reserved2: u32 = 0,
|
||||||
|
};
|
||||||
|
|
||||||
/// Upper bound on any message in this protocol — sizes the endpoint buffers.
|
/// Upper bound on any message in this protocol — sizes the endpoint buffers.
|
||||||
pub const message_maximum = 64;
|
/// Capped by the kernel's IPC MESSAGE_MAXIMUM (256): an EnumerateReply carries
|
||||||
|
/// up to ten ChildEntry records per call, plenty for the mirror's current
|
||||||
|
/// bounds; paging joins the protocol if a tree ever outgrows one message.
|
||||||
|
pub const message_maximum = 256;
|
||||||
|
|||||||
@@ -106,6 +106,84 @@ const maximum_drivers = 16;
|
|||||||
var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers;
|
var drivers: [maximum_drivers]Driver = .{Driver{}} ** maximum_drivers;
|
||||||
var manager_endpoint: runtime.ipc.Handle = 0;
|
var manager_endpoint: runtime.ipc.Handle = 0;
|
||||||
var test_restart_mode = false;
|
var test_restart_mode = false;
|
||||||
|
var test_usb_restart_mode = false;
|
||||||
|
var test_usb_killed = false;
|
||||||
|
var test_kill_pid: u32 = 0;
|
||||||
|
var test_kill_due_ns: u64 = 0;
|
||||||
|
|
||||||
|
/// The application subscribers (M18.3, the input-service pattern): endpoints
|
||||||
|
/// handed over as capabilities, each receiving every child add/remove as a
|
||||||
|
/// buffered message. A subscriber whose endpoint stops accepting (it died) is
|
||||||
|
/// dropped on the failed send.
|
||||||
|
const maximum_subscribers = 8;
|
||||||
|
var subscribers: [maximum_subscribers]?runtime.ipc.Handle = .{null} ** maximum_subscribers;
|
||||||
|
|
||||||
|
/// Publish one event (a ChildAdded or ChildRemoved struct, the same encoding
|
||||||
|
/// the bus drivers send) to every subscriber.
|
||||||
|
fn publishEvent(event: []const u8) void {
|
||||||
|
for (&subscribers) |*slot| {
|
||||||
|
if (slot.*) |handle| {
|
||||||
|
if (!runtime.ipc.send(handle, event)) slot.* = null; // dead subscriber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
||||||
|
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
||||||
|
/// driver instance reported it — that is what death-pruning sweeps by.
|
||||||
|
const Child = struct {
|
||||||
|
used: bool = false,
|
||||||
|
parent: u64 = 0,
|
||||||
|
bus_address: u64 = 0,
|
||||||
|
identity: u64 = 0,
|
||||||
|
reporter: u32 = 0, // the reporting driver instance's process id
|
||||||
|
};
|
||||||
|
|
||||||
|
const maximum_children = 32;
|
||||||
|
var children: [maximum_children]Child = .{Child{}} ** maximum_children;
|
||||||
|
|
||||||
|
/// Record (or refresh) a reported child. Refreshing matters: a restarted bus
|
||||||
|
/// driver re-reports what it rediscovers, and the same (parent, port) must not
|
||||||
|
/// duplicate.
|
||||||
|
fn addChild(parent: u64, bus_address: u64, identity: u64, reporter: u32) bool {
|
||||||
|
var free: ?*Child = null;
|
||||||
|
for (&children) |*child| {
|
||||||
|
if (child.used and child.parent == parent and child.bus_address == bus_address) {
|
||||||
|
child.identity = identity;
|
||||||
|
child.reporter = reporter;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!child.used and free == null) free = child;
|
||||||
|
}
|
||||||
|
const slot = free orelse return false;
|
||||||
|
slot.* = .{ .used = true, .parent = parent, .bus_address = bus_address, .identity = identity, .reporter = reporter };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prune every child a dead driver instance reported: the children describe
|
||||||
|
/// protocol state (slots, rings) that died with the process — keeping the nodes
|
||||||
|
/// would be keeping a lie. The restarted instance rediscovers and re-reports.
|
||||||
|
/// Watchers hear the honest story: removed now, added again on rediscovery.
|
||||||
|
fn pruneChildrenOf(reporter: u32) void {
|
||||||
|
for (&children) |*child| {
|
||||||
|
if (child.used and child.reporter == reporter) {
|
||||||
|
writeLine("device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
||||||
|
child.used = false;
|
||||||
|
const event = protocol.ChildRemoved{ .parent = child.parent, .bus_address = child.bus_address };
|
||||||
|
publishEvent(std.mem.asBytes(&event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many children a driver instance has reported (the test-usb-restart
|
||||||
|
/// trigger counts these).
|
||||||
|
fn childCountOf(reporter: u32) u32 {
|
||||||
|
var n: u32 = 0;
|
||||||
|
for (&children) |*child| {
|
||||||
|
if (child.used and child.reporter == reporter) n += 1;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
fn driverByProcess(process_id: u32) ?*Driver {
|
fn driverByProcess(process_id: u32) ?*Driver {
|
||||||
for (&drivers) |*driver| {
|
for (&drivers) |*driver| {
|
||||||
@@ -171,9 +249,11 @@ fn spawnDriver(driver: *Driver) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A driver died. The exit reason (M17.2) is the whole decision: a clean exit
|
/// A driver died. Prune what it reported first — then the exit reason (M17.2)
|
||||||
/// meant to stop; anything else restarts with backoff until the crash-loop cap.
|
/// is the whole restart decision: a clean exit meant to stop; anything else
|
||||||
|
/// restarts with backoff until the crash-loop cap.
|
||||||
fn onDriverExit(driver: *Driver) void {
|
fn onDriverExit(driver: *Driver) void {
|
||||||
|
pruneChildrenOf(driver.process_id);
|
||||||
const reason = runtime.process.exitReason(driver.process_id) orelse .fault;
|
const reason = runtime.process.exitReason(driver.process_id) orelse .fault;
|
||||||
if (reason == .exited) {
|
if (reason == .exited) {
|
||||||
driver.state = .stopped;
|
driver.state = .stopped;
|
||||||
@@ -201,6 +281,11 @@ fn onDriverExit(driver: *Driver) void {
|
|||||||
/// sweep serves every armed deadline.
|
/// sweep serves every armed deadline.
|
||||||
fn sweepDeadlines() void {
|
fn sweepDeadlines() void {
|
||||||
const now = system.clock();
|
const now = system.clock();
|
||||||
|
if (test_kill_pid != 0 and now >= test_kill_due_ns) {
|
||||||
|
writeLine("device-manager: test mode: killing the reporter\n", .{});
|
||||||
|
_ = system.kill(test_kill_pid);
|
||||||
|
test_kill_pid = 0;
|
||||||
|
}
|
||||||
for (&drivers) |*driver| {
|
for (&drivers) |*driver| {
|
||||||
if (!driver.used) continue;
|
if (!driver.used) continue;
|
||||||
switch (driver.state) {
|
switch (driver.state) {
|
||||||
@@ -260,10 +345,18 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
if (message.len < 1) return 0;
|
||||||
|
switch (message[0]) {
|
||||||
|
@intFromEnum(protocol.Operation.child_added) => return onChildAdded(message, reply, sender),
|
||||||
|
@intFromEnum(protocol.Operation.child_removed) => return onChildRemoved(message, reply, sender),
|
||||||
|
@intFromEnum(protocol.Operation.enumerate) => return onEnumerate(reply),
|
||||||
|
@intFromEnum(protocol.Operation.subscribe) => return onSubscribe(reply, capability),
|
||||||
|
@intFromEnum(protocol.Operation.hello) => {},
|
||||||
|
else => return 0,
|
||||||
|
}
|
||||||
if (message.len < protocol.hello_size) return 0;
|
if (message.len < protocol.hello_size) return 0;
|
||||||
const hello = std.mem.bytesToValue(protocol.Hello, message[0..protocol.hello_size]);
|
const hello = std.mem.bytesToValue(protocol.Hello, message[0..protocol.hello_size]);
|
||||||
if (hello.operation != @intFromEnum(protocol.Operation.hello)) return 0;
|
|
||||||
|
|
||||||
var status: i32 = 0;
|
var status: i32 = 0;
|
||||||
if (hello.version != protocol.version) {
|
if (hello.version != protocol.version) {
|
||||||
@@ -281,6 +374,85 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32) usize {
|
|||||||
return protocol.reply_size;
|
return protocol.reply_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A bus driver reported a discovered device: mirror it, and in
|
||||||
|
/// test-usb-restart mode kill the reporter once after its second child — the
|
||||||
|
/// deterministic trigger for prune -> backoff -> respawn -> re-report.
|
||||||
|
fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
|
||||||
|
if (message.len < protocol.child_added_size) return 0;
|
||||||
|
const report = std.mem.bytesToValue(protocol.ChildAdded, message[0..protocol.child_added_size]);
|
||||||
|
var status: i32 = 0;
|
||||||
|
if (driverByProcess(sender)) |driver| {
|
||||||
|
if (!addChild(report.parent, report.bus_address, report.identity, sender)) status = -1;
|
||||||
|
writeLine("device-manager: child added (device {d} port {d}, identity {d}) by {s}\n", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||||
|
if (status == 0) publishEvent(message[0..protocol.child_added_size]);
|
||||||
|
} else {
|
||||||
|
status = -1;
|
||||||
|
}
|
||||||
|
const report_reply = protocol.ReportReply{ .status = status };
|
||||||
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
||||||
|
if (test_usb_restart_mode and !test_usb_killed and childCountOf(sender) >= 2) {
|
||||||
|
// Delayed, not immediate: the device-list scenario's subscriber needs a
|
||||||
|
// window to enumerate and subscribe before the events start.
|
||||||
|
test_usb_killed = true;
|
||||||
|
test_kill_pid = sender;
|
||||||
|
test_kill_due_ns = system.clock() + 2_000_000_000;
|
||||||
|
_ = system.timerOnce(manager_endpoint, 2100);
|
||||||
|
}
|
||||||
|
return @sizeOf(protocol.ReportReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bus driver reported a device gone (hot-unplug; no sender exists yet, but
|
||||||
|
/// the handler is protocol-complete — death-pruning covers removal until then).
|
||||||
|
fn onChildRemoved(message: []const u8, reply: []u8, sender: u32) usize {
|
||||||
|
if (message.len < protocol.child_removed_size) return 0;
|
||||||
|
const report = std.mem.bytesToValue(protocol.ChildRemoved, message[0..protocol.child_removed_size]);
|
||||||
|
var status: i32 = -1;
|
||||||
|
for (&children) |*child| {
|
||||||
|
if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == sender) {
|
||||||
|
writeLine("device-manager: child removed (device {d} port {d})\n", .{ child.parent, child.bus_address });
|
||||||
|
child.used = false;
|
||||||
|
status = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const report_reply = protocol.ReportReply{ .status = status };
|
||||||
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
||||||
|
return @sizeOf(protocol.ReportReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An application asked for the tree: the mirror, as a header plus entries.
|
||||||
|
fn onEnumerate(reply: []u8) usize {
|
||||||
|
var count: u32 = 0;
|
||||||
|
var offset: usize = @sizeOf(protocol.EnumerateReply);
|
||||||
|
for (&children) |*child| {
|
||||||
|
if (!child.used) continue;
|
||||||
|
if (offset + @sizeOf(protocol.ChildEntry) > reply.len) break;
|
||||||
|
const entry = protocol.ChildEntry{ .parent = child.parent, .bus_address = child.bus_address, .identity = child.identity };
|
||||||
|
@memcpy(reply[offset..][0..@sizeOf(protocol.ChildEntry)], std.mem.asBytes(&entry));
|
||||||
|
offset += @sizeOf(protocol.ChildEntry);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
const header = protocol.EnumerateReply{ .status = 0, .count = count };
|
||||||
|
@memcpy(reply[0..@sizeOf(protocol.EnumerateReply)], std.mem.asBytes(&header));
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An application subscribed: its endpoint arrived as the call's capability.
|
||||||
|
fn onSubscribe(reply: []u8, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
var status: i32 = -1;
|
||||||
|
if (capability) |handle| {
|
||||||
|
for (&subscribers) |*slot| {
|
||||||
|
if (slot.* == null) {
|
||||||
|
slot.* = handle;
|
||||||
|
status = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const report_reply = protocol.ReportReply{ .status = status };
|
||||||
|
@memcpy(reply[0..@sizeOf(protocol.ReportReply)], std.mem.asBytes(&report_reply));
|
||||||
|
return @sizeOf(protocol.ReportReply);
|
||||||
|
}
|
||||||
|
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
||||||
const dead: u32 = @intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit));
|
const dead: u32 = @intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit));
|
||||||
@@ -293,6 +465,7 @@ fn onNotification(badge: u64) void {
|
|||||||
pub fn main(init: runtime.process.Init) void {
|
pub fn main(init: runtime.process.Init) void {
|
||||||
if (init.arguments.get(1)) |mode| {
|
if (init.arguments.get(1)) |mode| {
|
||||||
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
test_restart_mode = std.mem.eql(u8, mode, "test-restart");
|
||||||
|
test_usb_restart_mode = std.mem.eql(u8, mode, "test-usb-restart");
|
||||||
}
|
}
|
||||||
runtime.service.run(protocol.message_maximum, .{
|
runtime.service.run(protocol.message_maximum, .{
|
||||||
.service = .device_manager,
|
.service = .device_manager,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! /system/services/fdt — the devicetree discovery service: the ARM twin of the
|
||||||
|
//! acpi service (docs/m19-m20-plan.md decision 7). **Placeholder: not
|
||||||
|
//! implemented.** It exists so the build's `-Ddiscovery` option has both of its
|
||||||
|
//! values from day one; the implementation lands with the Raspberry Pi
|
||||||
|
//! bring-up (docs/arm.md).
|
||||||
|
//!
|
||||||
|
//! What it becomes: the per-firmware discoverer for boots that hand over a
|
||||||
|
//! flattened device tree instead of ACPI tables. It claims the
|
||||||
|
//! `devicetree-blob` node the kernel publishes (the FDT the loader received),
|
||||||
|
//! walks the tree — pure data, no bytecode, so unlike the acpi service it
|
||||||
|
//! needs no port grant and no interpreter — and, like any bus-shaped driver:
|
||||||
|
//! `device_register`s what it finds (containment against the blob node's
|
||||||
|
//! recorded apertures), reports each child to the device manager
|
||||||
|
//! (`child_added`, identity = the node's `compatible` string), and stays
|
||||||
|
//! resident under the manager's supervision (hello, restart, the usual
|
||||||
|
//! contract).
|
||||||
|
//!
|
||||||
|
//! Known prerequisite recorded in the plan: `DeviceDescriptor`'s 8-byte `hid`
|
||||||
|
//! cannot hold an FDT `compatible` string ("brcm,bcm2835-aux-uart") — identity
|
||||||
|
//! widens before this file grows a body.
|
||||||
|
|
||||||
|
const runtime = @import("runtime");
|
||||||
|
|
||||||
|
pub fn main() void {
|
||||||
|
// Not implemented: exit cleanly and silently (a bare spawn by the
|
||||||
|
// initial-ramdisk sweep must not derange other tests' markers). The
|
||||||
|
// supervisor reads a clean exit as "meant to stop" — correct for a
|
||||||
|
// placeholder.
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = runtime.panic;
|
||||||
|
comptime {
|
||||||
|
_ = &runtime.start._start; // pull the runtime entry shim into the image
|
||||||
|
}
|
||||||
@@ -51,8 +51,9 @@ fn awaitChildExit(endpoint: runtime.ipc.Handle) u32 {
|
|||||||
/// The harness-run child of the signals test: echoes requests, logs the two
|
/// The harness-run child of the signals test: echoes requests, logs the two
|
||||||
/// signals it handles. Terminate makes run() return, and returning from main is
|
/// signals it handles. Terminate makes run() return, and returning from main is
|
||||||
/// the clean exit the parent reads as ExitReason.exited.
|
/// the clean exit the parent reads as ExitReason.exited.
|
||||||
fn echo(message: []const u8, reply: []u8, sender: u32) usize {
|
fn echo(message: []const u8, reply: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
_ = sender;
|
_ = sender;
|
||||||
|
_ = capability;
|
||||||
const n = @min(message.len, reply.len);
|
const n = @min(message.len, reply.len);
|
||||||
@memcpy(reply[0..n], message[0..n]);
|
@memcpy(reply[0..n], message[0..n]);
|
||||||
return n;
|
return n;
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ fn releaseClientHandles(client: u32) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
||||||
fn handle(message: []const u8, out: []u8, sender: u32) usize {
|
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
||||||
|
_ = capability;
|
||||||
if (message.len < protocol.request_size) return fail(out);
|
if (message.len < protocol.request_size) return fail(out);
|
||||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||||
const payload = message[protocol.request_size..];
|
const payload = message[protocol.request_size..];
|
||||||
|
|||||||
@@ -258,6 +258,37 @@ CASES = [
|
|||||||
"smp": 4,
|
"smp": 4,
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M18.2: bus tree reports — the xHCI driver scans its root-hub ports and
|
||||||
|
# reports both QEMU devices; the manager mirrors, prunes on the reporter's
|
||||||
|
# death, and the respawned driver re-reports (docs/device-manager.md).
|
||||||
|
{"name": "usb-report",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 90,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
|
"expect": r"device-manager: child added[\s\S]*"
|
||||||
|
r"device-manager: child added[\s\S]*"
|
||||||
|
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||||
|
r"device-manager: child removed[\s\S]*"
|
||||||
|
r"device-manager: restarting usb-xhci-bus[\s\S]*"
|
||||||
|
r"device-manager: child added",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# M18.3: the application surface — device-list enumerates the tree over IPC,
|
||||||
|
# subscribes (endpoint as capability), and observes the removed/added events
|
||||||
|
# the reporter's test-kill produces (docs/device-manager.md).
|
||||||
|
{"name": "device-list",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 90,
|
||||||
|
"qemu_extra": ["-device", "qemu-xhci,id=xhci",
|
||||||
|
"-device", "usb-kbd,bus=xhci.0",
|
||||||
|
"-device", "usb-mouse,bus=xhci.0"],
|
||||||
|
"expect": r"device-list: 2 devices[\s\S]*"
|
||||||
|
r"device-list: subscribed[\s\S]*"
|
||||||
|
r"device-manager: test mode: killing the reporter[\s\S]*"
|
||||||
|
r"device-list: removed \(device[\s\S]*"
|
||||||
|
r"device-list: added \(device",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# M18.1: the device manager's hello + restart policy — xHCI hellos clean and
|
# M18.1: the device manager's hello + restart policy — xHCI hellos clean and
|
||||||
# stays; crash-test faults, is restarted with backoff (re-claiming its device
|
# stays; crash-test faults, is restarted with backoff (re-claiming its device
|
||||||
# each time), and hits the crash-loop cap (docs/device-manager.md).
|
# each time), and hits the crash-loop cap (docs/device-manager.md).
|
||||||
|
|||||||
Reference in New Issue
Block a user