diff --git a/02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md b/02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md index d6f70e1b..63340ecb 100644 --- a/02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md +++ b/02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md @@ -93,6 +93,10 @@ reports, `graphical-session`, still gates the display server itself. > included, is a dependency on the seat that receives it. What a contribution contains still follows > the tool's grain. +> **Extended — 2026-10-08, by [ADR 0255](0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md).** A seat may receive a kind as data in a shape the seat +> defines, rendered by its holder's template: `node-bar` receives blocks that way. A contribution may +> also be placed only where the machine reports a capability. + **5. The display server's module writes the session's start.** It writes a block at the start of `~/.xinitrc`, in this order: diff --git a/02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md b/02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md new file mode 100644 index 00000000..aff80b96 --- /dev/null +++ b/02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md @@ -0,0 +1,183 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-08 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md +--- + +# 255. The bar takes blocks as data from the module that knows the hardware, placed where the machine has it + +## Context + +The bar's module claims `node-bar` ([ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md)) and +writes both bars' files whole. When it was adopted from the workstations on 2026-10-04, three kinds of +block were left out because they were not the bar's to know: + +- the battery, which only the laptop has; +- the GPU's load, an AMD block on the laptop, commented out for NVIDIA on the workstation; +- three Bluetooth headsets by hardware address. + +The bar's own documentation said that a block which follows a machine's hardware belongs to the module +that knows that hardware. It also said such a module had no way in. ADR 0208 §4 lets a module +contribute to a holder's file "in the tool's own grain", and +[ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) made that one +general form: a seat, a kind the seat receives, and text in the tool's own grammar, which the controller +places and never reads. `node-bar` received nothing. Since then the laptop's bar has shown no battery. + +Measured on the laptop on 2026-10-08, the bar's memory block read 96 %. It counted the cache the kernel +gives back when asked as used. The `memory-pressure` module, which judges the same machine, read 73 %. + +Three facts shaped the decision: + +- **The text form would tie every contributor to one bar.** A module giving the bar a battery under + ADR 0212 would write i3status-rust's own grammar. A second bar module, waybar or polybar, could not + read it, and the seat exists so that its holder can be replaced. +- **The mesh already separates data from format once.** A module's `facts` give a path and a template. + The controller renders the mesh's roster through the template. The data is the mesh's and the format + is the module's. +- **What a machine has is already reported.** The node-engine reports detected capabilities, each with + what it saw and how. The controller uses them to refuse a module a machine cannot run. A battery is + not among them yet. + +## Considered Options + +1. **The seat receives blocks as data, and its holder renders them.** A contribution says what the block + shows in terms no bar owns. The holder turns it into its own grammar. Chosen by the operator on + 2026-10-08: "option 1 is exactly how it should work, other modules can 'provide' block information in + some form or another (use a generic boundary layer)". +2. **Blocks that hide themselves where they do not apply.** i3status-rust's common `if_command` option + (a test for the battery's directory, say) hides a block at run time on a machine without the + hardware. Rejected: the knowledge stays in the bar, every bar asks the same question again at run + time, and another bar module would have to carry the same tests in its own grammar. +3. **A text contribution in the bar's grammar, under ADR 0212 as it stands.** Rejected for the reason + above: the contributor would write one bar's grammar, and the seat could not change hands. +4. **A provision the bar requires.** Rejected: a provision is a service one module serves another + ([ADR 0040](0040-what-a-module-is.md)) and is resolved to one provider. A bar takes pieces from any + number of modules on its own machine, and that is what a seat's contributions already are. + +On the source of the battery, the operator ruled on the same day that generic information must not come +from a vendor's module. A battery is any laptop's. The laptop model's module was the first candidate +and is not the source. + +## Decision + +**1. A seat may receive a kind as data.** The seat defines the kind's shape: its fields, their values, +and which fields place a piece. A contribution of that kind gives `data` in that shape instead of +`content`. The holder's claim gives a template for the kind in `renders`: a template over one piece, +written in its tool's grammar. The controller renders each piece through it and places the result. +Each piece is preceded by a comment naming its module, as in ADR 0212 §3. A kind given as text stays +as ADR 0212 decided. + +**2. The holder places a data kind by its placing fields.** The placeholder is +`${contribution:::=,…}`. Within one placeholder, pieces go in the order the +shape names, then in module order. Across all of its files, the holder places every combination of +the placing fields exactly once. Its template must render every example the seat defines. If a +combination is missing, a contribution is lost. If it is placed twice, the contribution is written +twice. Both are refused at registration. A holder that places none of the kind is not refused. It was +written before the kind existed, and its contributions wait until it places them. + +**3. A contribution may name a capability the machine must report: `if-capability`.** The controller +composes it only for a machine whose node-engine reported that capability. Where nothing is known, +there is nothing. This holds for both forms, text and data. The machine's facts decide at composition. +The tool does not decide at run time. + +**4. A kind may be offered.** A contribution of an offered kind does not depend on the seat +(an exception to ADR 0212 §4). Where nobody holds the seat, nothing is placed, and the contributor +runs there all the same. + +**5. `node-bar` receives `block`, offered, as data.** A block has these fields: +- `bar`: `bottom` or `top`; +- `place`: `resources`, beside the processor, memory and disks, or `status`, beside the sound and + before the clock; +- `order`: a whole number from 0 to 99, 50 when absent; +- `shows`: `battery` (option `device`) or `command` (options `command` and `interval`, one line the + command prints, every interval seconds); +- `options`: the options of what it shows. + +Nothing in a block belongs to one bar. A new kind of block is added to the seat's shape, and every +holder's template must render it before the shape is accepted. + +**6. Generic hardware is shown by the module that holds the generic seat, conditioned on the machine's +facts.** A vendor's module contributes only what is that vendor's alone. The power module holds +`node-power` on every machine, so it contributes: +- the battery block, `if-capability: battery`; +- the power-draw block, `if-capability: power-meter`. + +The laptop model's module contributes no block. Something only its vendor's daemon knows would be its +block, such as the charge limit asusd holds. None is contributed yet. + +**7. The node-engine reports two more capabilities, read from the kernel:** +- `battery`: a battery that powers the machine. A device's battery, such as a mouse's or a headset's, + is not one, and neither is an empty bay. +- `power-meter`: something here measures the power drawn. That is a system battery, a processor + package's powercap energy counter (RAPL, on Intel and AMD), or a graphics device that reports its + own power. + +**8. The power drawn is measured honestly, and the block says what was measured.** +- **On battery:** the battery's discharge rate. That is the whole machine, and the block says + `machine`. +- **On mains, or on a machine without a battery:** the processor package's power, as a rate of + change of its energy counter. A discrete graphics device's own power is added where the device + reports it. The block says `CPU` or `CPU+GPU`, never the machine. + - An APU's integrated graphics report the whole package, so they are not counted a second time. + - A sleeping GPU is not woken to be asked. +- **The machine at the wall** can only be measured by a metered plug or a UPS. It is not measured. + +**9. The energy counter stays root's.** Read often, it leaks what the processor is doing (the +PLATYPUS attack, CVE-2020-8694), which is why the kernel makes it root's. No permission is loosened. +- The power module runs a sampler as root. It reads the counter every five seconds and publishes + only the averaged rate, as one line anyone may read. +- The bar's command prints that line. It says the power is not measured when the line is missing + or older than thirty seconds. + +**10. The memory block shows what programs hold,** not counting the cache. That is the figure +`memory-pressure` judges the same machine by. + +## Consequences + +- The laptop's bar gains the battery and the power draw. A workstation's bar gains the power draw, and its + processor's part is labelled as such. A server has no bar, and nothing is placed there. +- Replacing the bar's module costs one template. No contributor changes. +- `node-bar` is the first seat to receive data. `node-hotkeys`, `node-display-session` and + `node-backup` keep text. +- **Version skew:** an older controller refuses a manifest with `data`, `if-capability` or `renders`. + The controller is delivered before the catalogue, as a delivery group's order rules already require + ([ADR 0239](0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md) §4). + The node-engine is delivered first of all. Until a machine's node-engine reports `battery` and + `power-meter`, that machine's bar shows neither block, and nothing fails. +- **What got harder:** + - The controller now runs a holder's template, and a template can be wrong. Its examples are rendered + at registration, and a value it prints but does not have is refused, not written as ``. + - The kind's shape is code in the controller's seat table, as the seats' verbs are. A new block kind + is a change to the controller and to every holder's template. +- **Left open:** + - the machine's draw at the wall, from a metered plug or a UPS; + - a GPU-load block, which belongs to a generic module that does not exist; + - a vendor-only block, such as the laptop's charge limit; + - the headsets, which are one person's devices. + +## How it is checked + +| Rule | Checked by | +|---|---| +| A data contribution fits its seat's shape (fields, values, required options, order 0–99), and text and data are not mixed | the catalogue check; the controller's `TestABlockOutsideTheShapeIsRefused` | +| A holder places every combination of the placing fields once, with a template that renders every example | the catalogue check; `TestAHolderPlacesEveryCombinationOnceWithATemplateThatRendersEveryExample` | +| Pieces are rendered by the holder's template, in place and order, each named by module | `TestABlockIsRenderedByTheHolderInItsPlaceAndOrderNamedByModule` | +| `if-capability` places a contribution only where the machine reported the capability, for text and data alike | `TestAContributionIfACapabilityIsPlacedOnlyWhereTheMachineReportsIt` | +| An offered kind makes no dependency on its seat | `TestAnOfferedBlockMakesNoDependencyOnTheBar` | +| `battery` and `power-meter` are read from the kernel, and a device's battery is not the machine's | the node-engine's `power_test.go` | +| The power draw says `machine` only on battery, never counts an APU's graphics twice, and never wakes a sleeping GPU | the power module's `draw_test.go` | +| Only a rate leaves root, readable by anyone | the power module's `TestOnlyTheRateIsPublishedReadableByAnyone` | +| The bar's template renders a battery and a quoted command, each bar places both places once, and the memory block uses what programs hold | the i3status-rust module's `blocks_test.go` | +| On the machines | the merge gate's composition of every machine, and after rollout the bar's `i3status_rust_blocks` tool on the laptop and a workstation | + +## References + +- [ADR 0208](0208-the-graphical-session-is-one-module-per-piece-on-the-meshs-seats.md) §4, which this extends +- [ADR 0210](0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md), + [ADR 0212](0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md) §1, §3 and §4, whose + general form this widens +- [ADR 0211](0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md), the power seat +- [To-be 42](../03-DESIGN/01-to-be/42-the-machines-modules-in-order.md) diff --git a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md index eb2eec0e..8fae6b40 100644 --- a/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md +++ b/03-DESIGN/01-to-be/42-the-machines-modules-in-order.md @@ -2,7 +2,7 @@ layer: to-be status: in-progress code: [mesh-catalog, mesh-controller, mesh-host] -updated: 2026-10-04 +updated: 2026-10-08 decisions: - 02-DECISIONS/0215-the-machines-message-bus-is-a-node-seat-and-is-never-restarted-live.md - 02-DECISIONS/0173-the-operators-machine-is-the-meshs-and-a-module-is-what-it-declares.md @@ -19,6 +19,7 @@ decisions: - 02-DECISIONS/0212-a-seat-says-what-it-receives-and-the-machines-hotkeys-are-a-seat.md - 02-DECISIONS/0211-a-machines-power-is-a-node-seat-its-moments-take-contributions-and-its-states-are-events.md - 02-DECISIONS/0210-a-tools-configuration-is-its-seat-holders-and-every-other-module-extends-it-through-the-seat.md + - 02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md --- # 42. The machines' modules, in order @@ -118,6 +119,22 @@ laptop model's vendor keys become its contribution. The window-manager fragments clipboard, the wallpaper, the bar and the laptop model become `config` contributions to `node-display-session`. +**Added 2026-10-08.** `node-bar` receives blocks as data ([ADR 0255](../../02-DECISIONS/0255-the-bar-takes-blocks-as-data-from-the-module-that-knows-the-hardware-placed-where-the-machine-has-it.md)). A block +says which bar, which place (`resources` or `status`), an order, and what it shows (`battery`, or one +line a `command` prints). `i3status-rust` renders it with the template its claim gives and places +both places of both bars once. The bar writes no block that depends on the hardware: +- `power` contributes the battery where the node-engine reports `battery`, and the power draw where it + reports `power-meter`. On battery the draw is the machine's discharge; otherwise it is the processor + package's power from its energy counter, plus a discrete GPU's own, and says so. A root sampler + publishes only the rate, so the counter stays root's. +- The laptop model's module contributes no block. A vendor-only figure, such as its charge limit, would + be its block. +- The bar's memory block shows what programs hold, the figure `memory-pressure` judges by. + +The node-engine is delivered first, so the machines report the two capabilities. The controller comes +next, because it parses `data`, `if-capability` and `renders`. The catalogue comes last. Left open: the +draw at the wall (a metered plug or a UPS), a GPU-load block from a generic module, and the headsets. + ## Phase 3 — one machine model The laptop's hardware module (vendor daemon, GPU mode, charge limit, logind, brightness and vendor keys)