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 (`.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 }