diff --git a/Cargo.lock b/Cargo.lock index 708ed43915c..f6c30dc67bf 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", @@ -2378,6 +2378,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" @@ -4371,6 +4382,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" @@ -4768,6 +4790,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" @@ -4942,6 +4968,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 a05fee81c99..ff9c1da3fc7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ members = [ "lib/blackbox", "lib/blockhold", "lib/blockring", + "lib/blockserve", "lib/bootmap", "lib/crypto", "lib/device-memory", @@ -63,6 +64,7 @@ members = [ "lib/quiesce", "lib/random", "lib/rootimage", + "lib/scsi", "lib/sha2", "lib/sha2-hw", "lib/swap", @@ -115,6 +117,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..e9a28241a41 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,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"). -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`). - -**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 -orchestrator's ruling, #896, issuecomment-6131213771, ruling 4). +**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. + +**What a shipped image should drive**: every NVMe controller, by class code, +whatever machine it boots on — QEMU, another hypervisor, metal. + +**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; 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. 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 10def215757..464de72d9fa 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-usbhid = { path = "usbhid" } toyos-userbound = { path = "../lib/userbound" } 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..e6b0e2695d4 --- /dev/null +++ b/lib/blockring/src/disk.rs @@ -0,0 +1,251 @@ +//! 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. 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; +/// 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`] 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()?)) +} + +#[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); + 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/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..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. /// @@ -97,24 +109,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 +140,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 +217,18 @@ 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 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, /// 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 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 @@ -266,8 +284,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..102009d6dc0 --- /dev/null +++ b/lib/blockserve/src/link.rs @@ -0,0 +1,158 @@ +//! 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, +} + +/// 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. + 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 + } + + /// 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) { + 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; + } + 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()); + 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()); + } + told + } + + 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..69ba7551948 --- /dev/null +++ b/lib/blockserve/src/sessions.rs @@ -0,0 +1,394 @@ +//! 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() + } + + /// 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..ca2568ea073 100644 --- a/lib/input/src/wire.rs +++ b/lib/input/src/wire.rs @@ -1,6 +1,10 @@ //! 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: 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 //! nothing. @@ -17,21 +21,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 +64,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 +75,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 +101,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 +133,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 +165,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..63dc04f70ef 100644 --- a/system/diskserver/src/main.rs +++ b/system/diskserver/src/main.rs @@ -1,694 +1,558 @@ -//! 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. +//! **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`). //! -//! **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. - - +//! **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 +//! 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, 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. 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. 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: 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; +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; + +/// One partition a driver offered, on the disk the service numbered. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct Offered { + disk: u32, + part: Part, } -fn guid_text(bytes: [u8; 16]) -> String { - let mut buf = [0u8; GUID_TEXT_LEN]; - PartGuid(bytes).write_text(&mut buf).to_string() +/// How a driver last said its bring-up ended. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Fate { + Offered, + Unusable, + /// Its connection ended, and it has not settled since. + Ended, } -/// 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, +/// 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 { + /// The disk drivers the supervisor started before this service. + expected: Vec, + undriven: Vec, + fates: BTreeMap, + offered: 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 +impl Known { + /// Whether nothing a driver will say could change an answer: every driver + /// started before this service has settled, and no driver is between an + /// end and its next settling. + fn settled(&self) -> bool { + self.expected.iter().all(|d| self.fates.contains_key(d)) && !self.fates.values().any(|f| *f == Fate::Ended) } - 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") + + /// 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() } - 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; + + /// Why a partition no disk offers is not served, once nothing is awaited. + fn absent(&self) -> Refusal { + if !self.unsettled().is_empty() { + Refusal::Unusable + } else if !self.undriven.is_empty() { + Refusal::ClaimRefused + } else { + Refusal::NotFound } - let off = (at % BLOCK_BYTES as u64) as usize; - buf.copy_from_slice(&self.block[off..off + lba_bytes as usize]); - true } -} -/// 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(); - } - 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 }); + /// 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), + }) } - 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()) -} -/// 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); - } - match part.map(|p| &p.span) { - None => Err(Refusal::NotFound), - Some(Err(_)) => Err(Refusal::Unusable), - Some(Ok(span)) => Ok(*span), + /// Where an open of `guid` through `grant` is served, or its refusal, + /// 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 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[..] { + [] => Err(self.absent()), + [one] if one.part.blocks.is_some() => Ok(*one), + _ => 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, } -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 { + badge: Badge, + 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, + /// 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, + 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, +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() { + 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()); + } + 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 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) } 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, - }) - } - } - } - - /// 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); + 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); + let now = Instant::now(); + let deadlines = pending.iter().map(|p| p.since + HANDSHAKE_TIMEOUT); + 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(); + poller.wait(1, timeout, |token| ready.push(token)); + + let now = Instant::now(); + if ready.contains(&TOKEN_DRIVERS) { + match drivers.accept() { + 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") } - 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")); + 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:?}"), + } + } + 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); + } } } - } - } - /// 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.badge); + 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.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)); + 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. 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()); + 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.insert(d.badge, Fate::Ended).is_some() { + self.awaited_since = Instant::now(); + } else { + self.known.fates.remove(&d.badge); } - 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) { + 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 }); } - 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 } }); + } + + /// 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 >= self.awaited_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; } + println!( + "diskserver: {what} is answered with what is known, {DRIVER_BOUND:?} on; not settled: disk driver(s) {:?}", + 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 disk driver {}: {why:?}", d.badge); } - 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"); } + } +} + +fn refuse(conn: &Connection, why: Refusal) { + let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); +} - let ids: Vec = service.sessions.keys().copied().collect(); - for id in ids { - if ready.contains(&(TOKEN_SESSION + id)) { - service.hear(id); +/// 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(); + } +} + +/// 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:?}"), } } @@ -703,81 +567,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]; @@ -786,50 +580,165 @@ mod tests { const BOOT_KIND: [u8; 16] = [0x20; 16]; 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() - } + /// 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 }) } - /// 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 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) } + } + + 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(&[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 }); + } + 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)])); + 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 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 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) }); + assert_eq!(known.listing(data(), false), None, "the stick's driver has not settled"); + assert_eq!(known.open(LOG, unique(LOG), false), None); + 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 silent"); + + 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 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_under_either_grant() { + let mut known = two_disks(); + 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, Fate::Offered); + assert!(known.settled()); + } + + /// 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_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 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.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..ea30d0b016c 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)] @@ -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/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 cc76c981c39..161cfa657f4 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, @@ -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,21 @@ impl<'a> Supervisor<'a> { ), } } + refused + } + + /// 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 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()]); + } + args } /// One `drives` process of `row`, holding `function`'s claim under @@ -927,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); @@ -1119,7 +1174,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 +1525,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 +1541,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 +1710,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 +1745,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 +2436,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 +2452,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 +2708,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() { @@ -2775,12 +2845,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); @@ -2853,6 +2926,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. 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 e3dda6d6ebb..d465394a797 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -483,10 +483,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 @@ -2173,7 +2174,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", ]; @@ -4141,8 +4142,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 {