A hold answered keep on the phone could not be changed: its ask ended with the answer, nothing listed it, and a kept hold is never lifted by itself. The view lists every hold with its node, cause and answer (mesh-delivery-cli holds) and answers the standing hold ask with hold-release or hold-keep through its one write; the teller takes a later view answer that changes a hold's option into the answer of that asking, never an older one.
189 lines
6.8 KiB
Go
189 lines
6.8 KiB
Go
package view
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The assigner's holds, listed and answered from the operator's command (issue 1906 note 23). A hold ask answered once (on the
|
|
// phone or in the view) ended with that answer, so nothing listed it any more and an answer "keep" could not be changed: the
|
|
// operator kept systemd-resolved held and then could not release it, and a kept hold is never lifted by itself. Now the view
|
|
// lists every hold with its node, its cause and its answer, and answers the hold ask that stands, again if the operator changes
|
|
// their mind, through the view's one write (`<ask key>.view`), which the teller takes into the ask's answer. Nothing else writes:
|
|
// the answer still reaches the assigner only through the teller (mesh-delivery-teller viewanswers.go).
|
|
|
|
// The options of the hold ask (mesh-delivery-assigner internal/assign asks.go HoldAsk).
|
|
const (
|
|
HoldRelease = "release"
|
|
HoldKeep = "keep"
|
|
)
|
|
|
|
type heldEntry struct {
|
|
module, fp string
|
|
value delivery.Held
|
|
err string
|
|
at time.Time
|
|
}
|
|
|
|
// applyHeld takes a change of a held key of the assigner's wants, and says whether the key was one.
|
|
func (m *Model) applyHeld(e Entry) bool {
|
|
module, fp, err := delivery.ParseHeldKey(e.Key)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if e.Deleted {
|
|
delete(m.holds, e.Key)
|
|
return true
|
|
}
|
|
h, err := delivery.DecodeHeld(e.Value)
|
|
m.holds[e.Key] = heldEntry{module: module, fp: fp, value: h, err: errText(err), at: e.At}
|
|
return true
|
|
}
|
|
|
|
// HoldRow is one fingerprint the assigner holds: the module, the fingerprint, the node whose health check failed, the hold's
|
|
// words (its cause) and since when; the ask about it (its key and the asking's id, empty when the assigner asks none), and the
|
|
// answer that stands for that asking (the option and its words, empty when none), with the view's own answer when it waits for
|
|
// the teller.
|
|
type HoldRow struct {
|
|
Module string `json:"module"`
|
|
FP string `json:"fp"`
|
|
Node string `json:"node,omitempty"`
|
|
Words string `json:"words,omitempty"`
|
|
Since time.Time `json:"since,omitempty"`
|
|
AskKey string `json:"ask-key,omitempty"`
|
|
Asking string `json:"asking,omitempty"`
|
|
Answer string `json:"answer,omitempty"`
|
|
Answered string `json:"answered,omitempty"`
|
|
Pending string `json:"pending,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Holds is every hold, ordered by module and fingerprint.
|
|
func (m *Model) Holds() []HoldRow {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := []HoldRow{}
|
|
for _, h := range m.holds {
|
|
row := HoldRow{Module: h.module, FP: h.fp, Node: h.value.Node, Words: h.value.Words, Since: h.at, Error: h.err}
|
|
if key, err := delivery.HoldAskKey(h.module, h.fp); err == nil {
|
|
if a, ok := m.aasks[key]; ok && a.err == "" {
|
|
row.AskKey, row.Asking = key, a.ask.ID
|
|
if ans, ok := m.answerOf[key]; ok && m.answers[key] == a.ask.ID {
|
|
row.Answer, row.Answered = ans.Option, ans.Words
|
|
}
|
|
if c := m.chosenOf(key, a.ask); c != "" && c != row.Answer {
|
|
row.Pending = c
|
|
}
|
|
}
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Module != out[j].Module {
|
|
return out[i].Module < out[j].Module
|
|
}
|
|
return out[i].FP < out[j].FP
|
|
})
|
|
return out
|
|
}
|
|
|
|
// AnswerHold writes the view's answer to the hold ask of module at the fingerprint fp: release or keep, with the operator's
|
|
// words. fp is the whole fingerprint, or the start of its hex (as the rows print it), naming one hold of the module. Unlike
|
|
// Answer it answers an ask answered already, so the operator can change their mind; an answer equal to the one that stands is
|
|
// refused, as is a hold the assigner asks nothing about.
|
|
func (m *Model) AnswerHold(module, fp, option, words string) (HoldRow, error) {
|
|
option = strings.TrimSpace(option)
|
|
if option != HoldRelease && option != HoldKeep {
|
|
return HoldRow{}, fmt.Errorf("a hold is answered %s or %s, not %q", HoldRelease, HoldKeep, option)
|
|
}
|
|
var match []HoldRow
|
|
want := strings.TrimPrefix(strings.TrimSpace(fp), delivery.SourcePrefix2)
|
|
for _, r := range m.Holds() {
|
|
if r.Module == module && want != "" && strings.HasPrefix(strings.TrimPrefix(r.FP, delivery.SourcePrefix2), want) {
|
|
match = append(match, r)
|
|
}
|
|
}
|
|
switch {
|
|
case len(match) == 0:
|
|
return HoldRow{}, fmt.Errorf("the assigner holds no fingerprint %q of %s (mesh-delivery-cli holds lists them)", fp, module)
|
|
case len(match) > 1:
|
|
return HoldRow{}, fmt.Errorf("%q names %d holds of %s: give more of the fingerprint", fp, len(match), module)
|
|
}
|
|
r := match[0]
|
|
if r.AskKey == "" {
|
|
return r, fmt.Errorf("the assigner asks nothing about the hold of %s at %s, so there is nothing to answer", module, r.FP)
|
|
}
|
|
if r.Answer == option && r.Pending == "" {
|
|
return r, fmt.Errorf("the hold of %s at %s is answered %s already", module, r.FP, option)
|
|
}
|
|
m.mu.Lock()
|
|
a, w := m.aasks[r.AskKey], m.writer
|
|
m.mu.Unlock()
|
|
if !slices.ContainsFunc(a.ask.Options, func(o asks.Option) bool { return o.ID == option }) {
|
|
return r, fmt.Errorf("the hold ask %s offers no option %q", r.AskKey, option)
|
|
}
|
|
if w == nil {
|
|
return r, fmt.Errorf("the view cannot write its answers here")
|
|
}
|
|
key, err := delivery.ViewAnswerKey(r.AskKey)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
body, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: a.ask.ID, Option: option, Words: strings.TrimSpace(words)})
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
if err := w.Put(key, body); err != nil {
|
|
return r, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// askOn sends one ask to the socket at path and decodes the reply into out; the view's refusal is the error.
|
|
func askOn(path string, req ask, out any) error {
|
|
c, err := net.DialTimeout("unix", path, 5*time.Second)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot reach the view at %s: %w", path, err)
|
|
}
|
|
defer c.Close()
|
|
c.SetDeadline(time.Now().Add(10 * time.Second))
|
|
raw, _ := json.Marshal(req)
|
|
if _, err := c.Write(append(raw, '\n')); err != nil {
|
|
return err
|
|
}
|
|
var reply json.RawMessage
|
|
if err := json.NewDecoder(c).Decode(&reply); err != nil {
|
|
return fmt.Errorf("the view's reply cannot be read: %w", err)
|
|
}
|
|
var refusal struct {
|
|
Error string `json:"error"`
|
|
}
|
|
if json.Unmarshal(reply, &refusal) == nil && refusal.Error != "" {
|
|
return errors.New(refusal.Error)
|
|
}
|
|
return json.Unmarshal(reply, out)
|
|
}
|
|
|
|
// AskHolds asks the socket at path for every hold.
|
|
func AskHolds(path string) ([]HoldRow, error) {
|
|
var out []HoldRow
|
|
err := askOn(path, ask{Ask: "holds"}, &out)
|
|
return out, err
|
|
}
|
|
|
|
// AnswerHoldOn asks the socket at path to answer the hold ask of module at fp with option.
|
|
func AnswerHoldOn(path, module, fp, option, words string) (HoldRow, error) {
|
|
var out HoldRow
|
|
err := askOn(path, ask{Ask: "hold", Module: module, FP: fp, Option: option, Words: words}, &out)
|
|
return out, err
|
|
}
|