From 27b75c3556bf3f64ad7548c13ceb8f8f1efd4097 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 00:12:38 +0200 Subject: [PATCH 1/4] One block service and a driver per disk controller; input names each device; SCSI is its own crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 10's first half under the orchestrator's block ruling (recorded in the device-function track): a disk driver is a `drives` row receiving `disk`, reads its own table and offers its disks; diskserver is the block service alone, and hands each opened session's client connection and region to the offering driver, which answers the open and serves it. No block passes through the service. - toyos-blockring: `disk`, the driver/service frames; `Listed` names its disk. - lib/blockserve: a disk driver's side — its table, its link (it connects again and offers again when the service restarts), each disk's sessions over a `Device` it implements. - system/nvme: the NVMe driver, moved out of diskserver whole, one process per controller; it describes its claim once (the claim answers once). - diskserver: what is listed and admitted, by grant, across every offer; a listing and an open of what no disk offers wait until every boot-started driver has settled, bounded by DRIVER_BOUND (30 s) and naming the driver that did not. - supervisor: disk drivers start before the storage rows; the block service is told how many (`--drivers`) and which controllers got none (`--undriven`); the slot table is the one on the running ROOT's disk. - SDK: `Connection::received` and `into_owned`, to pass a connection on. - nvme_disk_boots requires each role served: it passed with all three absent while the driver's second describe was refused. toyos-input's records carry a u16 `source` on keys and pointers and a `gone` per source in place of `LOST`; the owners hold each connection's devices by source, at most `DEVICES` holding something, and drop a sender past it. ps2server's pointer is source 0 and its keyboard source 1. toyos-scsi is `lib/xhci/src/scsi.rs` moved, `no_std`, with no xHCI dependency; `Sense::of` reads only fixed format (SPC-4 table 27), refuses a short additional length, and carries a deferred error as one, which never reads as the current command's INVALID COMMAND OPERATION CODE. Closes scsi-is-decoded-inside-the-xhci-crate and sense-data-is-read-as-fixed-format-whatever-its-response-code-says. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 33 +- Cargo.toml | 3 + images/console.toml | 16 +- images/system.toml | 16 +- ...n-is-driven-from-its-arrival-to-its-end.md | 25 +- ...-boot-is-bound-inside-a-scheduling-pass.md | 2 +- ...kserver-claims-qemus-nvme-by-its-pci-id.md | 43 +- ...p-open-codes-what-toyos-pci-claim-holds.md | 6 +- .../scsi-is-decoded-inside-the-xhci-crate.md | 27 - ...-format-whatever-its-response-code-says.md | 33 - ...-small-interrupts-post-and-threads-wait.md | 12 +- kernel/Cargo.toml | 1 + kernel/src/drivers/xhci/wait/msc.rs | 11 +- lib/blockring/src/disk.rs | 232 ++++ lib/blockring/src/lib.rs | 11 +- lib/blockring/src/wire.rs | 39 +- lib/blockserve/Cargo.toml | 13 + lib/blockserve/src/lib.rs | 23 + lib/blockserve/src/link.rs | 144 ++ .../blockserve}/src/region.rs | 2 +- lib/blockserve/src/sessions.rs | 399 ++++++ lib/blockserve/src/table.rs | 61 + lib/input/src/lib.rs | 9 +- lib/input/src/merge.rs | 121 +- lib/input/src/wire.rs | 148 +- lib/input/tests/input.rs | 156 ++- lib/scsi/Cargo.toml | 6 + lib/{xhci/src/scsi.rs => scsi/src/lib.rs} | 122 +- lib/xhci/Cargo.toml | 4 + lib/xhci/src/bot.rs | 2 +- lib/xhci/src/lib.rs | 1 - sdk/toyos/src/ipc.rs | 17 +- system/compositor/src/session.rs | 58 +- system/console/src/main.rs | 25 +- system/diskserver/Cargo.toml | 14 +- system/diskserver/src/lib.rs | 11 +- system/diskserver/src/main.rs | 1224 +++++++---------- system/diskserver/src/session.rs | 2 +- system/nvme/Cargo.toml | 15 + system/nvme/src/main.rs | 226 +++ system/{diskserver => nvme}/src/nvme.rs | 23 +- system/ps2server/src/main.rs | 4 +- system/ps2server/src/probe.rs | 2 +- system/ps2server/src/stream.rs | 31 +- system/supervisor/src/main.rs | 109 +- tests/common/iommu.rs | 6 +- tests/common/qemu.rs | 6 +- .../guest/rust-tests/src/bin/acpi_mediated.rs | 4 +- tests/images/acpicase.toml | 16 +- tests/images/jobcase.toml | 16 +- tests/images/latencycase.toml | 16 +- tests/images/metalcase.toml | 16 +- tests/images/metaldevicecase.toml | 16 +- tests/images/netcase.toml | 16 +- tests/images/proctreecase.toml | 16 +- tests/images/testcases.toml | 16 +- tests/toyos.rs | 27 +- 57 files changed, 2519 insertions(+), 1134 deletions(-) delete mode 100644 issues/scsi-is-decoded-inside-the-xhci-crate.md delete mode 100644 issues/sense-data-is-read-as-fixed-format-whatever-its-response-code-says.md create mode 100644 lib/blockring/src/disk.rs create mode 100644 lib/blockserve/Cargo.toml create mode 100644 lib/blockserve/src/lib.rs create mode 100644 lib/blockserve/src/link.rs rename {system/diskserver => lib/blockserve}/src/region.rs (96%) create mode 100644 lib/blockserve/src/sessions.rs create mode 100644 lib/blockserve/src/table.rs create mode 100644 lib/scsi/Cargo.toml rename lib/{xhci/src/scsi.rs => scsi/src/lib.rs} (87%) create mode 100644 system/nvme/Cargo.toml create mode 100644 system/nvme/src/main.rs rename system/{diskserver => nvme}/src/nvme.rs (95%) diff --git a/Cargo.lock b/Cargo.lock index bdcf37105cb..cab793c1d22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -991,10 +991,9 @@ version = "0.0.0" dependencies = [ "toyos", "toyos-abi", - "toyos-blockhold", "toyos-blockring", + "toyos-blockserve", "toyos-disk", - "toyos-gpt", ] [[package]] @@ -1977,6 +1976,7 @@ dependencies = [ "toyos-quiesce", "toyos-random", "toyos-rootimage", + "toyos-scsi", "toyos-smmu", "toyos-symbols", "toyos-tco", @@ -2379,6 +2379,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "nvme" +version = "0.0.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-blockring", + "toyos-blockserve", + "toyos-gpt", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -4372,6 +4383,17 @@ dependencies = [ "toyos-transport", ] +[[package]] +name = "toyos-blockserve" +version = "0.1.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-blockhold", + "toyos-blockring", + "toyos-gpt", +] + [[package]] name = "toyos-bootmap" version = "0.1.0" @@ -4767,6 +4789,10 @@ dependencies = [ name = "toyos-rootimage" version = "0.1.0" +[[package]] +name = "toyos-scsi" +version = "0.1.0" + [[package]] name = "toyos-sha2" version = "0.1.0" @@ -4953,6 +4979,9 @@ dependencies = [ [[package]] name = "toyos-xhci" version = "0.1.0" +dependencies = [ + "toyos-scsi", +] [[package]] name = "toyos-xhci-sim" diff --git a/Cargo.toml b/Cargo.toml index 6e769c19d2a..c3d4c887d23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "lib/blockhold", "lib/blockring", "lib/blockring/transport", + "lib/blockserve", "lib/bootmap", "lib/crypto", "lib/device-memory", @@ -64,6 +65,7 @@ members = [ "lib/quiesce", "lib/random", "lib/rootimage", + "lib/scsi", "lib/sha2", "lib/sha2-hw", "lib/swap", @@ -116,6 +118,7 @@ members = [ "system/netstack/shard/testnet", "system/netstack/udp", "system/netstack/wire", + "system/nvme", "system/pkg", "system/ps2server", "system/ps2server/ps2", diff --git a/images/console.toml b/images/console.toml index a8ee84176ff..b839a9198f1 100644 --- a/images/console.toml +++ b/images/console.toml @@ -92,14 +92,20 @@ syscap = ["roster"] "bin/shutdown" = "/system/bin/toybox" "bin/stats" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/images/system.toml b/images/system.toml index ac0d0197803..3f9d3eae96a 100644 --- a/images/system.toml +++ b/images/system.toml @@ -261,14 +261,20 @@ starts = ["toybox", "shell", "terminal", "doom", "snake", "calc", "editor", "pai "bin/stats" = "/system/bin/toybox" "bin/tone" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md b/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md index b255dc52c27..58928562b34 100644 --- a/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md +++ b/issues/a-device-function-is-driven-from-its-arrival-to-its-end.md @@ -53,6 +53,22 @@ started. Nothing reaches userland when a device arrives or leaves. pull request before stage 1's guest smoke (`issues/a-cargo-run-boot-claims-every-pcidev-slot.md`). +**Ruled** (orchestrator, 2026-10-20, on the step-10 brief; it replaces #901's +`block@` ports, `each`, `pci@` and several block services): + +- One shared block service, diskserver, serves `block` to its clients + (unchanged: grants are supervisor-minted badges) and `disk` to disk drivers. + A disk driver is a `drives` row receiving `disk`: it reads its own GPT, + offers each disk's partitions, and says when a disk is gone. +- On an open the service checks the grant and hands the client's connection + and region to the offering driver, which answers `MSG_OPENED` on the + client's connection and serves the session itself; data never passes the + service (`toyos_blockring::disk`, `lib/blockserve`). +- The supervisor starts the `drives` processes before the storage rows and + tells the block service how many disk drivers it started. An open of a + partition no disk offers, and a listing, wait until every boot-started disk + driver has settled, each wait bounded and naming the driver that did not. + **Measured**: - **Releases versus the end**, before stage 1a (`usbd_drives_the_spare` on @@ -117,15 +133,20 @@ started. Nothing reaches userland when a device arrives or leaves. then, and refuses every later offer. A driver gone after soundserver began playing to it ends soundserver. QEMU's two-controller row is `hda_plays_to_the_controller_that_reaches_an_output`. + - **Done for NVMe.** `nvme` is a `drives` row, one process per + controller, offering its disk to diskserver, which is the block service + alone (the ruling above). - Next: virtio-sound is still driven inside soundserver (`system/soundserver/src/virtio.rs`); it becomes a driver on the same port. Then the output set: a second output offered while one plays, a driver's `gone`, and a stream moved to another output, none of which - soundserver serves yet; and netstack and diskserver. + soundserver serves yet; and netstack. 4. **usbd drives every xHCI**, one per controller, started by stage 1's `drives` rows. This is step 10 of `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, whose - `input` port is this track's per-device source and `gone`. + `input` port is this track's per-device source and `gone` (`toyos-input` + carries both), and whose sticks are disks offered on `disk` (the ruling + above). ## Folded here diff --git a/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md b/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md index 64d6e179b27..c5c18c48b97 100644 --- a/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md +++ b/issues/a-disk-plugged-in-after-boot-is-bound-inside-a-scheduling-pass.md @@ -7,7 +7,7 @@ opened: 2026-08-10 # A disk plugged in after boot is bound inside a scheduling pass The BOT round trip and the SCSI bring-up above it are pure machines, -`toyos_xhci::bot::RoundTrip` and `toyos_xhci::scsi::BringUp`, and the kernel +`toyos_xhci::bot::RoundTrip` and `toyos_scsi::BringUp`, and the kernel drives both blocking, in place. For the read/write entry points that is the caller's own time. For the bind it is not: `msc::bind` is the one call site where a scheduling pass can still spend its transfer budget inside xHCI — for a diff --git a/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md b/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md index bb64e18908b..bcd4c7573e1 100644 --- a/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md +++ b/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md @@ -4,17 +4,15 @@ kind: defect opened: 2026-10-12 --- -# diskserver claims QEMU's NVMe by its PCI id and no other controller - -Both shipped boot configs give diskserver one device, `pci:01:08:02/1b36:0010` -(`images/system.toml`, `images/console.toml`): the NVMe class with the vendor and device id of -QEMU's own `nvme` model, under Red Hat's vendor id. The supervisor matches the id exactly -(`toyos_manifest::device::PciMatch`, `toyos_supervisor::claims`), so on any other NVMe controller it -endows diskserver nothing, diskserver says `no NVMe controller this row names is on -this machine; serving no partition`, and DATA, LOG and BOOT are absent unless -the image is on a USB stick the kernel drives. The driver is not QEMU's: -`Controller::open` (`system/diskserver/src/nvme.rs`) takes any function whose -class code is `01:08:02`. +# The NVMe driver drives QEMU's NVMe by its PCI id and no other controller + +Both shipped boot configs give the `nvme` driver's `drives` row one matcher, +`pci:01:08:02/1b36:0010` (`images/system.toml`, `images/console.toml`): the NVMe class with the +vendor and device id of QEMU's own `nvme` model, under Red Hat's vendor id. The supervisor matches +the id exactly (`toyos_manifest::device::PciMatch`, `toyos_supervisor::drives`), so on any other +NVMe controller no driver starts, and DATA, LOG and BOOT are absent unless the image is on a USB +stick the kernel drives. The driver is not QEMU's: `Controller::open` (`system/nvme/src/nvme.rs`) +takes any function whose class code is `01:08:02`. Read, not booted: @@ -34,19 +32,18 @@ and bhyve each set. `issues/the-lan-is-not-yet-production-grade.md` plans the same for NICs ("init binds drivers to hardware from a declared match table by vendor, device and class"). -Two constraints a class row meets that an id row does not: a machine with two -NVMe controllers matches twice, which refuses diskserver's start, naming both -(`toyos_supervisor::claims`); and a machine whose NVMe holds another operating system, as the T14's -holds Ubuntu, is handed to diskserver, which then serves only the partitions -its table and grants name (`system/diskserver/src/main.rs`). +A constraint a class row meets that an id row does not: a machine whose NVMe +holds another operating system, as the T14's holds Ubuntu, has it driven and +offered to the block service, which then serves only the partitions its grants +name (`system/diskserver/src/main.rs`). A machine with two NVMe controllers +gets a driver on each. **Owner**: whoever holds `issues/toyos-runs-in-any-open-source-hypervisor.md`. -**Exit**: a `devices` row names a PCI function by class code, which -`toyos_manifest::device::PciMatch::parse` takes and refuses malformed (#926); both -shipped configs name NVMe that way; and a guest boot whose controller is -QEMU's `nvme` with `use-intel-id=on` serves DATA through diskserver, which a -row naming `1b36:0010` instead reds. The configs the T14 boots keep their id -row, outside this exit, until the T14's NVMe holds no Ubuntu: a class row there -would hand diskserver the disk the metal workflow reboots through (the +**Exit**: both shipped configs' `nvme` row names NVMe by class code, which +`toyos_manifest::device::PciMatch::parse` takes (#926); and a guest boot whose +controller is QEMU's `nvme` with `use-intel-id=on` serves DATA through the block +service, which a row naming `1b36:0010` instead reds. The configs the T14 boots +keep their id row, outside this exit, until the T14's NVMe holds no Ubuntu: a +class row there would drive the disk the metal workflow reboots through (the orchestrator's ruling, #896, issuecomment-6131213771, ruling 4). diff --git a/issues/diskservers-nvme-bring-up-open-codes-what-toyos-pci-claim-holds.md b/issues/diskservers-nvme-bring-up-open-codes-what-toyos-pci-claim-holds.md index c8b1e41ab50..b3e7a50f28e 100644 --- a/issues/diskservers-nvme-bring-up-open-codes-what-toyos-pci-claim-holds.md +++ b/issues/diskservers-nvme-bring-up-open-codes-what-toyos-pci-claim-holds.md @@ -4,9 +4,9 @@ kind: defect opened: 2026-10-10 --- -# diskserver's NVMe bring-up open-codes what `toyos-pci-claim` holds +# The NVMe driver's bring-up open-codes what `toyos-pci-claim` holds -`Controller::open` (`system/diskserver/src/nvme.rs`) maps BAR 0 with +`Controller::open` (`system/nvme/src/nvme.rs`) maps BAR 0 with `PciDev::map_bar` and an `unsafe Window::new` over it, allocates its grant with `PciDev::dma_alloc` and another `unsafe Window::new`, and names each kernel refusal as `Refusal::Kernel(call, e)`. That is `Bar::map`, `Grant::alloc` and @@ -14,5 +14,5 @@ refusal as `Refusal::Kernel(call, e)`. That is `Bar::map`, `Grant::alloc` and drivers and soundserver's virtio-sound driver use: two more `unsafe` sites whose argument is the one the crate already makes. -Exit: diskserver reaches its BAR and grant through `toyos-pci-claim`, or the +Exit: the NVMe driver reaches its BAR and grant through `toyos-pci-claim`, or the reason it cannot is at the crate's module header. diff --git a/issues/scsi-is-decoded-inside-the-xhci-crate.md b/issues/scsi-is-decoded-inside-the-xhci-crate.md deleted file mode 100644 index c0ce1c6659a..00000000000 --- a/issues/scsi-is-decoded-inside-the-xhci-crate.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-10 ---- - -# SCSI is decoded inside the xHCI crate - -`lib/xhci/src/scsi.rs` holds the SCSI half of a Bulk-Only disk: the -commands sent (SPC-4, SBC-3), the sense data and READ CAPACITY answers read, -and the bring-up from a configured interface to a disk with a size. None of -it is xHCI's. SCSI is the command set of the device behind the transport, and -those bytes are chosen by the device, so the module is an input boundary of -its own sitting in the crate whose subject is the root-hub port machine. Its -only consumer is the kernel's mass-storage driver -(`kernel/src/drivers/xhci/wait/msc.rs`, `stop.rs`), which takes it and the -xHCI decisions from one crate, so the two subjects are not yet pulled apart. -`lib/xhci/src/bot.rs`, the Bulk-Only Transport, is USB's and stays. - -Owned by usbd, the small-kernel track's step 10 -(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), which -takes mass storage out of the kernel and is the first program to consume the -SCSI decoder apart from the controller. - -**Exit condition.** When usbd takes mass storage, the SCSI decoder is a crate -of its own, `no_std` with no dependency on `toyos-xhci`, and its host tests -move with it; `toyos-xhci` no longer has a `scsi` module. diff --git a/issues/sense-data-is-read-as-fixed-format-whatever-its-response-code-says.md b/issues/sense-data-is-read-as-fixed-format-whatever-its-response-code-says.md deleted file mode 100644 index 1baa27b37b6..00000000000 --- a/issues/sense-data-is-read-as-fixed-format-whatever-its-response-code-says.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-09 ---- - -# Sense data is read as fixed format, and as the current command's, whatever its response code says - -`toyos_xhci::scsi::Sense::of` takes the sense key from byte 2 and the ASC and -ASCQ from bytes 12 and 13 of any fourteen bytes a device answers REQUEST SENSE -with. SPC-4 makes byte 0's response code say what the rest is: 0x70 current -and 0x71 deferred in fixed format, 0x72 and 0x73 in descriptor format, where -byte 2 is the ASC. So a descriptor-format answer is read from the wrong bytes, -and a deferred error, which is about a command that already completed, is read -as the current command's. The additional sense length in byte 7 is not held to -reach byte 13 either. - -Where it costs: `scsi::flushed` turns a SYNCHRONIZE CACHE refused with INVALID -COMMAND OPERATION CODE into `Flushed::NoCache`, the claim that every completed -write is durable. A descriptor-format sense whose bytes 2, 12 and 13 read -0x05, 0x20 and 0x00 makes that claim for a flush that failed. REQUEST SENSE -is sent with DESC 0, so such an answer is outside the standard, which is the -input the decoder exists to refuse. Found by reading the module; no device in -reach is known to answer this way. - -Owned by usbd, the small-kernel track's step 10 -(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`), whose -mass storage stands on this module. - -**Exit condition.** `Sense::of` refuses by name a response code other than -0x70 or 0x71 and an additional sense length short of byte 13, carries a -deferred error as one, and a host test holds each against SPC-4's response -code table. diff --git a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md index e4d94d5e4c7..bd4e0f1a27e 100644 --- a/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -72,8 +72,9 @@ Beside that order, each waiting on something outside it: session reopened over the page a killed server left, by fileserver's `a_directory_rename_is_whole_under_one_name_wherever_the_server_is_killed` for DATA, and for LOG by the FAT test step 10 owes. **Partly done** - (#525, #536): diskserver drives NVMe, and the kernel's NVMe driver is - gone. **Left**: on the T14, `/log` and `/home` are on the + (#525, #536): NVMe is driven from userland, and the kernel's NVMe + driver is gone; the `nvme` driver offers its disk to diskserver, the block + service. **Left**: on the T14, `/log` and `/home` are on the stick, which the kernel drives until step 10; the kernel's USB storage, block layer, page cache and refusal chain go at step 9. 4. **Where the data filesystem lives** — owner ruling, 2026-09-26, from the @@ -181,8 +182,11 @@ Beside that order, each waiting on something outside it: `issues/a-device-function-is-driven-from-its-arrival-to-its-end.md`: usbd is a `drives` row, one process per xHCI controller the kernel leaves. The whole xHCI moves, HID delivered - to the owner of input over the `input` port and mass storage over - `toyos-blockring`. A USB keyboard's transitions never reach the + to the owner of input over the `input` port, each device its own + `source` with its own `gone`, and each stick a disk offered to the block + service on `disk`, whose sessions usbd serves over `toyos-blockring` + (`issues/a-device-function-is-driven-from-its-arrival-to-its-end.md`'s + block ruling, which replaces #901's `block@` ports). A USB keyboard's transitions never reach the kernel, whose keyboard and mouse streams go when their last kernel source leaves (the ruling on #899, applied to usbd on #901). It depends on stage 7 step 2 landing first, because at step 10 usbd's diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 4d2c908f539..a6907d37e02 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -498,6 +498,7 @@ toyos-tsc = { path = "../lib/tsc" } toyos-rootimage = { path = "../lib/rootimage" } toyos-quiesce = { path = "../lib/quiesce" } toyos-random = { path = "../lib/random" } +toyos-scsi = { path = "../lib/scsi" } toyos-symbols = { path = "../lib/symbols" } toyos-untrusted = { path = "../lib/untrusted" } toyos-usbhid = { path = "usbhid" } diff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs index 0e1c645e515..a1134debeac 100644 --- a/kernel/src/drivers/xhci/wait/msc.rs +++ b/kernel/src/drivers/xhci/wait/msc.rs @@ -30,8 +30,8 @@ use toyos_xhci::job::CC_SUCCESS; use toyos_xhci::ladder::{self, AfterReset, AfterRung, Left, PortStep, Run, Rung}; use toyos_xhci::port; use toyos_xhci::reset_recovery::{self, Answered, GaveUp, Look, Pipe, Quiescing, SlotGoes, Step}; -use toyos_xhci::scsi::{self, BringUp, Cdb, Fail, Flushed, Geometry, Heard, Moved, Printable, Reply}; -use toyos_xhci::scsi::{Refusal, Sense, Transfer, HOST_BLOCK}; +use toyos_scsi::{self as scsi, BringUp, Cdb, Fail, Flushed, Geometry, Heard, Moved, Printable, Reply}; +use toyos_scsi::{Refusal, Sense, Transfer, HOST_BLOCK}; /// A region, not an address: the CBW's length is the region's own size, so /// no command can name a length its destination lacks. @@ -858,7 +858,10 @@ impl XhciController { Ok(Bot::Done { delivered }) => { let mut response = [0u8; scsi::SENSE_BYTES]; dma.copy_to(dev.block + MSC_SCRATCH, &mut response); - Sense::of(&response, delivered) + Sense::of(&response, delivered).unwrap_or_else(|why| { + log!("usb-storage: {} answered sense this driver does not read: {why:?}", self.slot(dev.slot_id)); + Sense::NONE + }) } _ => Sense::NONE, } @@ -1461,7 +1464,7 @@ enum Up { } /// Everything between a configured interface and a disk with a size, as -/// `toyos_xhci::scsi::BringUp` asks for it. +/// `toyos_scsi::BringUp` asks for it. fn bring_up(ctrl: &mut XhciController, dev: &mut MscDevice) -> Up { let slot = dev.slot_id; let dma = ctrl.dma(); diff --git a/lib/blockring/src/disk.rs b/lib/blockring/src/disk.rs new file mode 100644 index 00000000000..097a8db9f5e --- /dev/null +++ b/lib/blockring/src/disk.rs @@ -0,0 +1,232 @@ +//! The frames between the block service and a disk driver, on the `disk` +//! port the service serves and every disk driver receives. +//! +//! **A driver offers, the service routes.** A driver says who it is +//! ([`MSG_DRIVER`]), offers each disk it brought up with the partitions its +//! own table read names ([`MSG_OFFER`]), says once that every disk present at +//! its bring-up is offered ([`MSG_SETTLED`]), and that a disk left +//! ([`MSG_GONE`]). The service lists and admits what clients ask for against +//! those offers, and hands each opened session's client connection and region +//! to the driver offering its partition ([`MSG_SESSION`]): the driver answers +//! the client's open itself and serves the session, so a block never passes +//! through the service. +//! +//! **A disk is the driver's number for it**, unique for the life of its +//! connection; the service numbers disks across drivers itself. + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::wire::GUID_BYTES; + +/// The port the block service serves to disk drivers. +pub const PORT: &str = "disk"; + +/// Driver to service, first: the driver's name for the service's lines, +/// UTF-8, at most [`MAX_LABEL`] bytes. +pub const MSG_DRIVER: u32 = 1; +/// Driver to service: one disk and every partition its table names, an +/// [`Offer`]. +pub const MSG_OFFER: u32 = 2; +/// Driver to service, once: every disk present at its bring-up is offered, +/// or its controller is one it cannot use. A [`Settled`] word. +pub const MSG_SETTLED: u32 = 3; +/// Driver to service: the disk numbered by the payload's four bytes left, and +/// every session on it ended. +pub const MSG_GONE: u32 = 4; +/// Service to driver: serve this session, a [`Session`]. Handles, in order: +/// the client's connection, and the region it sent with its open. +pub const MSG_SESSION: u32 = 5; + +pub const MAX_LABEL: usize = 64; +/// The partitions one offer may name: a GPT's whole entry array at the +/// standard size. +pub const MAX_PARTS: usize = 128; + +/// One partition of an offered disk. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Part { + pub unique: [u8; GUID_BYTES], + pub kind: [u8; GUID_BYTES], + /// Its length in blocks, or `None` for one the driver will not serve: a + /// range that is not whole blocks, or one its own table refuses. + pub blocks: Option, +} + +impl Part { + pub const BYTES: usize = 2 * GUID_BYTES + 1 + 8; + + fn encode(&self, out: &mut Vec) { + out.extend_from_slice(&self.unique); + out.extend_from_slice(&self.kind); + out.push(u8::from(self.blocks.is_some())); + out.extend_from_slice(&self.blocks.unwrap_or(0).to_le_bytes()); + } + + fn decode(bytes: &[u8; Self::BYTES]) -> Option { + let blocks = u64::from_le_bytes(bytes[33..].try_into().ok()?); + let blocks = match bytes[32] { + 0 if blocks == 0 => None, + 1 => Some(blocks), + _ => return None, + }; + Some(Self { + unique: bytes[..GUID_BYTES].try_into().ok()?, + kind: bytes[GUID_BYTES..2 * GUID_BYTES].try_into().ok()?, + blocks, + }) + } +} + +/// One disk, as [`MSG_OFFER`] carries it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Offer { + pub disk: u32, + pub parts: Vec, +} + +impl Offer { + pub fn encode(&self) -> Vec { + assert!(self.parts.len() <= MAX_PARTS, "an offer of {} partitions", self.parts.len()); + let mut out = Vec::with_capacity(4 + self.parts.len() * Part::BYTES); + out.extend_from_slice(&self.disk.to_le_bytes()); + self.parts.iter().for_each(|p| p.encode(&mut out)); + out + } + + /// `None` for bytes that are not a disk number and whole entries, or + /// more entries than [`MAX_PARTS`]. + pub fn decode(bytes: &[u8]) -> Option { + let (disk, rest) = bytes.split_first_chunk::<4>()?; + let (entries, torn) = rest.as_chunks::<{ Part::BYTES }>(); + if !torn.is_empty() || entries.len() > MAX_PARTS { + return None; + } + let parts = entries.iter().map(Part::decode).collect::>>()?; + Some(Self { disk: u32::from_le_bytes(*disk), parts }) + } +} + +/// How a driver's bring-up ended. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Settled { + /// Every disk present at its bring-up is offered. + Offered, + /// Its controller cannot be used: what it would have offered is not + /// absent, and a listing says so. + Unusable, +} + +impl Settled { + pub fn encode(self) -> [u8; 4] { + match self { + Self::Offered => 0u32, + Self::Unusable => 1, + } + .to_le_bytes() + } + + pub fn decode(bytes: &[u8]) -> Option { + match u32::from_le_bytes(bytes.try_into().ok()?) { + 0 => Some(Self::Offered), + 1 => Some(Self::Unusable), + _ => None, + } + } +} + +/// A session the service admitted, as [`MSG_SESSION`] carries it: on the +/// driver's disk `disk`, the partition `unique`, taking writes or not. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Session { + pub disk: u32, + pub unique: [u8; GUID_BYTES], + pub writes: bool, +} + +impl Session { + pub const BYTES: usize = 4 + GUID_BYTES + 1; + + pub fn encode(&self) -> [u8; Self::BYTES] { + let mut out = [0u8; Self::BYTES]; + out[..4].copy_from_slice(&self.disk.to_le_bytes()); + out[4..4 + GUID_BYTES].copy_from_slice(&self.unique); + out[4 + GUID_BYTES] = u8::from(self.writes); + out + } + + pub fn decode(bytes: &[u8]) -> Option { + let bytes: &[u8; Self::BYTES] = bytes.try_into().ok()?; + let writes = match bytes[4 + GUID_BYTES] { + 0 => false, + 1 => true, + _ => return None, + }; + Some(Self { + disk: u32::from_le_bytes(bytes[..4].try_into().ok()?), + unique: bytes[4..4 + GUID_BYTES].try_into().ok()?, + writes, + }) + } +} + +/// A [`MSG_DRIVER`] or [`MSG_GONE`] payload. +pub fn label(bytes: &[u8]) -> Option { + (1..=MAX_LABEL).contains(&bytes.len()).then(|| core::str::from_utf8(bytes).ok().map(String::from))? +} + +pub fn disk(bytes: &[u8]) -> Option { + Some(u32::from_le_bytes(bytes.try_into().ok()?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_frame_survives_its_bytes_and_nothing_else_decodes() { + let offer = Offer { + disk: 7, + parts: vec![ + Part { unique: [1; GUID_BYTES], kind: [2; GUID_BYTES], blocks: Some(u64::MAX) }, + Part { unique: [3; GUID_BYTES], kind: [4; GUID_BYTES], blocks: None }, + ], + }; + let bytes = offer.encode(); + assert_eq!(Offer::decode(&bytes), Some(offer.clone())); + assert_eq!(Offer::decode(&bytes[..bytes.len() - 1]), None, "a torn entry"); + assert_eq!(Offer::decode(&bytes[..3]), None, "no disk number"); + assert_eq!(Offer::decode(&7u32.to_le_bytes()), Some(Offer { disk: 7, parts: vec![] }), "a disk with no table"); + let mut flag = bytes.clone(); + flag[4 + 32] = 2; + assert_eq!(Offer::decode(&flag), None, "a usable flag that is neither"); + let mut unusable_with_length = bytes; + let second = 4 + Part::BYTES; + unusable_with_length[second + 33] = 1; + assert_eq!(Offer::decode(&unusable_with_length), None, "an unserved partition with a length"); + let many = Offer { disk: 0, parts: vec![offer.parts[0]; MAX_PARTS] }; + assert!(Offer::decode(&many.encode()).is_some()); + let mut past = many.encode(); + past.extend_from_within(4..4 + Part::BYTES); + assert_eq!(Offer::decode(&past), None, "past a GPT's whole array"); + + for settled in [Settled::Offered, Settled::Unusable] { + assert_eq!(Settled::decode(&settled.encode()), Some(settled)); + } + assert_eq!(Settled::decode(&2u32.to_le_bytes()), None); + + let session = Session { disk: 3, unique: [9; GUID_BYTES], writes: true }; + assert_eq!(Session::decode(&session.encode()), Some(session)); + let mut bad = session.encode(); + bad[Session::BYTES - 1] = 2; + assert_eq!(Session::decode(&bad), None); + assert_eq!(Session::decode(&session.encode()[1..]), None); + + assert_eq!(label(b"nvme 01:00.0").as_deref(), Some("nvme 01:00.0")); + assert_eq!(label(b""), None); + assert_eq!(label(&[b'a'; MAX_LABEL + 1]), None); + assert_eq!(label(&[0xFF]), None); + assert_eq!(disk(&5u32.to_le_bytes()), Some(5)); + assert_eq!(disk(&[0; 3]), None); + } +} diff --git a/lib/blockring/src/lib.rs b/lib/blockring/src/lib.rs index 6b2beeab035..f4b3c78b85d 100644 --- a/lib/blockring/src/lib.rs +++ b/lib/blockring/src/lib.rs @@ -28,9 +28,15 @@ //! **Requests in flight at once are unordered**, as on the device: a client //! that needs one to follow another waits for the first's answer. //! +//! **One service, many disks.** The block service serves [`PORT`] to clients +//! and [`disk::PORT`] to disk drivers, each of which offers its disks and is +//! handed the sessions opened on them ([`disk`]); data moves between a +//! driver's device and the client's region alone. +//! //! Pure: `alloc`, `toyos-blockhold` and `toyos-transport`, no `unsafe`. The -//! ends that map the page — `system/diskserver` and its client — hand it the -//! page as words ([`toyos_transport::Word`]) and act on what it answers. +//! ends that map the page — a disk driver (`lib/blockserve`) and the client +//! (`system/diskserver`'s library) — hand it the page as words +//! ([`toyos_transport::Word`]) and act on what it answers. #![cfg_attr(not(test), no_std)] #![forbid(unsafe_code)] @@ -38,6 +44,7 @@ extern crate alloc; pub mod client; +pub mod disk; pub mod entry; pub mod layout; pub mod server; diff --git a/lib/blockring/src/wire.rs b/lib/blockring/src/wire.rs index 0092c1c02f1..afb6490ca1b 100644 --- a/lib/blockring/src/wire.rs +++ b/lib/blockring/src/wire.rs @@ -97,24 +97,27 @@ impl Opened { } } -/// One partition of the table a service drives, as [`MSG_LISTED`] carries -/// it: its unique and type GUIDs as the table stores them. Every entry the -/// grant admits is listed, one the service will not open among them, so a -/// client that finds its partition by type learns why from the open's refusal -/// rather than taking the partition for missing. +/// One partition a service serves, as [`MSG_LISTED`] carries it: the +/// service's number for the disk it is on, and its unique and type GUIDs as +/// the disk's table stores them. Every entry the grant admits is listed, one +/// the service will not open among them, so a client that finds its +/// partition by type learns why from the open's refusal rather than taking +/// the partition for missing. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Listed { + pub disk: u32, pub unique: [u8; GUID_BYTES], pub kind: [u8; GUID_BYTES], } impl Listed { - pub const BYTES: usize = 2 * GUID_BYTES; + pub const BYTES: usize = 4 + 2 * GUID_BYTES; pub fn encode(&self) -> [u8; Self::BYTES] { let mut out = [0u8; Self::BYTES]; - out[..GUID_BYTES].copy_from_slice(&self.unique); - out[GUID_BYTES..].copy_from_slice(&self.kind); + out[..4].copy_from_slice(&self.disk.to_le_bytes()); + out[4..4 + GUID_BYTES].copy_from_slice(&self.unique); + out[4 + GUID_BYTES..].copy_from_slice(&self.kind); out } @@ -125,8 +128,9 @@ impl Listed { } let (chunks, _) = bytes.as_chunks::<{ Self::BYTES }>(); Some(chunks.iter().map(|c| Self { - unique: c[..GUID_BYTES].try_into().expect("sixteen bytes"), - kind: c[GUID_BYTES..].try_into().expect("sixteen bytes"), + disk: u32::from_le_bytes(c[..4].try_into().expect("four bytes")), + unique: c[4..4 + GUID_BYTES].try_into().expect("sixteen bytes"), + kind: c[4 + GUID_BYTES..].try_into().expect("sixteen bytes"), })) } } @@ -201,16 +205,17 @@ pub enum Refusal { /// A session holds it already. Held, /// The partition is there and cannot be served: its range is not whole - /// blocks, its GUID is on two entries, its table did not read, or its - /// controller would not open — the answer to a listing then too. + /// blocks, its GUID is on two entries, or its controller would not open, + /// and a listing with a disk driver that never settled is refused the + /// same. Unusable, /// The frame, its handles or its region are not what this protocol sends. Malformed, /// The service is holding as many sessions as it serves. Exhausted, - /// The controller is on this machine and the kernel would not hand the - /// service its claim: nothing on it is served, and a listing is refused - /// the same. + /// A disk controller is on this machine and no driver of it started — the + /// kernel would not hand over its claim, or the driver would not spawn: + /// nothing on it is served, and a listing is refused the same. ClaimRefused, /// The connection's grant does not reach that partition, whether or not /// the service has it. A connection with no grant, or with bytes no @@ -266,8 +271,8 @@ mod tests { #[test] fn opened_and_refusal_survive_their_bytes_and_nothing_else_decodes() { let listed = [ - Listed { unique: [1; GUID_BYTES], kind: [2; GUID_BYTES] }, - Listed { unique: [4; GUID_BYTES], kind: [5; GUID_BYTES] }, + Listed { disk: 0, unique: [1; GUID_BYTES], kind: [2; GUID_BYTES] }, + Listed { disk: u32::MAX, unique: [4; GUID_BYTES], kind: [5; GUID_BYTES] }, ]; let bytes: Vec = listed.iter().flat_map(|l| l.encode()).collect(); assert_eq!(Listed::decode_all(&bytes).map(|all| all.collect::>()), Some(listed.to_vec())); diff --git a/lib/blockserve/Cargo.toml b/lib/blockserve/Cargo.toml new file mode 100644 index 00000000000..f05172ef34a --- /dev/null +++ b/lib/blockserve/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "toyos-blockserve" +description = "A disk driver's side of the block service: its partition table, its link on the `disk` port, and each disk's sessions served straight out of its clients' regions." +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } +toyos = { path = "../../sdk/toyos" } +toyos-blockring = { path = "../blockring" } +toyos-blockhold = { path = "../blockhold" } +toyos-gpt = { path = "../gpt" } diff --git a/lib/blockserve/src/lib.rs b/lib/blockserve/src/lib.rs new file mode 100644 index 00000000000..afc2ecf6158 --- /dev/null +++ b/lib/blockserve/src/lib.rs @@ -0,0 +1,23 @@ +//! A disk driver's side of the block service: the partitions its own table +//! names ([`table`]), its link to the service on the `disk` port, over which +//! it offers its disks and is handed the sessions opened on them ([`link`]), +//! and each disk's sessions, which it serves itself ([`sessions`]). The +//! session region, which both ends map, is [`region`]. +//! +//! Every disk driver links this, whatever its controller: what differs is the +//! [`sessions::Device`] beneath. + +pub mod link; +pub mod region; +pub mod sessions; +pub mod table; + +pub use sessions::{Device, Owner, Sessions}; +pub use table::Partition; + +/// A partition GUID as the lines say it. +pub fn guid_text(bytes: [u8; 16]) -> String { + use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; + let mut buf = [0u8; GUID_TEXT_LEN]; + PartGuid(bytes).write_text(&mut buf).to_string() +} diff --git a/lib/blockserve/src/link.rs b/lib/blockserve/src/link.rs new file mode 100644 index 00000000000..c9d2dfde9cc --- /dev/null +++ b/lib/blockserve/src/link.rs @@ -0,0 +1,144 @@ +//! A disk driver's connection to the block service. +//! +//! **What the service knows of a driver is what the link last said**: its +//! name, each disk it offers, and how its bring-up ended. A service that ends +//! loses all of it, so the link connects again at once — the port queues the +//! connection for the service started in its place — and says everything +//! again. Sessions already handed over are the driver's and go on. +//! +//! **A driver never blocks on the service**: every frame is one `try_send`, +//! and one the service will not take ends the connection, which is connected +//! again and told everything. + +use std::collections::BTreeMap; + +use toyos::endow::{self, EndowError}; +use toyos::ipc::{Connection, FrameRx, RxStep}; +use toyos::poller::{Poller, READABLE}; +use toyos_blockring::disk::{self, Offer, Part, Settled}; + +/// A session the service handed over: what to open, the client's connection +/// and the region it sent. +pub struct Handed { + pub session: disk::Session, + pub conn: Connection, + pub region: toyos::RawHandle, +} + +pub struct Link { + label: String, + /// `None` once the service's port has closed for good. + conn: Option, + rx: FrameRx<{ disk::Session::BYTES }>, + offers: BTreeMap>, + settled: Option, +} + +impl Link { + /// The link of the driver `label` names. + pub fn new(label: String) -> Self { + let mut link = Self { label, conn: None, rx: FrameRx::new(), offers: BTreeMap::new(), settled: None }; + link.connect(); + link + } + + fn connect(&mut self) { + self.rx = FrameRx::new(); + self.conn = match endow::service(disk::PORT) { + Ok(conn) => Some(conn), + Err(EndowError::ServerGone) => { + println!("{}: the block service's port is closed for good; nothing new is served", self.label); + return; + } + Err(why) => panic!("{}: the manifest gives this driver `{}`, and it answered {why:?}", self.label, disk::PORT), + }; + let mut told = self.send(disk::MSG_DRIVER, self.label.as_bytes().to_vec()); + let offers: Vec> = + self.offers.iter().map(|(&disk, parts)| Offer { disk, parts: parts.clone() }.encode()).collect(); + for offer in offers { + told &= self.send(disk::MSG_OFFER, offer); + } + if let Some(settled) = self.settled { + told &= self.send(disk::MSG_SETTLED, settled.encode().to_vec()); + } + if !told { + self.ended("it would not take what this driver offers"); + } + } + + fn send(&self, msg_type: u32, payload: Vec) -> bool { + self.conn.as_ref().is_some_and(|conn| conn.try_send_bytes(msg_type, &payload).is_ok()) + } + + fn ended(&mut self, why: &str) { + println!("{}: the block service's connection ended ({why}); connecting again", self.label); + self.connect(); + } + + fn tell(&mut self, msg_type: u32, payload: Vec) { + if self.conn.is_some() && !self.send(msg_type, payload) { + self.ended("it would not take a frame"); + } + } + + /// Offer disk `disk` and every partition its table names. + pub fn offer(&mut self, disk: u32, parts: Vec) { + let frame = Offer { disk, parts: parts.clone() }.encode(); + self.offers.insert(disk, parts); + self.tell(disk::MSG_OFFER, frame); + } + + /// Disk `disk` left: every session on it has ended. + pub fn gone(&mut self, disk: u32) { + if self.offers.remove(&disk).is_some() { + self.tell(disk::MSG_GONE, disk.to_le_bytes().to_vec()); + } + } + + /// Every disk present at bring-up is offered, or the controller cannot + /// be used. + pub fn settle(&mut self, settled: Settled) { + assert!(self.settled.is_none(), "{}: settled twice", self.label); + self.settled = Some(settled); + self.tell(disk::MSG_SETTLED, settled.encode().to_vec()); + } + + pub fn watch(&self, poller: &Poller, token: u64) { + if let Some(conn) = &self.conn { + poller.watch(conn, READABLE, token); + } + } + + /// Every session the service handed over since the last call. Anything + /// else it sends is the service breaking the protocol, which ends the + /// connection. + pub fn take(&mut self) -> Vec { + let mut handed = Vec::new(); + loop { + let Some(conn) = &self.conn else { return handed }; + let why = match self.rx.pump(conn) { + RxStep::Idle => return handed, + RxStep::Eof => "its process ended", + RxStep::Malformed => "a frame this protocol does not make", + RxStep::Frame { msg_type: disk::MSG_SESSION, payload_len } => { + let session = disk::Session::decode(self.rx.payload(payload_len)); + let handles = conn.recv_handles_exact::<2>(); + match (session, handles) { + (Some(session), Some([conn, region])) => { + let conn = Connection::received(conn); + handed.push(Handed { session, conn, region }); + continue; + } + (_, Some(handles)) => { + handles.into_iter().for_each(toyos_abi::syscall::close); + "a session frame this protocol does not make" + } + (_, None) => "a session without its two handles", + } + } + RxStep::Frame { .. } => "a frame this protocol does not make", + }; + self.ended(why); + } + } +} diff --git a/system/diskserver/src/region.rs b/lib/blockserve/src/region.rs similarity index 96% rename from system/diskserver/src/region.rs rename to lib/blockserve/src/region.rs index 5f4c62fa5fb..2ff2b3af306 100644 --- a/system/diskserver/src/region.rs +++ b/lib/blockserve/src/region.rs @@ -33,7 +33,7 @@ impl Region { /// The ring words, as the rings take them. pub fn words(&self) -> &[AtomicU32; RING_WORDS] { let base = self.memory.as_ptr(); - assert!(base as usize % align_of::() == 0); + assert!((base as usize).is_multiple_of(align_of::())); // SAFETY: the mapping is `SESSION_BYTES` long and lives as long as // `self.memory`, which the returned borrow cannot outlive; the first // `RING_WORDS` words of it are inside it (`layout`'s own assertion); diff --git a/lib/blockserve/src/sessions.rs b/lib/blockserve/src/sessions.rs new file mode 100644 index 00000000000..148bb69a977 --- /dev/null +++ b/lib/blockserve/src/sessions.rs @@ -0,0 +1,399 @@ +//! One disk's sessions, as its driver serves them. +//! +//! **A session is a partition held**, from the open the block service hands +//! over to the client's hang-up: one holder per partition +//! (`toyos-blockhold`), every request bounded to the partition and the arena +//! before the device sees it (`toyos_blockring::server`), the data moved by +//! the device straight into and out of the client's own region, which the +//! kernel maps into the controller's domain for the length of the session +//! (`SYS_DEVICE_DMA_MAP`) and takes back at its end — after the last command +//! naming it has been answered, never before. +//! +//! **A server never blocks on a client.** The open is answered with one +//! `try_send` on the client's own connection, and a doorbell is one +//! non-blocking write whose refusal is a full pipe — a doorbell already rung. + +use std::collections::BTreeMap; + +use toyos::ipc::Connection; +use toyos::poller::{Poller, READABLE}; +use toyos_abi::syscall::SyscallError; +use toyos_blockhold::Holds; +use toyos_blockring::disk::Part; +use toyos_blockring::entry::{Completion, Op}; +use toyos_blockring::layout::{ServerRings, DEPTH}; +use toyos_blockring::server::{ServerSession, Taken}; +use toyos_blockring::wire::{self, Opened, Refusal}; +use toyos_blockring::Status; + +use crate::region::Region; +use crate::Partition; + +/// Sessions one disk holds at once. Each costs its region's mapping in the +/// driver and in the controller's domain, which the kernel bounds per claim. +pub const MAX_SESSIONS: usize = 8; + +/// A request of a session, as its device answers it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Owner { + pub session: u64, + pub tag: u32, + pub write: bool, +} + +/// What a disk's sessions ask of the controller it is on. +pub trait Device { + /// Whether one more command can be issued now. + fn has_room(&self) -> bool; + /// Issue a read or a write of `blocks` blocks at the disk's block + /// `block`, to or from device address `at`, answered for `owner`. + fn submit_io(&mut self, write: bool, block: u64, blocks: u32, at: u64, owner: Owner); + /// Issue a flush of the disk's volatile cache for `owner`; `false` for a + /// disk with none, whose every write answered is on the medium already. + fn submit_flush(&mut self, owner: Owner) -> bool; + /// Map `region` into the controller's domain, answering its device + /// address. + fn dma_map(&mut self, region: toyos::RawHandle) -> Result; + fn dma_unmap(&mut self, device_addr: u64) -> Result<(), SyscallError>; +} + +/// A partition held, over a client's region. +struct Served { + conn: Connection, + region: Region, + device_addr: u64, + rings: ServerRings, + state: ServerSession, + unique: [u8; 16], + /// The client hung up, or broke the protocol: nothing more is taken, and + /// the session ends once nothing of it is on the device. + closing: bool, + requests: u64, + posted: bool, +} + +/// Every session on one disk, and the partitions its table names. +pub struct Sessions { + /// Who says this disk's lines. + name: String, + parts: Vec, + holds: Holds, + /// The device's loss count: every reset may have dropped its cache. + losses: u64, + sessions: BTreeMap, + next_id: u64, +} + +impl Sessions { + pub fn new(name: String, parts: Vec) -> Self { + Self { name, parts, holds: Holds::new(), losses: 0, sessions: BTreeMap::new(), next_id: 0 } + } + + /// What the block service is offered of this disk. + pub fn offered(&self) -> Vec { + self.parts + .iter() + .map(|p| Part { unique: p.unique, kind: p.kind, blocks: p.span.as_ref().ok().map(|&(_, n)| n) }) + .collect() + } + + /// The span a session on `unique` is served on, held for it; or its + /// refusal. The service admitted the open against this disk's offer, so + /// what is refused here is what only the disk knows. + /// + /// **A client that hung up holds nothing**: every session's end its client + /// has already made is read before the open is judged. + pub fn place(&mut self, unique: [u8; 16], dev: &mut impl Device) -> Result<(u64, u64), Refusal> { + let span = match self.parts.iter().find(|p| p.unique == unique && unique != [0; 16]) { + None => return Err(Refusal::NotFound), + Some(Partition { span: Err(_), .. }) => return Err(Refusal::Unusable), + Some(Partition { span: Ok(span), .. }) => *span, + }; + let ids: Vec = self.sessions.keys().copied().collect(); + for id in ids { + self.hear(id); + } + self.retire(dev); + if self.sessions.len() >= MAX_SESSIONS { + return Err(Refusal::Exhausted); + } + let (first, blocks) = span; + self.holds.hold(first, first + blocks, self.next_id).map_err(|_| Refusal::Held)?; + Ok(span) + } + + /// Serve the session the service handed over: answer the client's open on + /// its own connection, or refuse it there. `region` is the client's and + /// is consumed either way. + pub fn open(&mut self, dev: &mut impl Device, conn: Connection, region: toyos::RawHandle, unique: [u8; 16], writes: bool) { + let name = self.name.clone(); + let refuse = |conn: &Connection, why: Refusal| { + println!("{name}: an open of {} refused: {why:?}", crate::guid_text(unique)); + let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); + }; + // A region the kernel did not allocate, or longer than a session, + // which would spend the claim's bound on the kernel's side for every + // other client, is refused whole. + let Ok(region) = Region::lent(region) else { return refuse(&conn, Refusal::Malformed) }; + let (first, blocks) = match self.place(unique, dev) { + Ok(span) => span, + Err(why) => return refuse(&conn, why), + }; + let device_addr = match dev.dma_map(region.handle()) { + Ok(at) => at, + Err(why) => { + self.holds.release(first); + return refuse(&conn, if why == SyscallError::ResourceExhausted { Refusal::Exhausted } else { Refusal::Malformed }); + } + }; + let (rings, opened) = Opened::over(region.words(), blocks, unique); + if conn.try_send_bytes(wire::MSG_OPENED, &opened.encode()).is_err() { + // The client went before it heard: nothing of the session reached + // the device, and what was taken for it goes back. + if let Err(why) = dev.dma_unmap(device_addr) { + panic!("{}: the kernel would not take an abandoned session's region back: {why:?}", self.name); + } + return self.holds.release(first); + } + let id = self.next_id; + self.next_id += 1; + let state = ServerSession::new(first, opened.blocks(), writes); + let served = Served { conn, region, device_addr, rings, state, unique, closing: false, requests: 0, posted: false }; + self.sessions.insert(id, served); + let access = if writes { "" } else { ", read-only" }; + println!("{}: session {id} opened {} ({blocks} blocks{access})", self.name, crate::guid_text(unique)); + } + + /// Watch every session's connection, under `base` plus its id. + pub fn watch(&self, poller: &Poller, base: u64) { + for (id, s) in &self.sessions { + if !s.closing { + poller.watch(&s.conn, READABLE, base + id); + } + } + } + + pub fn ids(&self) -> Vec { + self.sessions.keys().copied().collect() + } + + /// Whether any session has a request on the device. + pub fn busy(&self) -> bool { + self.sessions.values().any(|s| s.state.inflight().len() > 0) + } + + /// Put `c` on its session's completion ring. [`Self::pull`] leaves room for + /// every answer, so a ring without it is a client whose head went back or + /// past what was posted: the session ends. + fn post(session: &mut Served, c: Completion) { + match session.rings.1.push(session.region.words(), c.encode()) { + Ok(true) => session.posted = true, + Ok(false) | Err(_) => session.closing = true, + } + } + + /// Hand one device answer to the session it is for. + pub fn answered(&mut self, owner: Owner, ok: bool, status: u32) { + let s = self.sessions.get_mut(&owner.session).unwrap_or_else(|| { + panic!("{}: an answer for session {}, which ended with requests on the device", self.name, owner.session) + }); + if let Some(c) = s.state.complete(owner.tag, ok, &mut self.holds, self.losses) { + if !ok { + println!("{}: session {}: request {} failed with status {status:#x}", self.name, owner.session, owner.tag); + } + if c.status == Status::Lost { + println!( + "{}: session {}: a flush found writes of its own the device lost since it acknowledged \ + them; the client is told Lost", + self.name, owner.session + ); + } + Self::post(s, c); + } + } + + /// Take what each session's client has published, while the device and + /// the session's completion ring have room for it. + pub fn pull(&mut self, dev: &mut impl Device) { + for (&id, s) in self.sessions.iter_mut() { + loop { + if s.closing { + break; + } + let page = s.region.words(); + // Room for every answer: what is on the device, and what is + // posted and not yet read, never exceeds the completion ring. + let Ok(space) = s.rings.1.space(page) else { + s.closing = true; + break; + }; + let unread = (DEPTH - space) as usize; + if s.state.inflight().len() + unread >= DEPTH as usize || !dev.has_room() { + break; + } + let words = match s.rings.0.pop(page) { + Ok(Some(words)) => words, + Ok(None) => break, + Err(_) => { + s.closing = true; + break; + } + }; + s.requests += 1; + match s.state.take(words) { + Taken::Answer(c) => Self::post(s, c), + Taken::Issue(req) => { + let write = matches!(req.op, Op::Write { .. }); + let owner = Owner { session: id, tag: req.tag, write }; + match req.op { + Op::Read { run, lba } | Op::Write { run, lba } => { + let at = s.device_addr + run.span().offset as u64; + dev.submit_io(write, s.state.first() + lba, run.count(), at, owner); + } + Op::Flush => { + if !dev.submit_flush(owner) { + let c = s.state.complete(req.tag, true, &mut self.holds, self.losses); + Self::post(s, c.expect("just taken")); + } + } + } + } + } + } + s.rings.0.release(s.region.words()); + } + } + + /// Publish what each session was answered, and ring its doorbell. + pub fn publish(&mut self) { + for s in self.sessions.values_mut() { + if !s.posted { + continue; + } + s.posted = false; + if s.rings.1.publish(s.region.words()) { + match s.conn.write_nonblock(&[1]) { + Ok(_) | Err(SyscallError::WouldBlock) => {} + Err(_) => s.closing = true, + } + } + } + } + + /// Consume session `id`'s doorbell bytes; once its client has hung up, + /// the session is closing. + pub fn hear(&mut self, id: u64) { + let s = self.sessions.get_mut(&id).expect("a session"); + let mut sink = [0u8; 64]; + while !s.closing { + match s.conn.read_nonblock(&mut sink) { + Ok(0) => s.closing = true, + Ok(_) => {} + Err(SyscallError::WouldBlock) => return, + Err(_) => s.closing = true, + } + } + } + + /// End every session that is closing and has nothing on the device: its + /// region leaves the controller's domain, and its partition is free. + /// Answers how many ended. + pub fn retire(&mut self, dev: &mut impl Device) -> usize { + let done: Vec = self + .sessions + .iter() + .filter(|(_, s)| s.closing && s.state.inflight().len() == 0) + .map(|(id, _)| *id) + .collect(); + let ended = done.len(); + for id in done { + let s = self.sessions.remove(&id).expect("just listed"); + if let Err(why) = dev.dma_unmap(s.device_addr) { + panic!("{}: the kernel would not take session {id}'s region back: {why:?}", self.name); + } + self.holds.release(s.state.first()); + println!("{}: session {id} on {} closed after {} requests", self.name, crate::guid_text(s.unique), s.requests); + } + ended + } + + /// The device was reset under every command it held: each is answered + /// not done, and the loss count goes up, since the reset may have + /// dropped the device's cache. Answers how many were the sessions'. + pub fn reset(&mut self) -> usize { + self.losses += 1; + let mut answered = 0; + for s in self.sessions.values_mut() { + for c in s.state.abort_all() { + answered += 1; + Self::post(s, c); + } + } + answered + } + + pub fn losses(&self) -> u64 { + self.losses + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// No device: `place` reaches none of it but `retire`, which has nothing + /// to retire. + struct Nothing; + + impl Device for Nothing { + fn has_room(&self) -> bool { + unreachable!() + } + fn submit_io(&mut self, _: bool, _: u64, _: u32, _: u64, _: Owner) { + unreachable!() + } + fn submit_flush(&mut self, _: Owner) -> bool { + unreachable!() + } + fn dma_map(&mut self, _: toyos::RawHandle) -> Result { + unreachable!() + } + fn dma_unmap(&mut self, _: u64) -> Result<(), SyscallError> { + unreachable!() + } + } + + fn disk() -> Sessions { + Sessions::new( + "test".into(), + vec![ + Partition { unique: [1; 16], kind: [9; 16], span: Ok((0, 100)) }, + Partition { unique: [2; 16], kind: [9; 16], span: Err("not whole blocks".into()) }, + ], + ) + } + + /// What only the disk knows refuses an open the service admitted: a + /// partition its table no longer names, one it will not serve, the zero + /// GUID, and one a session holds. + #[test] + fn a_disk_refuses_what_its_own_table_and_holds_say() { + let mut disk = disk(); + assert_eq!(disk.place([3; 16], &mut Nothing), Err(Refusal::NotFound)); + assert_eq!(disk.place([0; 16], &mut Nothing), Err(Refusal::NotFound)); + assert_eq!(disk.place([2; 16], &mut Nothing), Err(Refusal::Unusable)); + assert_eq!(disk.place([1; 16], &mut Nothing), Ok((0, 100))); + assert_eq!(disk.place([1; 16], &mut Nothing), Err(Refusal::Held)); + } + + #[test] + fn a_disk_offers_every_partition_its_table_names_and_which_it_serves() { + assert_eq!( + disk().offered(), + [ + Part { unique: [1; 16], kind: [9; 16], blocks: Some(100) }, + Part { unique: [2; 16], kind: [9; 16], blocks: None }, + ] + ); + } +} diff --git a/lib/blockserve/src/table.rs b/lib/blockserve/src/table.rs new file mode 100644 index 00000000000..9effecbbb15 --- /dev/null +++ b/lib/blockserve/src/table.rs @@ -0,0 +1,61 @@ +//! Every partition a disk's table names, read once at the disk's bring-up. +//! +//! Every partition lies outside every other and the table outside all of them, +//! so no session can change it: the table a driver read is the one it serves +//! for the disk's life. A partition whose range is not whole 4 KiB blocks, or +//! that its own table refuses, is offered and refused by name. + +use toyos_blockring::BLOCK_BYTES; + +/// One partition of the table, in blocks of the disk. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Partition { + pub unique: [u8; 16], + pub kind: [u8; 16], + /// Where it starts and how long it is, or why it cannot be served. + pub span: Result<(u64, u64), String>, +} + +/// Every partition the table on `sectors` names, each checked past +/// `toyos_gpt::locate`'s range and overlap checks and held to whole blocks; +/// none for a disk whose table does not read, said by `name`. +pub fn read(name: &str, sectors: &mut impl toyos_gpt::Sectors) -> Vec { + let per = BLOCK_BYTES as u64 / u64::from(sectors.lba_bytes()); + let mut found = [None; 128]; + if let Err(why) = toyos_gpt::list(sectors, &mut found) { + println!("{name}: the disk carries no partition table this driver reads: {why:?}"); + return Vec::new(); + } + let mut parts = Vec::new(); + for entry in found.iter().flatten() { + let listed = match entry { + Ok(listed) => listed, + Err(unplaced) => { + let span = Err(format!("its blocks are no partition on this disk: {unplaced:?}")); + parts.push(Partition { unique: unplaced.unique_guid.0, kind: unplaced.type_guid.0, span }); + continue; + } + }; + let span = match toyos_gpt::locate(sectors, listed.unique_guid()) { + Err(why) => Err(format!("its own table refuses it: {why:?}")), + Ok(located) => { + let p = located.partition(); + let (first, count) = (p.first_lba(), p.lba_count().get()); + if first % per != 0 || count % per != 0 { + Err(format!("LBA {first}+{count} is not whole 4 KiB blocks of {}-byte sectors", sectors.lba_bytes())) + } else { + Ok((first / per, count / per)) + } + } + }; + parts.push(Partition { unique: listed.unique_guid().0, kind: listed.type_guid().0, span }); + } + for part in &parts { + let guid = crate::guid_text(part.unique); + match &part.span { + Ok((first, blocks)) => println!("{name}: partition {guid} at block {first}, {blocks} blocks"), + Err(why) => println!("{name}: partition {guid} is not served: {why}"), + } + } + parts +} diff --git a/lib/input/src/lib.rs b/lib/input/src/lib.rs index 43df3d91171..a32397b6dee 100644 --- a/lib/input/src/lib.rs +++ b/lib/input/src/lib.rs @@ -3,9 +3,10 @@ //! **A source says what one device did and nothing more**: a key's //! transition with no modifier mask, a pointer's own buttons and its motion //! as the device gave it. The owner of input keeps a [`Merge`] and a -//! [`Pointer`] over every source it holds, so a key or a button is held while -//! any source holds it, and a source that ends releases what it held and -//! nothing another holds. +//! [`Pointer`] over every source it holds, keyed by the connection and the +//! device's `source`, so a key or a button is held while any device holds +//! it; a device's `gone` releases what it held, a connection's end what +//! every device on it held, and neither anything another holds. //! //! [`wire`] is the one reader of a source's records, whether they came off the //! `input` port or the kernel's streams: what does not decode is refused by @@ -20,4 +21,4 @@ pub mod merge; pub mod wire; pub use keys::Keys; -pub use merge::{Merge, Pointer, Slot, State}; +pub use merge::{Full, Merge, Pointer, Slot, State}; diff --git a/lib/input/src/merge.rs b/lib/input/src/merge.rs index c8b85df34e8..0615ffd9af6 100644 --- a/lib/input/src/merge.rs +++ b/lib/input/src/merge.rs @@ -4,7 +4,7 @@ use toyos_abi::input::{RawKeyEvent, MOD_ALT, MOD_CTRL, MOD_GUI, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE_MAX}; use crate::keys::{Keys, Transitions, Usages}; -use crate::wire::{Motion, Report, CONNECTIONS}; +use crate::wire::{Motion, Report, CONNECTIONS, DEVICES}; /// The sources the owner merges: the kernel's streams, until usbd takes the /// last kernel source, and one per connection on the port. @@ -24,37 +24,100 @@ impl Slot { } } -/// One keyboard made of every source's: a key is held while any source holds -/// it. -#[derive(Default)] +/// A connection's [`DEVICES`]th device holding something already, and a +/// record for another: the sender broke the protocol. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Full { + pub source: u16, +} + +/// What each device of one slot holds, by its `source`. A device holding +/// nothing has no entry. +struct Devices { + held: [Option<(u16, T)>; DEVICES], +} + +impl Devices { + const EMPTY: Self = Self { held: [const { None }; DEVICES] }; + + /// `source`'s state, with `change` made to it; an entry left holding + /// nothing, as `empty` judges, goes. + fn change(&mut self, source: u16, empty: fn(&T) -> bool, change: impl FnOnce(&mut T) -> R) -> Result { + let at = match self.held.iter().position(|d| matches!(d, Some((s, _)) if *s == source)) { + Some(at) => at, + None => { + let at = self.held.iter().position(Option::is_none).ok_or(Full { source })?; + self.held[at] = Some((source, T::default())); + at + } + }; + let (_, state) = self.held[at].as_mut().expect("just found or made"); + let changed = change(state); + if empty(state) { + self.held[at] = None; + } + Ok(changed) + } + + fn gone(&mut self, source: u16) { + self.held.iter_mut().filter(|d| matches!(d, Some((s, _)) if *s == source)).for_each(|d| *d = None); + } + + fn end(&mut self) { + self.held = [const { None }; DEVICES]; + } + + fn states(&self) -> impl Iterator { + self.held.iter().flatten().map(|(_, state)| state) + } +} + +/// One keyboard made of every device's: a key is held while any device +/// holds it. pub struct Merge { - sources: [Keys; SLOTS], + slots: [Devices; SLOTS], union: Keys, } +impl Default for Merge { + fn default() -> Self { + Self { slots: [const { Devices::EMPTY }; SLOTS], union: Keys::default() } + } +} + impl Merge { - /// `key` from `slot`: what it changed of the union, each transition with - /// the union's modifiers after it. A repeated make, or a key another - /// source already holds, changes nothing. - pub fn key(&mut self, slot: Slot, key: RawKeyEvent) -> Delivered { - let mut now = self.sources[slot.0].held(); - if key.pressed() { - now.insert(key.keycode); - } else { - now.remove(key.keycode); - } - self.sources[slot.0].hold(now); + /// `key` from `slot`'s device `source`: what it changed of the union, + /// each transition with the union's modifiers after it. A repeated make, + /// or a key another device already holds, changes nothing. + pub fn key(&mut self, slot: Slot, source: u16, key: RawKeyEvent) -> Result { + let empty = |keys: &Keys| keys.held() == Usages::NONE; + self.slots[slot.0].change(source, empty, |keys| { + let mut now = keys.held(); + if key.pressed() { + now.insert(key.keycode); + } else { + now.remove(key.keycode); + } + keys.hold(now); + })?; + Ok(self.merge()) + } + + /// `slot`'s device `source` holds nothing: it left, or lost what it held. + pub fn gone(&mut self, slot: Slot, source: u16) -> Delivered { + self.slots[slot.0].gone(source); self.merge() } - /// `slot` holds nothing: its source ended, or lost what it held. + /// Every device `slot` carried holds nothing: its source ended. pub fn end(&mut self, slot: Slot) -> Delivered { - self.sources[slot.0].hold(Usages::NONE); + self.slots[slot.0].end(); self.merge() } fn merge(&mut self) -> Delivered { - let union = self.sources.iter().fold(Usages::NONE, |all, keys| all.union(keys.held())); + let held = self.slots.iter().flat_map(Devices::states); + let union = held.fold(Usages::NONE, |all, keys| all.union(keys.held())); let before = self.union.held(); Delivered { transitions: self.union.hold(union), held: before } } @@ -110,7 +173,7 @@ fn rel_scale_for(width: u32, height: u32) -> (u16, u16) { /// absolute space, `0..=POINTER_ABSOLUTE_MAX` on each axis. pub struct Pointer { /// Each pointer's buttons, by its slot and its record's `source`. - buttons: [[u8; 256]; SLOTS], + buttons: [Devices; SLOTS], x: i32, y: i32, scale: (u16, u16), @@ -129,7 +192,7 @@ impl Pointer { /// At the middle of a `width` by `height` screen, holding nothing. pub fn new(width: u32, height: u32) -> Self { let middle = (POINTER_ABSOLUTE_MAX as i32 + 1) / 2; - Self { buttons: [[0; 256]; SLOTS], x: middle, y: middle, scale: rel_scale_for(width, height) } + Self { buttons: [const { Devices::EMPTY }; SLOTS], x: middle, y: middle, scale: rel_scale_for(width, height) } } /// The screen relative motion is scaled for. @@ -139,8 +202,8 @@ impl Pointer { /// `report` from `slot`: relative motion moves the position by the /// screen's scale, and absolute motion sets it. - pub fn report(&mut self, slot: Slot, report: Report) -> State { - self.buttons[slot.0][usize::from(report.source)] = report.buttons; + pub fn report(&mut self, slot: Slot, report: Report) -> Result { + self.buttons[slot.0].change(report.source, |held| *held == 0, |held| *held = report.buttons)?; let most = i32::from(POINTER_ABSOLUTE_MAX); match report.motion { Motion::Relative { dx, dy } => { @@ -149,17 +212,23 @@ impl Pointer { } Motion::Absolute { x, y } => (self.x, self.y) = (i32::from(x), i32::from(y)), } - self.state(report.scroll) + Ok(self.state(report.scroll)) + } + + /// `slot`'s pointer `source` holds nothing. + pub fn gone(&mut self, slot: Slot, source: u16) -> State { + self.buttons[slot.0].gone(source); + self.state(0) } /// Every pointer `slot` carried holds nothing. pub fn end(&mut self, slot: Slot) -> State { - self.buttons[slot.0] = [0; 256]; + self.buttons[slot.0].end(); self.state(0) } fn state(&self, scroll: i8) -> State { - let buttons = self.buttons.iter().flatten().fold(0, |all, &held| all | held); + let buttons = self.buttons.iter().flat_map(Devices::states).fold(0, |all, &held| all | held); // Both are clamped to `0..=POINTER_ABSOLUTE_MAX`. State { buttons, scroll, x: self.x as u16, y: self.y as u16 } } diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs index bbf22e57884..0b33ddc53dd 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -1,6 +1,9 @@ //! The `input` port's frames, and the records the kernel's streams carry, //! decoded by one reader whichever source sent them. //! +//! A sender numbers its devices `source`s and never reuses one for the life +//! of its connection, so a late record never reaches a later device. +//! //! Every byte here is one a source chose, and a source is outside the owner's //! trust: a record that does not decode is refused by name and changes //! nothing. @@ -17,21 +20,34 @@ pub const HOLDER: &str = "ps2server"; /// The most connections the owner serves at once; one past it is refused. pub const CONNECTIONS: usize = 4; -/// A key: one [`RawKeyEvent`]. +/// A key: [`KEY_LEN`] bytes, the device's `source` little endian, the usage, +/// and [`MOD_RELEASED`] or 0. pub const MSG_KEY: u32 = 1; -/// A pointer report: one [`MouseEvent`]. +/// A pointer report: [`POINTER_LEN`] bytes, the device's `source` little +/// endian, then the buttons, the wheel, the kind and the two axes little endian. pub const MSG_POINTER: u32 = 2; -/// No payload: the source's keyboard lost a transition, so every key it -/// holds is released. Its pointers keep their buttons: a report restates them. -pub const MSG_LOST: u32 = 3; - -/// A key's bytes on the wire. -pub const KEY_LEN: usize = core::mem::size_of::(); -/// A pointer report's bytes on the wire. -pub const POINTER_LEN: usize = core::mem::size_of::(); +/// [`GONE_LEN`] bytes, a `source` little endian: that device left, or lost a +/// transition, and everything it holds is released. The source's other +/// devices keep what they hold. +pub const MSG_GONE: u32 = 3; + +pub const KEY_LEN: usize = 4; +pub const POINTER_LEN: usize = 9; +pub const GONE_LEN: usize = 2; /// The longest record's payload: the owner refuses a frame declaring more. pub const MAX_PAYLOAD: usize = POINTER_LEN; +/// The devices one connection may hold anything at once: a record naming a +/// further `source` while this many hold something is the sender breaking the +/// protocol, and its connection is dropped. A sender binds no more devices +/// than this. +pub const DEVICES: usize = 16; + +/// A key record's bytes on the kernel's keyboard stream: one [`RawKeyEvent`]. +pub const STREAM_KEY_LEN: usize = core::mem::size_of::(); +/// A pointer record's bytes on the kernel's mouse stream: one [`MouseEvent`]. +pub const STREAM_POINTER_LEN: usize = core::mem::size_of::(); + /// The buttons boot-mouse order names. pub const BUTTONS: u8 = 0x1F; @@ -47,8 +63,8 @@ pub enum Motion { /// One pointer report, believed. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Report { - /// Which of its sender's pointers this is. - pub source: u8, + /// Which of its sender's devices this is. + pub source: u16, /// That pointer's own buttons, in boot-mouse order. pub buttons: u8, pub scroll: i8, @@ -58,9 +74,11 @@ pub struct Report { /// One thing a source said. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Record { - Key(RawKeyEvent), + /// A transition of `source`'s keyboard; its modifiers are + /// [`MOD_RELEASED`] or nothing. + Key { source: u16, key: RawKeyEvent }, Pointer(Report), - Lost, + Gone { source: u16 }, } /// Why a record was not believed. @@ -82,33 +100,31 @@ pub enum Refused { Position { x: i16, y: i16 }, } -impl Report { - /// This report as its wire record. - pub fn event(&self) -> MouseEvent { - let (kind, x, y) = match self.motion { - Motion::Relative { dx, dy } => (POINTER_RELATIVE, dx, dy), - // Both are at most `POINTER_ABSOLUTE_MAX`, which an `i16` holds. - Motion::Absolute { x, y } => (POINTER_ABSOLUTE, x as i16, y as i16), - }; - MouseEvent { source: self.source, buttons: self.buttons, scroll: self.scroll, kind, x, y } - } -} - impl Record { /// The frame this record is: its type, and its payload in `out`'s first /// bytes, whose count is the third word. - pub fn frame(&self, out: &mut [u8; POINTER_LEN]) -> (u32, usize) { - let (msg_type, bytes): (u32, &[u8]) = match self { - Self::Key(key) => (MSG_KEY, toyos_abi::usersafe::bytes(key)), - Self::Pointer(report) => { - let event = report.event(); - out.copy_from_slice(toyos_abi::usersafe::bytes(&event)); - return (MSG_POINTER, POINTER_LEN); + pub fn frame(&self, out: &mut [u8; MAX_PAYLOAD]) -> (u32, usize) { + match *self { + Self::Key { source, key } => { + let [a, b] = source.to_le_bytes(); + out[..KEY_LEN].copy_from_slice(&[a, b, key.keycode, key.modifiers]); + (MSG_KEY, KEY_LEN) + } + Self::Pointer(Report { source, buttons, scroll, motion }) => { + let (kind, x, y) = match motion { + Motion::Relative { dx, dy } => (POINTER_RELATIVE, dx, dy), + // Both are at most `POINTER_ABSOLUTE_MAX`, which an `i16` holds. + Motion::Absolute { x, y } => (POINTER_ABSOLUTE, x as i16, y as i16), + }; + let ([s0, s1], [x0, x1], [y0, y1]) = (source.to_le_bytes(), x.to_le_bytes(), y.to_le_bytes()); + *out = [s0, s1, buttons, scroll as u8, kind, x0, x1, y0, y1]; + (MSG_POINTER, POINTER_LEN) + } + Self::Gone { source } => { + out[..GONE_LEN].copy_from_slice(&source.to_le_bytes()); + (MSG_GONE, GONE_LEN) } - Self::Lost => (MSG_LOST, &[]), - }; - out[..bytes.len()].copy_from_slice(bytes); - (msg_type, bytes.len()) + } } } @@ -116,16 +132,28 @@ impl Record { pub fn decode(msg_type: u32, payload: &[u8]) -> Result { let length = || Refused::Length { msg_type, len: payload.len() }; match msg_type { - MSG_KEY => key(payload.try_into().map_err(|_| length())?).map(Record::Key), - MSG_POINTER => pointer(payload.try_into().map_err(|_| length())?).map(Record::Pointer), - MSG_LOST if payload.is_empty() => Ok(Record::Lost), - MSG_LOST => Err(length()), + MSG_KEY => { + let [s0, s1, keycode, modifiers]: [u8; KEY_LEN] = payload.try_into().map_err(|_| length())?; + Ok(Record::Key { source: u16::from_le_bytes([s0, s1]), key: key(&[keycode, modifiers])? }) + } + MSG_POINTER => { + let [s0, s1, buttons, scroll, kind, x0, x1, y0, y1]: [u8; POINTER_LEN] = + payload.try_into().map_err(|_| length())?; + let motion = motion(kind, i16::from_le_bytes([x0, x1]), i16::from_le_bytes([y0, y1]))?; + let report = Report { source: u16::from_le_bytes([s0, s1]), buttons, scroll: scroll as i8, motion }; + believed(report).map(Record::Pointer) + } + MSG_GONE => { + let source: [u8; GONE_LEN] = payload.try_into().map_err(|_| length())?; + Ok(Record::Gone { source: u16::from_le_bytes(source) }) + } other => Err(Refused::Type(other)), } } -/// One key, as a source's frame or the kernel's keyboard stream carries it. -pub fn key(bytes: &[u8; KEY_LEN]) -> Result { +/// One key's usage and transition, as a source's frame or the kernel's +/// keyboard stream carries it. +pub fn key(bytes: &[u8; STREAM_KEY_LEN]) -> Result { let [keycode, modifiers] = *bytes; if keycode == 0 { return Err(Refused::NoUsage); @@ -136,20 +164,26 @@ pub fn key(bytes: &[u8; KEY_LEN]) -> Result { Ok(RawKeyEvent { keycode, modifiers }) } -/// One pointer report, as a source's frame or the kernel's mouse stream -/// carries it. -pub fn pointer(bytes: &[u8; POINTER_LEN]) -> Result { +/// One pointer report, as the kernel's mouse stream carries it. +pub fn pointer(bytes: &[u8; STREAM_POINTER_LEN]) -> Result { let [source, buttons, scroll, kind, x0, x1, y0, y1] = *bytes; - let (x, y) = (i16::from_ne_bytes([x0, x1]), i16::from_ne_bytes([y0, y1])); - if buttons & !BUTTONS != 0 { - return Err(Refused::Buttons(buttons)); - } + let motion = motion(kind, i16::from_ne_bytes([x0, x1]), i16::from_ne_bytes([y0, y1]))?; + believed(Report { source: u16::from(source), buttons, scroll: scroll as i8, motion }) +} + +fn motion(kind: u8, x: i16, y: i16) -> Result { let on_surface = |v: i16| (0..=POINTER_ABSOLUTE_MAX).contains(&v); - let motion = match kind { - POINTER_RELATIVE => Motion::Relative { dx: x, dy: y }, - POINTER_ABSOLUTE if on_surface(x) && on_surface(y) => Motion::Absolute { x: x as u16, y: y as u16 }, - POINTER_ABSOLUTE => return Err(Refused::Position { x, y }), - other => return Err(Refused::Kind(other)), - }; - Ok(Report { source, buttons, scroll: scroll as i8, motion }) + match kind { + POINTER_RELATIVE => Ok(Motion::Relative { dx: x, dy: y }), + POINTER_ABSOLUTE if on_surface(x) && on_surface(y) => Ok(Motion::Absolute { x: x as u16, y: y as u16 }), + POINTER_ABSOLUTE => Err(Refused::Position { x, y }), + other => Err(Refused::Kind(other)), + } +} + +fn believed(report: Report) -> Result { + match report.buttons & !BUTTONS { + 0 => Ok(report), + _ => Err(Refused::Buttons(report.buttons)), + } } diff --git a/lib/input/tests/input.rs b/lib/input/tests/input.rs index 809ae04ef53..28ec20d7b56 100644 --- a/lib/input/tests/input.rs +++ b/lib/input/tests/input.rs @@ -1,8 +1,11 @@ //! The owner of input's merge and the one reader of a source's records. use toyos_abi::input::{MouseEvent, RawKeyEvent, MOD_RELEASED, MOD_SHIFT, POINTER_ABSOLUTE, POINTER_RELATIVE}; -use toyos_input::wire::{self, Motion, Record, Refused, Report, KEY_LEN, MSG_KEY, MSG_LOST, MSG_POINTER, POINTER_LEN}; -use toyos_input::{Merge, Pointer, Slot, State}; +use toyos_input::wire::{ + self, Motion, Record, Refused, Report, DEVICES, GONE_LEN, KEY_LEN, MAX_PAYLOAD, MSG_GONE, MSG_KEY, MSG_POINTER, POINTER_LEN, + STREAM_POINTER_LEN, +}; +use toyos_input::{Full, Merge, Pointer, Slot, State}; const SHIFT: u8 = 0xE1; const A: u8 = 0x04; @@ -19,11 +22,11 @@ fn slot(n: usize) -> Slot { Slot::connection(n).expect("a connection the port holds") } -fn bytes(event: &MouseEvent) -> [u8; POINTER_LEN] { +fn bytes(event: &MouseEvent) -> [u8; STREAM_POINTER_LEN] { toyos_abi::usersafe::bytes(event).try_into().expect("a record's length") } -fn relative(source: u8, buttons: u8, dx: i16, dy: i16) -> Report { +fn relative(source: u16, buttons: u8, dx: i16, dy: i16) -> Report { Report { source, buttons, scroll: 0, motion: Motion::Relative { dx, dy } } } @@ -66,27 +69,44 @@ fn a_pointer_report_past_five_buttons_of_no_kind_or_off_the_surface_is_refused() #[test] fn a_frame_of_another_type_or_length_is_refused() { assert_eq!(wire::decode(0, &[]), Err(Refused::Type(0))); - assert_eq!(wire::decode(4, &[A, 0]), Err(Refused::Type(4))); - for len in [0, KEY_LEN - 1, KEY_LEN + 1, POINTER_LEN] { + assert_eq!(wire::decode(4, &[0, 0, A, 0]), Err(Refused::Type(4))); + for len in [0, 2, KEY_LEN - 1, KEY_LEN + 1, POINTER_LEN] { assert_eq!(wire::decode(MSG_KEY, &vec![A; len]), Err(Refused::Length { msg_type: MSG_KEY, len })); } for len in [0, KEY_LEN, POINTER_LEN - 1, POINTER_LEN + 1] { assert_eq!(wire::decode(MSG_POINTER, &vec![0; len]), Err(Refused::Length { msg_type: MSG_POINTER, len })); } - assert_eq!(wire::decode(MSG_LOST, &[0]), Err(Refused::Length { msg_type: MSG_LOST, len: 1 })); + for len in [0, GONE_LEN - 1, GONE_LEN + 1] { + assert_eq!(wire::decode(MSG_GONE, &vec![0; len]), Err(Refused::Length { msg_type: MSG_GONE, len })); + } +} + +/// A frame's key and pointer fields are refused as the kernel's streams' are. +#[test] +fn a_frames_key_and_pointer_are_held_to_the_same_rules() { + assert_eq!(wire::decode(MSG_KEY, &[1, 0, 0, 0]), Err(Refused::NoUsage)); + assert_eq!(wire::decode(MSG_KEY, &[1, 0, A, MOD_SHIFT]), Err(Refused::Modifiers(MOD_SHIFT))); + let pointer = |buttons, kind, x: i16| { + let [x0, x1] = x.to_le_bytes(); + wire::decode(MSG_POINTER, &[0, 0, buttons, 0, kind, x0, x1, 0, 0]) + }; + assert_eq!(pointer(1 << 5, POINTER_RELATIVE, 0), Err(Refused::Buttons(1 << 5))); + assert_eq!(pointer(0, 2, 0), Err(Refused::Kind(2))); + assert_eq!(pointer(0, POINTER_ABSOLUTE, -1), Err(Refused::Position { x: -1, y: 0 })); } #[test] fn every_record_decodes_as_the_frame_it_was_sent_as() { let records = [ - Record::Key(press(A)), - Record::Key(lift(SHIFT)), + Record::Key { source: 0, key: press(A) }, + Record::Key { source: 0xBEEF, key: lift(SHIFT) }, Record::Pointer(relative(3, 0b101, -7, 300)), - Record::Pointer(Report { source: 0, buttons: 0, scroll: -2, motion: Motion::Absolute { x: 32767, y: 9 } }), - Record::Lost, + Record::Pointer(Report { source: 0x1234, buttons: 0, scroll: -2, motion: Motion::Absolute { x: 32767, y: 9 } }), + Record::Gone { source: 0 }, + Record::Gone { source: u16::MAX }, ]; for record in records { - let mut out = [0; POINTER_LEN]; + let mut out = [0; MAX_PAYLOAD]; let (msg_type, len) = record.frame(&mut out); assert_eq!(wire::decode(msg_type, &out[..len]), Ok(record)); } @@ -95,29 +115,29 @@ fn every_record_decodes_as_the_frame_it_was_sent_as() { #[test] fn shift_from_one_source_and_a_from_another_arrive_as_a_shifted_a() { let mut merge = Merge::default(); - assert_eq!(merge.key(slot(0), press(SHIFT)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_SHIFT }]); - assert_eq!(merge.key(Slot::KERNEL, press(A)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT }]); - assert_eq!(merge.key(slot(0), lift(SHIFT)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); - assert_eq!(merge.key(Slot::KERNEL, lift(A)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_RELEASED }]); + assert_eq!(merge.key(slot(0), 0, press(SHIFT)).unwrap().collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_SHIFT }]); + assert_eq!(merge.key(Slot::KERNEL, 0, press(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT }]); + assert_eq!(merge.key(slot(0), 0, lift(SHIFT)).unwrap().collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); + assert_eq!(merge.key(Slot::KERNEL, 0, lift(A)).unwrap().collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_RELEASED }]); } #[test] fn a_repeated_make_from_one_source_delivers_nothing() { let mut merge = Merge::default(); - assert_eq!(merge.key(slot(1), press(A)).count(), 1); - assert_eq!(merge.key(slot(1), press(A)).count(), 0); - assert_eq!(merge.key(slot(2), press(A)).count(), 0, "a key another source holds"); - assert_eq!(merge.key(slot(1), lift(A)).count(), 0, "still held by the other source"); - assert_eq!(merge.key(slot(2), lift(A)).collect::>(), [lift(A)]); - assert_eq!(merge.key(slot(2), lift(A)).count(), 0, "a release of what is not held"); + assert_eq!(merge.key(slot(1), 0, press(A)).unwrap().count(), 1); + assert_eq!(merge.key(slot(1), 0, press(A)).unwrap().count(), 0); + assert_eq!(merge.key(slot(2), 0, press(A)).unwrap().count(), 0, "a key another source holds"); + assert_eq!(merge.key(slot(1), 0, lift(A)).unwrap().count(), 0, "still held by the other source"); + assert_eq!(merge.key(slot(2), 0, lift(A)).unwrap().collect::>(), [lift(A)]); + assert_eq!(merge.key(slot(2), 0, lift(A)).unwrap().count(), 0, "a release of what is not held"); } #[test] fn a_source_that_ends_releases_what_it_held_and_nothing_another_holds() { let mut merge = Merge::default(); - merge.key(slot(0), press(SHIFT)).for_each(drop); - merge.key(slot(0), press(A)).for_each(drop); - merge.key(slot(3), press(SHIFT)).for_each(drop); + merge.key(slot(0), 0, press(SHIFT)).unwrap().for_each(drop); + merge.key(slot(0), 0, press(A)).unwrap().for_each(drop); + merge.key(slot(3), 0, press(SHIFT)).unwrap().for_each(drop); assert_eq!(merge.end(slot(0)).collect::>(), [RawKeyEvent { keycode: A, modifiers: MOD_SHIFT | MOD_RELEASED }]); assert_eq!(merge.end(slot(0)).count(), 0, "it holds nothing now"); assert_eq!(merge.end(slot(3)).collect::>(), [RawKeyEvent { keycode: SHIFT, modifiers: MOD_RELEASED }]); @@ -126,19 +146,19 @@ fn a_source_that_ends_releases_what_it_held_and_nothing_another_holds() { #[test] fn two_pointers_buttons_or_and_ones_empty_report_clears_none_of_the_others() { let mut pointer = Pointer::new(1920, 1080); - assert_eq!(pointer.report(slot(0), relative(0, 1, 0, 0)).buttons, 1); - assert_eq!(pointer.report(Slot::KERNEL, relative(4, 2, 0, 0)).buttons, 3, "another slot's pointer"); - assert_eq!(pointer.report(Slot::KERNEL, relative(5, 0, 0, 0)).buttons, 3, "a third pointer, holding nothing"); - assert_eq!(pointer.report(Slot::KERNEL, relative(4, 0, 0, 0)).buttons, 1); - assert_eq!(pointer.report(slot(0), relative(1, 4, 0, 0)).buttons, 5, "one slot's two pointers"); + assert_eq!(pointer.report(slot(0), relative(0, 1, 0, 0)).unwrap().buttons, 1); + assert_eq!(pointer.report(Slot::KERNEL, relative(4, 2, 0, 0)).unwrap().buttons, 3, "another slot's pointer"); + assert_eq!(pointer.report(Slot::KERNEL, relative(5, 0, 0, 0)).unwrap().buttons, 3, "a third pointer, holding nothing"); + assert_eq!(pointer.report(Slot::KERNEL, relative(4, 0, 0, 0)).unwrap().buttons, 1); + assert_eq!(pointer.report(slot(0), relative(1, 4, 0, 0)).unwrap().buttons, 5, "one slot's two pointers"); } #[test] fn a_connections_end_clears_its_pointers_buttons_and_no_others() { let mut pointer = Pointer::new(1920, 1080); - pointer.report(slot(2), relative(0, 1, 0, 0)); - pointer.report(slot(2), relative(9, 4, 0, 0)); - pointer.report(slot(1), relative(0, 2, 0, 0)); + pointer.report(slot(2), relative(0, 1, 0, 0)).unwrap(); + pointer.report(slot(2), relative(9, 4, 0, 0)).unwrap(); + pointer.report(slot(1), relative(0, 2, 0, 0)).unwrap(); assert_eq!(pointer.end(slot(2)).buttons, 2); assert_eq!(pointer.end(slot(1)).buttons, 0); } @@ -147,15 +167,75 @@ fn a_connections_end_clears_its_pointers_buttons_and_no_others() { fn relative_motion_scales_by_the_screen_and_absolute_motion_sets() { // 1080 rows' worth of 64 counts each, so a count is 64 down and 64 * 1080 / 1920 = 36 across. let mut pointer = Pointer::new(1920, 1080); - let start = pointer.report(slot(0), relative(0, 0, 0, 0)); + let start = pointer.report(slot(0), relative(0, 0, 0, 0)).unwrap(); assert_eq!((start.x, start.y), (16384, 16384), "it starts in the middle"); - let moved = pointer.report(slot(0), relative(0, 0, 10, -3)); + let moved = pointer.report(slot(0), relative(0, 0, 10, -3)).unwrap(); assert_eq!((moved.x, moved.y), (16384 + 10 * 36, 16384 - 3 * 64)); - let set = pointer.report(slot(1), Report { source: 0, buttons: 0, scroll: 1, motion: Motion::Absolute { x: 7, y: 9 } }); + let set = pointer.report(slot(1), Report { source: 0, buttons: 0, scroll: 1, motion: Motion::Absolute { x: 7, y: 9 } }).unwrap(); assert_eq!(set, State { buttons: 0, scroll: 1, x: 7, y: 9 }); - let clamped = pointer.report(slot(0), relative(0, 0, i16::MIN, i16::MAX)); + let clamped = pointer.report(slot(0), relative(0, 0, i16::MIN, i16::MAX)).unwrap(); assert_eq!((clamped.x, clamped.y), (0, 32767), "the edges hold it"); pointer.screen(800, 800); - let square = pointer.report(slot(0), relative(0, 0, 1, -1)); + let square = pointer.report(slot(0), relative(0, 0, 1, -1)).unwrap(); assert_eq!((square.x, square.y), (64, 32767 - 64)); } + +/// A key usbd held at its death is released by the owner: its connection's +/// end releases every device it carried. A `gone` releases its own device +/// alone, and neither touches another connection's. +#[test] +fn a_connections_end_releases_every_device_it_carried_and_a_gone_only_its_own_source() { + const B: u8 = 0x05; + const C: u8 = 0x06; + let mut merge = Merge::default(); + let mut pointer = Pointer::new(1920, 1080); + // Connection 0 carries two keyboards and two pointers; connection 1 one of each. + merge.key(slot(0), 7, press(A)).unwrap().for_each(drop); + merge.key(slot(0), 9, press(B)).unwrap().for_each(drop); + merge.key(slot(1), 0, press(C)).unwrap().for_each(drop); + pointer.report(slot(0), relative(7, 0b001, 0, 0)).unwrap(); + pointer.report(slot(0), relative(9, 0b010, 0, 0)).unwrap(); + pointer.report(slot(1), relative(0, 0b100, 0, 0)).unwrap(); + + assert_eq!(merge.gone(slot(0), 7).collect::>(), [lift(A)], "source 7's key, and no other"); + assert_eq!(pointer.gone(slot(0), 7).buttons, 0b110, "source 7's button, and no other"); + + assert_eq!(merge.end(slot(0)).collect::>(), [lift(B)], "every device left on the connection"); + assert_eq!(pointer.end(slot(0)).buttons, 0b100, "every pointer on the connection"); + + assert_eq!(merge.end(slot(1)).collect::>(), [lift(C)]); + assert_eq!(pointer.end(slot(1)).buttons, 0); +} + +/// A device's number is the sender's and is never mistaken for another's: a +/// `gone` of a source holding nothing releases nothing. +#[test] +fn a_gone_of_a_source_that_holds_nothing_releases_nothing() { + let mut merge = Merge::default(); + merge.key(slot(0), 1, press(A)).unwrap().for_each(drop); + assert_eq!(merge.gone(slot(0), 2).count(), 0); + assert_eq!(merge.gone(slot(1), 1).count(), 0, "the same number on another connection"); + assert_eq!(merge.key(slot(0), 1, lift(A)).unwrap().collect::>(), [lift(A)]); +} + +/// A connection holds at most `DEVICES` devices holding something; the next +/// is the sender breaking the protocol, refused by name and changing nothing, +/// and a device that holds nothing again frees its place. +#[test] +fn a_connection_past_its_devices_is_refused_and_an_idle_device_holds_no_place() { + let mut merge = Merge::default(); + let mut pointer = Pointer::new(1920, 1080); + for source in 0..DEVICES as u16 { + merge.key(slot(0), source, press(A)).unwrap().for_each(drop); + pointer.report(slot(0), relative(source, 1, 0, 0)).unwrap(); + } + let past = DEVICES as u16; + assert_eq!(merge.key(slot(0), past, press(SHIFT)).err(), Some(Full { source: past })); + assert_eq!(pointer.report(slot(0), relative(past, 2, 0, 0)), Err(Full { source: past })); + assert_eq!(pointer.report(slot(1), relative(past, 2, 0, 0)).unwrap().buttons, 3, "another connection has room"); + // One device lets go of everything, and the next has its place. + merge.key(slot(0), 0, lift(A)).unwrap().for_each(drop); + pointer.report(slot(0), relative(0, 0, 0, 0)).unwrap(); + assert!(merge.key(slot(0), past, press(SHIFT)).is_ok()); + assert!(pointer.report(slot(0), relative(past, 2, 0, 0)).is_ok()); +} diff --git a/lib/scsi/Cargo.toml b/lib/scsi/Cargo.toml new file mode 100644 index 00000000000..31fa2e631f8 --- /dev/null +++ b/lib/scsi/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "toyos-scsi" +description = "The SCSI command set a Bulk-Only disk speaks (SPC-4, SBC-3): the commands sent, the answers believed, and the bring-up to a disk with a size." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" diff --git a/lib/xhci/src/scsi.rs b/lib/scsi/src/lib.rs similarity index 87% rename from lib/xhci/src/scsi.rs rename to lib/scsi/src/lib.rs index cd99a0663f2..75c734a58a3 100644 --- a/lib/xhci/src/scsi.rs +++ b/lib/scsi/src/lib.rs @@ -7,7 +7,11 @@ //! size this driver cannot address, a block size that does not divide the host //! block, a peripheral that is not a disk. -use crate::Nanos; +#![no_std] +#![forbid(unsafe_code)] + +/// A monotonic instant in nanoseconds, on the caller's clock. +pub type Nanos = u64; /// The block size everything above this driver is written in; a device whose /// sectors do not divide it is refused, not approximated. @@ -76,38 +80,69 @@ impl Cdb { /// Bytes a REQUEST SENSE asks for. pub const SENSE_BYTES: usize = 18; -/// The sense key, ASC and ASCQ of fixed-format sense data (SPC-4 §4.5.3). +/// The sense key, ASC and ASCQ of fixed-format sense data (SPC-4 §4.5.3), +/// and whether it reports a deferred error: one about a command that already +/// completed, and never the current command's answer (§4.5.5). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Sense { pub key: u8, pub asc: u8, pub ascq: u8, + pub deferred: bool, +} + +/// Why sense data is not believed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum SenseRefused { + /// Fewer than the fourteen bytes that reach ASCQ arrived. + Truncated { delivered: u32 }, + /// A response code other than fixed format's two (SPC-4 §4.5.1, table 27): + /// 0x72 and 0x73 are descriptor format, whose byte 2 is the ASC, and 0x7F + /// is vendor specific. REQUEST SENSE is sent with DESC 0, so any of them + /// is a device outside the standard. + Format(u8), + /// An additional sense length that ends before ASCQ (§4.5.3, byte 7). + Short { additional: u8 }, } impl Sense { /// What a device that would not say is taken to have said: zero is the /// failing side of every decision made from it. - pub const NONE: Self = Self { key: 0, asc: 0, ascq: 0 }; + pub const NONE: Self = Self { key: 0, asc: 0, ascq: 0, deferred: false }; - /// The sense `response` carries, of which `delivered` bytes arrived. ASCQ - /// is byte 13, so fourteen must have or none of it is believed. - pub fn of(response: &[u8; SENSE_BYTES], delivered: u32) -> Self { + /// The fixed-format sense `response` carries, of which `delivered` bytes + /// arrived. + pub fn of(response: &[u8; SENSE_BYTES], delivered: u32) -> Result { if delivered < 14 { - return Self::NONE; + return Err(SenseRefused::Truncated { delivered }); + } + // Bit 7 is VALID, which speaks for the INFORMATION field alone. + let deferred = match response[0] & 0x7F { + 0x70 => false, + 0x71 => true, + other => return Err(SenseRefused::Format(other)), + }; + // Byte 7 counts the bytes after it, so ASCQ at byte 13 needs six. + if response[7] < 6 { + return Err(SenseRefused::Short { additional: response[7] }); } - Self { key: response[2] & 0x0F, asc: response[12], ascq: response[13] } + Ok(Self { key: response[2] & 0x0F, asc: response[12], ascq: response[13], deferred }) } - /// ILLEGAL REQUEST / INVALID COMMAND OPERATION CODE: an answer, not a - /// failure, for a command SBC makes optional. + /// ILLEGAL REQUEST / INVALID COMMAND OPERATION CODE about the current + /// command: an answer, not a failure, for a command SBC makes optional. pub fn unimplemented(self) -> bool { - (self.key, self.asc, self.ascq) == (0x05, 0x20, 0x00) + !self.deferred && (self.key, self.asc, self.ascq) == (0x05, 0x20, 0x00) } } impl core::fmt::Display for Sense { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:#04x}/{:#04x}/{:#04x}", self.key, self.asc, self.ascq) + write!(f, "{:#04x}/{:#04x}/{:#04x}", self.key, self.asc, self.ascq)?; + if self.deferred { + f.write_str(" (deferred)")?; + } + Ok(()) } } @@ -571,20 +606,55 @@ mod tests { assert_eq!(usize::from(Query::Capacity16.cdb().bytes()[13]), Query::Capacity16.allocation()); } + fn fixed(code: u8, additional: u8, key: u8, asc: u8, ascq: u8) -> [u8; SENSE_BYTES] { + let mut response = [0u8; SENSE_BYTES]; + response[0] = code; + response[2] = key; + response[7] = additional; + response[12] = asc; + response[13] = ascq; + response + } + #[test] fn sense_is_believed_only_when_its_ascq_arrived() { - let mut response = [0u8; SENSE_BYTES]; - response[2] = 0xF5; - response[12] = 0x20; - response[13] = 0x00; - assert_eq!(Sense::of(&response, 14), Sense { key: 0x05, asc: 0x20, ascq: 0 }); - assert!(Sense::of(&response, 18).unimplemented()); - assert_eq!(Sense::of(&response, 13), Sense::NONE); + let response = fixed(0x70, 10, 0xF5, 0x20, 0x00); + let sense = Sense { key: 0x05, asc: 0x20, ascq: 0, deferred: false }; + assert_eq!(Sense::of(&response, 14), Ok(sense)); + assert!(Sense::of(&response, 18).is_ok_and(Sense::unimplemented)); + assert_eq!(Sense::of(&response, 13), Err(SenseRefused::Truncated { delivered: 13 })); assert!(!Sense::NONE.unimplemented()); - assert!(!Sense { key: 0x04, asc: 0x44, ascq: 0 }.unimplemented()); + assert!(!Sense { key: 0x04, asc: 0x44, ..Sense::NONE }.unimplemented()); extern crate std; use std::string::ToString; - assert_eq!(Sense { key: 0x04, asc: 0x44, ascq: 0 }.to_string(), "0x04/0x44/0x00"); + assert_eq!(Sense { key: 0x04, asc: 0x44, ..Sense::NONE }.to_string(), "0x04/0x44/0x00"); + } + + /// SPC-4 §4.5.1's response codes, table 27: 0x70 and 0x71 are fixed + /// format, current and deferred; 0x72 and 0x73 descriptor format; 0x7F + /// vendor specific. Only the first two are read as fixed format, VALID + /// whatever it says, and a deferred error is never the current command's. + #[test] + fn sense_is_read_only_in_the_fixed_format_its_response_code_names() { + for valid in [0x00, 0x80] { + let current = Sense::of(&fixed(0x70 | valid, 10, 0x05, 0x20, 0x00), 18).expect("current, fixed"); + assert!(!current.deferred && current.unimplemented()); + let deferred = Sense::of(&fixed(0x71 | valid, 10, 0x05, 0x20, 0x00), 18).expect("deferred, fixed"); + assert!(deferred.deferred, "0x71 is a deferred error"); + assert!(!deferred.unimplemented(), "a deferred error is not about the command just sent"); + for code in [0x72, 0x73, 0x7F, 0x00, 0x6F] { + let response = fixed(code | valid, 10, 0x05, 0x20, 0x00); + assert_eq!(Sense::of(&response, 18), Err(SenseRefused::Format(code)), "{code:#x}"); + } + } + } + + /// Byte 7 counts the bytes after it: six reach ASCQ at byte 13, five do not. + #[test] + fn sense_whose_additional_length_ends_before_ascq_is_refused() { + assert!(Sense::of(&fixed(0x70, 6, 0x02, 0x04, 0x01), 18).is_ok()); + assert_eq!(Sense::of(&fixed(0x70, 5, 0x02, 0x04, 0x01), 18), Err(SenseRefused::Short { additional: 5 })); + assert_eq!(Sense::of(&fixed(0x70, 0, 0x02, 0x04, 0x01), 18), Err(SenseRefused::Short { additional: 0 })); } const STICK: Geometry = Geometry { sector_bytes: 512, sectors: 160, sectors_per_block: 8, blocks: 20 }; @@ -632,7 +702,7 @@ mod tests { let first = transfer.next().expect("a batch"); assert_eq!(transfer.answered(&first, Reply::Budget), Moved::Ended(Fail::Budget)); assert_eq!(transfer.answered(&first, Reply::Broken), Moved::Ended(Fail::Device)); - let sense = Sense { key: 3, asc: 0x11, ascq: 0 }; + let sense = Sense { key: 3, asc: 0x11, ..Sense::NONE }; assert_eq!(transfer.answered(&first, Reply::Refused(sense)), Moved::Refused(sense)); assert_eq!(transfer.answered(&first, Reply::Ok { delivered: 8 * 4096 }), Moved::Whole); let second = transfer.next().expect("a batch"); @@ -643,8 +713,8 @@ mod tests { #[test] fn a_flush_the_device_does_not_implement_is_no_failure_and_every_other_refusal_is() { - let unimplemented = Sense { key: 0x05, asc: 0x20, ascq: 0 }; - let failed = Sense { key: 0x04, asc: 0x44, ascq: 0 }; + let unimplemented = Sense { key: 0x05, asc: 0x20, ..Sense::NONE }; + let failed = Sense { key: 0x04, asc: 0x44, ..Sense::NONE }; assert_eq!(flushed(Reply::Refused(unimplemented)), Flushed::NoCache); assert_eq!(flushed(Reply::Refused(failed)), Flushed::Refused(failed)); assert_eq!(flushed(Reply::Ok { delivered: 0 }), Flushed::Emptied); @@ -720,7 +790,7 @@ mod tests { Heard::CheckCondition } Ask::TestUnitReady => Heard::Good, - Ask::RequestSense => Heard::Sense(Sense { key: 0x02, asc: 0x04, ascq: 0x01 }), + Ask::RequestSense => Heard::Sense(Sense { key: 0x02, asc: 0x04, ascq: 0x01, deferred: false }), Ask::Recover => Heard::Recovered { offline: device.offline }, Ask::Read(query) => { let data = match query { @@ -790,7 +860,7 @@ mod tests { fn a_disk_that_never_becomes_ready_is_given_up_on_at_its_budget() { let (inquiry, capacity) = (inquiry(0), capacity10(8191, 512)); let route = bring_up(Device { not_ready: u32::MAX, ..disk(&inquiry, &capacity) }, 100 * MS); - assert_eq!(route.up, Up::Unready { sense: Sense { key: 0x02, asc: 0x04, ascq: 0x01 }, offline: false }); + assert_eq!(route.up, Up::Unready { sense: Sense { key: 0x02, asc: 0x04, ascq: 0x01, deferred: false }, offline: false }); assert_eq!(route.asks().filter(|a| *a == Ask::TestUnitReady).count(), 3, "at 0, 200 and 400 ms"); assert_eq!(route.disk, None); } diff --git a/lib/xhci/Cargo.toml b/lib/xhci/Cargo.toml index e0e19625e2c..88f00d497d9 100644 --- a/lib/xhci/Cargo.toml +++ b/lib/xhci/Cargo.toml @@ -18,3 +18,7 @@ std = [] # selects one. The kernel does not enable it, so no production path can reach a # flawed decision — the field does not exist there. flaws = [] + +[dependencies] +# The command a Bulk-Only wrapper carries. +toyos-scsi = { path = "../scsi" } diff --git a/lib/xhci/src/bot.rs b/lib/xhci/src/bot.rs index 5c40d51923f..b1bbbb10b72 100644 --- a/lib/xhci/src/bot.rs +++ b/lib/xhci/src/bot.rs @@ -20,7 +20,7 @@ use crate::job::{CC_SHORT_PACKET, CC_STALL, CC_SUCCESS}; use crate::ladder::{self, Left}; use crate::reset_recovery::Pipe; -use crate::scsi::Cdb; +use toyos_scsi::Cdb; /// Where one Bulk-Only round trip stands, published by the driver as it walks /// the three phases. diff --git a/lib/xhci/src/lib.rs b/lib/xhci/src/lib.rs index 50439270d66..bc2edad02f2 100644 --- a/lib/xhci/src/lib.rs +++ b/lib/xhci/src/lib.rs @@ -26,7 +26,6 @@ pub mod recovery; pub mod reset_recovery; pub mod ring; pub mod scan; -pub mod scsi; pub mod xecp; pub use job::{Await, Outcome, Outstanding}; diff --git a/sdk/toyos/src/ipc.rs b/sdk/toyos/src/ipc.rs index 7c94f1c9f3c..73fd9e05cd3 100644 --- a/sdk/toyos/src/ipc.rs +++ b/sdk/toyos/src/ipc.rs @@ -168,8 +168,9 @@ pub enum TrySendError { Syscall(SyscallError), } -/// An IPC connection. A server gets one from [`crate::port::Acceptor::accept`] -/// and a client from [`crate::endow::service`]. +/// An IPC connection. A server gets one from [`crate::port::Acceptor::accept`], +/// a client from [`crate::endow::service`], and a server one end a peer +/// handed it from [`Self::received`]. pub struct Connection(pub(crate) OwnedHandle); impl AsHandle for Connection { @@ -177,6 +178,18 @@ impl AsHandle for Connection { } impl Connection { + /// A connection end a peer sent, consumed. Its type is the sender's + /// word: the kernel refuses each connection call on a handle of another + /// type, so trust the sender only as far as its protocol says. + pub fn received(raw: RawHandle) -> Self { + Self(OwnedHandle(raw)) + } + + /// This end as a handle, to send it on ([`Self::send_handles`]). + pub fn into_owned(self) -> OwnedHandle { + self.0 + } + pub fn send(&self, msg_type: u32, payload: &T) -> Result<(), IpcError> { send(self.0.raw(), msg_type, payload) } diff --git a/system/compositor/src/session.rs b/system/compositor/src/session.rs index 708a0db3c56..fc5865c5862 100644 --- a/system/compositor/src/session.rs +++ b/system/compositor/src/session.rs @@ -22,8 +22,9 @@ use toyos::{ipc, system, AsHandle, Keyboard, Mouse}; use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::DeviceType; use toyos_abi::RawHandle; -use toyos_input::wire::{self, Record, CONNECTIONS, KEY_LEN, MAX_PAYLOAD, POINTER_LEN}; -use toyos_input::{Merge, Pointer, Slot, State}; +use toyos_input::wire::{self, Record, CONNECTIONS, MAX_PAYLOAD, STREAM_KEY_LEN, STREAM_POINTER_LEN}; +use toyos_input::wire::DEVICES; +use toyos_input::{Full, Merge, Pointer, Slot, State}; use toyos_desktop::clipboard::{Landing, Watch}; use toyos_desktop::volume::{overlay_rect, Overlay, Request, Requests, Shown}; use toyos::audio::{Volume, VolumeStep, MSG_VOLUME, MSG_VOLUME_MUTE, MSG_VOLUME_STEP, MSG_VOLUME_UNAVAILABLE}; @@ -669,28 +670,37 @@ impl Session { /// The kernel's keyboard stream, one source to the merge. fn keys(&mut self) { - let mut buf = [0u8; 8 * KEY_LEN]; + let mut buf = [0u8; 8 * STREAM_KEY_LEN]; // Never read blocking here. The kernel wakes only when a report queued // an event, so readiness and `has_data` agree — but a blocking read on // an empty queue parks the compositor until the next real key, and one // spurious wake anywhere would freeze it. let n = self.kb.read_nonblock(&mut buf).unwrap_or(0); - let (events, torn) = buf[..n].as_chunks::(); + let (events, torn) = buf[..n].as_chunks::(); assert!(torn.is_empty(), "compositor: the keyboard read {n} bytes, not whole events"); for raw in events { - match wire::key(raw) { - Ok(key) => self.merged_key(Slot::KERNEL, key), - Err(why) => eprintln!("compositor: the kernel's keyboard stream carried {raw:02x?}, refused: {why:?}"), + let merged = wire::key(raw).map(|key| self.merged_key(Slot::KERNEL, 0, key)); + if !matches!(merged, Ok(Ok(()))) { + eprintln!("compositor: the kernel's keyboard stream carried {raw:02x?}, refused: {merged:?}"); } } } - /// `key` from `slot`, and whatever it changed of the merged keyboard. - fn merged_key(&mut self, slot: Slot, key: RawKeyEvent) { - let delivered: Vec = self.merge.key(slot, key).collect(); + /// `key` from `slot`'s device `source`, and whatever it changed of the + /// merged keyboard. + fn merged_key(&mut self, slot: Slot, source: u16, key: RawKeyEvent) -> Result<(), Full> { + let delivered: Vec = self.merge.key(slot, source, key)?.collect(); for event in delivered { self.key(event); } + Ok(()) + } + + /// `slot`'s device `source` went: what it held is released. + fn gone(&mut self, slot: Slot, source: u16) -> State { + let released: Vec = self.merge.gone(slot, source).collect(); + released.into_iter().for_each(|key| self.key(key)); + self.pointers.gone(slot, source) } /// One transition of the merged keyboard. @@ -767,15 +777,15 @@ impl Session { /// The kernel's mouse stream, one source to the merge. fn pointer(&mut self) { - let mut buf = [0u8; 64 * POINTER_LEN]; + let mut buf = [0u8; 64 * STREAM_POINTER_LEN]; let n = self.mouse.read_nonblock(&mut buf).unwrap_or(0); - let (reports, torn) = buf[..n].as_chunks::(); + let (reports, torn) = buf[..n].as_chunks::(); assert!(torn.is_empty(), "compositor: the mouse read {n} bytes, not whole reports"); let mut states = Vec::with_capacity(reports.len()); for raw in reports { - match wire::pointer(raw) { - Ok(report) => states.push(self.pointers.report(Slot::KERNEL, report)), - Err(why) => eprintln!("compositor: the kernel's mouse stream carried {raw:02x?}, refused: {why:?}"), + match wire::pointer(raw).map(|report| self.pointers.report(Slot::KERNEL, report)) { + Ok(Ok(state)) => states.push(state), + refused => eprintln!("compositor: the kernel's mouse stream carried {raw:02x?}, refused: {refused:?}"), } } self.motion(&states); @@ -1024,12 +1034,20 @@ impl Session { RxStep::Frame { msg_type, payload_len } => wire::decode(msg_type, source.rx.payload(payload_len)), }; match record { - Ok(Record::Key(key)) => self.merged_key(slot, key), - Ok(Record::Pointer(report)) => states.push(self.pointers.report(slot, report)), - Ok(Record::Lost) => { - let released: Vec = self.merge.end(slot).collect(); - released.into_iter().for_each(|key| self.key(key)); + Ok(Record::Key { source, key }) => { + if let Err(full) = self.merged_key(slot, source, key) { + ended = Some(format!("{full:?}: more than {DEVICES} of its devices hold something")); + break; + } } + Ok(Record::Pointer(report)) => match self.pointers.report(slot, report) { + Ok(state) => states.push(state), + Err(full) => { + ended = Some(format!("{full:?}: more than {DEVICES} of its devices hold something")); + break; + } + }, + Ok(Record::Gone { source }) => states.push(self.gone(slot, source)), Err(why) => { ended = Some(format!("a record refused: {why:?}")); break; diff --git a/system/console/src/main.rs b/system/console/src/main.rs index fbeee528051..1c796d91ede 100644 --- a/system/console/src/main.rs +++ b/system/console/src/main.rs @@ -39,7 +39,7 @@ use toyos::surface::{self, Delivery, Host, Notice}; use toyos::{FramebufferDev, Keyboard, Pipe}; use toyos_abi::input::RawKeyEvent; use toyos_abi::syscall::{DeviceType, SyscallError}; -use toyos_input::wire::{self, Record, CONNECTIONS, KEY_LEN, MAX_PAYLOAD}; +use toyos_input::wire::{self, Record, CONNECTIONS, DEVICES, MAX_PAYLOAD, STREAM_KEY_LEN}; use toyos_input::{Merge, Slot}; use toyos_logstream::{Lines, Showing, Source}; use window::Screen; @@ -367,9 +367,15 @@ fn main() { RxStep::Frame { msg_type, payload_len } => wire::decode(msg_type, rx.payload(payload_len)), }; match record { - Ok(Record::Key(key)) => keys.extend(merge.key(slot, key)), + Ok(Record::Key { source, key }) => match merge.key(slot, source, key) { + Ok(delivered) => keys.extend(delivered), + Err(full) => { + ended = Some(format!("{full:?}: more than {DEVICES} of its devices hold something")); + break; + } + }, Ok(Record::Pointer(_)) => {} - Ok(Record::Lost) => keys.extend(merge.end(slot)), + Ok(Record::Gone { source }) => keys.extend(merge.gone(slot, source)), Err(why) => { ended = Some(format!("a record refused: {why:?}")); break; @@ -383,14 +389,17 @@ fn main() { } } if ready[TOKEN_KEYBOARD as usize] { - let mut buf = [0u8; 16 * KEY_LEN]; + let mut buf = [0u8; 16 * STREAM_KEY_LEN]; // Non-blocking for the reason `Keyboard::read_nonblock` documents: // an event loop that can park on an empty queue is a frozen screen. let n = kb.read_nonblock(&mut buf).unwrap_or(0); - for raw in buf[..n].as_chunks::().0 { - match wire::key(raw) { - Ok(key) => keys.extend(merge.key(Slot::KERNEL, key)), - Err(why) => eprintln!("console: the kernel's keyboard stream carried {raw:02x?}, refused: {why:?}"), + for raw in buf[..n].as_chunks::().0 { + match wire::key(raw).map(|key| merge.key(Slot::KERNEL, 0, key)) { + Ok(Ok(delivered)) => keys.extend(delivered), + refused => eprintln!( + "console: the kernel's keyboard stream carried {raw:02x?}, refused: {:?}", + refused.map(|r| r.map(drop)) + ), } } } diff --git a/system/diskserver/Cargo.toml b/system/diskserver/Cargo.toml index 38c0f59575b..4b5ea2034c8 100644 --- a/system/diskserver/Cargo.toml +++ b/system/diskserver/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "diskserver" -description = "One NVMe controller driven from userland, its partitions served as toyos-blockring sessions; the library is what a client of a block service links." +description = "The block service: every partition a disk driver offers, listed and opened for the clients its grants admit, each session handed to its driver; the library is what a client links." edition = "2024" license = "MIT OR Apache-2.0" -# The library is what a client of the service needs — the session region as a -# process sees it, and the session itself — so a program that opens a -# partition links it and nothing of the driver. +# The library is what a client of the service needs — the session and a +# partition's blocks — so a program that opens a partition links it and +# nothing of the service. [lib] path = "src/lib.rs" doctest = false @@ -19,10 +19,10 @@ path = "src/main.rs" toyos-abi = { path = "../../sdk/abi" } toyos = { path = "../../sdk/toyos" } toyos-blockring = { path = "../../lib/blockring" } -toyos-blockhold = { path = "../../lib/blockhold" } -toyos-gpt = { path = "../../lib/gpt" } +# The session region, which a client maps as its driver does. +toyos-blockserve = { path = "../../lib/blockserve" } # The trait every partition is read and written through. toyos-disk = { path = "../../lib/disk" } [package.metadata.toyos.host] -exempt.owns = "the NVMe controller ToyOS claims for it" +exempt.owns = "the block service between ToyOS disk drivers and their clients" diff --git a/system/diskserver/src/lib.rs b/system/diskserver/src/lib.rs index e4d6c10ecd5..9531fb99dc0 100644 --- a/system/diskserver/src/lib.rs +++ b/system/diskserver/src/lib.rs @@ -1,13 +1,8 @@ -//! What a program that opens a partition through a block service links — the -//! session region as a process sees it ([`region`]), the session itself -//! ([`session`]), and a partition's blocks however it is held ([`disk`]) — -//! and the NVMe driver the service runs ([`nvme`]), which is a -//! library so a test can aim the controller at an address itself. The service -//! is `src/main.rs` beside this. +//! What a program that opens a partition through the block service links: +//! the session itself ([`session`]), and a partition's blocks however it is +//! held ([`disk`]). The service is `src/main.rs` beside this. pub mod disk; -pub mod nvme; -pub mod region; pub mod session; pub use session::{list, Answer, Error, Session, Unsent, Waited}; diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index aecd1929cb9..519dd019427 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -1,694 +1,512 @@ -//! diskserver: one NVMe controller, driven from userland, serving its partitions -//! as `toyos-blockring` sessions. +//! diskserver: the block service — every disk a driver offers, listed and +//! opened for the clients its grants admit, and each opened session handed to +//! the driver whose disk it is on. //! -//! **It holds a claim on one PCI function and nothing else of the machine.** -//! The claim arrives under the `dev:pci:` label its starter minted it under — -//! the supervisor for a manifest row, or a test's supervisor — and the port it serves -//! under `serve:block`; the kernel keeps config space, the interrupt vector -//! and the function's address space, and diskserver never names an address the -//! kernel did not hand it ([`nvme`]). -//! -//! **A session is a partition held**, from the client's open to its hang-up: -//! one holder per partition (`toyos-blockhold`), every request bounded to the -//! partition and the arena before the device sees it -//! (`toyos_blockring::server`), the data moved by the device straight into and -//! out of the client's own region, which the kernel maps into this -//! controller's domain for the length of the session (`SYS_DEVICE_DMA_MAP`) -//! and takes back at its end — after the last command naming it has been -//! answered, never before. -//! -//! **The partition table is read once, at start.** Every partition lies -//! outside every other and the table outside all of them, so no session can -//! change it; the table this process read is the one it serves for its life. -//! A partition whose range is not whole 4 KiB blocks, or whose GUID the table -//! carries twice, is listed and refused by name. -//! -//! **A controller its row names that is on the machine and whose claim the -//! kernel refused is not an absent one**: its starter names it -//! (`--claim-refused `), and a listing and every open are refused -//! `ClaimRefused`, so a file server says so rather than finding no disk. +//! **It drives no device.** It serves `block` to clients and `disk` to disk +//! drivers (`toyos_blockring::disk`), one per controller, each of which reads +//! its own partition table and offers its disks. An open the grant admits is +//! handed whole to the offering driver — the client's connection and the +//! region it sent — and the driver answers it and serves the session, so a +//! block never passes through here. //! //! **A connection reaches what its grant says** ([`wire::Grant`]): the badge -//! the supervisor minted its connector with on this service's port, which the -//! kernel stamps on it and answers this acceptor alone. A listing names the -//! partitions the grant admits, an open of any other is refused `NotGranted` -//! whether or not the table carries it, a connection with no grant reaches -//! nothing, and a session whose grant does not write answers every write -//! `ReadOnly` before the device sees it. +//! the supervisor minted its connector with, which the kernel stamps on it and +//! answers this acceptor alone. A listing names the partitions the grant +//! admits, an open of any other is refused `NotGranted` whether or not a disk +//! carries it, a connection with no grant reaches nothing, and a session whose +//! grant does not write is handed over as one, whose driver answers every +//! write `ReadOnly`. //! -//! **A client may ask what is served** ([`wire::MSG_LIST`]) before it opens -//! anything, which is how a file server finds its role's partition by type. +//! **"Not yet" is never answered as "absent".** The supervisor starts the disk +//! drivers before this service and says how many (`--drivers`), and names +//! each disk controller it started no driver on (`--undriven`). An open of a +//! partition no driver offers, and a listing, wait until every driver started +//! at boot has settled and every driver that ended has settled again, or for +//! [`DRIVER_BOUND`]; then a listing names what is offered, and what is not +//! offered is refused by what is known: `Unusable` while a driver cannot use +//! its controller or has not settled, `ClaimRefused` while a controller has +//! no driver, and `NotFound` otherwise. //! -//! **A server never blocks on a client.** Accept and the open frame are two -//! events, the open is buffered until whole, every answer is one `try_send`, -//! and a doorbell is one non-blocking write whose refusal is a full pipe — -//! a doorbell already rung. - - +//! **A server never blocks on a client or a driver.** Accept and the first +//! frame are two events, a frame is buffered until whole, and every answer is +//! one `try_send`. use std::collections::BTreeMap; use std::time::{Duration, Instant}; -use diskserver::region::Region; -use diskserver::nvme::{Controller, Done, Owner}; -use toyos::endow::{self, Endowments}; +use toyos::endow; use toyos::ipc::{self, Connection, RxStep}; use toyos::poller::{Poller, READABLE}; -use toyos::AsHandle; -use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; -use toyos_abi::syscall::{DEV_PREFIX, MAX_BADGE, SyscallError}; -use toyos_blockhold::Holds; -use toyos_blockring::entry::{Completion, Op}; -use toyos_blockring::layout::{ServerRings, DEPTH}; -use toyos_blockring::server::{ServerSession, Taken}; -use toyos_blockring::wire::{self, Grant, Opened, Refusal}; -use toyos_blockring::{BLOCK_BYTES, PORT}; - -/// How long a command may go unanswered before the controller is reset to -/// take it back. Policy: NVMe defines no per-command bound, and a flush of a -/// large cache is the slowest thing a healthy drive does. -const COMMAND_SILENCE: Duration = Duration::from_secs(10); - -/// Sessions held at once. Each costs its region's mapping in this process and -/// in the controller's domain, which the kernel bounds per claim. -const MAX_SESSIONS: usize = 8; - -/// Connections accepted and not yet opened, and how long one may stay so. +use toyos::{AsHandle, OwnedHandle}; +use toyos_abi::syscall::{SyscallError, MAX_BADGE}; +use toyos_blockring::disk::{self, Offer, Part, Settled}; +use toyos_blockring::wire::{self, Grant, Listed, Refusal}; +use toyos_blockring::PORT; +use toyos_blockserve::guid_text; + +/// How long a disk driver may take from its start to say how its bring-up +/// ended, and so how long a listing or an open waits for it. Policy: a stick's +/// bring-up is a port's reset and enumeration and a Bulk-Only disk's first +/// commands, each bounded far below this. +const DRIVER_BOUND: Duration = Duration::from_secs(30); + +/// Connections accepted and not yet asked, and how long one may stay so. const MAX_PENDING: usize = 16; const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); - -/// Argv, followed by a device name of this service's row: on this machine, and -/// its claim refused. -const CLAIM_REFUSED: &str = "--claim-refused"; - -const TOKEN_IRQ: u64 = 0; -const TOKEN_ACCEPT: u64 = 1; +/// Requests that wait for a driver at once. +const MAX_WAITING: usize = 16; +/// Disk drivers connected at once. +const MAX_DRIVERS: usize = 16; + +/// Argv: the disk drivers the supervisor started before this service, and +/// each disk controller it started none on. +const DRIVERS: &str = "--drivers"; +const UNDRIVEN: &str = "--undriven"; + +const TOKEN_ACCEPT: u64 = 0; +const TOKEN_DRIVERS: u64 = 1; const TOKEN_PENDING: u64 = 0x1_0000; -const TOKEN_SESSION: u64 = 0x2_0000; - -/// One partition of the table, in 4 KiB blocks of the device. -struct Part { - unique: [u8; 16], - kind: [u8; 16], - /// Where it starts and how long it is, or why it cannot be served. - span: Result<(u64, u64), String>, -} +const TOKEN_DRIVER: u64 = 0x2_0000; -fn guid_text(bytes: [u8; 16]) -> String { - let mut buf = [0u8; GUID_TEXT_LEN]; - PartGuid(bytes).write_text(&mut buf).to_string() +/// One partition a driver offered, on the disk the service numbered. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Offered { + disk: u32, + part: Part, } -/// The controller as `toyos_gpt` reads it: its own sectors, served a 4 KiB -/// block at a time, and floored to whole blocks. -struct Sectors<'a> { - ctrl: &'a mut Controller, - block: Vec, +/// How a driver, by its name, last said its bring-up ended. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Fate { + Offered, + Unusable, + /// Its connection ended, and no driver of its name has settled since. + Ended, } -impl toyos_gpt::Sectors for Sectors<'_> { - fn lba_bytes(&self) -> u32 { - self.ctrl.lba_bytes - } - fn lba_count(&self) -> u64 { - let per = BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64; - self.ctrl.sectors / per * per - } - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::new(BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64).expect("at least one") - } - fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { - let lba_bytes = self.ctrl.lba_bytes as u64; - let at = lba * lba_bytes; - if !self.ctrl.read_block(at / BLOCK_BYTES as u64, &mut self.block) { - return false; - } - let off = (at % BLOCK_BYTES as u64) as usize; - buf.copy_from_slice(&self.block[off..off + lba_bytes as usize]); - true - } +/// Everything the drivers said, and what the supervisor said of them. +#[derive(Default)] +struct Known { + /// How many disk drivers were started before this service. + expected: usize, + undriven: Vec, + fates: BTreeMap, + offered: Vec, } -/// Every partition the table names, each checked the way the kernel checks a -/// claim's: past `toyos_gpt::locate`'s range and overlap checks, and whole -/// 4 KiB blocks. -fn read_table(ctrl: &mut Controller) -> Vec { - let per = BLOCK_BYTES as u64 / ctrl.lba_bytes as u64; - let mut sectors = Sectors { ctrl, block: vec![0u8; BLOCK_BYTES] }; - let mut found = [None; 128]; - if let Err(why) = toyos_gpt::list(&mut sectors, &mut found) { - println!("diskserver: the disk carries no partition table this driver reads: {why:?}"); - return Vec::new(); +impl Known { + /// Whether nothing a driver will say could change an answer: every driver + /// started at boot has settled, and no driver is between an end and its + /// next settling. + fn settled(&self) -> bool { + self.fates.len() >= self.expected && !self.fates.values().any(|f| *f == Fate::Ended) } - let mut parts = Vec::new(); - for entry in found.iter().flatten() { - let listed = match entry { - Ok(listed) => listed, - Err(unplaced) => { - let span = Err(format!("its blocks are no partition on this disk: {unplaced:?}")); - parts.push(Part { unique: unplaced.unique_guid.0, kind: unplaced.type_guid.0, span }); - continue; - } - }; - let unique = listed.unique_guid().0; - let kind = listed.type_guid().0; - let span = match toyos_gpt::locate(&mut sectors, listed.unique_guid()) { - Err(why) => Err(format!("its own table refuses it: {why:?}")), - Ok(located) => { - let p = located.partition(); - let (first, count) = (p.first_lba(), p.lba_count().get()); - if first % per != 0 || count % per != 0 { - Err(format!( - "LBA {first}+{count} is not whole 4 KiB blocks of {}-byte sectors", - sectors.ctrl.lba_bytes - )) - } else { - Ok((first / per, count / per)) - } - } - }; - parts.push(Part { unique, kind, span }); - } - parts -} -/// The partitions a listing through `grant` names: every one it admits, of -/// `parts`, the table of a drive or why it has none. **The grant is asked -/// first**, so a connection with none learns nothing of the drive. -fn listed(parts: Result<&[Part], Refusal>, grant: Option) -> Result, Refusal> { - let grant = grant.ok_or(Refusal::NotGranted)?; - Ok(parts? - .iter() - .filter(|p| grant.admits(p.unique, p.kind)) - .flat_map(|p| wire::Listed { unique: p.unique, kind: p.kind }.encode()) - .collect()) -} + /// The drivers whose controllers' partitions cannot be said absent. + fn unsettled(&self) -> Vec<&str> { + self.fates.iter().filter(|(_, f)| **f != Fate::Offered).map(|(l, _)| l.as_str()).collect() + } -/// Where an open of `guid` through `grant` is served, before anything is -/// held for it; or its refusal. **The grant is asked first**, so a -/// connection learns nothing of a partition it does not reach, not even -/// whether the table carries it; then the drive, then the table. -fn admitted(parts: Result<&[Part], Refusal>, guid: [u8; 16], grant: Option) -> Result<(u64, u64), Refusal> { - let grant = grant.ok_or(Refusal::NotGranted)?; - let part = parts?.iter().find(|p| p.unique == guid && guid != [0; 16]); - // A partition the table does not carry has no type: only a unique grant - // naming it reaches it, to be told `NotFound`. - if !grant.admits(guid, part.map_or([0; 16], |p| p.kind)) { - return Err(Refusal::NotGranted); + /// Why a partition no disk offers is not served, once nothing is awaited. + fn absent(&self) -> Refusal { + let missing = self.fates.len() < self.expected; + if missing || !self.unsettled().is_empty() { + Refusal::Unusable + } else if !self.undriven.is_empty() { + Refusal::ClaimRefused + } else { + Refusal::NotFound + } } - match part.map(|p| &p.span) { - None => Err(Refusal::NotFound), - Some(Err(_)) => Err(Refusal::Unusable), - Some(Ok(span)) => Ok(*span), + + /// What a listing through `grant` answers, or `None` while it waits: + /// every partition offered that it admits. **The grant is asked first**, + /// so a connection with none learns nothing. + fn listing(&self, grant: Option, expired: bool) -> Option, Refusal>> { + let Some(grant) = grant else { return Some(Err(Refusal::NotGranted)) }; + if !self.settled() && !expired { + return None; + } + let listed: Vec = self + .offered + .iter() + .filter(|o| grant.admits(o.part.unique, o.part.kind)) + .map(|o| Listed { disk: o.disk, unique: o.part.unique, kind: o.part.kind }) + .collect(); + Some(match self.absent() { + Refusal::NotFound => Ok(listed), + _ if !listed.is_empty() => Ok(listed), + why => Err(why), + }) + } + + /// Where an open of `guid` through `grant` is served, or its refusal, + /// or `None` while it waits. **The grant is asked first**, so a + /// connection learns nothing of a partition it does not reach. + fn open(&self, guid: [u8; 16], grant: Option, expired: bool) -> Option> { + let Some(grant) = grant else { return Some(Err(Refusal::NotGranted)) }; + let carrying: Vec<&Offered> = self.offered.iter().filter(|o| o.part.unique == guid && guid != [0; 16]).collect(); + // A partition no disk offers has no type: only a unique grant naming + // it reaches it, to wait for it or be told why it is not there. + let kind = carrying.first().map_or([0; 16], |o| o.part.kind); + if !grant.admits(guid, kind) { + return Some(Err(Refusal::NotGranted)); + } + Some(match carrying[..] { + [] if !self.settled() && !expired => return None, + [] => Err(self.absent()), + [one] if one.part.blocks.is_some() => Ok(*one), + [_] => Err(Refusal::Unusable), + _ => Err(Refusal::Unusable), + }) } } -/// A connection that has not opened a session yet. +/// A connection that has not asked anything yet. struct Pending { conn: Connection, rx: ipc::FrameRx<{ wire::GUID_BYTES }>, since: Instant, } -/// A partition held, over a client's region. -struct Served { - conn: Connection, - region: Region, - device_addr: u64, - rings: ServerRings, - state: ServerSession, - unique: [u8; 16], - /// The client hung up, or broke the protocol: nothing more is taken, and - /// the session ends once nothing of it is on the device. - closing: bool, - requests: u64, - posted: bool, +/// What a waiting client asked. +enum Ask { + List, + Open { guid: [u8; 16], region: OwnedHandle }, } -/// The controller this service drives and the partitions its table names, or -/// why it drives none. Without one no partition is listed and no session -/// opened, and a connection says one frame and is answered, or is let go. -enum Drive { - Up(Controller, Vec), - /// No controller this row names is on this machine: a listing is empty - /// and an open `NotFound`. - Absent, - /// A controller this service cannot use: a listing and an open are - /// refused `Unusable`, so a file server says the disk failed rather than - /// that there is none. - Unusable, - /// A controller this row names is on this machine and its claim was - /// refused: a listing and an open are refused `ClaimRefused`. - ClaimRefused, +/// A client whose question waits for the drivers. +struct Waiting { + conn: Connection, + grant: Option, + ask: Ask, + since: Instant, } -impl Drive { - /// The controller a session is on: a service without one has no partition - /// to open. - fn up(&mut self) -> &mut Controller { - match self { - Drive::Up(ctrl, _) => ctrl, - Drive::Absent | Drive::Unusable | Drive::ClaimRefused => { - unreachable!("diskserver: a session with no controller") - } - } - } - - /// The table a listing and an open are judged against: none on a drive - /// that is absent, and a drive this service cannot use, or was refused, - /// refused by that word. - fn parts(&self) -> Result<&[Part], Refusal> { - match self { - Drive::Up(_, parts) => Ok(parts), - Drive::Absent => Ok(&[]), - Drive::Unusable => Err(Refusal::Unusable), - Drive::ClaimRefused => Err(Refusal::ClaimRefused), - } - } - - fn oldest(&self) -> Option { - match self { - Drive::Up(ctrl, _) => ctrl.oldest(), - Drive::Absent | Drive::Unusable | Drive::ClaimRefused => None, - } - } +/// One disk driver's connection. +struct Driver { + conn: Connection, + rx: ipc::FrameRx<{ 4 + disk::MAX_PARTS * Part::BYTES }>, + label: Option, + settled: bool, + /// The driver's number for each disk it offers, and the service's. + disks: BTreeMap, + since: Instant, } struct Service { - ctrl: Drive, - holds: Holds, - /// The device's loss count: every reset may have dropped its cache. - losses: u64, - sessions: BTreeMap, - next_id: u64, + known: Known, + drivers: BTreeMap, + next_driver: u64, + next_disk: u32, + waiting: Vec, } -/// A session decided on and not yet told to its client. -struct Opening { - region: Region, - blocks: u64, - device_addr: u64, - first: u64, - writes: bool, -} - -impl Service { - fn new(ctrl: Drive) -> Self { - Self { ctrl, holds: Holds::new(), losses: 0, sessions: BTreeMap::new(), next_id: 0 } - } - - /// What a listing through `grant` answers: every partition of the table - /// it admits. - fn listing(&self, grant: Option) -> Result, Refusal> { - listed(self.ctrl.parts(), grant) - } - - /// The span an open of `guid` through `grant` is served on, held for it; - /// or its refusal. - /// - /// **A client that hung up holds nothing**: every session's end its client - /// has already made is read before the open is judged, so an open made - /// after another holder closed its connection is never refused `Held` for - /// a holder that is gone — only for one whose commands are still on the - /// device. - fn place(&mut self, guid: [u8; 16], grant: Option) -> Result<(u64, u64), Refusal> { - let (first, blocks) = admitted(self.ctrl.parts(), guid, grant)?; - let ids: Vec = self.sessions.keys().copied().collect(); - for id in ids { - self.hear(id); - } - self.retire(); - if self.sessions.len() >= MAX_SESSIONS { - return Err(Refusal::Exhausted); - } - if self.holds.hold(first, first + blocks, self.next_id).is_err() { - return Err(Refusal::Held); - } - Ok((first, blocks)) - } - - /// What an open answers: a session to admit, or its refusal. `region` is - /// the client's and is consumed either way. - fn open(&mut self, guid: [u8; 16], region: toyos::RawHandle, grant: Option) -> Result { - // A region the kernel did not allocate, or longer than a session, which would spend the claim's bound on the kernel's side for every other client, is refused whole. - let region = Region::lent(region).map_err(|_| Refusal::Malformed)?; - let (first, blocks) = self.place(guid, grant)?; - let writes = grant.expect("placed through a grant").writes; - match self.ctrl.up().claim().dma_map(region.handle()) { - Ok(mapping) => Ok(Opening { region, blocks, device_addr: mapping.device_addr, first, writes }), - Err(why) => { - self.holds.release(first); - Err(match why { - SyscallError::ResourceExhausted => Refusal::Exhausted, - _ => Refusal::Malformed, - }) +fn main() { + let args: Vec = std::env::args().collect(); + let mut known = Known::default(); + let mut at = 1; + while at < args.len() { + let value = args.get(at + 1).unwrap_or_else(|| panic!("diskserver: {} takes a value", args[at])); + match args[at].as_str() { + DRIVERS => known.expected = value.parse().unwrap_or_else(|_| panic!("diskserver: {DRIVERS} {value}")), + UNDRIVEN => { + println!("diskserver: {value} is on this machine and no driver of it started; what it carries is refused"); + known.undriven.push(value.clone()); } + other => panic!("diskserver: {other} is no argument of this service"), } + at += 2; } + let clients = endow::acceptor(PORT).unwrap_or_else(|| panic!("diskserver: started serving no `{PORT}` port")); + let drivers = endow::acceptor(disk::PORT).unwrap_or_else(|| panic!("diskserver: started serving no `{}` port", disk::PORT)); + println!("diskserver: serving the disks of {} driver(s) started before it", known.expected); + let mut service = Service { known, drivers: BTreeMap::new(), next_driver: 0, next_disk: 0, waiting: Vec::new() }; + service.serve(&clients, &drivers) +} - /// The client heard its session is open, over the ends `Opened::over` - /// made: from here it is served. - fn admit(&mut self, opening: Opening, rings: ServerRings, opened: Opened, conn: Connection) -> u64 { - let id = self.next_id; - self.next_id += 1; - self.sessions.insert( - id, - Served { - conn, - region: opening.region, - device_addr: opening.device_addr, - rings, - state: ServerSession::new(opening.first, opened.blocks(), opening.writes), - unique: opened.unique(), - closing: false, - requests: 0, - posted: false, - }, - ); - id - } - - /// The client went before it heard: nothing of the session reached the - /// device, and what was taken for it goes back. - fn abandon(&mut self, opening: Opening) { - if let Err(why) = self.ctrl.up().claim().dma_unmap(opening.device_addr) { - panic!("diskserver: the kernel would not take an abandoned session's region back: {why:?}"); - } - self.holds.release(opening.first); - } - - /// Put `c` on its session's completion ring. [`Self::pull`] leaves room for - /// every answer, so a ring without it is a client whose head went back or - /// past what was posted: the session ends. - fn post(session: &mut Served, c: Completion) { - match session.rings.1.push(session.region.words(), c.encode()) { - Ok(true) => session.posted = true, - Ok(false) | Err(_) => session.closing = true, - } - } - - /// Hand one device answer to the session it is for. - fn deliver(&mut self, done: Done) { - let Owner::Session { session, tag, .. } = done.owner else { - panic!("diskserver: the device answered a command of this driver's own while serving"); - }; - let s = self.sessions.get_mut(&session).expect("diskserver: an answer for a session that ended"); - if let Some(c) = s.state.complete(tag, done.ok, &mut self.holds, self.losses) { - if !done.ok { - println!("diskserver: session {session}: request {tag} failed with status {:#x}", done.status); +impl Service { + fn serve(&mut self, clients: &toyos::port::Acceptor, drivers: &toyos::port::Acceptor) -> ! { + let poller = Poller::new(2 + MAX_PENDING as u32 + MAX_DRIVERS as u32); + let mut pending: Vec = Vec::new(); + let mut ready: Vec = Vec::new(); + let mut was_settled = false; + loop { + if pending.len() < MAX_PENDING && self.waiting.len() < MAX_WAITING { + poller.watch(clients, READABLE, TOKEN_ACCEPT); } - if c.status == toyos_blockring::Status::Lost { - println!( - "diskserver: session {session}: a flush found writes of its own the device lost \ - since it acknowledged them; the client is told Lost" - ); + if self.drivers.len() < MAX_DRIVERS { + poller.watch(drivers, READABLE, TOKEN_DRIVERS); } - Self::post(s, c); - } - } - - /// Take what `id`'s client has published, while the device and the - /// session's completion ring have room for it. - fn pull(&mut self, id: u64) { - let s = self.sessions.get_mut(&id).expect("a live session"); - loop { - if s.closing { - break; + for p in &pending { + poller.watch(&p.conn, READABLE, TOKEN_PENDING + u64::from(p.conn.as_handle().0)); } - let page = s.region.words(); - // Room for every answer: what is on the device, and what is posted - // and not yet read, never exceeds the completion ring. - let Ok(space) = s.rings.1.space(page) else { - s.closing = true; - break; - }; - let unread = (DEPTH - space) as usize; - if s.state.inflight().len() + unread >= DEPTH as usize || !self.ctrl.up().has_room() { - break; + for (id, d) in &self.drivers { + poller.watch(&d.conn, READABLE, TOKEN_DRIVER + id); } - let words = match s.rings.0.pop(page) { - Ok(Some(words)) => words, - Ok(None) => break, - Err(_) => { - s.closing = true; - break; - } - }; - s.requests += 1; - match s.state.take(words) { - Taken::Answer(c) => Self::post(s, c), - Taken::Issue(req) => { - let write = matches!(req.op, Op::Write { .. }); - let owner = Owner::Session { session: id, tag: req.tag, write }; - match req.op { - Op::Read { run, lba } | Op::Write { run, lba } => { - let at = s.device_addr + run.span().offset as u64; - let block = s.state.first() + lba; - self.ctrl.up().submit_io(write, block, run.count(), at, owner); - } - Op::Flush if self.ctrl.up().vwc => self.ctrl.up().submit_flush(owner), - // No volatile cache: every write answered is on the - // medium already. - Op::Flush => { - let c = s.state.complete(req.tag, true, &mut self.holds, self.losses); - Self::post(s, c.expect("just taken")); - } + let now = Instant::now(); + let deadlines = pending.iter().map(|p| p.since + HANDSHAKE_TIMEOUT); + let deadlines = deadlines.chain(self.waiting.iter().map(|w| w.since + DRIVER_BOUND)); + let deadlines = deadlines.chain(self.drivers.values().filter(|d| d.label.is_none()).map(|d| d.since + HANDSHAKE_TIMEOUT)); + let timeout = deadlines.min().map_or(u64::MAX, |at| at.saturating_duration_since(now).as_nanos() as u64); + ready.clear(); + poller.wait(1, timeout, |token| ready.push(token)); + + let now = Instant::now(); + if ready.contains(&TOKEN_DRIVERS) { + match drivers.accept() { + Ok(conn) => { + let id = self.next_driver; + self.next_driver += 1; + let d = Driver { conn, rx: ipc::FrameRx::new(), label: None, settled: false, disks: BTreeMap::new(), since: now }; + self.drivers.insert(id, d); } + Err(why) => println!("diskserver: the kernel refused a driver's connection: {why:?}"), + } + } + let ids: Vec = self.drivers.keys().copied().collect(); + for id in ids { + let unnamed = self.drivers[&id].label.is_none() && now >= self.drivers[&id].since + HANDSHAKE_TIMEOUT; + if unnamed { + self.drop_driver(id, "it never said its name"); + } else if ready.contains(&(TOKEN_DRIVER + id)) { + self.hear_driver(id); } } - } - s.rings.0.release(s.region.words()); - } - /// Publish what each session was answered, and ring its doorbell. - fn publish(&mut self) { - for s in self.sessions.values_mut() { - if !s.posted { - continue; + pending.retain(|p| now < p.since + HANDSHAKE_TIMEOUT); + if ready.contains(&TOKEN_ACCEPT) { + match clients.accept() { + Ok(conn) => pending.push(Pending { conn, rx: ipc::FrameRx::new(), since: now }), + Err(why) => panic!("diskserver: its own acceptor refused an accept: {why:?}"), + } } - s.posted = false; - if s.rings.1.publish(s.region.words()) { - match s.conn.write_nonblock(&[1]) { - Ok(_) | Err(SyscallError::WouldBlock) => {} - Err(_) => s.closing = true, + let mut i = 0; + while i < pending.len() { + let token = TOKEN_PENDING + u64::from(pending[i].conn.as_handle().0); + if !ready.contains(&token) { + i += 1; + continue; + } + let step = { + let p = &mut pending[i]; + p.rx.pump(&p.conn) + }; + match step { + RxStep::Idle => i += 1, + RxStep::Eof | RxStep::Malformed => { + pending.remove(i); + } + RxStep::Frame { msg_type, payload_len } => { + let p = pending.remove(i); + self.asked(clients, p, msg_type, payload_len, now); + } } } - } - } - /// Consume session `id`'s doorbell bytes; once its client has hung up, - /// the session is closing. - fn hear(&mut self, id: u64) { - let s = self.sessions.get_mut(&id).expect("a session"); - let mut sink = [0u8; 64]; - while !s.closing { - match s.conn.read_nonblock(&mut sink) { - Ok(0) => s.closing = true, - Ok(_) => {} - Err(SyscallError::WouldBlock) => return, - Err(_) => s.closing = true, + if self.known.settled() && !was_settled { + println!("diskserver: every disk driver has settled, {} partition(s) offered", self.known.offered.len()); } + was_settled = self.known.settled(); + self.answer_waiting(now); } } - /// End every session that is closing and has nothing on the device: its - /// region leaves the controller's domain, and its partition is free. - fn retire(&mut self) { - let done: Vec = self - .sessions - .iter() - .filter(|(_, s)| s.closing && s.state.inflight().len() == 0) - .map(|(id, _)| *id) - .collect(); - for id in done { - let s = self.sessions.remove(&id).expect("just listed"); - let ctrl = self.ctrl.up(); - if let Err(why) = ctrl.claim().dma_unmap(s.device_addr) { - panic!("diskserver: the kernel would not take session {id}'s region back: {why:?}"); + /// Everything driver `id` sent; a frame this protocol does not make + /// ends it. + fn hear_driver(&mut self, id: u64) { + loop { + let d = self.drivers.get_mut(&id).expect("a live driver"); + let (msg_type, payload) = match d.rx.pump(&d.conn) { + RxStep::Idle => return, + RxStep::Eof => return self.drop_driver(id, "its connection ended"), + RxStep::Malformed => return self.drop_driver(id, "a frame this protocol does not make"), + RxStep::Frame { msg_type, payload_len } => (msg_type, d.rx.payload(payload_len).to_vec()), + }; + if let Err(why) = self.heard(id, msg_type, &payload) { + return self.drop_driver(id, why); } - self.holds.release(s.state.first()); - println!( - "diskserver: session {id} on {} closed after {} requests; the device has held at most {} \ - commands at once, issued per queue {}", - guid_text(s.unique), - s.requests, - ctrl.peak, - ctrl.spread() - ); } } - /// Reset the controller under every command it holds. - fn reset(&mut self) { - println!( - "diskserver: a command went unanswered for {COMMAND_SILENCE:?}; resetting the controller" - ); - let mut done = Vec::new(); - let aborted = self - .ctrl - .up() - .reset(&mut done) - .unwrap_or_else(|why| panic!("diskserver: the controller did not come back from its reset: {why}")); - for d in done { - self.deliver(d); - } - self.losses += 1; - let mut answered = 0; - for s in self.sessions.values_mut() { - for c in s.state.abort_all() { - answered += 1; - Self::post(s, c); + /// One frame of driver `id`'s, or why it ends the driver. + fn heard(&mut self, id: u64, msg_type: u32, payload: &[u8]) -> Result<(), &'static str> { + let d = self.drivers.get_mut(&id).expect("a live driver"); + let Some(label) = d.label.clone() else { + if msg_type != disk::MSG_DRIVER { + return Err("a frame before its name"); + } + let label = disk::label(payload).ok_or("a name that is not one")?; + println!("diskserver: disk driver {label} connected"); + d.label = Some(label); + return Ok(()); + }; + match msg_type { + disk::MSG_OFFER => { + let offer = Offer::decode(payload).ok_or("an offer that is not one")?; + if d.disks.contains_key(&offer.disk) { + return Err("a disk it already offered"); + } + self.offer(id, offer); } + disk::MSG_SETTLED => { + let settled = Settled::decode(payload).ok_or("a settling that is not one")?; + if std::mem::replace(&mut d.settled, true) { + return Err("a second settling"); + } + let fate = match settled { + Settled::Offered => Fate::Offered, + Settled::Unusable => { + println!("diskserver: disk driver {label} cannot use its controller; nothing it would carry is served"); + Fate::Unusable + } + }; + println!("diskserver: disk driver {label} settled with {} disk(s)", d.disks.len()); + self.known.fates.insert(label, fate); + } + disk::MSG_GONE => { + let ours = disk::disk(payload).and_then(|theirs| d.disks.remove(&theirs)); + let ours = ours.ok_or("the end of a disk it never offered")?; + self.known.offered.retain(|o| o.disk != ours); + println!("diskserver: disk {ours} left"); + } + disk::MSG_DRIVER => return Err("its name a second time"), + _ => return Err("a frame this protocol does not make"), } - println!( - "diskserver: controller reset; {} commands it held were answered not done ({answered} of \ - them sessions'), and the loss count is {}", - aborted.len(), - self.losses - ); + Ok(()) } -} - -/// The PCI function this process was endowed. -fn claim() -> Option { - let label = Endowments::get() - .labels() - .find(|l| l.starts_with(DEV_PREFIX) && l[DEV_PREFIX.len()..].starts_with("pci:")) - .map(str::to_string)?; - Some(Endowments::get().take(&label).expect("diskserver: the claim its label names")) -} -fn main() { - let args: Vec = std::env::args().collect(); - let refused = args.iter().position(|a| a == CLAIM_REFUSED).map(|at| { - args.get(at + 1).unwrap_or_else(|| panic!("diskserver: {CLAIM_REFUSED} takes the device its claim was refused on")) - }); - let acceptor = endow::acceptor(PORT).unwrap_or_else(|| panic!("diskserver: started serving no `{PORT}` port")); - if let Some(name) = refused { + fn offer(&mut self, id: u64, offer: Offer) { + let ours = self.next_disk; + self.next_disk += 1; + let d = self.drivers.get_mut(&id).expect("a live driver"); + d.disks.insert(offer.disk, ours); println!( - "diskserver: NOT SERVING — {name} is on this machine and the kernel refused this service its \ - claim; every partition on it is refused" + "diskserver: disk {ours}, {}'s disk {}, offers {} partition(s)", + d.label.as_deref().unwrap_or("?"), + offer.disk, + offer.parts.len() ); - serve(&mut Service::new(Drive::ClaimRefused), &acceptor); - } - let Some(dev) = claim() else { - println!("diskserver: no NVMe controller this row names is on this machine; serving no partition"); - serve(&mut Service::new(Drive::Absent), &acceptor); - }; - let mut ctrl = match Controller::open(dev) { - Ok(ctrl) => ctrl, - Err(why) => { - println!("diskserver: NOT SERVING — {why}; serving no partition"); - serve(&mut Service::new(Drive::Unusable), &acceptor); - } - }; - println!( - "diskserver: NVMe up: {} I/O queues of {} commands, volatile write cache {}, {}-byte sectors, \ - {} sectors", - ctrl.queues(), - diskserver::nvme::COMMANDS_PER_QUEUE, - if ctrl.vwc { "present, so a flush issues Flush" } else { "absent, so a flush issues nothing" }, - ctrl.lba_bytes, - ctrl.sectors - ); - let parts = read_table(&mut ctrl); - for part in &parts { - match &part.span { - Ok((first, blocks)) => { - println!("diskserver: partition {} at block {first}, {blocks} blocks", guid_text(part.unique)) + for part in &offer.parts { + if self.known.offered.iter().any(|o| o.part.unique == part.unique) { + println!("diskserver: partition {} is on two disks; neither is served", guid_text(part.unique)); } - Err(why) => println!("diskserver: partition {} is not served: {why}", guid_text(part.unique)), } + self.known.offered.extend(offer.parts.into_iter().map(|part| Offered { disk: ours, part })); } - serve(&mut Service::new(Drive::Up(ctrl, parts)), &acceptor); -} -fn serve(service: &mut Service, acceptor: &toyos::port::Acceptor) -> ! { - let poller = Poller::new(2 + MAX_PENDING as u32 + MAX_SESSIONS as u32); - let mut pending: Vec = Vec::new(); - let mut ready: Vec = Vec::new(); - let mut done: Vec = Vec::new(); - loop { - if let Drive::Up(ctrl, _) = &service.ctrl { - poller.watch(ctrl.claim(), READABLE, TOKEN_IRQ); + /// Driver `id` is gone, and every disk it offered with it; a driver whose + /// connection ended is awaited until a driver of its name settles again. + fn drop_driver(&mut self, id: u64, why: &str) { + let d = self.drivers.remove(&id).expect("a live driver"); + let label = d.label.unwrap_or_else(|| "an unnamed driver".into()); + println!("diskserver: dropping disk driver {label} — {why}; its {} disk(s) leave", d.disks.len()); + let gone: Vec = d.disks.values().copied().collect(); + self.known.offered.retain(|o| !gone.contains(&o.disk)); + if self.known.fates.contains_key(&label) { + self.known.fates.insert(label, Fate::Ended); } - if pending.len() < MAX_PENDING { - poller.watch(acceptor, READABLE, TOKEN_ACCEPT); - } - for p in &pending { - poller.watch(&p.conn, READABLE, TOKEN_PENDING + p.conn.as_handle().0 as u64); + } + + /// One client's first frame: a listing, or an open. + fn asked(&mut self, clients: &toyos::port::Acceptor, p: Pending, msg_type: u32, payload_len: usize, now: Instant) { + let grant = grant(clients, &p.conn); + if msg_type == wire::MSG_LIST && payload_len == 0 { + return self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::List, since: now }); } - for (id, s) in &service.sessions { - if !s.closing { - poller.watch(&s.conn, READABLE, TOKEN_SESSION + id); + let guid = wire::guid(p.rx.payload(payload_len)); + let handles = p.conn.recv_handles_exact::<1>(); + let (Some(guid), Some([region]), wire::MSG_OPEN) = (guid, handles, msg_type) else { + if let Some([region]) = handles { + toyos_abi::syscall::close(region); } - } - let now = Instant::now(); - let timeout = pending - .iter() - .map(|p| HANDSHAKE_TIMEOUT.saturating_sub(now.duration_since(p.since))) - .chain(service.ctrl.oldest().map(|at| COMMAND_SILENCE.saturating_sub(now.duration_since(at)))) - .min() - .map_or(u64::MAX, |left| left.as_nanos() as u64); - ready.clear(); - poller.wait(1, timeout, |token| ready.push(token)); - - if let Drive::Up(ctrl, _) = &mut service.ctrl { - if let Err(why) = ctrl.take_interrupt() { - panic!( - "diskserver: the claim refused its interrupt read ({why:?}): the unit refused this \ - controller an access, and the claim answers nothing now" - ); + return refuse(&p.conn, Refusal::Malformed); + }; + // SAFETY: the handle arrived with this frame and nothing else in this + // process names it. + let region = unsafe { OwnedHandle::from_raw(region) }; + self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::Open { guid, region }, since: now }); + } + + /// Answer every waiting question the drivers have now answered, or whose + /// wait is over. + fn answer_waiting(&mut self, now: Instant) { + let waiting = std::mem::take(&mut self.waiting); + for w in waiting { + let expired = now >= w.since + DRIVER_BOUND; + match w.ask { + Ask::List => match self.known.listing(w.grant, expired) { + None => self.waiting.push(w), + Some(answer) => { + self.waited(expired, "a listing"); + list(&w.conn, answer); + } + }, + Ask::Open { guid, region } => match self.known.open(guid, w.grant, expired) { + None => self.waiting.push(Waiting { ask: Ask::Open { guid, region }, ..w }), + Some(Err(why)) => { + self.waited(expired, "an open"); + println!("diskserver: an open of {} refused: {why:?}", guid_text(guid)); + refuse(&w.conn, why); + } + Some(Ok(offered)) => { + let writes = w.grant.expect("admitted through a grant").writes; + self.hand_over(offered, guid, writes, w.conn, region); + } + }, } - ctrl.reap(&mut done); - } - for d in done.drain(..) { - service.deliver(d); } - if service.ctrl.oldest().is_some_and(|at| at.elapsed() >= COMMAND_SILENCE) { - service.reset(); + } + + /// Say which drivers a question answered at its bound waited for. + fn waited(&self, expired: bool, what: &str) { + if !expired || self.known.settled() { + return; } + let started = self.known.expected.saturating_sub(self.known.fates.len()); + println!( + "diskserver: {what} waited {DRIVER_BOUND:?} and is answered with what is known; not settled: {:?}, and \ + {started} driver(s) started before this service never said who they are", + self.known.unsettled() + ); + } - let now = Instant::now(); - pending.retain(|p| now.duration_since(p.since) < HANDSHAKE_TIMEOUT); - if ready.contains(&TOKEN_ACCEPT) { - match acceptor.accept() { - Ok(conn) => pending.push(Pending { conn, rx: ipc::FrameRx::new(), since: now }), - Err(why) => panic!("diskserver: its own acceptor refused an accept: {why:?}"), - } + /// The session goes to the driver whose disk `offered` is: the client's + /// connection and region, then the frame naming them. + fn hand_over(&mut self, offered: Offered, guid: [u8; 16], writes: bool, conn: Connection, region: OwnedHandle) { + let Some((&id, d)) = self.drivers.iter().find(|(_, d)| d.disks.values().any(|&ours| ours == offered.disk)) else { + unreachable!("diskserver: disk {} is offered and no driver has it", offered.disk) + }; + let theirs = *d.disks.iter().find(|(_, ours)| **ours == offered.disk).expect("just found").0; + let session = disk::Session { disk: theirs, unique: guid, writes }; + // The client hears nothing from here: its open is the driver's to + // answer, and a refused move closes the client's connection. + if let Err(why) = d.conn.send_handles([conn.into_owned(), region]) { + return println!("diskserver: the kernel would not move a session to {:?}: {why:?}", d.label); } - let mut i = 0; - while i < pending.len() { - let token = TOKEN_PENDING + pending[i].conn.as_handle().0 as u64; - if !ready.contains(&token) { - i += 1; - continue; - } - let step = { - let p = &mut pending[i]; - p.rx.pump(&p.conn) - }; - match step { - RxStep::Idle => i += 1, - RxStep::Eof | RxStep::Malformed => { - pending.remove(i); - } - RxStep::Frame { msg_type, payload_len } => { - let p = pending.remove(i); - handshake(service, acceptor, p, msg_type, payload_len); - } - } + // Its handles are in the driver's queue ahead of any later frame's, + // so a frame the driver will not take ends the driver. + if d.conn.try_send_bytes(disk::MSG_SESSION, &session.encode()).is_err() { + return self.drop_driver(id, "it would not take a session"); } + } +} - let ids: Vec = service.sessions.keys().copied().collect(); - for id in ids { - if ready.contains(&(TOKEN_SESSION + id)) { - service.hear(id); +fn refuse(conn: &Connection, why: Refusal) { + let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); +} + +/// One frame, and the connection is done with: a listing larger than a frame +/// is one this service lists no part of rather than half of. +fn list(conn: &Connection, answer: Result, Refusal>) { + match answer { + Err(why) => refuse(conn, why), + Ok(listed) => { + let bytes: Vec = listed.iter().flat_map(Listed::encode).collect(); + if conn.try_send_bytes(wire::MSG_LISTED, &bytes).is_err() { + refuse(conn, Refusal::Exhausted); } - service.pull(id); } - service.publish(); - service.retire(); } } @@ -703,81 +521,11 @@ fn grant(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { } } -/// Answer one connection's first frame: a listing, or an open. -fn handshake(service: &mut Service, acceptor: &toyos::port::Acceptor, p: Pending, msg_type: u32, payload_len: usize) { - let refuse = |conn: &Connection, why: Refusal| { - let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); - }; - let grant = grant(acceptor, &p.conn); - if msg_type == wire::MSG_LIST && payload_len == 0 { - match service.listing(grant) { - Err(why) => refuse(&p.conn, why), - // One frame, and the connection is done with: a table larger than - // a frame is one this service lists no part of rather than half of. - Ok(listing) => { - if p.conn.try_send_bytes(wire::MSG_LISTED, &listing).is_err() { - refuse(&p.conn, Refusal::Exhausted); - } - } - } - return; - } - let guid = wire::guid(p.rx.payload(payload_len)); - let handles = p.conn.recv_handles_exact::<1>(); - let (Some(guid), Some([region]), wire::MSG_OPEN) = (guid, handles, msg_type) else { - if let Some([region]) = handles { - toyos_abi::syscall::close(region); - } - refuse(&p.conn, Refusal::Malformed); - return; - }; - match service.open(guid, region, grant) { - Err(why) => { - println!("diskserver: an open of {} refused: {why:?}", guid_text(guid)); - refuse(&p.conn, why); - } - Ok(opening) => { - let (rings, opened) = Opened::over(opening.region.words(), opening.blocks, guid); - if p.conn.try_send_bytes(wire::MSG_OPENED, &opened.encode()).is_err() { - service.abandon(opening); - return; - } - let writes = opening.writes; - let id = service.admit(opening, rings, opened, p.conn); - let access = if writes { "" } else { ", read-only" }; - println!("diskserver: session {id} opened {} ({} blocks{access})", guid_text(guid), opened.blocks()); - } - } -} - #[cfg(test)] mod tests { use super::*; use toyos_blockring::wire::Scope; - /// A controller this service cannot use, or one whose claim was refused, - /// is a disk there and never a machine without one: its listing and its - /// open are refused by that word, where an absent controller's listing is - /// empty and its open `NotFound`. - #[test] - fn an_unusable_or_unclaimed_controller_is_refused_and_an_absent_one_lists_nothing() { - let guid = [7; 16]; - let grant = Some(Grant { scope: Scope::Unique(guid), writes: true }); - let mut unusable = Service::new(Drive::Unusable); - assert_eq!(unusable.listing(grant), Err(Refusal::Unusable)); - assert_eq!(unusable.place(guid, grant), Err(Refusal::Unusable)); - let mut unclaimed = Service::new(Drive::ClaimRefused); - assert_eq!(unclaimed.listing(grant), Err(Refusal::ClaimRefused)); - assert_eq!(unclaimed.place(guid, grant), Err(Refusal::ClaimRefused)); - let mut absent = Service::new(Drive::Absent); - assert_eq!(absent.listing(grant), Ok(Vec::new())); - assert_eq!(absent.place(guid, grant), Err(Refusal::NotFound)); - for mut service in [unusable, unclaimed, absent] { - assert_eq!(service.listing(None), Err(Refusal::NotGranted)); - assert_eq!(service.place(guid, None), Err(Refusal::NotGranted)); - } - } - const LOG: [u8; 16] = [1; 16]; const BOOT: [u8; 16] = [2; 16]; const DATA: [u8; 16] = [3; 16]; @@ -787,49 +535,121 @@ mod tests { const DATA_KIND: [u8; 16] = [0x30; 16]; const ROOT_KIND: [u8; 16] = [0x40; 16]; - fn table() -> Vec { - [(LOG, LOG_KIND), (BOOT, BOOT_KIND), (DATA, DATA_KIND), (ROOT, ROOT_KIND)] - .into_iter() - .enumerate() - .map(|(i, (unique, kind))| Part { unique, kind, span: Ok((i as u64 * 100, 100)) }) - .collect() - } - fn unique(guid: [u8; 16]) -> Option { Some(Grant { scope: Scope::Unique(guid), writes: true }) } - /// A unique grant opens its one partition and is refused every other of - /// the same table by name, as is a connection with no grant; a kind grant - /// opens every partition of its type and no other. + fn part(unique: [u8; 16], kind: [u8; 16]) -> Part { + Part { unique, kind, blocks: Some(100) } + } + + /// One settled driver offering LOG, BOOT and ROOT on disk 0, and another + /// offering DATA on disk 1. + fn two_disks() -> Known { + let mut known = Known { expected: 2, ..Known::default() }; + known.fates.insert("stick".into(), Fate::Offered); + known.fates.insert("nvme".into(), Fate::Offered); + for p in [part(LOG, LOG_KIND), part(BOOT, BOOT_KIND), part(ROOT, ROOT_KIND)] { + known.offered.push(Offered { disk: 0, part: p }); + } + known.offered.push(Offered { disk: 1, part: part(DATA, DATA_KIND) }); + known + } + + /// A unique grant opens its one partition on whichever disk carries it, + /// and is refused every other by name, as is a connection with no grant; + /// a kind grant opens every partition of its type and no other. #[test] fn an_open_reaches_only_what_its_grant_admits() { - let parts = table(); - assert_eq!(admitted(Ok(&parts), LOG, unique(LOG)), Ok((0, 100))); + let known = two_disks(); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Ok(Offered { disk: 0, part: part(LOG, LOG_KIND) }))); for other in [BOOT, DATA, ROOT] { - assert_eq!(admitted(Ok(&parts), other, unique(LOG)), Err(Refusal::NotGranted)); - assert_eq!(admitted(Ok(&parts), other, None), Err(Refusal::NotGranted)); + assert_eq!(known.open(other, unique(LOG), false), Some(Err(Refusal::NotGranted))); + assert_eq!(known.open(other, None, false), Some(Err(Refusal::NotGranted))); } let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); - assert_eq!(admitted(Ok(&parts), DATA, data), Ok((200, 100))); - assert_eq!(admitted(Ok(&parts), LOG, data), Err(Refusal::NotGranted)); - // Nothing of a GUID the table does not carry, but to the grant that names it. - assert_eq!(admitted(Ok(&parts), [9; 16], data), Err(Refusal::NotGranted)); - assert_eq!(admitted(Ok(&parts), [9; 16], unique([9; 16])), Err(Refusal::NotFound)); + assert_eq!(known.open(DATA, data, false), Some(Ok(Offered { disk: 1, part: part(DATA, DATA_KIND) }))); + assert_eq!(known.open(LOG, data, false), Some(Err(Refusal::NotGranted))); + assert_eq!(known.open([9; 16], data, false), Some(Err(Refusal::NotGranted)), "nothing of a GUID no disk offers"); + assert_eq!(known.open([9; 16], unique([9; 16]), false), Some(Err(Refusal::NotFound))); } - /// A listing names exactly what its grant admits, and nothing to a - /// connection with none. + /// A listing names exactly what its grant admits across every disk, and + /// nothing to a connection with none. #[test] fn a_listing_names_only_what_its_grant_admits() { - let parts = table(); - let decoded = |bytes: Vec| -> Vec<[u8; 16]> { - wire::Listed::decode_all(&bytes).expect("whole entries").map(|l| l.unique).collect() + let known = two_disks(); + let uniques = |answer: Option, Refusal>>| { + answer.expect("answered").map(|all| all.iter().map(|l| (l.disk, l.unique)).collect::>()) }; - assert_eq!(listed(Ok(&parts), unique(BOOT)).map(decoded), Ok(vec![BOOT])); - let roots = Some(Grant { scope: Scope::Kind(ROOT_KIND), writes: false }); - assert_eq!(listed(Ok(&parts), roots).map(decoded), Ok(vec![ROOT])); - assert_eq!(listed(Ok(&parts), unique([9; 16])).map(decoded), Ok(vec![])); - assert_eq!(listed(Ok(&parts), None), Err(Refusal::NotGranted)); + assert_eq!(uniques(known.listing(unique(BOOT), false)), Ok(vec![(0, BOOT)])); + let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: false }); + assert_eq!(uniques(known.listing(data, false)), Ok(vec![(1, DATA)])); + assert_eq!(uniques(known.listing(unique([9; 16]), false)), Ok(vec![])); + assert_eq!(known.listing(None, false), Some(Err(Refusal::NotGranted))); + } + + /// Until every driver started at boot has settled, a listing and an open + /// of what no disk offers wait; an open of what is offered is answered at + /// once. At the bound each is answered with what is known, and a missing + /// driver is never taken for a machine without the partition. + #[test] + fn what_no_disk_offers_waits_for_every_driver_started_at_boot() { + let mut known = Known { expected: 2, ..Known::default() }; + known.fates.insert("nvme".into(), Fate::Offered); + known.offered.push(Offered { disk: 0, part: part(DATA, DATA_KIND) }); + let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); + assert_eq!(known.listing(data, false), None, "the stick's driver has not settled"); + assert_eq!(known.open(LOG, unique(LOG), false), None); + assert!(known.open(DATA, data, false).is_some_and(|r| r.is_ok()), "offered: answered at once"); + assert_eq!(known.open(LOG, unique(LOG), true), Some(Err(Refusal::Unusable)), "the bound passed, a driver missing"); + assert!(known.listing(data, true).is_some_and(|r| r.is_ok_and(|l| l.len() == 1))); + let log = Some(Grant { scope: Scope::Kind(LOG_KIND), writes: true }); + assert_eq!(known.listing(log, true), Some(Err(Refusal::Unusable)), "nothing listed, a driver missing"); + + known.fates.insert("stick".into(), Fate::Offered); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::NotFound)), "every driver said"); + assert_eq!(known.listing(log, false), Some(Ok(vec![]))); + } + + /// A driver that ended is awaited until one of its name settles again, + /// as a file server reopening after a driver's restart needs. + #[test] + fn a_driver_that_ended_is_awaited_until_it_settles_again() { + let mut known = two_disks(); + known.fates.insert("stick".into(), Fate::Ended); + known.offered.retain(|o| o.disk != 0); + assert_eq!(known.open(LOG, unique(LOG), false), None); + known.fates.insert("stick".into(), Fate::Offered); + known.offered.push(Offered { disk: 2, part: part(LOG, LOG_KIND) }); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Ok(Offered { disk: 2, part: part(LOG, LOG_KIND) }))); + } + + /// A controller its driver cannot use, or whose claim was refused, is a + /// disk there and never a machine without one. + #[test] + fn an_unusable_or_unclaimed_controller_is_refused_and_an_absent_one_lists_nothing() { + let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); + let mut unusable = Known { expected: 1, ..Known::default() }; + unusable.fates.insert("nvme".into(), Fate::Unusable); + assert_eq!(unusable.listing(data, false), Some(Err(Refusal::Unusable))); + assert_eq!(unusable.open(DATA, unique(DATA), false), Some(Err(Refusal::Unusable))); + let unclaimed = Known { undriven: vec!["pci:01:08:02".into()], ..Known::default() }; + assert_eq!(unclaimed.listing(data, false), Some(Err(Refusal::ClaimRefused))); + assert_eq!(unclaimed.open(DATA, unique(DATA), false), Some(Err(Refusal::ClaimRefused))); + let absent = Known::default(); + assert_eq!(absent.listing(data, false), Some(Ok(vec![]))); + assert_eq!(absent.open(DATA, unique(DATA), false), Some(Err(Refusal::NotFound))); + } + + /// A partition two disks offer, or one its driver will not serve, is + /// listed and refused `Unusable`. + #[test] + fn a_partition_on_two_disks_or_unserved_is_unusable() { + let mut known = two_disks(); + known.offered.push(Offered { disk: 1, part: part(LOG, LOG_KIND) }); + known.offered.push(Offered { disk: 1, part: Part { unique: [7; 16], kind: LOG_KIND, blocks: None } }); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::Unusable))); + assert_eq!(known.open([7; 16], unique([7; 16]), false), Some(Err(Refusal::Unusable))); } } diff --git a/system/diskserver/src/session.rs b/system/diskserver/src/session.rs index 8122bc3c6c1..314cfc263a8 100644 --- a/system/diskserver/src/session.rs +++ b/system/diskserver/src/session.rs @@ -27,7 +27,7 @@ use toyos_blockring::layout::{self, ClientRings, ARENA, MAX_REQUEST_BLOCKS}; use toyos_blockring::wire::{self, Opened, Refusal}; use toyos_blockring::{Run, BLOCK_BYTES}; -use crate::region::Region; +use toyos_blockserve::region::Region; /// Why a session could not be opened or go on. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/system/nvme/Cargo.toml b/system/nvme/Cargo.toml new file mode 100644 index 00000000000..b4946fde522 --- /dev/null +++ b/system/nvme/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "nvme" +description = "One NVMe controller driven from userland, its namespace offered to the block service as a disk whose sessions it serves itself." +edition = "2024" +license = "MIT OR Apache-2.0" + +[dependencies] +toyos-abi = { path = "../../sdk/abi" } +toyos = { path = "../../sdk/toyos" } +toyos-blockring = { path = "../../lib/blockring" } +toyos-blockserve = { path = "../../lib/blockserve" } +toyos-gpt = { path = "../../lib/gpt" } + +[package.metadata.toyos.host] +exempt.owns = "the NVMe controller ToyOS claims for it" diff --git a/system/nvme/src/main.rs b/system/nvme/src/main.rs new file mode 100644 index 00000000000..57ac2f44fb3 --- /dev/null +++ b/system/nvme/src/main.rs @@ -0,0 +1,226 @@ +//! nvme: one NVMe controller, driven from userland, its one namespace offered +//! to the block service as a disk. +//! +//! **It holds a claim on one PCI function and a connector to the block +//! service's `disk` port, and nothing else of the machine.** The supervisor +//! starts one per function its `drives` row names; the kernel keeps config +//! space, the interrupt vector and the function's address space, and this +//! driver never names an address the kernel did not hand it ([`nvme`]). +//! +//! **It reads its own partition table** and offers the disk with every +//! partition it names; the service hands it each session opened on one, +//! which it serves itself out of the client's region (`toyos_blockserve`). +//! A controller it cannot use is said once and offered as such, so a listing +//! says the disk failed rather than that there is none, and it serves +//! nothing. + +use std::time::{Duration, Instant}; + +use toyos::endow::Endowments; +use toyos::poller::Poller; +use toyos::PciDev; +use toyos_abi::syscall::{SyscallError, DEV_PREFIX}; +use toyos_blockring::disk::Settled; +use toyos_blockring::BLOCK_BYTES; +use toyos_blockserve::link::Link; +use toyos_blockserve::sessions::MAX_SESSIONS; +use toyos_blockserve::{Device, Sessions}; + +mod nvme; + +use nvme::{Controller, Done, Owner}; + +/// How long a command may go unanswered before the controller is reset to +/// take it back. Policy: NVMe defines no per-command bound, and a flush of a +/// large cache is the slowest thing a healthy drive does. +const COMMAND_SILENCE: Duration = Duration::from_secs(10); + +/// The one disk a controller's first namespace is. +const DISK: u32 = 0; + +const TOKEN_IRQ: u64 = 0; +const TOKEN_LINK: u64 = 1; +const TOKEN_SESSION: u64 = 0x1_0000; + +/// The controller as `toyos_gpt` reads it: its own sectors, served a 4 KiB +/// block at a time, and floored to whole blocks. +struct Sectors<'a> { + ctrl: &'a mut Controller, + block: Vec, +} + +impl toyos_gpt::Sectors for Sectors<'_> { + fn lba_bytes(&self) -> u32 { + self.ctrl.lba_bytes + } + fn lba_count(&self) -> u64 { + let per = BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64; + self.ctrl.sectors / per * per + } + fn lba_count_granularity(&self) -> core::num::NonZeroU64 { + core::num::NonZeroU64::new(BLOCK_BYTES as u64 / self.ctrl.lba_bytes as u64).expect("at least one") + } + fn read_lba(&mut self, lba: u64, buf: &mut [u8]) -> bool { + let lba_bytes = self.ctrl.lba_bytes as u64; + let at = lba * lba_bytes; + if !self.ctrl.read_block(at / BLOCK_BYTES as u64, &mut self.block) { + return false; + } + let off = (at % BLOCK_BYTES as u64) as usize; + buf.copy_from_slice(&self.block[off..off + lba_bytes as usize]); + true + } +} + +impl Device for Controller { + fn has_room(&self) -> bool { + Controller::has_room(self) + } + fn submit_io(&mut self, write: bool, block: u64, blocks: u32, at: u64, owner: toyos_blockserve::Owner) { + Controller::submit_io(self, write, block, blocks, at, Owner::Session(owner)); + } + fn submit_flush(&mut self, owner: toyos_blockserve::Owner) -> bool { + // No volatile cache: every write answered is on the medium already. + if self.vwc { + Controller::submit_flush(self, Owner::Session(owner)); + } + self.vwc + } + fn dma_map(&mut self, region: toyos::RawHandle) -> Result { + self.claim().dma_map(region).map(|m| m.device_addr) + } + fn dma_unmap(&mut self, device_addr: u64) -> Result<(), SyscallError> { + self.claim().dma_unmap(device_addr) + } +} + +/// The one `dev:pci:` claim this process was started with. +fn claim() -> PciDev { + let label = Endowments::get() + .labels() + .find(|l| l.strip_prefix(DEV_PREFIX).is_some_and(|name| name.starts_with("pci:"))) + .map(str::to_string) + .unwrap_or_else(|| panic!("nvme: started with no `{DEV_PREFIX}pci:` claim")); + Endowments::get().take(&label).expect("nvme: the claim its label names") +} + +fn main() { + let dev = claim(); + let info = dev.describe().unwrap_or_else(|why| panic!("nvme: the claim would not describe itself: {why:?}")); + let name = format!("nvme {:02x}:{:02x}.{}", info.bus, info.dev, info.func); + let mut link = Link::new(name.clone()); + let mut ctrl = match Controller::open(dev, &info) { + Ok(ctrl) => ctrl, + Err(why) => { + println!("{name}: NOT SERVING — {why}"); + link.settle(Settled::Unusable); + idle(&mut link); + } + }; + println!( + "{name}: up: {} I/O queues of {} commands, volatile write cache {}, {}-byte sectors, {} sectors", + ctrl.queues(), + nvme::COMMANDS_PER_QUEUE, + if ctrl.vwc { "present, so a flush issues Flush" } else { "absent, so a flush issues nothing" }, + ctrl.lba_bytes, + ctrl.sectors + ); + let parts = toyos_blockserve::table::read(&name, &mut Sectors { ctrl: &mut ctrl, block: vec![0u8; BLOCK_BYTES] }); + let mut disk = Sessions::new(name.clone(), parts); + link.offer(DISK, disk.offered()); + link.settle(Settled::Offered); + serve(&name, &mut ctrl, &mut disk, &mut link) +} + +/// A driver whose controller cannot be used: it keeps its link, so the +/// service goes on saying so, and opens nothing. +fn idle(link: &mut Link) -> ! { + let poller = Poller::new(1); + loop { + link.watch(&poller, TOKEN_LINK); + poller.wait(1, u64::MAX, |_| {}); + for handed in link.take() { + toyos_abi::syscall::close(handed.region); + drop(handed.conn); + } + } +} + +fn serve(name: &str, ctrl: &mut Controller, disk: &mut Sessions, link: &mut Link) -> ! { + let poller = Poller::new(2 + MAX_SESSIONS as u32); + let mut ready: Vec = Vec::new(); + let mut done: Vec = Vec::new(); + loop { + poller.watch(ctrl.claim(), toyos::poller::READABLE, TOKEN_IRQ); + link.watch(&poller, TOKEN_LINK); + disk.watch(&poller, TOKEN_SESSION); + let timeout = ctrl + .oldest() + .map_or(u64::MAX, |at| COMMAND_SILENCE.saturating_sub(at.elapsed()).as_nanos() as u64); + ready.clear(); + poller.wait(1, timeout, |token| ready.push(token)); + + if let Err(why) = ctrl.take_interrupt() { + panic!( + "{name}: the claim refused its interrupt read ({why:?}): the unit refused this controller an \ + access, and the claim answers nothing now" + ); + } + ctrl.reap(&mut done); + for d in done.drain(..) { + answer(name, disk, d); + } + if ctrl.oldest().is_some_and(|at: Instant| at.elapsed() >= COMMAND_SILENCE) { + reset(name, ctrl, disk); + } + + for handed in link.take() { + if handed.session.disk != DISK { + println!("{name}: the block service handed a session on disk {}, which this driver never offered", handed.session.disk); + toyos_abi::syscall::close(handed.region); + continue; + } + disk.open(ctrl, handed.conn, handed.region, handed.session.unique, handed.session.writes); + } + for id in disk.ids() { + if ready.contains(&(TOKEN_SESSION + id)) { + disk.hear(id); + } + } + disk.pull(ctrl); + disk.publish(); + if disk.retire(ctrl) > 0 { + println!( + "{name}: the device has held at most {} commands at once, issued per queue {}", + ctrl.peak, + ctrl.spread() + ); + } + } +} + +fn answer(name: &str, disk: &mut Sessions, done: Done) { + match done.owner { + Owner::Session(owner) => disk.answered(owner, done.ok, u32::from(done.status)), + Owner::Driver => panic!("{name}: the device answered a command of this driver's own while serving"), + } +} + +/// Reset the controller under every command it holds. +fn reset(name: &str, ctrl: &mut Controller, disk: &mut Sessions) { + println!("{name}: a command went unanswered for {COMMAND_SILENCE:?}; resetting the controller"); + let mut done = Vec::new(); + let aborted = ctrl + .reset(&mut done) + .unwrap_or_else(|why| panic!("{name}: the controller did not come back from its reset: {why}")); + for d in done { + answer(name, disk, d); + } + let answered = disk.reset(); + println!( + "{name}: controller reset; {} commands it held were answered not done ({answered} of them sessions'), \ + and the loss count is {}", + aborted.len(), + disk.losses() + ); +} diff --git a/system/diskserver/src/nvme.rs b/system/nvme/src/nvme.rs similarity index 95% rename from system/diskserver/src/nvme.rs rename to system/nvme/src/nvme.rs index 14efa834882..8a02edbfb1b 100644 --- a/system/diskserver/src/nvme.rs +++ b/system/nvme/src/nvme.rs @@ -1,4 +1,4 @@ -//! The NVMe controller diskserver drives, NVMe Base Specification 2.0 throughout: +//! One NVMe controller, NVMe Base Specification 2.0 throughout: //! §3.1.4 for the registers, §3.3 for the queues, §3.5.1 for bring-up, §5 for //! the admin commands, and the NVM Command Set 1.0 for read, write and flush. //! @@ -89,8 +89,7 @@ const _: () = assert!(IO_DEPTH as usize * SQE_BYTES <= PAGE); pub enum Owner { /// This driver's own, waited for in place. Driver, - /// A session's request `tag`, and whether it writes. - Session { session: u64, tag: u32, write: bool }, + Session(toyos_blockserve::Owner), } /// One command's answer. @@ -215,14 +214,14 @@ pub struct Controller { impl Controller { /// Bring the controller up (§3.5.1): disable, program the admin queue, /// enable, identify, and make the I/O queue pairs. - pub fn open(dev: PciDev) -> Result { + /// `info` is the claim's one description. + pub fn open(dev: PciDev, info: &toyos_abi::pci::PciFunctionInfo) -> Result { let class = dev .config_read(0x08, toyos_abi::syscall::RegWidth::U32) .map_err(|e| Refusal::Kernel("the class code", e))?; if class >> 8 != 0x01_08_02 { return Err(Refusal::NotNvme(class >> 8)); } - let info = dev.describe().map_err(|e| Refusal::Kernel("the claim's description", e))?; let bar_bytes = info.bar_bytes[0]; if bar_bytes <= DOORBELLS as u64 { return Err(Refusal::NoRegisters); @@ -439,7 +438,7 @@ impl Controller { Some(i) => &mut self.io[i], None => &mut self.admin, }; - let cid = q.free.pop().expect("diskserver: a submission with no free slot"); + let cid = q.free.pop().expect("nvme: a submission with no free slot"); q.slots[cid as usize] = Some(Slot { owner, since: Instant::now() }); let mut cmd = cmd; // Written into this slot's own list page before the entry that names @@ -484,7 +483,7 @@ impl Controller { if !*stray { *stray = true; println!( - "diskserver: queue {} answered identifier {cid}, which names no command \ + "nvme: queue {} answered identifier {cid}, which names no command \ waiting; dropped, and said once", q.qid ); @@ -520,7 +519,7 @@ impl Controller { self.poller.watch(&self.dev, READABLE, 0); self.poller.wait(1, bound.as_nanos() as u64, |_| {}); if let Err(why) = self.take_interrupt() { - panic!("diskserver: the claim refused its interrupt read ({why:?}): the unit refused this controller an access"); + panic!("nvme: the claim refused its interrupt read ({why:?}): the unit refused this controller an access"); } } @@ -555,7 +554,7 @@ impl Controller { /// # Panics /// With no room: the caller asks [`Self::has_room`] first. pub fn submit_io(&mut self, write: bool, block: u64, blocks: u32, at: u64, owner: Owner) { - let queue = self.roomiest().expect("diskserver: an I/O submission with no queue room"); + let queue = self.roomiest().expect("nvme: an I/O submission with no queue room"); let per = (toyos_blockring::BLOCK_BYTES as u32 / self.lba_bytes) as u64; let sectors = blocks as u64 * per; let pages = blocks as usize; @@ -588,7 +587,7 @@ impl Controller { /// Issue a Flush for `owner` (§7.1 of the NVM Command Set). pub fn submit_flush(&mut self, owner: Owner) { - let queue = self.roomiest().expect("diskserver: a flush with no queue room"); + let queue = self.roomiest().expect("nvme: a flush with no queue room"); self.submit(Some(queue), Command { opcode: IO_FLUSH, nsid: 1, ..Command::default() }, owner, |_| 0); self.issued[queue] += 1; self.peak = self.peak.max(self.busy()); @@ -598,7 +597,7 @@ impl Controller { /// own scratch page: before any session exists, for the partition table. pub fn read_block(&mut self, block: u64, out: &mut [u8]) -> bool { assert_eq!(out.len(), toyos_blockring::BLOCK_BYTES); - assert_eq!(self.busy(), 0, "diskserver: a waited read beside other commands"); + assert_eq!(self.busy(), 0, "nvme: a waited read beside other commands"); self.submit_io(false, block, 1, self.dma_addr + OFF_SCRATCH as u64, Owner::Driver); let start = Instant::now(); let mut done = Vec::new(); @@ -607,7 +606,7 @@ impl Controller { if let Some(d) = done.pop() { break d.ok; } - assert!(start.elapsed() < self.ready_bound * 4, "diskserver: a waited read was not answered"); + assert!(start.elapsed() < self.ready_bound * 4, "nvme: a waited read was not answered"); self.wait_interrupt(self.ready_bound); }; if ok { diff --git a/system/ps2server/src/main.rs b/system/ps2server/src/main.rs index a9645702d46..b4a2c44160b 100644 --- a/system/ps2server/src/main.rs +++ b/system/ps2server/src/main.rs @@ -28,7 +28,7 @@ use toyos::endow::{self, EndowError}; use toyos::ipc::Connection; use toyos::Device; use toyos_abi::syscall::IsaId; -use toyos_input::wire::{self, Record, POINTER_LEN}; +use toyos_input::wire::{self, Record, MAX_PAYLOAD}; use crate::bus::Controller; use crate::probe::Probed; @@ -79,7 +79,7 @@ fn connect() -> Connection { /// for again, and the new connection is a new source to it. fn deliver(owner: &mut Connection, records: &[Record]) { for record in records { - let mut payload = [0u8; POINTER_LEN]; + let mut payload = [0u8; MAX_PAYLOAD]; let (msg_type, len) = record.frame(&mut payload); if owner.send_bytes(msg_type, &payload[..len]).is_err() { *owner = connect(); diff --git a/system/ps2server/src/probe.rs b/system/ps2server/src/probe.rs index d7849fbc6bf..51cefaf4d04 100644 --- a/system/ps2server/src/probe.rs +++ b/system/ps2server/src/probe.rs @@ -371,7 +371,7 @@ mod tests { } fn typed_a(records: &[Record]) -> bool { - records.iter().any(|r| matches!(r, Record::Key(k) if k.keycode == 0x04 && k.pressed())) + records.iter().any(|r| matches!(r, Record::Key { key, .. } if key.keycode == 0x04 && key.pressed())) } fn line<'a>(said: &'a [String], start: &str) -> &'a str { diff --git a/system/ps2server/src/stream.rs b/system/ps2server/src/stream.rs index 713441eb307..713fb3c0ceb 100644 --- a/system/ps2server/src/stream.rs +++ b/system/ps2server/src/stream.rs @@ -24,9 +24,11 @@ //! `0xA7` alone (C 12), so its line never rises again, and its buttons are //! released by a report holding none. //! -//! **A loss is the device's that lost something.** The keyboard's overrun -//! code sends `LOST`, which releases keys alone; a pointer byte dropped sends -//! nothing, since every packet states the buttons whole. +//! **The pointer is `source` [`POINTER`] of the connection and the keyboard +//! [`KEYBOARD`]. A loss is the device's that lost something**: the keyboard's +//! overrun code sends the keyboard's `gone`, which releases its keys alone; a +//! pointer byte dropped sends nothing, since every packet states the buttons +//! whole. use toyos_abi::input::{RawKeyEvent, MOD_RELEASED}; use toyos_input::wire::{Motion, Record, Report}; @@ -34,6 +36,10 @@ use toyos_ps2::{KeyDecoder, KeyOutcome, MouseDecoder, MouseOutcome}; use crate::bus::*; +/// The two devices' numbers on the connection. +pub const POINTER: u16 = 0; +pub const KEYBOARD: u16 = 1; + /// The most bytes one wake reads before it calls the controller wedged. pub const BURST: usize = 32; @@ -204,12 +210,12 @@ impl Stream { KeyOutcome::Key { usage, pressed } => { self.counts.keys += 1; let modifiers = if pressed { 0 } else { MOD_RELEASED }; - out.push(Record::Key(RawKeyEvent { keycode: usage, modifiers })); + out.push(Record::Key { source: KEYBOARD, key: RawKeyEvent { keycode: usage, modifiers } }); } // The keyboard's overrun code: what it lost may be a release. KeyOutcome::Lost => { self.counts.overruns += 1; - out.push(Record::Lost); + out.push(Record::Gone { source: KEYBOARD }); } KeyOutcome::None => { let run = self.kbd_run.blame(byte, false); @@ -314,9 +320,8 @@ impl Stream { } } -/// The one pointer's report: it is source 0 of this connection. fn pointer(buttons: u8, dx: i16, dy: i16) -> Record { - Record::Pointer(Report { source: 0, buttons, scroll: 0, motion: Motion::Relative { dx, dy } }) + Record::Pointer(Report { source: POINTER, buttons, scroll: 0, motion: Motion::Relative { dx, dy } }) } #[cfg(test)] @@ -360,17 +365,17 @@ mod tests { let (records, _) = run(&mut fake, &mut stream, at + 10 * MS); let made = toyos_abi::input::RawKeyEvent { keycode: 0x04, modifiers: 0 }; let broke = toyos_abi::input::RawKeyEvent { keycode: 0x04, modifiers: MOD_RELEASED }; - assert_eq!(records, [Record::Key(made), Record::Key(broke)]); + assert_eq!(records, [Record::Key { source: KEYBOARD, key: made }, Record::Key { source: KEYBOARD, key: broke }]); } #[test] - fn an_overrun_code_sends_lost() { + fn an_overrun_code_sends_the_keyboards_gone() { let mut fake = Fake::qemu(); let mut stream = armed(&mut fake); let at = fake.now + MS; fake.kbd_sends(at, &[0x00]); let (records, _) = run(&mut fake, &mut stream, at + 10 * MS); - assert_eq!(records, [Record::Lost]); + assert_eq!(records, [Record::Gone { source: KEYBOARD }]); } /// `E0 2A` is the fake Shift around PrtScn, which maps to nothing: the run @@ -409,7 +414,7 @@ mod tests { fake.aux_sends(at, &[0xAA, 0x00]); fake.kbd_sends(at + MS, &[0x1C, 0x1C]); let (records, said) = run(&mut fake, &mut stream, at + 30 * MS); - let keys = records.iter().filter(|r| matches!(r, Record::Key(k) if k.keycode == 0x04)).count(); + let keys = records.iter().filter(|r| matches!(r, Record::Key { key, .. } if key.keycode == 0x04)).count(); assert_eq!(keys, 2, "{records:?}"); assert!(said.iter().any(|l| l == "ps2server: aux reset itself, reporting enabled again"), "{said:#?}"); } @@ -425,7 +430,7 @@ mod tests { fake.aux_sends(at + 5 * MS, &[0x00]); let (records, _) = run(&mut fake, &mut stream, at + 20 * MS); let shift = toyos_abi::input::RawKeyEvent { keycode: 0xE1, modifiers: 0 }; - assert_eq!(records, [Record::Key(shift)]); + assert_eq!(records, [Record::Key { source: KEYBOARD, key: shift }]); assert_eq!(stream.counts.discarded, 1, "the byte reached the framer, which dropped it"); } @@ -456,6 +461,6 @@ mod tests { fake.aux_sends(at, &[0x09, 0x05, 0x03]); let (records, _) = run(&mut fake, &mut stream, at + 100 * MS); let motion = Motion::Relative { dx: 5, dy: -3 }; - assert_eq!(records, [Record::Pointer(Report { source: 0, buttons: 1, scroll: 0, motion })]); + assert_eq!(records, [Record::Pointer(Report { source: POINTER, buttons: 1, scroll: 0, motion })]); } } diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index af41d793de3..67d5f8c8aa8 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -496,6 +496,7 @@ fn main() { drivers: drives::matchers(system), drives: Drives::default(), instances: Vec::new(), + undriven: Vec::new(), log, worker: Worker::start(), restarting: false, @@ -582,7 +583,12 @@ impl Worker { /// file server. Started before every other row, and never made a home, which /// would be a directory on the volume it serves. fn is_storage(program: &Program) -> bool { - !program.roles.is_empty() || is_block(program) + !program.roles.is_empty() || is_block(program) || is_disk_driver(program) +} + +/// A `drives` row that offers its disks to the block service. +fn is_disk_driver(program: &Program) -> bool { + !program.drives.is_empty() && program.receives.iter().any(|r| r == toyos_blockring::disk::PORT) } /// A row serving the block port. @@ -620,6 +626,9 @@ struct Supervisor<'a> { drives: Drives, /// The `drives` processes running. instances: Vec, + /// Every disk driver's function on this machine the boot started no + /// driver on, whose disks the block service cannot say are absent. + undriven: Vec, /// Where a service the supervisor (re)starts writes its stdout and stderr, whichever /// process ends up holding its row. log: Log, @@ -688,6 +697,7 @@ impl<'a> Service<'a> { &mut self, path: &str, owed: &[String], + block: Vec, system: &Manifest, syscap: &SysCap, connectors: &BTreeMap<&str, Connector>, @@ -706,7 +716,10 @@ impl<'a> Service<'a> { std::io::Error::other(format!("{path} no longer holds what was installed: {why}")) })?; } - let storage = storage_endowment(self.role, syscap, grants)?; + let mut storage = storage_endowment(self.role, syscap, grants)?; + if is_block(self.program) { + storage.args.extend(block); + } let ports = self.ports.borrow(); let served = Served::Keep { acceptors: &ports, owed }; let (child, devices) = start( @@ -861,6 +874,9 @@ impl<'a> Supervisor<'a> { self.services.push(service); } } + // The disk drivers first, so the block service is started knowing how + // many to wait for and a file server's disk is coming before it asks. + self.undriven = self.drive_where(is_disk_driver); let mut homes_made = false; for at in 0..self.services.len() { let program = self.services[at].program; @@ -869,9 +885,19 @@ impl<'a> Supervisor<'a> { homes_made = true; } self.make_home(program); + let block = self.block_args(); let service = &mut self.services[at]; - let started = - service.spawn(&program.path, &[], system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log); + let started = service.spawn( + &program.path, + &[], + block, + system, + self.syscap, + &self.connectors, + &self.grants, + &self.minted, + &mut self.log, + ); match started { Ok(_) => {} Err(StartError::Refused(why)) => { @@ -891,19 +917,28 @@ impl<'a> Supervisor<'a> { /// Start a `drives` process on each function the inventory says is owed /// one, and say which functions are given up on. fn drive(&mut self) { + self.drive_where(|_| true); + } + + /// [`Self::drive`], for the rows `which` picks; answers each function a + /// start was refused on, by its entry and address. + fn drive_where(&mut self, which: fn(&Program) -> bool) -> Vec { + let mut refused = Vec::new(); + let rows: Vec<(&'static Program, PciMatch)> = self.drivers.iter().filter(|(row, _)| which(row)).copied().collect(); let records = match pci_records(self.syscap) { Ok(records) => records, Err(why) => { say!("supervisor: {why}, so no `drives` process starts on it"); - return; + return refused; } }; - for act in self.drives.decide(&self.drivers, &records, Instant::now()) { + for act in self.drives.decide(&rows, &records, Instant::now()) { match act { Act::Start { row, entry, function } => match self.start_instance(row, entry, function) { Ok(child) => self.instances.push(Instance { row, function, child }), Err(why) => { say!("supervisor: {} was not started on PCI {}: {why}", row.name, pci_at(function.at)); + refused.push(format!("{entry} at PCI {}", pci_at(function.at))); self.drives.refused(function.arrival); } }, @@ -916,6 +951,19 @@ impl<'a> Supervisor<'a> { ), } } + refused + } + + /// What the block service is started with: how many disk drivers run, + /// which it waits for, and every disk driver's function the boot started + /// none on. + fn block_args(&self) -> Vec { + let running = self.instances.iter().filter(|i| is_disk_driver(i.row)).count(); + let mut args = vec!["--drivers".to_string(), running.to_string()]; + for undriven in &self.undriven { + args.extend(["--undriven".to_string(), undriven.clone()]); + } + args } /// One `drives` process of `row`, holding `function`'s claim under @@ -1119,7 +1167,7 @@ impl<'a> Supervisor<'a> { let tables = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_SLOTS))?; let boots = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_BOOT))?; let roots = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_ROOT))?; - let (table, table_guid, listed) = self.files("the slot table", move || served_slots(tables, boots, roots))??; + let (table, table_guid, listed) = self.files("the slot table", move || served_slots(running, tables, boots, roots))??; let Some(runs) = listed.iter().find(|p| p.unique_guid == running && p.type_guid == SLOT_KINDS.root) else { return Err("the ROOT this boot runs is on no disk the kernel or the block service serves".into()); }; @@ -1470,6 +1518,7 @@ impl<'a> Supervisor<'a> { let owed = self.services[index].devices.clone(); let program = self.services[index].program; self.make_home(program); + let block = self.block_args(); let service = &mut self.services[index]; swapped( &name, @@ -1485,8 +1534,17 @@ impl<'a> Supervisor<'a> { let _ = old.kill(); let _ = old.wait(); } - let started = - service.spawn(&path, &owed, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log); + let started = service.spawn( + &path, + &owed, + block, + self.system, + self.syscap, + &self.connectors, + &self.grants, + &self.minted, + &mut self.log, + ); match started { Ok(pid) => { swapped( @@ -1645,8 +1703,10 @@ impl<'a> Supervisor<'a> { AfterEnd::Restart => { let (path, owed, program) = (service.path.clone(), service.devices.clone(), service.program); self.make_home(program); + let block = self.block_args(); let service = &mut self.services[index]; - match service.spawn(&path, &owed, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log) { + let (system, syscap, connectors, grants) = (self.system, self.syscap, &self.connectors, &self.grants); + match service.spawn(&path, &owed, block, system, syscap, connectors, grants, &self.minted, &mut self.log) { Ok(new) => say!("supervisor: {label} (pid {pid}) ended; started again as pid {new}"), Err(e) => { service.ports.borrow_mut().clear(); @@ -1678,9 +1738,10 @@ impl<'a> Supervisor<'a> { fn restore(&mut self, index: usize, previous: &str, owed: &[String]) { let program = self.services[index].program; self.make_home(program); + let block = self.block_args(); let service = &mut self.services[index]; let name = service.program.name.clone(); - match service.spawn(previous, owed, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log) { + match service.spawn(previous, owed, block, self.system, self.syscap, &self.connectors, &self.grants, &self.minted, &mut self.log) { Ok(pid) => { service.swapping = false; swapped(&name, Word::Restored, &format!("{previous} as pid {pid}")); @@ -2368,6 +2429,7 @@ const SLOT_KINDS: toyos_update::slots::Kinds = /// service the supervisor restarts, from its loop alone, would wait in the /// port's queue for a process only the loop can start. fn served_slots( + running: [u8; 16], tables: Connector, boots: Connector, roots: Connector, @@ -2383,24 +2445,31 @@ fn served_slots( diskserver::list(&names(connector)?, toyos_blockring::PORT) .map_err(|why| format!("the block service would not list the slots' partitions: {why:?}")) }; + let root_parts = list(&roots)?; let table_parts = list(&tables)?; - let [table_part] = table_parts[..] else { - return Err(format!("the block service serves {} slot tables, and a grant needs one", table_parts.len())); + // The table is the one on the running ROOT's disk. + let disk = root_parts.iter().find(|p| p.unique == running).map(|p| p.disk); + let on_its_disk: Vec<_> = table_parts.iter().filter(|p| Some(p.disk) == disk).collect(); + let [table_part] = on_its_disk[..] else { + return Err(format!( + "the block service serves {} slot tables on the running ROOT's disk, and a grant needs one", + on_its_disk.len() + )); }; let session = diskserver::Session::open(names(&tables)?, toyos_blockring::PORT, table_part.unique) .map_err(|why| format!("the slot table would not open: {why:?}"))?; + let table_unique = table_part.unique; let mut disk = diskserver::disk::Served::new(session); let (table, _) = toyos_update::slots::read(|copies| disk.read(0, copies.as_flattened_mut())).map_err(|why| why.to_string())?; let mut listed = table_parts; listed.extend(list(&boots)?); - listed.extend(list(&roots)?); - // One block service is one disk. + listed.extend(root_parts); let listed = listed .into_iter() - .map(|p| toyos_update::slots::Listed { device: 0, type_guid: p.kind, unique_guid: p.unique }) + .map(|p| toyos_update::slots::Listed { device: p.disk, type_guid: p.kind, unique_guid: p.unique }) .collect(); - Ok((table, table_part.unique, listed)) + Ok((table, table_unique, listed)) } /// What the supervisor says about a device it could not mint a claim for. @@ -2632,12 +2701,6 @@ fn start<'a>( // sentence for all of them sends whoever reads the line looking in // the wrong place. say!("supervisor: {}: {}", program.name, missing(name, &why)); - // A block service is told, so the partitions on a controller the - // machine has are refused and never taken for none. - let absent = matches!(why, Missing::Unresolved(Unresolved::Absent) | Missing::Refused(SyscallError::NotFound)); - if !absent && is_block(program) { - command.args(["--claim-refused", name.as_str()]); - } } if !program.syscap.is_empty() { diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index 9b4ea8add99..e138f1acc16 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -152,7 +152,7 @@ pub fn iommu_virtio_platform() -> Result<(), String> { } /// The slot QEMU's `-device` order puts `tests/images/netcase.toml`'s NVMe controller on, -/// the one its diskserver row claims. +/// the one its nvme row drives. const NVME_AT: &str = "00:02.0"; /// The slot QEMU's `-device` order puts the virtio-sound function on, the one @@ -184,7 +184,9 @@ fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { log.must_say(¬_handed_over(SOUND_AT, NOT_REMAPPED))?; log.must_say("supervisor: soundserver: pci:04:01:00/1af4:1059 is on this machine and could not be handed over")?; log.must_say(¬_handed_over(NVME_AT, NOT_REMAPPED))?; - log.must_say("supervisor: diskserver: pci:01:08:02/1b36:0010 is on this machine and could not be handed over")?; + log.must_say(&format!( + "supervisor: nvme was not started on PCI {NVME_AT}: pci:01:08:02/1b36:0010 is on this machine and could not be handed over" + ))?; // And this machine handed *nothing* over, which is more than the claim's // own refusal says: with no unit there is no function any process could be // given an address space for. diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 323d866b1a5..173afd16c0a 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -996,7 +996,7 @@ enum Storage { Stick { nvme_bytes: u64 }, /// One NVMe disk and no stick: DATA, of this many bytes, and the image /// installed after it (`toyos_build::image::install`). The firmware boots - /// it, and after the loader every partition of it is `diskserver`'s. + /// it, and after the loader every partition of it is the block service's. Disk { data_bytes: u64 }, } @@ -2328,9 +2328,9 @@ fn qemu_command( // with no NVMe is a shape, and the argv is the only place it is visible: // no console line and no screendump can see a device that is absent. // - // Its MSI-X table in BAR 0, QEMU's default, which diskserver's claim maps + // Its MSI-X table in BAR 0, QEMU's default, which the nvme driver's claim maps // with the registers. One controller and one namespace whichever disk it - // is: diskserver reads namespace 1 of the one controller its row names. + // is: the nvme driver reads namespace 1 of the controller it drives. let namespace = match shape.storage { Storage::Stick { nvme_bytes: 0 } => None, Storage::Stick { .. } => { diff --git a/tests/guest/rust-tests/src/bin/acpi_mediated.rs b/tests/guest/rust-tests/src/bin/acpi_mediated.rs index 377457be53f..6c5250349ba 100644 --- a/tests/guest/rust-tests/src/bin/acpi_mediated.rs +++ b/tests/guest/rust-tests/src/bin/acpi_mediated.rs @@ -526,7 +526,7 @@ fn configuration(holder: &Holder, info: &AcpiInfo) { assert_eq!(holder.write(Space::PciConfig, host_bridge(0x44), Width::Byte, held), Ok(()), "acpi: 0x44 by address"); assert_eq!(holder.write(Space::SystemMemory, ecam + 0x44, Width::Byte, held), Ok(()), "acpi: 0x44 through ECAM"); assert_eq!(holder.read(Space::PciConfig, host_bridge(0x44), Width::Byte), Ok(held)); - // The NVMe controller diskserver holds is its driver's whole. Each write + // The NVMe controller its driver holds is that driver's whole. Each write // below is of the value the register holds, so one a broken fence let // through would be red here and nowhere else. const NVME: u64 = 0x0010_1b36; @@ -547,7 +547,7 @@ fn configuration(holder: &Holder, info: &AcpiInfo) { } /// What [`configuration`] says after the host bridge's identity. -const CONFIGURATION_SAID: &str = "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written"; +const CONFIGURATION_SAID: &str = "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller its driver holds HeldFunction, and the host bridge's 0x44 was written"; fn lock(holder: &Holder, info: &AcpiInfo) { let fadt = holder.table(info.rsdp, b"FACP"); diff --git a/tests/images/acpicase.toml b/tests/images/acpicase.toml index 889652cdb23..d40321ce61a 100644 --- a/tests/images/acpicase.toml +++ b/tests/images/acpicase.toml @@ -29,14 +29,20 @@ args = ["test_rs_acpi_mediated", "reboot"] [symlinks] "bin/reboot" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/jobcase.toml b/tests/images/jobcase.toml index 7593af8c917..a69765133fe 100644 --- a/tests/images/jobcase.toml +++ b/tests/images/jobcase.toml @@ -20,14 +20,20 @@ args = ["reboot"] [symlinks] "bin/reboot" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/latencycase.toml b/tests/images/latencycase.toml index aa8393be155..ea1173b747f 100644 --- a/tests/images/latencycase.toml +++ b/tests/images/latencycase.toml @@ -28,14 +28,20 @@ syscap = ["rt", "dup", "counters"] [symlinks] "bin/reboot" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/metalcase.toml b/tests/images/metalcase.toml index bab54d97216..0d36127c7b0 100644 --- a/tests/images/metalcase.toml +++ b/tests/images/metalcase.toml @@ -73,14 +73,20 @@ devices = ["isa:0060,0064:1,12"] receives = ["compositor", "soundserver", "netstack", "log"] syscap = ["logread"] -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/metaldevicecase.toml b/tests/images/metaldevicecase.toml index 7fe8cb42534..3e99260e4f1 100644 --- a/tests/images/metaldevicecase.toml +++ b/tests/images/metaldevicecase.toml @@ -61,14 +61,20 @@ syscap = ["device", "dup"] "bin/usbread" = "/system/bin/metalprobe" "bin/reboot" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/netcase.toml b/tests/images/netcase.toml index 52242cae070..0f89cbad160 100644 --- a/tests/images/netcase.toml +++ b/tests/images/netcase.toml @@ -35,14 +35,20 @@ syscap = ["rt"] [programs.test-runner] -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/proctreecase.toml b/tests/images/proctreecase.toml index d43b01f1cf8..9f51cb8a43b 100644 --- a/tests/images/proctreecase.toml +++ b/tests/images/proctreecase.toml @@ -48,14 +48,20 @@ receives = ["grants"] [symlinks] "bin/cat" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/images/testcases.toml b/tests/images/testcases.toml index 9889f9eae99..e773190987e 100644 --- a/tests/images/testcases.toml +++ b/tests/images/testcases.toml @@ -95,14 +95,20 @@ receives = ["soundserver"] # `endowment_denied` holds every other applet's grant of it as declared. "bin/tone" = "/system/bin/toybox" -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. +# The block service: every disk a driver offers on `disk`, listed and opened +# for the clients `block` grants, each session handed to its disk's driver. +# Started again when it ends; its drivers connect again and offer again. [programs.diskserver] service = true restart = true -serves = ["block"] -devices = ["pci:01:08:02/1b36:0010"] +serves = ["block", "disk"] + +# The NVMe driver: one process per such controller, reading its own table and +# offering its disk to the block service. QEMU's controller alone, so a metal +# boot leaves the bench's own NVMe disk to its firmware. +[programs.nvme] +drives = ["pci:01:08:02/1b36:0010"] +receives = ["disk"] # The file servers: DATA, the log and the boot volume, one process # each, serving every program the directories of its role. Started again when diff --git a/tests/toyos.rs b/tests/toyos.rs index f9bd3b3a6d9..6b9fbebaf70 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -477,10 +477,11 @@ const MACHINE_TESTS: &[&str] = &[ // which no host build has and the T14, whose CPUs offer x2APIC, never // reaches; the AMD laptop that needs it is not on the bench. "desktop_without_x2apic", - // A machine whose one NVMe disk is all it has boots the image off it: the - // firmware that reads it and the loader and kernel it hands the image to - // have no host build, and the T14 boots from a stick beside an NVMe disk - // that is another system's. + // A machine whose one NVMe disk is all it has boots the image off it and + // serves every role off it: the firmware that reads it, the loader and + // kernel it hands the image to, and the claim the nvme driver serves the + // block service's sessions through have no host build, and the T14 boots + // from a stick beside an NVMe disk that is another system's. "nvme_disk_boots", // clang and LLD built for ToyOS, compiling and linking inside it: the // claim is that they run on ToyOS, which no host test can make, and the @@ -2161,7 +2162,7 @@ const ACPI_MEDIATED_SAID: [&str; 10] = [ "acpi: the FACS read through as type 10, its write was refused FacsWrite, and the memory after it was written and put back", "acpi: COM1, the CMOS index, the 8259 and the configuration mechanism were refused KernelPort; the i8042's row ClaimedPort; PM1a_CNT read and refused its write ReadOnlyPort; the POST port was written", "acpi: ACPI_ENABLE and ACPI_DISABLE were refused SMI_CMD as KernelCommand and SCI_EN stayed set, a write wider than a byte was refused CommandSpan, every firmware call made was read back from the port and counted on the boot processor, and a storm of them was refused CommandRate", - "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller diskserver holds HeldFunction, and the host bridge's 0x44 was written", + "by its address and through ECAM; every header and the LPC bridge's PMBASE were refused AddressMap, the NVMe controller its driver holds HeldFunction, and the host bridge's 0x44 was written", "acpi: the lock a dead holder left taken read free; it was taken and given back, given back with GBL_RLS where the firmware had asked, and found pending while the firmware owned it", "acpi: a sleep type wider than three bits was refused InvalidArgument, the next holder's replaced a dead one's, and a second under one claim was refused AlreadyExists", ]; @@ -4127,8 +4128,22 @@ fn nvme_disk_boots(test_config: &Path) -> Result<(), String> { return Err(format!("this machine was to have no USB controller, and QEMU is given {usb}")); } let qemu = QemuInstance::boot_with_options(test_config, &[], &[], options); - serial::Serial::boot(&qemu).must_be_clean() + let log = serial::Serial::boot(&qemu); + log.must_be_clean()?; + // Every role is on this disk, so each is served off the nvme driver's + // sessions through the block service, or the boot left it absent. + for role in NVME_ROLES { + log.must_say(&format!("fileserver: {role}"))?; + } + Ok(()) } + +/// How each file server says its role is served off `nvme_disk_boots`'s disk. +const NVME_ROLES: [&str; 3] = [ + "Log serving /log — FAT32, ", + "Boot serving /boot — FAT32 read-only, ", + "Data serving /apps, /config, /home, /state — bcachefs, ", +]; /// Run `command` as a job of the boot, to exit 0: everything said in its /// window, the kernel's records with the program lines. fn job_window(qemu: &mut QemuInstance, command: &str) -> Result { From 649e0cbe31e02b6ba7c7b6adf8065db49bec34df Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 00:28:25 +0200 Subject: [PATCH 2/4] issues: the nvme row's QEMU id is a compromise, with its owner and exit #962 names NVMe by QEMU's id in every image so no metal boot drives the bench's own disk, which holds the Ubuntu the metal loop reboots through. The record now says what a shipped image should drive (every NVMe by class), what keeps another system's disk unwritten under a class row (no grant reaches its partitions; read, not measured), and what closes it. Co-Authored-By: Claude Opus 5.5 --- ...kserver-claims-qemus-nvme-by-its-pci-id.md | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md b/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md index bcd4c7573e1..e9a28241a41 100644 --- a/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md +++ b/issues/diskserver-claims-qemus-nvme-by-its-pci-id.md @@ -32,18 +32,34 @@ and bhyve each set. `issues/the-lan-is-not-yet-production-grade.md` plans the same for NICs ("init binds drivers to hardware from a declared match table by vendor, device and class"). -A constraint a class row meets that an id row does not: a machine whose NVMe -holds another operating system, as the T14's holds Ubuntu, has it driven and -offered to the block service, which then serves only the partitions its grants -name (`system/diskserver/src/main.rs`). A machine with two NVMe controllers -gets a driver on each. +**A compromise #962 keeps**, accepted by the orchestrator there: the `nvme` +row names QEMU's id in every image, shipped and test alike, so that no metal +boot drives a bench's own NVMe disk — the T14's holds the Ubuntu the metal +loop reboots through after every boot (`tests/metal/src/lib.rs`). That is a +machine with an NVMe disk and no ToyOS driver for it. -**Owner**: whoever holds `issues/toyos-runs-in-any-open-source-hypervisor.md`. +**What a shipped image should drive**: every NVMe controller, by class code, +whatever machine it boots on — QEMU, another hypervisor, metal. -**Exit**: both shipped configs' `nvme` row names NVMe by class code, which -`toyos_manifest::device::PciMatch::parse` takes (#926); and a guest boot whose +**What keeps another system's disk unwritten under a class row**, by reading +and not measured: the driver reads its table and resets and enables the +controller, and writes nothing outside a session. A session opens only what a +grant admits (`system/diskserver/src/main.rs`), and the supervisor mints +grants for the loader's LOG and BOOT GUIDs, for the `TOYOS-DATA` type, and for +`update` on the slots of the running ROOT's disk (`storage_endowment`, +`Supervisor::slot_grant`, `system/supervisor/src/main.rs`), none of which an +Ubuntu partition carries. Whether Ubuntu boots off a controller ToyOS drove +and reset is not measured. + +**Owner**: the orchestrator, which dispatches it; the hypervisor track +(`issues/toyos-runs-in-any-open-source-hypervisor.md`) waits on it. + +**Exit**: every image's `nvme` row names NVMe by class code, which +`toyos_manifest::device::PciMatch::parse` takes (#926); a guest boot whose controller is QEMU's `nvme` with `use-intel-id=on` serves DATA through the block -service, which a row naming `1b36:0010` instead reds. The configs the T14 boots -keep their id row, outside this exit, until the T14's NVMe holds no Ubuntu: a -class row there would drive the disk the metal workflow reboots through (the -orchestrator's ruling, #896, issuecomment-6131213771, ruling 4). +service, which a row naming `1b36:0010` instead reds; a supervisor host test +holds that every block grant it mints names a loader-named GUID, a ToyOS +partition type, or the running ROOT disk's slots, red on a grant by any other +type; and a T14 row whose readback shows the nvme driver offering the T14's +disk with no session opened on any of its partitions, read after the metal +loop's own reboot into Ubuntu off that disk. From e07354fb1d2f49ea559f6b9c59d85f4997bc260b Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 00:56:16 +0200 Subject: [PATCH 3/4] diskserver routes nothing until its drivers settle, and knows a driver by its badge Review round 1 of #962. - Nothing is routed until every disk driver started before the service has settled and every driver that ended has settled again. An open the grant admits on a disk already offered waits too, so a GUID a second disk also carries is refused on both rather than sent to whichever offered first, and a type-granted DATA reopen after its driver's restart waits for the offer rather than being refused NotGranted for a type no offer carries. - A driver is the badge the supervisor minted its `disk` connector with (its function's arrival, `toyos_blockring::disk::badge`), never the name it gives itself. The supervisor names each running disk driver by it (`--driver`), so a late driver never stands in for one that never spoke, and a second connection with a live badge is let go. - DRIVER_BOUND counts from the service's start or a settled driver's last end, so past it every answer is immediate; the client bounds its wait for a listing's or an open's answer by ANSWER_BOUND_NS, twice that. - The driver's link connects again in a bounded loop, not by recursion. - Comments that said what the code does not: MSG_GONE's payload, a `source`'s life, the listing's refusals. Sessions::busy is deleted. Co-Authored-By: Claude Opus 5.5 --- lib/blockring/src/disk.rs | 23 ++- lib/blockring/src/wire.rs | 21 ++- lib/blockserve/src/link.rs | 42 +++-- lib/blockserve/src/sessions.rs | 5 - lib/input/src/wire.rs | 5 +- system/diskserver/src/main.rs | 298 ++++++++++++++++++++----------- system/diskserver/src/session.rs | 24 ++- system/supervisor/src/main.rs | 42 +++-- 8 files changed, 314 insertions(+), 146 deletions(-) diff --git a/lib/blockring/src/disk.rs b/lib/blockring/src/disk.rs index 097a8db9f5e..e6b0e2695d4 100644 --- a/lib/blockring/src/disk.rs +++ b/lib/blockring/src/disk.rs @@ -19,9 +19,23 @@ use alloc::vec::Vec; use crate::wire::GUID_BYTES; -/// The port the block service serves to disk drivers. +/// The port the block service serves to disk drivers. Each driver's +/// connector is minted with the badge [`badge`] makes of who the supervisor +/// started it as, which is the driver's identity to the service. pub const PORT: &str = "disk"; +/// The badge a disk driver's connector is minted with: the supervisor's +/// number for the driver, which is its function's arrival. +pub fn badge(identity: u32) -> [u8; 4] { + identity.to_le_bytes() +} + +/// The identity a driver connection's badge names, `None` for bytes +/// [`badge`] does not make. +pub fn identity(badge: &[u8]) -> Option { + Some(u32::from_le_bytes(badge.try_into().ok()?)) +} + /// Driver to service, first: the driver's name for the service's lines, /// UTF-8, at most [`MAX_LABEL`] bytes. pub const MSG_DRIVER: u32 = 1; @@ -170,11 +184,13 @@ impl Session { } } -/// A [`MSG_DRIVER`] or [`MSG_GONE`] payload. +/// A [`MSG_DRIVER`] payload: the driver's name for the service's lines, which +/// never says who the driver is. pub fn label(bytes: &[u8]) -> Option { (1..=MAX_LABEL).contains(&bytes.len()).then(|| core::str::from_utf8(bytes).ok().map(String::from))? } +/// A [`MSG_GONE`] payload: the disk's number. pub fn disk(bytes: &[u8]) -> Option { Some(u32::from_le_bytes(bytes.try_into().ok()?)) } @@ -228,5 +244,8 @@ mod tests { assert_eq!(label(&[0xFF]), None); assert_eq!(disk(&5u32.to_le_bytes()), Some(5)); assert_eq!(disk(&[0; 3]), None); + assert_eq!(identity(&badge(0x0102_0304)), Some(0x0102_0304)); + assert_eq!(identity(&[1, 2, 3]), None); + assert_eq!(identity(&[1, 2, 3, 4, 5]), None); } } diff --git a/lib/blockring/src/wire.rs b/lib/blockring/src/wire.rs index afb6490ca1b..9c9a0363d63 100644 --- a/lib/blockring/src/wire.rs +++ b/lib/blockring/src/wire.rs @@ -39,6 +39,18 @@ pub const MSG_LISTED: u32 = 5; /// The bytes of a GUID payload. pub const GUID_BYTES: usize = 16; +/// How long the service waits for its disk drivers to settle, from its start +/// or a settled driver's end, before it answers a listing or an open with +/// what it knows. Policy: a stick's bring-up is a port's reset and +/// enumeration and a Bulk-Only disk's first commands, each bounded far below +/// this. +pub const DRIVER_BOUND_NS: u64 = 30_000_000_000; + +/// How long a client waits for a listing's or an open's answer before it +/// takes the service for wedged: the service's wait for its drivers, and as +/// long again for the driver's answer to an open it hands over. +pub const ANSWER_BOUND_NS: u64 = 2 * DRIVER_BOUND_NS; + /// What an open was answered with: the partition's length in blocks, and its /// unique GUID as the table stores it. /// @@ -205,9 +217,9 @@ pub enum Refusal { /// A session holds it already. Held, /// The partition is there and cannot be served: its range is not whole - /// blocks, its GUID is on two entries, or its controller would not open, - /// and a listing with a disk driver that never settled is refused the - /// same. + /// blocks, its GUID is on two disks, or its controller would not open. A + /// listing that finds nothing it admits is refused the same while a disk + /// driver cannot use its controller or never settled. Unusable, /// The frame, its handles or its region are not what this protocol sends. Malformed, @@ -215,7 +227,8 @@ pub enum Refusal { Exhausted, /// A disk controller is on this machine and no driver of it started — the /// kernel would not hand over its claim, or the driver would not spawn: - /// nothing on it is served, and a listing is refused the same. + /// nothing on it is served, and a listing that finds nothing it admits is + /// refused the same. ClaimRefused, /// The connection's grant does not reach that partition, whether or not /// the service has it. A connection with no grant, or with bytes no diff --git a/lib/blockserve/src/link.rs b/lib/blockserve/src/link.rs index c9d2dfde9cc..102009d6dc0 100644 --- a/lib/blockserve/src/link.rs +++ b/lib/blockserve/src/link.rs @@ -25,6 +25,10 @@ pub struct Handed { pub region: toyos::RawHandle, } +/// Fresh connections in a row that may refuse what the driver says before it +/// gives up: a frame refused on an empty connection is not backpressure. +const CONNECTS: u32 = 3; + pub struct Link { label: String, /// `None` once the service's port has closed for good. @@ -42,28 +46,38 @@ impl Link { link } + /// Connect, and say everything again; a fresh connection that will not + /// take it is let go and another made. A service that will not take one + /// connection's frames on any of them is a refusal that does not change, + /// and the driver ends by name. fn connect(&mut self) { - self.rx = FrameRx::new(); - self.conn = match endow::service(disk::PORT) { - Ok(conn) => Some(conn), - Err(EndowError::ServerGone) => { - println!("{}: the block service's port is closed for good; nothing new is served", self.label); + for _ in 0..CONNECTS { + self.rx = FrameRx::new(); + self.conn = match endow::service(disk::PORT) { + Ok(conn) => Some(conn), + Err(EndowError::ServerGone) => { + println!("{}: the block service's port is closed for good; nothing new is served", self.label); + return; + } + Err(why) => panic!("{}: the manifest gives this driver `{}`, and it answered {why:?}", self.label, disk::PORT), + }; + if self.tell_everything() { return; } - Err(why) => panic!("{}: the manifest gives this driver `{}`, and it answered {why:?}", self.label, disk::PORT), - }; + println!("{}: the block service's connection would not take what this driver offers; connecting again", self.label); + } + panic!("{}: {CONNECTS} connections to the block service in a row would not take what this driver offers", self.label); + } + + fn tell_everything(&self) -> bool { let mut told = self.send(disk::MSG_DRIVER, self.label.as_bytes().to_vec()); - let offers: Vec> = - self.offers.iter().map(|(&disk, parts)| Offer { disk, parts: parts.clone() }.encode()).collect(); - for offer in offers { - told &= self.send(disk::MSG_OFFER, offer); + for (&disk, parts) in &self.offers { + told &= self.send(disk::MSG_OFFER, Offer { disk, parts: parts.clone() }.encode()); } if let Some(settled) = self.settled { told &= self.send(disk::MSG_SETTLED, settled.encode().to_vec()); } - if !told { - self.ended("it would not take what this driver offers"); - } + told } fn send(&self, msg_type: u32, payload: Vec) -> bool { diff --git a/lib/blockserve/src/sessions.rs b/lib/blockserve/src/sessions.rs index 148bb69a977..69ba7551948 100644 --- a/lib/blockserve/src/sessions.rs +++ b/lib/blockserve/src/sessions.rs @@ -177,11 +177,6 @@ impl Sessions { self.sessions.keys().copied().collect() } - /// Whether any session has a request on the device. - pub fn busy(&self) -> bool { - self.sessions.values().any(|s| s.state.inflight().len() > 0) - } - /// Put `c` on its session's completion ring. [`Self::pull`] leaves room for /// every answer, so a ring without it is a client whose head went back or /// past what was posted: the session ends. diff --git a/lib/input/src/wire.rs b/lib/input/src/wire.rs index 0b33ddc53dd..ca2568ea073 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -1,8 +1,9 @@ //! The `input` port's frames, and the records the kernel's streams carry, //! decoded by one reader whichever source sent them. //! -//! A sender numbers its devices `source`s and never reuses one for the life -//! of its connection, so a late record never reaches a later device. +//! A sender numbers its devices `source`s: a source names one device for the +//! life of its connection, so a late record never reaches a later device. A +//! device's `gone` releases what it holds, and the same source may go on. //! //! Every byte here is one a source chose, and a source is outside the owner's //! trust: a record that does not decode is refused by name and changes diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index 519dd019427..2bdd3b4aa37 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -17,15 +17,19 @@ //! grant does not write is handed over as one, whose driver answers every //! write `ReadOnly`. //! -//! **"Not yet" is never answered as "absent".** The supervisor starts the disk -//! drivers before this service and says how many (`--drivers`), and names -//! each disk controller it started no driver on (`--undriven`). An open of a -//! partition no driver offers, and a listing, wait until every driver started -//! at boot has settled and every driver that ended has settled again, or for -//! [`DRIVER_BOUND`]; then a listing names what is offered, and what is not -//! offered is refused by what is known: `Unusable` while a driver cannot use -//! its controller or has not settled, `ClaimRefused` while a controller has -//! no driver, and `NotFound` otherwise. +//! **A driver is who the supervisor says it is**: the badge it minted the +//! driver's `disk` connector with. It starts the disk drivers before this +//! service and names each by that badge (`--driver`), and names each disk +//! controller it started no driver on (`--undriven`). +//! +//! **Nothing is answered while a driver may still change the answer.** A +//! listing and an open wait until every driver started before this service +//! has settled and every driver that ended has settled again, or until +//! [`DRIVER_BOUND`] has passed since this service started or a settled driver +//! last ended; then a listing names what is offered, a GUID two disks offer is +//! refused on both, and what is not offered is refused by what is known: +//! `Unusable` while a driver cannot use its controller or has not settled, +//! `ClaimRefused` while a controller has no driver, and `NotFound` otherwise. //! //! **A server never blocks on a client or a driver.** Accept and the first //! frame are two events, a frame is buffered until whole, and every answer is @@ -44,11 +48,11 @@ use toyos_blockring::wire::{self, Grant, Listed, Refusal}; use toyos_blockring::PORT; use toyos_blockserve::guid_text; -/// How long a disk driver may take from its start to say how its bring-up -/// ended, and so how long a listing or an open waits for it. Policy: a stick's -/// bring-up is a port's reset and enumeration and a Bulk-Only disk's first -/// commands, each bounded far below this. -const DRIVER_BOUND: Duration = Duration::from_secs(30); +/// How long, from this service's start or a settled driver's end, every +/// driver is waited for before a question is answered with what is known; +/// past it every answer is immediate. Below the client's own bound on an +/// answer, which it is taken from. +const DRIVER_BOUND: Duration = Duration::from_nanos(wire::DRIVER_BOUND_NS); /// Connections accepted and not yet asked, and how long one may stay so. const MAX_PENDING: usize = 16; @@ -58,9 +62,10 @@ const MAX_WAITING: usize = 16; /// Disk drivers connected at once. const MAX_DRIVERS: usize = 16; -/// Argv: the disk drivers the supervisor started before this service, and -/// each disk controller it started none on. -const DRIVERS: &str = "--drivers"; +/// Argv: each disk driver the supervisor started before this service, by the +/// badge of its `disk` connector, and each disk controller it started none +/// on. +const DRIVER: &str = "--driver"; const UNDRIVEN: &str = "--undriven"; const TOKEN_ACCEPT: u64 = 0; @@ -75,42 +80,48 @@ struct Offered { part: Part, } -/// How a driver, by its name, last said its bring-up ended. +/// How a driver last said its bring-up ended. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Fate { Offered, Unusable, - /// Its connection ended, and no driver of its name has settled since. + /// Its connection ended, and it has not settled since. Ended, } +/// A disk driver as the supervisor named it: the badge it minted the +/// driver's `disk` connector with, which the kernel stamps on the connection +/// and the driver cannot choose. What a driver calls itself is for the lines. +type Badge = u32; + /// Everything the drivers said, and what the supervisor said of them. #[derive(Default)] struct Known { - /// How many disk drivers were started before this service. - expected: usize, + /// The disk drivers the supervisor started before this service. + expected: Vec, undriven: Vec, - fates: BTreeMap, + fates: BTreeMap, offered: Vec, } impl Known { /// Whether nothing a driver will say could change an answer: every driver - /// started at boot has settled, and no driver is between an end and its - /// next settling. + /// started before this service has settled, and no driver is between an + /// end and its next settling. fn settled(&self) -> bool { - self.fates.len() >= self.expected && !self.fates.values().any(|f| *f == Fate::Ended) + self.expected.iter().all(|d| self.fates.contains_key(d)) && !self.fates.values().any(|f| *f == Fate::Ended) } - /// The drivers whose controllers' partitions cannot be said absent. - fn unsettled(&self) -> Vec<&str> { - self.fates.iter().filter(|(_, f)| **f != Fate::Offered).map(|(l, _)| l.as_str()).collect() + /// The drivers whose controllers' partitions cannot be said absent: those + /// expected and never heard, and those unusable or ended. + fn unsettled(&self) -> Vec { + let silent = self.expected.iter().filter(|d| !self.fates.contains_key(d)).copied(); + silent.chain(self.fates.iter().filter(|(_, f)| **f != Fate::Offered).map(|(d, _)| *d)).collect() } /// Why a partition no disk offers is not served, once nothing is awaited. fn absent(&self) -> Refusal { - let missing = self.fates.len() < self.expected; - if missing || !self.unsettled().is_empty() { + if !self.unsettled().is_empty() { Refusal::Unusable } else if !self.undriven.is_empty() { Refusal::ClaimRefused @@ -141,22 +152,28 @@ impl Known { } /// Where an open of `guid` through `grant` is served, or its refusal, - /// or `None` while it waits. **The grant is asked first**, so a - /// connection learns nothing of a partition it does not reach. + /// or `None` while it waits. + /// + /// **Nothing is routed until every driver has settled**: a disk that has + /// not offered yet may carry the same GUID, which refuses it on both, and + /// one that left may be offering again — a partition a type grant reaches + /// among them. So a connection with no grant is refused at once, and + /// every other waits and is judged against every offer. fn open(&self, guid: [u8; 16], grant: Option, expired: bool) -> Option> { let Some(grant) = grant else { return Some(Err(Refusal::NotGranted)) }; + if !self.settled() && !expired { + return None; + } let carrying: Vec<&Offered> = self.offered.iter().filter(|o| o.part.unique == guid && guid != [0; 16]).collect(); // A partition no disk offers has no type: only a unique grant naming - // it reaches it, to wait for it or be told why it is not there. + // it reaches it, to be told why it is not there. let kind = carrying.first().map_or([0; 16], |o| o.part.kind); if !grant.admits(guid, kind) { return Some(Err(Refusal::NotGranted)); } Some(match carrying[..] { - [] if !self.settled() && !expired => return None, [] => Err(self.absent()), [one] if one.part.blocks.is_some() => Ok(*one), - [_] => Err(Refusal::Unusable), _ => Err(Refusal::Unusable), }) } @@ -180,11 +197,11 @@ struct Waiting { conn: Connection, grant: Option, ask: Ask, - since: Instant, } /// One disk driver's connection. struct Driver { + badge: Badge, conn: Connection, rx: ipc::FrameRx<{ 4 + disk::MAX_PARTS * Part::BYTES }>, label: Option, @@ -196,6 +213,9 @@ struct Driver { struct Service { known: Known, + /// What [`DRIVER_BOUND`] is counted from: this service's start, or the + /// last end of a driver that had settled. + awaited_since: Instant, drivers: BTreeMap, next_driver: u64, next_disk: u32, @@ -209,7 +229,7 @@ fn main() { while at < args.len() { let value = args.get(at + 1).unwrap_or_else(|| panic!("diskserver: {} takes a value", args[at])); match args[at].as_str() { - DRIVERS => known.expected = value.parse().unwrap_or_else(|_| panic!("diskserver: {DRIVERS} {value}")), + DRIVER => known.expected.push(value.parse().unwrap_or_else(|_| panic!("diskserver: {DRIVER} {value}"))), UNDRIVEN => { println!("diskserver: {value} is on this machine and no driver of it started; what it carries is refused"); known.undriven.push(value.clone()); @@ -220,8 +240,15 @@ fn main() { } let clients = endow::acceptor(PORT).unwrap_or_else(|| panic!("diskserver: started serving no `{PORT}` port")); let drivers = endow::acceptor(disk::PORT).unwrap_or_else(|| panic!("diskserver: started serving no `{}` port", disk::PORT)); - println!("diskserver: serving the disks of {} driver(s) started before it", known.expected); - let mut service = Service { known, drivers: BTreeMap::new(), next_driver: 0, next_disk: 0, waiting: Vec::new() }; + println!("diskserver: serving the disks of the driver(s) started before it, {:?}", known.expected); + let mut service = Service { + known, + awaited_since: Instant::now(), + drivers: BTreeMap::new(), + next_driver: 0, + next_disk: 0, + waiting: Vec::new(), + }; service.serve(&clients, &drivers) } @@ -246,7 +273,7 @@ impl Service { } let now = Instant::now(); let deadlines = pending.iter().map(|p| p.since + HANDSHAKE_TIMEOUT); - let deadlines = deadlines.chain(self.waiting.iter().map(|w| w.since + DRIVER_BOUND)); + let deadlines = deadlines.chain((!self.waiting.is_empty()).then_some(self.awaited_since + DRIVER_BOUND)); let deadlines = deadlines.chain(self.drivers.values().filter(|d| d.label.is_none()).map(|d| d.since + HANDSHAKE_TIMEOUT)); let timeout = deadlines.min().map_or(u64::MAX, |at| at.saturating_duration_since(now).as_nanos() as u64); ready.clear(); @@ -255,12 +282,19 @@ impl Service { let now = Instant::now(); if ready.contains(&TOKEN_DRIVERS) { match drivers.accept() { - Ok(conn) => { - let id = self.next_driver; - self.next_driver += 1; - let d = Driver { conn, rx: ipc::FrameRx::new(), label: None, settled: false, disks: BTreeMap::new(), since: now }; - self.drivers.insert(id, d); - } + Ok(conn) => match driver_badge(drivers, &conn) { + None => println!("diskserver: a `disk` connection with no badge the supervisor mints is let go"), + Some(badge) if self.drivers.values().any(|d| d.badge == badge) => { + println!("diskserver: a second connection of disk driver {badge} is let go") + } + Some(badge) => { + let id = self.next_driver; + self.next_driver += 1; + let rx = ipc::FrameRx::new(); + let d = Driver { badge, conn, rx, label: None, settled: false, disks: BTreeMap::new(), since: now }; + self.drivers.insert(id, d); + } + }, Err(why) => println!("diskserver: the kernel refused a driver's connection: {why:?}"), } } @@ -299,7 +333,7 @@ impl Service { } RxStep::Frame { msg_type, payload_len } => { let p = pending.remove(i); - self.asked(clients, p, msg_type, payload_len, now); + self.asked(clients, p, msg_type, payload_len); } } } @@ -337,7 +371,7 @@ impl Service { return Err("a frame before its name"); } let label = disk::label(payload).ok_or("a name that is not one")?; - println!("diskserver: disk driver {label} connected"); + println!("diskserver: disk driver {} ({label}) connected", d.badge); d.label = Some(label); return Ok(()); }; @@ -361,8 +395,8 @@ impl Service { Fate::Unusable } }; - println!("diskserver: disk driver {label} settled with {} disk(s)", d.disks.len()); - self.known.fates.insert(label, fate); + println!("diskserver: disk driver {} ({label}) settled with {} disk(s)", d.badge, d.disks.len()); + self.known.fates.insert(d.badge, fate); } disk::MSG_GONE => { let ours = disk::disk(payload).and_then(|theirs| d.disks.remove(&theirs)); @@ -399,20 +433,22 @@ impl Service { /// connection ended is awaited until a driver of its name settles again. fn drop_driver(&mut self, id: u64, why: &str) { let d = self.drivers.remove(&id).expect("a live driver"); - let label = d.label.unwrap_or_else(|| "an unnamed driver".into()); - println!("diskserver: dropping disk driver {label} — {why}; its {} disk(s) leave", d.disks.len()); + let label = d.label.unwrap_or_else(|| "unnamed".into()); + println!("diskserver: dropping disk driver {} ({label}) — {why}; its {} disk(s) leave", d.badge, d.disks.len()); let gone: Vec = d.disks.values().copied().collect(); self.known.offered.retain(|o| !gone.contains(&o.disk)); - if self.known.fates.contains_key(&label) { - self.known.fates.insert(label, Fate::Ended); + if self.known.fates.insert(d.badge, Fate::Ended).is_some() { + self.awaited_since = Instant::now(); + } else { + self.known.fates.remove(&d.badge); } } /// One client's first frame: a listing, or an open. - fn asked(&mut self, clients: &toyos::port::Acceptor, p: Pending, msg_type: u32, payload_len: usize, now: Instant) { + fn asked(&mut self, clients: &toyos::port::Acceptor, p: Pending, msg_type: u32, payload_len: usize) { let grant = grant(clients, &p.conn); if msg_type == wire::MSG_LIST && payload_len == 0 { - return self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::List, since: now }); + return self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::List }); } let guid = wire::guid(p.rx.payload(payload_len)); let handles = p.conn.recv_handles_exact::<1>(); @@ -425,7 +461,7 @@ impl Service { // SAFETY: the handle arrived with this frame and nothing else in this // process names it. let region = unsafe { OwnedHandle::from_raw(region) }; - self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::Open { guid, region }, since: now }); + self.waiting.push(Waiting { conn: p.conn, grant, ask: Ask::Open { guid, region } }); } /// Answer every waiting question the drivers have now answered, or whose @@ -433,7 +469,7 @@ impl Service { fn answer_waiting(&mut self, now: Instant) { let waiting = std::mem::take(&mut self.waiting); for w in waiting { - let expired = now >= w.since + DRIVER_BOUND; + let expired = now >= self.awaited_since + DRIVER_BOUND; match w.ask { Ask::List => match self.known.listing(w.grant, expired) { None => self.waiting.push(w), @@ -463,10 +499,8 @@ impl Service { if !expired || self.known.settled() { return; } - let started = self.known.expected.saturating_sub(self.known.fates.len()); println!( - "diskserver: {what} waited {DRIVER_BOUND:?} and is answered with what is known; not settled: {:?}, and \ - {started} driver(s) started before this service never said who they are", + "diskserver: {what} is answered with what is known, {DRIVER_BOUND:?} on; not settled: disk driver(s) {:?}", self.known.unsettled() ); } @@ -482,7 +516,7 @@ impl Service { // The client hears nothing from here: its open is the driver's to // answer, and a refused move closes the client's connection. if let Err(why) = d.conn.send_handles([conn.into_owned(), region]) { - return println!("diskserver: the kernel would not move a session to {:?}: {why:?}", d.label); + return println!("diskserver: the kernel would not move a session to disk driver {}: {why:?}", d.badge); } // Its handles are in the driver's queue ahead of any later frame's, // so a frame the driver will not take ends the driver. @@ -510,6 +544,17 @@ fn list(conn: &Connection, answer: Result, Refusal>) { } } +/// Who the driver on `conn`, accepted from `acceptor`, is: the badge the +/// supervisor minted its connector with. +fn driver_badge(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { + let mut badge = [0u8; MAX_BADGE]; + match acceptor.badge(conn, &mut badge) { + Ok(bytes) => disk::identity(bytes), + Err(SyscallError::NotFound) => None, + Err(why) => panic!("diskserver: its own acceptor would not say a connection's badge: {why:?}"), + } +} + /// The grant `conn`, accepted from `acceptor`, was minted with; `None` for a /// connection through an unbadged connector or with bytes no minter stamps. fn grant(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { @@ -534,21 +579,32 @@ mod tests { const BOOT_KIND: [u8; 16] = [0x20; 16]; const DATA_KIND: [u8; 16] = [0x30; 16]; const ROOT_KIND: [u8; 16] = [0x40; 16]; + /// Two drivers, by the badges the supervisor minted them. + const STICK: Badge = 7; + const NVME: Badge = 9; fn unique(guid: [u8; 16]) -> Option { Some(Grant { scope: Scope::Unique(guid), writes: true }) } + fn data() -> Option { + Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }) + } + fn part(unique: [u8; 16], kind: [u8; 16]) -> Part { Part { unique, kind, blocks: Some(100) } } - /// One settled driver offering LOG, BOOT and ROOT on disk 0, and another - /// offering DATA on disk 1. + fn known(expected: &[Badge]) -> Known { + Known { expected: expected.to_vec(), ..Known::default() } + } + + /// Both drivers settled: the stick offering LOG, BOOT and ROOT on disk 0, + /// and the NVMe disk offering DATA on disk 1. fn two_disks() -> Known { - let mut known = Known { expected: 2, ..Known::default() }; - known.fates.insert("stick".into(), Fate::Offered); - known.fates.insert("nvme".into(), Fate::Offered); + let mut known = known(&[STICK, NVME]); + known.fates.insert(STICK, Fate::Offered); + known.fates.insert(NVME, Fate::Offered); for p in [part(LOG, LOG_KIND), part(BOOT, BOOT_KIND), part(ROOT, ROOT_KIND)] { known.offered.push(Offered { disk: 0, part: p }); } @@ -567,10 +623,9 @@ mod tests { assert_eq!(known.open(other, unique(LOG), false), Some(Err(Refusal::NotGranted))); assert_eq!(known.open(other, None, false), Some(Err(Refusal::NotGranted))); } - let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); - assert_eq!(known.open(DATA, data, false), Some(Ok(Offered { disk: 1, part: part(DATA, DATA_KIND) }))); - assert_eq!(known.open(LOG, data, false), Some(Err(Refusal::NotGranted))); - assert_eq!(known.open([9; 16], data, false), Some(Err(Refusal::NotGranted)), "nothing of a GUID no disk offers"); + assert_eq!(known.open(DATA, data(), false), Some(Ok(Offered { disk: 1, part: part(DATA, DATA_KIND) }))); + assert_eq!(known.open(LOG, data(), false), Some(Err(Refusal::NotGranted))); + assert_eq!(known.open([9; 16], data(), false), Some(Err(Refusal::NotGranted)), "nothing of a GUID no disk offers"); assert_eq!(known.open([9; 16], unique([9; 16]), false), Some(Err(Refusal::NotFound))); } @@ -583,62 +638,95 @@ mod tests { answer.expect("answered").map(|all| all.iter().map(|l| (l.disk, l.unique)).collect::>()) }; assert_eq!(uniques(known.listing(unique(BOOT), false)), Ok(vec![(0, BOOT)])); - let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: false }); - assert_eq!(uniques(known.listing(data, false)), Ok(vec![(1, DATA)])); + assert_eq!(uniques(known.listing(data(), false)), Ok(vec![(1, DATA)])); assert_eq!(uniques(known.listing(unique([9; 16]), false)), Ok(vec![])); assert_eq!(known.listing(None, false), Some(Err(Refusal::NotGranted))); } - /// Until every driver started at boot has settled, a listing and an open - /// of what no disk offers wait; an open of what is offered is answered at - /// once. At the bound each is answered with what is known, and a missing + /// Until every driver started before the service has settled, a listing + /// and every open wait — one the grant admits on a disk already offered + /// too. At the bound each is answered with what is known, and a missing /// driver is never taken for a machine without the partition. #[test] - fn what_no_disk_offers_waits_for_every_driver_started_at_boot() { - let mut known = Known { expected: 2, ..Known::default() }; - known.fates.insert("nvme".into(), Fate::Offered); + fn nothing_is_answered_until_every_driver_started_before_the_service_has_settled() { + let mut known = known(&[STICK, NVME]); + known.fates.insert(NVME, Fate::Offered); known.offered.push(Offered { disk: 0, part: part(DATA, DATA_KIND) }); - let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); - assert_eq!(known.listing(data, false), None, "the stick's driver has not settled"); + assert_eq!(known.listing(data(), false), None, "the stick's driver has not settled"); assert_eq!(known.open(LOG, unique(LOG), false), None); - assert!(known.open(DATA, data, false).is_some_and(|r| r.is_ok()), "offered: answered at once"); - assert_eq!(known.open(LOG, unique(LOG), true), Some(Err(Refusal::Unusable)), "the bound passed, a driver missing"); - assert!(known.listing(data, true).is_some_and(|r| r.is_ok_and(|l| l.len() == 1))); + assert_eq!(known.open(DATA, data(), false), None, "offered, and another disk may carry it too"); + assert_eq!(known.open(DATA, None, false), Some(Err(Refusal::NotGranted)), "no grant waits for nothing"); + assert_eq!(known.open(LOG, unique(LOG), true), Some(Err(Refusal::Unusable)), "the bound passed, a driver silent"); + assert!(known.open(DATA, data(), true).is_some_and(|r| r.is_ok())); + assert!(known.listing(data(), true).is_some_and(|r| r.is_ok_and(|l| l.len() == 1))); let log = Some(Grant { scope: Scope::Kind(LOG_KIND), writes: true }); - assert_eq!(known.listing(log, true), Some(Err(Refusal::Unusable)), "nothing listed, a driver missing"); + assert_eq!(known.listing(log, true), Some(Err(Refusal::Unusable)), "nothing listed, a driver silent"); - known.fates.insert("stick".into(), Fate::Offered); + known.fates.insert(STICK, Fate::Offered); assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::NotFound)), "every driver said"); assert_eq!(known.listing(log, false), Some(Ok(vec![]))); } - /// A driver that ended is awaited until one of its name settles again, - /// as a file server reopening after a driver's restart needs. + /// A GUID the first driver offers is not routed while another may offer + /// it too: once the second settles carrying it, it is refused on both, + /// and no session ever reached either. + #[test] + fn a_guid_a_late_disk_also_carries_is_never_routed_to_the_first() { + let mut known = known(&[STICK, NVME]); + known.fates.insert(NVME, Fate::Offered); + known.offered.push(Offered { disk: 0, part: part(LOG, LOG_KIND) }); + assert_eq!(known.open(LOG, unique(LOG), false), None); + known.fates.insert(STICK, Fate::Offered); + known.offered.push(Offered { disk: 1, part: part(LOG, LOG_KIND) }); + assert_eq!(known.open(LOG, unique(LOG), false), Some(Err(Refusal::Unusable))); + } + + /// A driver that ended is awaited until it settles again, whatever the + /// grant: a file server reopening its type-granted DATA after a driver's + /// restart waits for the offer rather than being refused for a type no + /// offer carries any more. #[test] - fn a_driver_that_ended_is_awaited_until_it_settles_again() { + fn a_driver_that_ended_is_awaited_until_it_settles_again_under_either_grant() { let mut known = two_disks(); - known.fates.insert("stick".into(), Fate::Ended); - known.offered.retain(|o| o.disk != 0); + known.fates.insert(NVME, Fate::Ended); + known.offered.retain(|o| o.disk != 1); + assert_eq!(known.open(DATA, data(), false), None, "a type grant waits for the offer"); + assert_eq!(known.open(DATA, unique(DATA), false), None); + assert_eq!(known.listing(data(), false), None); + known.fates.insert(NVME, Fate::Offered); + known.offered.push(Offered { disk: 2, part: part(DATA, DATA_KIND) }); + assert_eq!(known.open(DATA, data(), false), Some(Ok(Offered { disk: 2, part: part(DATA, DATA_KIND) }))); + } + + /// A driver is its badge: one the service did not expect, settling, says + /// nothing of one it expects that never spoke, so the wait goes on; and + /// two drivers are two whatever they call themselves, since a name never + /// reaches what is known. + #[test] + fn a_late_driver_never_stands_in_for_one_started_before_the_service() { + let mut known = known(&[STICK, NVME]); + known.fates.insert(NVME, Fate::Offered); + known.fates.insert(11, Fate::Offered); + assert!(!known.settled(), "a late driver counted for the stick's"); + assert_eq!(known.unsettled(), vec![STICK]); assert_eq!(known.open(LOG, unique(LOG), false), None); - known.fates.insert("stick".into(), Fate::Offered); - known.offered.push(Offered { disk: 2, part: part(LOG, LOG_KIND) }); - assert_eq!(known.open(LOG, unique(LOG), false), Some(Ok(Offered { disk: 2, part: part(LOG, LOG_KIND) }))); + known.fates.insert(STICK, Fate::Offered); + assert!(known.settled()); } - /// A controller its driver cannot use, or whose claim was refused, is a - /// disk there and never a machine without one. + /// A controller its driver cannot use, or one with no driver, is a disk + /// there and never a machine without one. #[test] - fn an_unusable_or_unclaimed_controller_is_refused_and_an_absent_one_lists_nothing() { - let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); - let mut unusable = Known { expected: 1, ..Known::default() }; - unusable.fates.insert("nvme".into(), Fate::Unusable); - assert_eq!(unusable.listing(data, false), Some(Err(Refusal::Unusable))); + fn an_unusable_or_undriven_controller_is_refused_and_an_absent_one_lists_nothing() { + let mut unusable = known(&[NVME]); + unusable.fates.insert(NVME, Fate::Unusable); + assert_eq!(unusable.listing(data(), false), Some(Err(Refusal::Unusable))); assert_eq!(unusable.open(DATA, unique(DATA), false), Some(Err(Refusal::Unusable))); - let unclaimed = Known { undriven: vec!["pci:01:08:02".into()], ..Known::default() }; - assert_eq!(unclaimed.listing(data, false), Some(Err(Refusal::ClaimRefused))); - assert_eq!(unclaimed.open(DATA, unique(DATA), false), Some(Err(Refusal::ClaimRefused))); + let undriven = Known { undriven: vec!["pci:01:08:02 at PCI 00:02.0".into()], ..Known::default() }; + assert_eq!(undriven.listing(data(), false), Some(Err(Refusal::ClaimRefused))); + assert_eq!(undriven.open(DATA, unique(DATA), false), Some(Err(Refusal::ClaimRefused))); let absent = Known::default(); - assert_eq!(absent.listing(data, false), Some(Ok(vec![]))); + assert_eq!(absent.listing(data(), false), Some(Ok(vec![]))); assert_eq!(absent.open(DATA, unique(DATA), false), Some(Err(Refusal::NotFound))); } diff --git a/system/diskserver/src/session.rs b/system/diskserver/src/session.rs index 314cfc263a8..ea30d0b016c 100644 --- a/system/diskserver/src/session.rs +++ b/system/diskserver/src/session.rs @@ -42,6 +42,8 @@ pub enum Error { Kernel(SyscallError), /// The service said something this protocol does not. Protocol, + /// No answer came within [`wire::ANSWER_BOUND_NS`]. + Unanswered, } /// One ticket's answer, and a read's data. @@ -352,8 +354,9 @@ impl Session { /// Open the same partition over the same region through the same name, /// after the service ended; writes the client holds go out again first. /// - /// Blocks until the service answers: a name whose server is being - /// restarted queues the connection until the new one accepts it. + /// Blocks until the service answers, within [`wire::ANSWER_BOUND_NS`]: a + /// name whose server is being restarted queues the connection until the + /// new one accepts it. pub fn reconnect(&mut self) -> Result<(), Error> { assert!(self.conn.is_none(), "diskserver: reconnect while a session is open"); // Before the region goes to the new server: it must find this end's @@ -438,6 +441,19 @@ fn connect(names: &Namespace, service: &str) -> Result { }) } +/// The header of the service's answer on `conn`, once it is there, within +/// [`wire::ANSWER_BOUND_NS`]. +fn answer(conn: &Connection) -> Result { + let poller = Poller::new(1); + poller.watch(conn, READABLE, 0); + let mut ready = false; + poller.wait(1, wire::ANSWER_BOUND_NS, |_| ready = true); + if !ready { + return Err(Error::Unanswered); + } + conn.recv_header().map_err(|_| Error::Ended) +} + /// Connect, send the region with an open, and wait for the answer. fn handshake( names: &Namespace, @@ -448,7 +464,7 @@ fn handshake( let conn = connect(names, service)?; conn.send_handles([region.share().map_err(Error::Kernel)?]).map_err(|_| Error::Ended)?; conn.send_bytes(wire::MSG_OPEN, &guid).map_err(|_| Error::Ended)?; - let header = conn.recv_header().map_err(|_| Error::Ended)?; + let header = answer(&conn)?; let mut payload = [0u8; Opened::BYTES]; let len = conn.recv_bytes(&header, &mut payload).map_err(|_| Error::Ended)?; match header.msg_type { @@ -463,7 +479,7 @@ fn handshake( pub fn list(names: &Namespace, service: &str) -> Result, Error> { let conn = connect(names, service)?; conn.signal(wire::MSG_LIST).map_err(|_| Error::Ended)?; - let header = conn.recv_header().map_err(|_| Error::Ended)?; + let header = answer(&conn)?; let mut payload = vec![0u8; header.len() as usize]; let len = conn.recv_bytes(&header, &mut payload).map_err(|_| Error::Ended)?; match header.msg_type { diff --git a/system/supervisor/src/main.rs b/system/supervisor/src/main.rs index 67d5f8c8aa8..85760e278c4 100644 --- a/system/supervisor/src/main.rs +++ b/system/supervisor/src/main.rs @@ -123,7 +123,7 @@ use toyos_blockring::wire::{Grant as BlockGrant, Scope}; use toyos_logstream::{ Registration, Tag, ALIVE, CONSOLE, FLUSH, FLUSHED, LOGKEEPER, MAX_TAG, ORIGINS, REGISTER, RESUME, STOPPING, }; -use toyos_abi::inventory::{Pci, PciAddr, Record}; +use toyos_abi::inventory::{Arrival, Pci, PciAddr, Record}; use toyos_abi::syscall::{ DeviceRequest, FileType, SyscallError, DEV_PREFIX, MAX_BADGE, PROVIDE_PREFIX, SERVE_PREFIX, SVC_LABEL, SYSCAP_LABEL, @@ -954,12 +954,14 @@ impl<'a> Supervisor<'a> { refused } - /// What the block service is started with: how many disk drivers run, - /// which it waits for, and every disk driver's function the boot started - /// none on. + /// What the block service is started with: each disk driver running, by + /// its badge, which it waits for, and every disk driver's function the + /// boot started none on. fn block_args(&self) -> Vec { - let running = self.instances.iter().filter(|i| is_disk_driver(i.row)).count(); - let mut args = vec!["--drivers".to_string(), running.to_string()]; + let mut args = Vec::new(); + for instance in self.instances.iter().filter(|i| is_disk_driver(i.row)) { + args.extend(["--driver".to_string(), instance.function.arrival.0.to_string()]); + } for undriven in &self.undriven { args.extend(["--undriven".to_string(), undriven.clone()]); } @@ -975,7 +977,12 @@ impl<'a> Supervisor<'a> { let name = entry.to_string(); let claim = self.syscap.claim_function::(function.arrival).map_err(|e| refused(&name, e))?; self.make_home(row); - let extra = Extra { claims: vec![(format!("{DEV_PREFIX}{name}"), claim)], ..Extra::default() }; + let mut extra = Extra { claims: vec![(format!("{DEV_PREFIX}{name}"), claim)], ..Extra::default() }; + // A disk driver is who this start says it is: its `disk` connector + // carries the function's arrival, which the block service keys it by. + if is_disk_driver(row) { + extra.ports.push((toyos_blockring::disk::PORT.to_string(), self.grants.disk(function.arrival)?)); + } let launcher = (&self.minted, self.sessions.machine()); let served = Served::Keep { acceptors: &[], owed: &[] }; let output = Output::Boot(&mut self.log); @@ -2837,12 +2844,15 @@ fn build_namespace( // Never the swap port nor the grants port: [`swap_namespace`] and // [`grants_namespace`] say why. Never the block port's own connector, // which reaches nothing: a holder of `block` holds one minted for what it - // reaches, in `view`; nor netstack's, which nothing holds: a holder of - // `netstack` holds one minted for its row and session, in `view` too. + // reaches, in `view`, and a disk driver one of `disk` minted for who it + // is; nor netstack's, which nothing holds: a holder of `netstack` holds + // one minted for its row and session, in `view` too. let receives: Vec<&String> = program .receives .iter() - .filter(|name| *name != toyos_swap::PORT && *name != grants::PORT && *name != toyos_blockring::PORT && *name != NETSTACK) + .filter(|name| { + ![toyos_swap::PORT, grants::PORT, toyos_blockring::PORT, toyos_blockring::disk::PORT, NETSTACK].contains(&name.as_str()) + }) .collect(); if receives.is_empty() && extras.is_empty() && view.is_empty() { return Ok(None); @@ -2915,6 +2925,18 @@ impl Grants<'_> { .map_err(|e| format!("no connector to the block service could be minted: {e:?}")) } + /// The block service's `disk` port minted for the disk driver of the + /// function that arrived as `arrival`. + fn disk(&self, arrival: Arrival) -> Result { + let ports = self.block.as_ref().ok_or("no block service runs to offer a disk to")?.borrow(); + let Some((_, acceptor)) = ports.iter().find(|(name, _)| name == toyos_blockring::disk::PORT) else { + return Err("the block service's port has closed for good".into()); + }; + acceptor + .mint(&toyos_blockring::disk::badge(arrival.0)) + .map_err(|e| format!("no connector to the block service could be minted: {e:?}")) + } + /// [`NETSTACK`] minted with `program`'s row and `session`, for a row whose /// `receives` names it: the badge is the view netstack keeps its DNS /// answers in. None once the port has closed for good, as for a role. From b21b38cdd5d04db1ea604584ad0b4d5718a9d94d Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 07:53:38 +0200 Subject: [PATCH 4/4] diskserver: drop_driver names the badge it waits on Review round 2 of #962. drop_driver's doc still described identity by name. The code waits for the same badge, and only for a driver that had settled. DRIVER_BOUND's doc now says how the client's bound derives from it. Co-Authored-By: Claude Opus 5.5 --- system/diskserver/src/main.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/system/diskserver/src/main.rs b/system/diskserver/src/main.rs index 2bdd3b4aa37..63dc04f70ef 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -50,8 +50,8 @@ use toyos_blockserve::guid_text; /// How long, from this service's start or a settled driver's end, every /// driver is waited for before a question is answered with what is known; -/// past it every answer is immediate. Below the client's own bound on an -/// answer, which it is taken from. +/// past it every answer is immediate. A client waits twice this for an +/// answer (`wire::ANSWER_BOUND_NS`), which leaves the driver room for its own. const DRIVER_BOUND: Duration = Duration::from_nanos(wire::DRIVER_BOUND_NS); /// Connections accepted and not yet asked, and how long one may stay so. @@ -429,8 +429,9 @@ impl Service { self.known.offered.extend(offer.parts.into_iter().map(|part| Offered { disk: ours, part })); } - /// Driver `id` is gone, and every disk it offered with it; a driver whose - /// connection ended is awaited until a driver of its name settles again. + /// Driver `id` is gone, and every disk it offered with it. One that had + /// settled is awaited until a driver with its badge settles again; one that + /// never settled is forgotten. fn drop_driver(&mut self, id: u64, why: &str) { let d = self.drivers.remove(&id).expect("a live driver"); let label = d.label.unwrap_or_else(|| "unnamed".into());