mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery delivered
A round in which no PING reached the laptop's console within the 750 ms window was taken as the mesh's answer that nothing is there, and replaced the index: every seat and module call was refused for five seconds at a time. Discovery now waits up to its patience for a first PING, a round that heard nothing is not kept, a runtime not heard this round is resolved from what it announced last (said with its age), and a round that misses runtimes is logged once. A PING carries a digest of what the instance serves, so an unchanged announcement is not pulled again every round (some 880 kB to the laptop).
156 lines
4.9 KiB
Go
156 lines
4.9 KiB
Go
package console
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
)
|
|
|
|
// Tool is a tool as its module — or, for a role's tool, the mesh's records — describes it.
|
|
type Tool struct {
|
|
Module string
|
|
Name string
|
|
Description string
|
|
Input json.RawMessage
|
|
Seat bool
|
|
Scope string
|
|
Subjects []string
|
|
}
|
|
|
|
// Listing is what the mesh could say about its tools; silence is named, never dropped (design 34 §3).
|
|
type Listing struct {
|
|
Tools []Tool
|
|
NotAnswering []string
|
|
}
|
|
|
|
// Seats is the seats and their verbs, so `<seat>.<verb>` resolves to the role.
|
|
type Seats map[string]map[string]bool
|
|
|
|
func seatsIn(l *Listing) Seats {
|
|
out := Seats{}
|
|
for _, t := range l.Tools {
|
|
if !t.Seat {
|
|
continue
|
|
}
|
|
if out[t.Module] == nil {
|
|
out[t.Module] = map[string]bool{}
|
|
}
|
|
out[t.Module][t.Name] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// toolKey is the key a call uses: a role's when the prefix is a seat declaring that verb.
|
|
func toolKey(name string, seats Seats) string {
|
|
if strings.HasPrefix(name, "seat:") {
|
|
return name
|
|
}
|
|
dot := strings.Index(name, ".")
|
|
if dot < 0 {
|
|
return name
|
|
}
|
|
if seats[name[:dot]][name[dot+1:]] {
|
|
return "seat:" + name
|
|
}
|
|
return name
|
|
}
|
|
|
|
// toolsOn is the flat catalogue (MESH_CONSOLE_FLAT=1): what announced itself on the bus, every
|
|
// tool and seat verb, and the assignments with tools that did not (novox/hq ADR 0197).
|
|
func toolsOn(conn *bus.Conn) (*Listing, error) {
|
|
x, err := indexOn(conn, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return x.Listing, nil
|
|
}
|
|
|
|
// callTool calls `<module>.<tool>[@<node>]`, on the subject the listing names for it when it names one.
|
|
func callTool(conn *bus.Conn, key string, args any, seats Seats, l *Listing) (bus.Answered, error) {
|
|
name, node, _ := strings.Cut(key, "@")
|
|
if !strings.Contains(name, ".") {
|
|
return bus.Answered{}, fmt.Errorf("%q does not name a tool: write <module>.<tool>, as `mesh tools` lists "+
|
|
"them, or <module>.<tool>@<node> for the instance on one machine", key)
|
|
}
|
|
resolved := toolKey(name, seats)
|
|
if node != "" {
|
|
resolved += "@" + node
|
|
}
|
|
return conn.Ask(resolved, args, subjectListed(name, node, l))
|
|
}
|
|
|
|
func subjectListed(name, node string, l *Listing) string {
|
|
if l == nil {
|
|
return ""
|
|
}
|
|
dot := strings.Index(name, ".")
|
|
module, tool := name[:dot], name[dot+1:]
|
|
for _, t := range l.Tools {
|
|
if t.Module == module && t.Name == tool && !t.Seat {
|
|
if len(t.Subjects) == 0 {
|
|
return ""
|
|
}
|
|
if node == "" {
|
|
return t.Subjects[0]
|
|
}
|
|
for _, s := range t.Subjects {
|
|
if strings.HasSuffix(s, "."+node) {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
noResponders = regexp.MustCompile(`(?i)no responders|503`)
|
|
// The bus's own two refusals, by their whole phrase. "authorization" alone also matched a tool's own
|
|
// answer that merely mentions the word — the runtime refusing a state value with an Authorization
|
|
// header (novox/hq ADR 0201) read as "this account may not call", which sent the reader the wrong way.
|
|
refused = regexp.MustCompile(`(?i)permissions violation|authorization violation`)
|
|
timedOut = regexp.MustCompile(`(?i)timeout`)
|
|
)
|
|
|
|
// controllerSeat is the seat whose holder keeps what came of every call (novox/hq issue 265).
|
|
const controllerSeat = "mesh-controller"
|
|
|
|
// whyItFailed says why a call failed, so the remedy is in the words.
|
|
func whyItFailed(key string, err error) string {
|
|
if err == nil {
|
|
err = errors.New("failed")
|
|
}
|
|
msg := err.Error()
|
|
switch {
|
|
case noResponders.MatchString(msg):
|
|
extra := ""
|
|
if strings.HasPrefix(key, "seat:") {
|
|
extra = ", or nothing holds that seat"
|
|
}
|
|
return "nothing serves " + key + ". The module may not be assigned to any machine, or it is down" +
|
|
extra + " — `mesh tools` lists what answered."
|
|
case refused.MatchString(msg):
|
|
return "this account may not call " + key + ". What it may call was fixed when it was issued — a " +
|
|
"person's by `operator issue`, the console's by its manifest."
|
|
case timedOut.MatchString(msg):
|
|
// **A timeout is not a failure** (novox/hq issue 265). Something took the call, so it may
|
|
// still be running and may already have done what was asked: a push said this while it pushed.
|
|
said := key + " did not answer within " + bus.RequestTimeout.String() + ". That is not a failure: " +
|
|
"something is serving it, so it may still be running and may already have done what was " +
|
|
"asked — check before repeating a call that changes the mesh."
|
|
if strings.HasPrefix(strings.TrimPrefix(key, "seat:"), controllerSeat+".") {
|
|
// The controller answers every call within seconds, with an id while it runs, so silence
|
|
// from it is an answer lost on the way — which it keeps.
|
|
said += " The controller answers every call within seconds, so its answer was lost on the " +
|
|
"way; " + controllerSeat + ".calls lists its recent calls and what came of each."
|
|
}
|
|
return said
|
|
}
|
|
return key + " failed: " + msg
|
|
}
|