From d7516fc57004d75d2afef8864092874a2664f8af Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Tue, 8 Nov 2022 17:05:49 +0100 Subject: [PATCH] feat: add Tracer, a debugging aid Adds a Tracer interface that is capable of logging all executed tea.Cmd and all processed tea.Msg. RemoteTracer is one standard implementation, which provides access to a live stream of these event logs, through a TCP socket. --- options.go | 8 ++++ tea.go | 44 ++++++++++++++++++++ tracer/tracer.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 tracer/tracer.go diff --git a/options.go b/options.go index d9ce42c..2c22e93 100644 --- a/options.go +++ b/options.go @@ -4,6 +4,7 @@ import ( "context" "io" + "github.com/charmbracelet/bubbletea/tracer" "github.com/muesli/termenv" ) @@ -151,3 +152,10 @@ func WithANSICompressor() ProgramOption { p.startupOptions |= withANSICompressor } } + +// WithTracer sets a tracer for the program. This is useful for debugging. +func WithTracer(t tracer.Tracer) ProgramOption { + return func(p *Program) { + p.tracer = t + } +} diff --git a/tea.go b/tea.go index 4de805e..faf715b 100644 --- a/tea.go +++ b/tea.go @@ -16,10 +16,13 @@ import ( "io" "os" "os/signal" + "reflect" "runtime/debug" "sync" "syscall" + "time" + "github.com/charmbracelet/bubbletea/tracer" "github.com/containerd/console" isatty "github.com/mattn/go-isatty" "github.com/muesli/cancelreader" @@ -122,6 +125,8 @@ type Program struct { // as this value only comes into play on Windows, hence the ignore comment // below. windowsStdin *os.File //nolint:golint,structcheck,unused + + tracer tracer.Tracer } // Quit is a special command that tells the Bubble Tea program to exit. @@ -244,8 +249,17 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { // possible to cancel them so we'll have to leak the goroutine // until Cmd returns. go func() { + tc := tracer.NewCommand() + if p.tracer != nil { + p.tracer.Command(tc) + } + msg := cmd() // this can be long. p.Send(msg) + + if p.tracer != nil { + p.traceCommand(tc, msg) + } }() } } @@ -254,6 +268,34 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { return ch } +func (p *Program) traceCommand(tc tracer.Command, msg Msg) { + t := "nil" + if msg != nil { + t = reflect.TypeOf(msg).String() + } + + tc.Finished = time.Now() + tc.Type = t + tc.Msg = fmt.Sprintf("%v", msg) + + p.tracer.Command(tc) +} + +func (p *Program) traceMessage(model Model, msg Msg) { + if p.tracer != nil { + t := "nil" + if msg != nil { + t = reflect.TypeOf(msg).String() + } + + p.tracer.Message(tracer.Message{ + Model: reflect.TypeOf(model).String(), + Type: t, + Msg: fmt.Sprintf("%v", msg), + }) + } +} + // eventLoop is the central message loop. It receives and handles the default // Bubble Tea messages, update the model and triggers redraws. func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { @@ -266,6 +308,8 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { return model, err case msg := <-p.msgs: + p.traceMessage(model, msg) + // Handle special internal messages. switch msg := msg.(type) { case quitMsg: diff --git a/tracer/tracer.go b/tracer/tracer.go new file mode 100644 index 0000000..f52de1e --- /dev/null +++ b/tracer/tracer.go @@ -0,0 +1,105 @@ +package tracer + +import ( + "encoding/json" + "net" + "sync" + "time" +) + +type Tracer interface { + Command(c Command) + Message(m Message) +} + +type Event struct { + Timestamp time.Time + Message Message + Command Command +} + +type Message struct { + Model string + Type string + Msg string +} + +type Command struct { + ID int + Started time.Time + Finished time.Time + Type string + Msg string +} + +func NewCommand() Command { + mtx.Lock() + defer mtx.Unlock() + id++ + + return Command{ + ID: id, + Started: time.Now(), + } +} + +var ( + ch = make(chan Event) + mtx sync.Mutex + id int +) + +type RemoteTracer struct { +} + +func NewRemoteTracer() (*RemoteTracer, error) { + listen, err := net.Listen("tcp", ":13337") + if err != nil { + return nil, err + } + + go func() { + for { + conn, err := listen.Accept() + if err != nil { + return + } + go handleTraceConn(conn) + } + }() + + t := &RemoteTracer{} + return t, nil +} + +func (rt *RemoteTracer) Command(c Command) { + ev := Event{ + Timestamp: time.Now(), + Command: c, + } + + ch <- ev +} + +func (rt *RemoteTracer) Message(m Message) { + ev := Event{ + Timestamp: time.Now(), + Message: m, + } + + ch <- ev +} + +func handleTraceConn(conn net.Conn) { + for ev := range ch { + b, _ := json.Marshal(ev) + _, _ = conn.Write(b) + _, err := conn.Write([]byte("\n")) + if err != nil { + return + } + } + + // close conn + _ = conn.Close() +}