mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-29 12:32:24 +00:00

The use of html/template causes reflect-based linker bloat. Longer term we have options to bring the UI back to iOS, but for now, cut it out. Updates #15297 Signed-off-by: David Anderson <dave@tailscale.com>
241 lines
5.4 KiB
Go
241 lines
5.4 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ios
|
|
|
|
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
|
|
}
|