Modules talk over the bus, and the power draw reached the bar through a file. A block may now show state its contributor keeps, the contributor only its own; the holder on the same machine is granted the read without naming the module, and the template sees which machine it renders for.
266 lines
10 KiB
Go
266 lines
10 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// A module contributes to a seat it does not hold (novox/hq ADR 0212).
|
|
//
|
|
// ADR 0210 made a tool's configuration its seat holder's, and every other module's way in a
|
|
// contribution to the seat. The environment, the shell's slots and the power moments each became a
|
|
// field of their own; this is the general form, so a new seat that takes contributions is a row in
|
|
// the seat table rather than a change to the manifest: a contribution names a seat, a kind that
|
|
// seat receives, and text in the tool's own grammar, which the controller never reads.
|
|
|
|
// HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5).
|
|
const HotkeysSeat = "node-hotkeys"
|
|
|
|
// MessageBusSeat is the machine's D-Bus (novox/hq ADR 0215).
|
|
const MessageBusSeat = "node-message-bus"
|
|
|
|
// BackupSeat is the machine's backups (novox/hq ADR 0214, to-be 43).
|
|
const BackupSeat = "node-backup"
|
|
|
|
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
|
|
type SeatContribution struct {
|
|
// Seat is the seat whose holder places it.
|
|
Seat string `json:"seat"`
|
|
// Kind is which of the seat's receivable kinds it is.
|
|
Kind string `json:"kind"`
|
|
// Content is the text, in the tool's own grammar. Never interpreted.
|
|
Content string `json:"content,omitempty"`
|
|
// Data is the piece in the seat's own shape, for a kind the seat receives as data (novox/hq ADR
|
|
// 0255): the holder renders it with its template, and Content is then empty.
|
|
Data map[string]any `json:"data,omitempty"`
|
|
// IfCapability names a capability the machine must report for this contribution to be placed
|
|
// there (novox/hq ADR 0255): a battery's block only where the node-engine found a battery. The
|
|
// composition decides, from the machine's facts, not the tool at run time.
|
|
IfCapability string `json:"if-capability,omitempty"`
|
|
}
|
|
|
|
// ofContribution is where a holder places a kind: ${contribution:<seat>:<kind>}. Loose inside the
|
|
// braces, so a misspelt seat or kind is found and refused rather than written out as text.
|
|
var ofContribution = regexp.MustCompile(`\$\{contribution:([^}]*)\}`)
|
|
|
|
// receivable is what a seat receives of a kind, with the seat's canonical name; false when the seat
|
|
// is unknown or does not receive it.
|
|
func receivable(seat, kind string) (Seat, Receivable, bool) {
|
|
s, known := SeatNamed(seat)
|
|
if !known {
|
|
return Seat{}, Receivable{}, false
|
|
}
|
|
for _, r := range s.Receives {
|
|
if r.Kind == kind {
|
|
return s, r, true
|
|
}
|
|
}
|
|
return s, Receivable{}, false
|
|
}
|
|
|
|
// kindsOf names a seat's receivable kinds for a refusal.
|
|
func kindsOf(s Seat) string {
|
|
if len(s.Receives) == 0 {
|
|
return "it receives no contributions"
|
|
}
|
|
var kinds []string
|
|
for _, r := range s.Receives {
|
|
kinds = append(kinds, r.Kind)
|
|
}
|
|
return "it receives " + strings.Join(kinds, ", ")
|
|
}
|
|
|
|
// seatContributionProblems is what is wrong with this module's contributions, from the manifest
|
|
// alone (novox/hq ADR 0212 §2).
|
|
func (m Manifest) seatContributionProblems() []string {
|
|
var problems []string
|
|
for i, c := range m.Contributions {
|
|
s, r, ok := receivable(c.Seat, c.Kind)
|
|
// A directory a contribution names must be one of this module's own, here rather than on the
|
|
// machine — where a `${dir:x}` nobody declared would reach the holder as the literal text.
|
|
if ok && r.Dirs {
|
|
declared := map[string]string{}
|
|
for _, res := range m.Resources {
|
|
if fmt.Sprint(res["type"]) == "directory" {
|
|
declared[fmt.Sprint(res["id"])] = ""
|
|
}
|
|
}
|
|
if _, err := dirFill(c.Content, declared, m.Module); err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d: %v", m.Module, i+1, err))
|
|
}
|
|
}
|
|
switch {
|
|
case s.Name == "":
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's contribution %d is to the seat %q, which the mesh does not define", m.Module, i+1, c.Seat))
|
|
case !ok:
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's contribution %d to %s is of the kind %q; %s (novox/hq ADR 0212)",
|
|
m.Module, i+1, s.Name, c.Kind, kindsOf(s)))
|
|
}
|
|
// Only on a kind the seat offers (novox/hq ADR 0255): a piece shown where the hardware is. On a
|
|
// kind a holder depends on — what a backup keeps, a key's trigger — a machine missing the
|
|
// capability would lose the piece without a word.
|
|
if c.IfCapability != "" {
|
|
switch {
|
|
case !capabilityName.MatchString(c.IfCapability):
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d is if-capability %q, which is not a "+
|
|
"capability's name", m.Module, i+1, c.IfCapability))
|
|
case !oneOf(KnownCapabilities, c.IfCapability):
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d is if-capability %q, which no machine "+
|
|
"reports; the node-engine detects %s", m.Module, i+1, c.IfCapability, strings.Join(KnownCapabilities, ", ")))
|
|
case ok && !r.Offered:
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d to %s (%s) is if-capability %s; only a "+
|
|
"kind the seat offers may be left out where a machine lacks something (novox/hq ADR 0255)",
|
|
m.Module, i+1, s.Name, c.Kind, c.IfCapability))
|
|
}
|
|
}
|
|
switch {
|
|
case ok && r.Shape != nil:
|
|
problems = append(problems, shapedProblems(m.Module, i, r, c)...)
|
|
problems = append(problems, ownStateProblems(m, i, r, c)...)
|
|
case len(c.Data) > 0:
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d has data; %s receives %s as text, "+
|
|
"given as `content`", m.Module, i+1, s.Name, c.Kind))
|
|
case strings.TrimSpace(c.Content) == "":
|
|
problems = append(problems, fmt.Sprintf("%s's contribution %d has no content", m.Module, i+1))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// seatPlaceholderProblems is what is wrong with one resource's ${contribution:…}: placed only in a
|
|
// file's content, naming a seat and a kind it receives, and only by a module that claims that seat
|
|
// — another would be a second writer of a file there is one of (novox/hq ADR 0212 §3).
|
|
func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
|
|
var problems []string
|
|
for _, field := range sortedKeys(r) {
|
|
v, ok := r[field].(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
found := ofContribution.FindAllStringSubmatch(v, -1)
|
|
if len(found) == 0 {
|
|
continue
|
|
}
|
|
if field != "content" {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s in its %s; contributions are placed only in a file's content",
|
|
m.Module, r["id"], found[0][0], field))
|
|
continue
|
|
}
|
|
for _, f := range found {
|
|
seat, rest, two := strings.Cut(f[1], ":")
|
|
// A kind received as data may be narrowed by its shape's placing fields (novox/hq ADR 0255).
|
|
kind, where, _ := strings.Cut(rest, ":")
|
|
s, rcv, ok := receivable(seat, kind)
|
|
if two && ok {
|
|
if _, err := placeholderWhere(rcv, where); err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s's resource %v names %s: %v", m.Module, r["id"], f[0], err))
|
|
}
|
|
}
|
|
if !two || !ok {
|
|
detail := "the mesh defines no seat " + fmt.Sprintf("%q", seat)
|
|
if s.Name != "" {
|
|
detail = s.Name + ": " + kindsOf(s)
|
|
}
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s; a contribution is ${contribution:<seat>:<kind>} (%s)",
|
|
m.Module, r["id"], f[0], detail))
|
|
continue
|
|
}
|
|
if !m.ClaimsSeat(s.Name) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s's resource %v names %s and %s does not claim %s; every module's contributions to a "+
|
|
"seat are placed by its holder alone (novox/hq ADR 0212)",
|
|
m.Module, r["id"], f[0], m.Module, s.Name))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
|
|
// in module order, each module's pieces in the order it declared them, each module's preceded by a
|
|
// comment line naming it in the tool's grammar, and empty when nothing is contributed. A kind that
|
|
// takes directories has each contributor's `${dir:<id>}` filled with where that module's directory
|
|
// is on this machine (novox/hq to-be 43).
|
|
//
|
|
// A directory placed in a person's home is named `${machine:account-home}/…` (novox/hq to-be 29); in a
|
|
// kind that takes directories that is filled from the machine's facts as well, so the holder reads a
|
|
// path, never a placeholder (novox/hq ADR 0233: an item in the operator's home is backed up too).
|
|
//
|
|
// A kind received as data is rendered through the holder's template instead, narrowed by where
|
|
// (novox/hq ADR 0255); and a contribution naming a capability this machine did not report is left
|
|
// out, of either form.
|
|
//
|
|
// What it could not render is answered as unplaced, each naming its module and why, and left out: one
|
|
// piece the holder's template does not know is that piece's fault, not the machine's.
|
|
func seatContributions(modules []Manifest, holder Manifest, placeholder string, with Rendering, facts map[string]string, caps map[string]bool) (string, []string, error) {
|
|
seat, rest, _ := strings.Cut(placeholder, ":")
|
|
kind, whereText, _ := strings.Cut(rest, ":")
|
|
s, r, ok := receivable(seat, kind)
|
|
if !ok {
|
|
return "", nil, nil
|
|
}
|
|
if r.Shape != nil {
|
|
where, err := placeholderWhere(r, whereText)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
return shapedContributions(modules, holder, facts["name"], s, r, where, caps)
|
|
}
|
|
var failed error
|
|
var b strings.Builder
|
|
for _, m := range inModuleOrder(modules) {
|
|
named := false
|
|
for _, c := range m.allContributions() {
|
|
if c.Kind != kind || !capable(c, caps) {
|
|
continue
|
|
}
|
|
if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name {
|
|
continue
|
|
}
|
|
if !named {
|
|
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
|
|
named = true
|
|
}
|
|
content := c.Content
|
|
if r.Dirs {
|
|
filled, err := dirFill(content, dirsFor(m, with), m.Module)
|
|
if err != nil && failed == nil {
|
|
failed = err
|
|
}
|
|
// An operator's path the module was given, as an item of data on it (novox/hq ADR 0233).
|
|
if accessRef.MatchString(filled) {
|
|
_, byID, err := accessesFor(m, with.Settings[m.Module])
|
|
if err == nil {
|
|
filled, err = accessFill(filled, byID, m.Module)
|
|
}
|
|
if err != nil && failed == nil {
|
|
failed = err
|
|
}
|
|
}
|
|
for _, key := range machineUsed(filled) {
|
|
value, has := facts[key]
|
|
if !has {
|
|
if failed == nil {
|
|
failed = fmt.Errorf("%s's %s for %s says ${machine:%s}, and this machine says %s",
|
|
m.Module, kind, s.Name, key, orNothing(namesOfFacts(facts)))
|
|
}
|
|
continue
|
|
}
|
|
filled = strings.ReplaceAll(filled, "${machine:"+key+"}", value)
|
|
}
|
|
content = filled
|
|
}
|
|
b.WriteString(content)
|
|
if !strings.HasSuffix(content, "\n") {
|
|
b.WriteString("\n")
|
|
}
|
|
}
|
|
}
|
|
return b.String(), nil, failed
|
|
}
|