Files
mesh-delivery/modules/mesh-delivery-view/internal/view/serve.go
T
jschoubben da8384d10e VW5: the view takes each watched branch's source from the gate's verb source, per branch
The view read the gate's state source and its history, fixed to the branch main. Each watched branch's source is now
the seat mesh-delivery's verb source given that branch, asked again on its source-moved; the source row and the event
delivered are per branch, a repository's reach is from the source of its modules' registration's first branch, laid
over the reach the view's own state kept at its start, and view_tiers given no revision answers per branch. The
manifest drops its read of mesh-delivery-gate.source and names the source answer's fields it relies on.

Builds: design 47 G §507, §512, §532, §558, A-173, A-225

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:21:49 +01:00

504 lines
17 KiB
Go

package view
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"time"
)
// ListenSocket listens on the unix socket at path, in a directory of mode 0755, with mode 0666 on the socket: the
// status it carries is no secret and the socket can change nothing.
func ListenSocket(path string) (net.Listener, error) {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
if err := os.Chmod(dir, 0o755); err != nil {
return nil, err
}
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
return nil, err
}
l, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
if err := os.Chmod(path, 0o666); err != nil {
l.Close()
return nil, err
}
return l, nil
}
type ask struct {
Ask string `json:"ask"`
Key string `json:"key,omitempty"`
Option string `json:"option,omitempty"`
Words string `json:"words,omitempty"`
Module string `json:"module,omitempty"`
FP string `json:"fp,omitempty"`
}
// ServeSocket answers {"ask":"rows"} on l, line-delimited JSON, until ctx ends, and {"ask":"answer","key":…,"option":…,"words":…}
// with the model's Answer (the one thing the view writes, issue 1297 B9). Any other ask is refused.
func ServeSocket(ctx context.Context, l net.Listener, m *Model) error {
go func() { <-ctx.Done(); l.Close() }()
for {
c, err := l.Accept()
if err != nil {
if ctx.Err() != nil {
return nil
}
return err
}
go answer(c, m)
}
}
func answer(c net.Conn, m *Model) {
defer c.Close()
c.SetDeadline(time.Now().Add(10 * time.Second))
line, err := bufio.NewReader(io.LimitReader(c, 4096)).ReadBytes('\n')
if err != nil && len(line) == 0 {
return
}
var a ask
if json.Unmarshal(line, &a) != nil || (a.Ask != "rows" && a.Ask != "answer" && a.Ask != "holds" && a.Ask != "hold") {
json.NewEncoder(c).Encode(map[string]string{"error": `refused: this socket answers {"ask":"rows"}, {"ask":"answer"}, {"ask":"holds"} and {"ask":"hold"} and nothing else`})
return
}
switch a.Ask {
case "holds":
json.NewEncoder(c).Encode(m.Holds())
return
case "hold":
r, err := m.AnswerHold(a.Module, a.FP, a.Option, a.Words)
if err != nil {
json.NewEncoder(c).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(c).Encode(r)
return
}
if a.Ask == "answer" {
if err := m.Answer(a.Key, a.Option, a.Words); err != nil {
json.NewEncoder(c).Encode(map[string]string{"error": err.Error()})
return
}
json.NewEncoder(c).Encode(map[string]string{"answered": a.Key})
return
}
json.NewEncoder(c).Encode(m.Snapshot())
}
// AnswerAsk asks the socket at path to write the view's answer to the open ask at key. The view's refusal is the error.
func AnswerAsk(path, key, option, words string) 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))
req, _ := json.Marshal(ask{Ask: "answer", Key: key, Option: option, Words: words})
if _, err := c.Write(append(req, '\n')); err != nil {
return err
}
var reply struct {
Answered string `json:"answered"`
Error string `json:"error"`
}
if err := json.NewDecoder(c).Decode(&reply); err != nil {
return fmt.Errorf("the view's reply is not an answer: %w", err)
}
if reply.Error != "" {
return errors.New(reply.Error)
}
return nil
}
// Ask asks the socket at path for the rows.
func Ask(path string) (Snapshot, error) {
c, err := net.DialTimeout("unix", path, 5*time.Second)
if err != nil {
return Snapshot{}, fmt.Errorf("cannot reach the view at %s: %w", path, err)
}
defer c.Close()
c.SetDeadline(time.Now().Add(10 * time.Second))
if _, err := c.Write([]byte(`{"ask":"rows"}` + "\n")); err != nil {
return Snapshot{}, err
}
var reply struct {
Snapshot
Error string `json:"error"`
}
if err := json.NewDecoder(c).Decode(&reply); err != nil {
return Snapshot{}, fmt.Errorf("the view's answer is not rows: %w", err)
}
if reply.Error != "" {
return Snapshot{}, errors.New(reply.Error)
}
return reply.Snapshot, nil
}
func since(t time.Time) string {
if t.IsZero() {
return ""
}
return t.Local().Format("2006-01-02 15:04:05")
}
func rowText(r Row) []string {
what := r.Waits
if r.Error != "" {
what = "unreadable: " + r.Error
}
if (r.Status == StatusLeftBehind || r.Status == StatusRemoved) && what == "" {
what = r.Status
} else if r.Status == StatusLeftBehind || r.Status == StatusRemoved {
what = r.Status + "; " + what
}
return []string{r.Node, r.Module, r.Assigned, r.Have, r.Phase, r.Health, since(r.Since), r.Detail, what}
}
// groupText is one line per pull request of a group: the branch, `<repository>#<number>`, its head, its modules.
func groupText(s Snapshot) [][]string {
var out [][]string
for _, g := range s.Groups {
for i, p := range g.Pulls {
group := ""
if i == 0 {
group = g.Group
}
out = append(out, []string{group, fmt.Sprintf("%s#%d", p.Repository, p.Number), p.Head, strings.Join(p.Modules, ", ")})
}
}
return out
}
// fingerprintText is one line per module: the indexer's fingerprint beside the build's, or the reason it has none.
func fingerprintText(s Snapshot) [][]string {
var out [][]string
for _, f := range s.Fingerprints {
what := f.Indexer
switch f.State {
case FPDiffers:
what = "indexer " + f.Indexer + "; build " + f.Build
case FPNone:
what = f.Reason
case FPNoBuild:
what = "indexer " + f.Indexer + "; no successful trunk build recorded a fingerprint"
case FPBuildFailed, FPBuilding:
what = "indexer " + f.Indexer + "; builder: " + f.Reason
case FPNoKey:
what = "indexer " + f.Indexer + "; the builder holds no key of it"
}
out = append(out, []string{f.Module, f.State, what})
}
return out
}
// askText is one line per open ask: its key, headline, words on one line, the pull requests it covers and how to answer.
func askText(s Snapshot) [][]string {
var out [][]string
for _, a := range s.Asks {
how := a.How
switch {
case a.Chosen != "":
how = "answered " + a.Chosen + " in the view; the teller fills it"
case len(a.Options) > 0:
how += ": " + strings.Join(a.Options, " or ")
}
out = append(out, []string{a.Key, a.Headline, strings.Join(strings.Fields(strings.ReplaceAll(a.Words, "\n", " / ")), " "), strings.Join(a.Pulls, ", "), how})
}
return out
}
// wantText is one line per want: the node, the module, what the assigner wants, what the node runs, where the two stand and the
// assigner's words (the rule that holds a node back, when one does).
func wantText(s Snapshot) [][]string {
var out [][]string
for _, w := range s.Wants {
want, have := w.Want, w.Have
if want == "" {
want = "none"
}
if have == "" {
have = "none"
}
words := w.Words
if w.Rule != 0 {
words = fmt.Sprintf("rule %d: %s", w.Rule, w.Words)
}
judged := w.Judgement
if judged == "" {
judged = "none"
} else if w.Judged != "" {
words = strings.TrimSpace(words + " " + w.Judged)
}
out = append(out, []string{w.Node, w.Module, want, have, w.State, judged, words})
}
return out
}
// sourceText is each watched branch's source revision's derived state in one line, then one line for each module on a node it
// waits for.
func sourceText(s Snapshot) []string {
var out []string
for _, src := range s.Sources {
out = append(out, fmt.Sprintf("%s: source revision %d: %s", src.Branch, src.Revision, src.State))
out = append(out, src.Waiting...)
}
return out
}
// backlogText is one line per language and kind: its waiting asks, the asks held by a node, the oldest waiting ask's age.
func backlogText(s Snapshot) [][]string {
var out [][]string
for _, b := range s.Backlog {
age := ""
if !b.Oldest.IsZero() {
age = (time.Duration(b.OldestSecs) * time.Second).String() + " (since " + since(b.Oldest) + ")"
}
if b.Error != "" {
age = "unreadable: " + b.Error
}
out = append(out, []string{b.Language, b.Kind, fmt.Sprint(b.Waiting), fmt.Sprint(b.Held), age})
}
return out
}
var backlogHead = []string{"LANGUAGE", "KIND", "WAITING", "HELD", "OLDEST WAITING"}
var wantHead = []string{"NODE", "MODULE", "WANT", "HAVE", "STATE", "MOVE JUDGED", "WORDS"}
var head = []string{"NODE", "MODULE", "ASSIGNED", "HAVE", "PHASE", "HEALTH", "SINCE", "DETAIL", "WAITS"}
// Table prints the snapshot as a table.
func Table(w io.Writer, s Snapshot) {
tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0)
row := func(cells []string) {
for i, c := range cells {
if i > 0 {
fmt.Fprint(tw, "\t")
}
fmt.Fprint(tw, c)
}
fmt.Fprintln(tw)
}
row(head)
for _, r := range s.Rows {
row(rowText(r))
}
if len(s.Nodes) > 0 {
fmt.Fprintln(tw)
row([]string{"NODE", "WINDOWS", "BUSY", "STRAYS", "SINCE"})
for _, n := range s.Nodes {
row([]string{n.Node, fmt.Sprint(n.Windows), fmt.Sprint(n.Busy), fmt.Sprint(n.Strays), since(n.Since)})
}
}
if g := groupText(s); len(g) > 0 {
fmt.Fprintln(tw)
row([]string{"CANDIDATES", "PULL REQUEST", "HEAD", "MODULES"})
for _, cells := range g {
row(cells)
}
}
if a := askText(s); len(a) > 0 {
fmt.Fprintln(tw)
row([]string{"ASKS", "HEADLINE", "WORDS", "PULL REQUESTS", "ANSWER"})
for _, cells := range a {
row(cells)
}
}
if b := backlogText(s); len(b) > 0 {
fmt.Fprintln(tw)
row([]string{"BACKLOG OF BUILD ASKS"})
row(backlogHead)
for _, cells := range b {
row(cells)
}
}
if src := sourceText(s); len(src) > 0 {
fmt.Fprintln(tw)
for _, line := range src {
row([]string{line})
}
}
if d := wantText(s); len(d) > 0 {
fmt.Fprintln(tw)
row([]string{"WANTS AGAINST HAVES"})
row(wantHead)
for _, cells := range d {
row(cells)
}
}
if f := fingerprintText(s); len(f) > 0 {
fmt.Fprintln(tw)
row([]string{"MODULE", "FINGERPRINT", "INDEXER AGAINST BUILD"})
for _, cells := range f {
row(cells)
}
}
if len(s.Offline) > 0 {
fmt.Fprintln(tw)
row([]string{"OFFLINE", "NAME", "STATE", "SINCE"})
for _, o := range s.Offline {
row([]string{o.Kind, o.name(), "offline", since(o.Since)})
}
}
if len(s.Stuck) > 0 {
fmt.Fprintln(tw)
row([]string{"STUCK", "KEY", "WAITED FOR", "SINCE"})
for _, k := range s.Stuck {
row([]string{k.Bucket, k.Key, k.Waited, since(k.Since)})
}
}
if len(s.Unreadable) > 0 {
fmt.Fprintln(tw)
row([]string{"UNREADABLE", "KEY", "WHY"})
for _, u := range s.Unreadable {
row([]string{u.Bucket, u.Key, u.Error})
}
}
tw.Flush()
}
func (o OfflineRow) name() string {
if o.Kind == "part" {
return o.Part + "." + o.Instance
}
return o.Node
}
var page = template.Must(template.New("p").Parse(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="5">
<title>Delivery</title><style>body{font:14px sans-serif;margin:1em}table{border-collapse:collapse}td,th{padding:2px 10px;text-align:left}</style></head>
<body><h1>What each node runs</h1><table><tr>{{range .Head}}<th>{{.}}</th>{{end}}</tr>
{{range .Rows}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>
{{if .Groups}}<h2>CANDIDATES</h2><table><tr><th>GROUP</th><th>PULL REQUEST</th><th>HEAD</th><th>MODULES</th></tr>{{range .Groups}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Asks}}<h2>ASKS</h2><table><tr><th>ASK</th><th>HEADLINE</th><th>WORDS</th><th>PULL REQUESTS</th><th>ANSWER</th></tr>{{range .Asks}}<tr>{{range .Cells}}<td>{{.}}</td>{{end}}<td>{{if .Options}}<form method="post" action="/answer"><input type="hidden" name="key" value="{{.Key}}"><input name="words" placeholder="words (optional)">{{range .Options}} <button name="option" value="{{.}}">{{.}}</button>{{end}}</form>{{end}}</td></tr>{{end}}</table>{{end}}
{{if .Backlog}}<h2>BACKLOG OF BUILD ASKS</h2><table><tr>{{range .BacklogHead}}<th>{{.}}</th>{{end}}</tr>{{range .Backlog}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Source}}<h2>SOURCE REVISION</h2><p>{{range .Source}}{{.}}<br>{{end}}</p>{{end}}{{if .Wants}}<h2>WANTS AGAINST HAVES</h2><table><tr>{{range .WantHead}}<th>{{.}}</th>{{end}}</tr>{{range .Wants}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Fingerprints}}<h2>FINGERPRINTS</h2><table><tr><th>MODULE</th><th>STATE</th><th>INDEXER AGAINST BUILD</th></tr>{{range .Fingerprints}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Offline}}<h2>OFFLINE</h2><table>{{range .Offline}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Stuck}}<h2>STUCK</h2><table><tr><th>BUCKET</th><th>KEY</th><th>WAITED FOR</th><th>SINCE</th></tr>{{range .Stuck}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}
{{if .Unreadable}}<h2>UNREADABLE</h2><table>{{range .Unreadable}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>{{end}}</body></html>`))
var wantsPage = template.Must(template.New("w").Parse(`<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="5">
<title>Wants against haves</title><style>body{font:14px sans-serif;margin:1em}table{border-collapse:collapse}td,th{padding:2px 10px;text-align:left}</style></head>
<body><p><a href="/">What each node runs</a></p><h1>WANTS AGAINST HAVES</h1>
{{if .Wants}}<table><tr>{{range .Head}}<th>{{.}}</th>{{end}}</tr>{{range .Wants}}<tr>{{range .}}<td>{{.}}</td>{{end}}</tr>{{end}}</table>
{{else}}<p>no difference: every want is what the controller sends.</p>{{end}}</body></html>`))
// Web is the read-only web view of the same rows, refreshed by the browser.
func Web(m *Model) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/answer" && r.Method == http.MethodPost {
answerForm(m, w, r)
return
}
if r.URL.Path == "/answer" {
w.Header().Set("Allow", "POST")
http.Error(w, "an answer is posted from the form of the page", http.StatusMethodNotAllowed)
return
}
if r.Method != http.MethodGet && r.Method != http.MethodHead {
w.Header().Set("Allow", "GET, HEAD")
http.Error(w, "this view is read-only", http.StatusMethodNotAllowed)
return
}
if r.URL.Path == "/wants" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
wantsPage.Execute(w, map[string]any{"Head": wantHead, "Wants": wantText(m.Snapshot())})
return
}
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
var rows [][]string
for _, row := range m.Snapshot().Rows {
rows = append(rows, rowText(row))
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
snap := m.Snapshot()
var off, stuck, bad [][]string
for _, o := range snap.Offline {
off = append(off, []string{o.Kind, o.name(), "offline", since(o.Since)})
}
for _, k := range snap.Stuck {
stuck = append(stuck, []string{k.Bucket, k.Key, k.Waited, since(k.Since)})
}
for _, u := range snap.Unreadable {
bad = append(bad, []string{u.Bucket, u.Key, u.Error})
}
page.Execute(w, map[string]any{"Head": head, "Rows": rows, "Groups": groupText(snap), "Asks": askPage(snap), "Fingerprints": fingerprintText(snap), "Wants": wantText(snap), "Source": sourceText(snap), "Backlog": backlogText(snap), "BacklogHead": backlogHead, "WantHead": wantHead, "Offline": off, "Stuck": stuck, "Unreadable": bad})
})
}
// askPageRow is an open ask on the page: its cells, and the options a form offers when the view may answer it.
type askPageRow struct {
Cells []string
Key string
Options []string
}
func askPage(s Snapshot) []askPageRow {
var out []askPageRow
cells := askText(s)
for i, a := range s.Asks {
row := askPageRow{Cells: cells[i], Key: a.Key}
if a.Chosen == "" {
row.Options = a.Options
}
out = append(out, row)
}
return out
}
// loopback says whether a Host header names the machine itself, as the web view listens on 127.0.0.1 only: a page of another
// origin, or a name an attacker's DNS points here, writes nothing.
func loopback(host string) bool {
h, _, err := net.SplitHostPort(host)
if err != nil {
h = host
}
return h == "127.0.0.1" || h == "localhost" || h == "[::1]" || h == "::1"
}
// answerForm is the one write the web view makes: POST /answer with key, option and words, from a form of this page.
func answerForm(m *Model, w http.ResponseWriter, r *http.Request) {
if !loopback(r.Host) {
http.Error(w, "this view answers only on its own address", http.StatusForbidden)
return
}
if o := r.Header.Get("Origin"); o != "" && o != "http://"+r.Host {
http.Error(w, "this view answers only a form of its own page", http.StatusForbidden)
return
}
if r.Header.Get("Sec-Fetch-Site") == "cross-site" {
http.Error(w, "this view answers only a form of its own page", http.StatusForbidden)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 8192)
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := m.Answer(r.PostFormValue("key"), r.PostFormValue("option"), r.PostFormValue("words")); err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}