From d83024a63fe6f7ef6836ece13f13cf748014ebb9 Mon Sep 17 00:00:00 2001 From: David Anderson Date: Fri, 7 Mar 2025 08:18:33 -0800 Subject: [PATCH] util/eventbus: add a debug HTTP handler for the bus Updates #15160 Signed-off-by: David Anderson --- util/eventbus/assets/event.html | 6 + util/eventbus/assets/htmx-websocket.min.js.gz | Bin 0 -> 4249 bytes util/eventbus/assets/htmx.min.js.gz | Bin 0 -> 16409 bytes util/eventbus/assets/main.html | 97 +++++++ util/eventbus/assets/monitor.html | 5 + util/eventbus/assets/style.css | 90 +++++++ util/eventbus/bus.go | 4 +- util/eventbus/debug.go | 11 +- util/eventbus/debughttp.go | 238 ++++++++++++++++++ util/eventbus/fetch-htmx.go | 93 +++++++ 10 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 util/eventbus/assets/event.html create mode 100644 util/eventbus/assets/htmx-websocket.min.js.gz create mode 100644 util/eventbus/assets/htmx.min.js.gz create mode 100644 util/eventbus/assets/main.html create mode 100644 util/eventbus/assets/monitor.html create mode 100644 util/eventbus/assets/style.css create mode 100644 util/eventbus/debughttp.go create mode 100644 util/eventbus/fetch-htmx.go diff --git a/util/eventbus/assets/event.html b/util/eventbus/assets/event.html new file mode 100644 index 000000000..8e016f583 --- /dev/null +++ b/util/eventbus/assets/event.html @@ -0,0 +1,6 @@ +
  • +
    + {{.Count}}: {{.Type}} from {{.Event.From.Name}}, {{len .Event.To}} recipients + {{.Event.Event}} +
    +
  • diff --git a/util/eventbus/assets/htmx-websocket.min.js.gz b/util/eventbus/assets/htmx-websocket.min.js.gz new file mode 100644 index 0000000000000000000000000000000000000000..4ed53be492425280da60f662a48eebee9cc8e0b1 GIT binary patch literal 4249 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

    #5>|N2udB*J1?oKb5L)OY8MKTGhNR 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 literal 0 HcmV?d00001 diff --git a/util/eventbus/assets/main.html b/util/eventbus/assets/main.html new file mode 100644 index 000000000..51d6b22ad --- /dev/null +++ b/util/eventbus/assets/main.html @@ -0,0 +1,97 @@ + + + + + + + + +

    Event bus

    + +
    +

    General

    + {{with $.PublishQueue}} + {{len .}} pending + {{end}} + + +
    + +
    +

    Clients

    + + + + + + + + + + + {{range .Clients}} + + + + + + + {{end}} +
    NamePublishingSubscribingPending
    {{.Name}} +
      + {{range .Publish}} +
    • {{.}}
    • + {{end}} +
    +
    +
      + {{range .Subscribe}} +
    • {{.}}
    • + {{end}} +
    +
    + {{len ($.SubscribeQueue .Client)}} +
    +
    + +
    +

    Types

    + + {{range .Types}} + +
    +

    {{.Name}}

    +

    Definition

    + {{prettyPrintStruct .}} + +

    Published by:

    + {{if len (.Publish)}} +
      + {{range .Publish}} +
    • {{.Name}}
    • + {{end}} +
    + {{else}} +
      +
    • No publishers.
    • +
    + {{end}} + +

    Received by:

    + {{if len (.Subscribe)}} +
      + {{range .Subscribe}} +
    • {{.Name}}
    • + {{end}} +
    + {{else}} +
      +
    • No subscribers.
    • +
    + {{end}} +
    + {{end}} + +
    + + diff --git a/util/eventbus/assets/monitor.html b/util/eventbus/assets/monitor.html new file mode 100644 index 000000000..1af5bdce6 --- /dev/null +++ b/util/eventbus/assets/monitor.html @@ -0,0 +1,5 @@ +
    +
      +
    + +
    diff --git a/util/eventbus/assets/style.css b/util/eventbus/assets/style.css new file mode 100644 index 000000000..690bd4f17 --- /dev/null +++ b/util/eventbus/assets/style.css @@ -0,0 +1,90 @@ +/* CSS reset, thanks Josh Comeau: https://www.joshwcomeau.com/css/custom-css-reset/ */ +*, *::before, *::after { box-sizing: border-box; } +* { margin: 0; } +input, button, textarea, select { font: inherit; } +p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; } +p { text-wrap: pretty; } +h1, h2, h3, h4, h5, h6 { text-wrap: balance; } +#root, #__next { isolation: isolate; } +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +/* Local styling begins */ + +body { + padding: 12px; +} + +div { + width: 100%; +} + +section { + display: flex; + flex-direction: column; + flex-gap: 6px; + align-items: flex-start; + padding: 12px 0; +} + +section > * { + margin-left: 24px; +} + +section > h2, section > h3 { + margin-left: 0; + padding-bottom: 6px; + padding-top: 12px; +} + +details { + padding-bottom: 12px; +} + +table { + table-layout: fixed; + width: calc(100% - 48px); + border-collapse: collapse; + border: 1px solid black; +} + +th, td { + padding: 12px; + border: 1px solid black; +} + +td.list { + vertical-align: top; +} + +ul { + list-style: none; +} + +td ul { + margin: 0; + padding: 0; +} + +code { + padding: 12px; + white-space: pre; +} + +#monitor { + width: calc(100% - 48px); + resize: vertical; + padding: 12px; + overflow: scroll; + height: 15lh; + border: 1px inset; + min-height: 1em; + display: flex; + flex-direction: column-reverse; +} diff --git a/util/eventbus/bus.go b/util/eventbus/bus.go index 96cafc98b..45d12da2f 100644 --- a/util/eventbus/bus.go +++ b/util/eventbus/bus.go @@ -73,8 +73,8 @@ func (b *Bus) Client(name string) *Client { } // Debugger returns the debugging facility for the bus. -func (b *Bus) Debugger() Debugger { - return Debugger{b} +func (b *Bus) Debugger() *Debugger { + return &Debugger{b} } // Close closes the bus. Implicitly closes all clients, publishers and diff --git a/util/eventbus/debug.go b/util/eventbus/debug.go index 31123e6ba..832d72ac0 100644 --- a/util/eventbus/debug.go +++ b/util/eventbus/debug.go @@ -4,11 +4,14 @@ package eventbus import ( + "cmp" "fmt" "reflect" "slices" "sync" "sync/atomic" + + "tailscale.com/tsweb" ) // A Debugger offers access to a bus's privileged introspection and @@ -29,7 +32,11 @@ type Debugger struct { // Clients returns a list of all clients attached to the bus. func (d *Debugger) Clients() []*Client { - return d.bus.listClients() + ret := d.bus.listClients() + slices.SortFunc(ret, func(a, b *Client) int { + return cmp.Compare(a.Name(), b.Name()) + }) + return ret } // PublishQueue returns the contents of the publish queue. @@ -130,6 +137,8 @@ func (d *Debugger) SubscribeTypes(client *Client) []reflect.Type { return client.subscribeTypes() } +func (d *Debugger) RegisterHTTP(td *tsweb.DebugHandler) { registerHTTPDebugger(d, td) } + // A hook collects hook functions that can be run as a group. type hook[T any] struct { sync.Mutex diff --git a/util/eventbus/debughttp.go b/util/eventbus/debughttp.go new file mode 100644 index 000000000..bbd929efb --- /dev/null +++ b/util/eventbus/debughttp.go @@ -0,0 +1,238 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package eventbus + +import ( + "bytes" + "cmp" + "embed" + "fmt" + "html/template" + "io" + "io/fs" + "log" + "net/http" + "path/filepath" + "reflect" + "slices" + "strings" + "sync" + + "github.com/coder/websocket" + "tailscale.com/tsweb" +) + +type httpDebugger struct { + *Debugger +} + +func registerHTTPDebugger(d *Debugger, td *tsweb.DebugHandler) { + dh := httpDebugger{d} + td.Handle("bus", "Event bus", dh) + td.HandleSilent("bus/monitor", http.HandlerFunc(dh.serveMonitor)) + td.HandleSilent("bus/style.css", serveStatic("style.css")) + td.HandleSilent("bus/htmx.min.js", serveStatic("htmx.min.js.gz")) + td.HandleSilent("bus/htmx-websocket.min.js", serveStatic("htmx-websocket.min.js.gz")) +} + +//go:embed assets/*.html +var templatesSrc embed.FS + +var templates = sync.OnceValue(func() *template.Template { + d, err := fs.Sub(templatesSrc, "assets") + if err != nil { + panic(fmt.Errorf("getting eventbus debughttp templates subdir: %w", err)) + } + ret := template.New("").Funcs(map[string]any{ + "prettyPrintStruct": prettyPrintStruct, + }) + return template.Must(ret.ParseFS(d, "*")) +}) + +//go:generate go run fetch-htmx.go + +//go:embed assets/*.css assets/*.min.js.gz +var static embed.FS + +func serveStatic(name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasSuffix(name, ".css"): + w.Header().Set("Content-Type", "text/css") + case strings.HasSuffix(name, ".min.js.gz"): + w.Header().Set("Content-Type", "text/javascript") + w.Header().Set("Content-Encoding", "gzip") + case strings.HasSuffix(name, ".js"): + w.Header().Set("Content-Type", "text/javascript") + default: + http.Error(w, "not found", http.StatusNotFound) + return + } + + f, err := static.Open(filepath.Join("assets", name)) + if err != nil { + http.Error(w, fmt.Sprintf("opening asset: %v", err), http.StatusInternalServerError) + return + } + defer f.Close() + if _, err := io.Copy(w, f); err != nil { + http.Error(w, fmt.Sprintf("serving asset: %v", err), http.StatusInternalServerError) + return + } + }) +} + +func render(w http.ResponseWriter, name string, data any) { + err := templates().ExecuteTemplate(w, name+".html", data) + if err != nil { + err := fmt.Errorf("rendering template: %v", err) + log.Print(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (h httpDebugger) ServeHTTP(w http.ResponseWriter, r *http.Request) { + type clientInfo struct { + *Client + Publish []reflect.Type + Subscribe []reflect.Type + } + type typeInfo struct { + reflect.Type + Publish []*Client + Subscribe []*Client + } + type info struct { + *Debugger + Clients map[string]*clientInfo + Types map[string]*typeInfo + } + + data := info{ + Debugger: h.Debugger, + Clients: map[string]*clientInfo{}, + Types: map[string]*typeInfo{}, + } + + getTypeInfo := func(t reflect.Type) *typeInfo { + if data.Types[t.Name()] == nil { + data.Types[t.Name()] = &typeInfo{ + Type: t, + } + } + return data.Types[t.Name()] + } + + for _, c := range h.Clients() { + ci := &clientInfo{ + Client: c, + Publish: h.PublishTypes(c), + Subscribe: h.SubscribeTypes(c), + } + slices.SortFunc(ci.Publish, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) }) + slices.SortFunc(ci.Subscribe, func(a, b reflect.Type) int { return cmp.Compare(a.Name(), b.Name()) }) + data.Clients[c.Name()] = ci + + for _, t := range ci.Publish { + ti := getTypeInfo(t) + ti.Publish = append(ti.Publish, c) + } + for _, t := range ci.Subscribe { + ti := getTypeInfo(t) + ti.Subscribe = append(ti.Subscribe, c) + } + } + + render(w, "main", data) +} + +func (h httpDebugger) serveMonitor(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Upgrade") == "websocket" { + h.serveMonitorStream(w, r) + return + } + + render(w, "monitor", nil) +} + +func (h httpDebugger) serveMonitorStream(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, nil) + if err != nil { + return + } + defer conn.CloseNow() + wsCtx := conn.CloseRead(r.Context()) + + mon := h.WatchBus() + defer mon.Close() + + i := 0 + for { + select { + case <-r.Context().Done(): + return + case <-wsCtx.Done(): + return + case <-mon.Done(): + return + case event := <-mon.Events(): + msg, err := conn.Writer(r.Context(), websocket.MessageText) + if err != nil { + return + } + data := map[string]any{ + "Count": i, + "Type": reflect.TypeOf(event.Event), + "Event": event, + } + i++ + if err := templates().ExecuteTemplate(msg, "event.html", data); err != nil { + log.Println(err) + return + } + if err := msg.Close(); err != nil { + return + } + } + } +} + +func prettyPrintStruct(t reflect.Type) string { + if t.Kind() != reflect.Struct { + return t.String() + } + var rec func(io.Writer, int, reflect.Type) + rec = func(out io.Writer, indent int, t reflect.Type) { + ind := strings.Repeat(" ", indent) + fmt.Fprintf(out, "%s", t.String()) + fs := collectFields(t) + if len(fs) > 0 { + io.WriteString(out, " {\n") + for _, f := range fs { + fmt.Fprintf(out, "%s %s ", ind, f.Name) + if f.Type.Kind() == reflect.Struct { + rec(out, indent+1, f.Type) + } else { + fmt.Fprint(out, f.Type) + } + io.WriteString(out, "\n") + } + fmt.Fprintf(out, "%s}", ind) + } + } + + var ret bytes.Buffer + rec(&ret, 0, t) + return ret.String() +} + +func collectFields(t reflect.Type) (ret []reflect.StructField) { + for _, f := range reflect.VisibleFields(t) { + if !f.IsExported() { + continue + } + ret = append(ret, f) + } + return ret +} diff --git a/util/eventbus/fetch-htmx.go b/util/eventbus/fetch-htmx.go new file mode 100644 index 000000000..f80d50257 --- /dev/null +++ b/util/eventbus/fetch-htmx.go @@ -0,0 +1,93 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build ignore + +// Program fetch-htmx fetches and installs local copies of the HTMX +// library and its dependencies, used by the debug UI. It is meant to +// be run via go generate. +package main + +import ( + "compress/gzip" + "crypto/sha512" + "encoding/base64" + "fmt" + "io" + "log" + "net/http" + "os" +) + +func main() { + // Hash from https://htmx.org/docs/#installing + htmx, err := fetchHashed("https://unpkg.com/htmx.org@2.0.4", "HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+") + if err != nil { + log.Fatalf("fetching htmx: %v", err) + } + + // Hash SHOULD be from https://htmx.org/extensions/ws/ , but the + // hash is currently incorrect, see + // https://github.com/bigskysoftware/htmx-extensions/issues/153 + // + // Until that bug is resolved, hash was obtained by rebuilding the + // extension from git source, and verifying that the hash matches + // what unpkg is serving. + ws, err := fetchHashed("https://unpkg.com/htmx-ext-ws@2.0.2", "932iIqjARv+Gy0+r6RTGrfCkCKS5MsF539Iqf6Vt8L4YmbnnWI2DSFoMD90bvXd0") + if err != nil { + log.Fatalf("fetching htmx-websockets: %v", err) + } + + if err := writeGz("assets/htmx.min.js.gz", htmx); err != nil { + log.Fatalf("writing htmx.min.js.gz: %v", err) + } + if err := writeGz("assets/htmx-websocket.min.js.gz", ws); err != nil { + log.Fatalf("writing htmx-websocket.min.js.gz: %v", err) + } +} + +func writeGz(path string, bs []byte) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + + g, err := gzip.NewWriterLevel(f, gzip.BestCompression) + if err != nil { + return err + } + + if _, err := g.Write(bs); err != nil { + return err + } + + if err := g.Flush(); err != nil { + return err + } + if err := f.Close(); err != nil { + return err + } + return nil +} + +func fetchHashed(url, wantHash string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetching %q returned error status: %s", url, resp.Status) + } + ret, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading file from %q: %v", url, err) + } + h := sha512.Sum384(ret) + got := base64.StdEncoding.EncodeToString(h[:]) + if got != wantHash { + return nil, fmt.Errorf("wrong hash for %q: got %q, want %q", url, got, wantHash) + } + return ret, nil +}