From 587c8f9396c38392e6c0be2039d97e3d3ac1eb47 Mon Sep 17 00:00:00 2001 From: pablu Date: Sat, 28 Mar 2026 17:18:26 +0100 Subject: [PATCH] Stepping and showing execution point now work --- .gitignore | 1 + cmd/pybug/main.go | 48 +++------- internal/bridge/client.go | 171 ++++++++++++++++++++++++++---------- internal/bridge/protocol.go | 1 + ui/model.go | 30 +++++-- ui/update.go | 14 +++ ui/view.go | 7 +- 7 files changed, 184 insertions(+), 88 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6184bd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +app.log diff --git a/cmd/pybug/main.go b/cmd/pybug/main.go index c76b67f..6abf863 100644 --- a/cmd/pybug/main.go +++ b/cmd/pybug/main.go @@ -1,53 +1,27 @@ package main import ( - // "fmt" - // "log/slog" - // "time" - "log/slog" + "os" b "git.pablu.de/pablu/pybug/internal/bridge" "git.pablu.de/pablu/pybug/ui" ) func main() { - // slog.SetLogLoggerLevel(slog.LevelDebug) - - // - // fmt.Println("Started bridge") - // - // err = bridge.Breakpoint("test.py", 5) - // bridge.OnBreakpoint("test.py", 5, func() { - // locals, err := bridge.Locals() - // if err != nil { - // slog.Error("Encountered error on callback", "error", err) - // return - // } - // - // for key, val := range locals { - // slog.Info("found local variable", "key", key, "value", val) - // } - // }) - // - // bridge.Continue() - // - // time.Sleep(5 * time.Second) - // - // bridge.Continue() - // - // err = bridge.Wait() - // if err != nil { - // panic(err) - // } - - slog.SetLogLoggerLevel(slog.LevelError) - - bridge := b.NewBridge("test.py") - err := bridge.Start() + f, err := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { panic(err) } + defer f.Close() + + handler := slog.NewTextHandler(f, &slog.HandlerOptions{ + AddSource: true, + }) + + slog.SetDefault(slog.New(handler)) + slog.SetLogLoggerLevel(slog.LevelDebug) + bridge := b.NewBridge("test.py") err = ui.Run(bridge) if err != nil { diff --git a/internal/bridge/client.go b/internal/bridge/client.go index 89087b2..95c63a6 100644 --- a/internal/bridge/client.go +++ b/internal/bridge/client.go @@ -11,6 +11,14 @@ import ( "sync" ) +var ErrNotRunning = errors.New("bridge not running") +var ErrAlreadyStarted = errors.New("bridge is already running") + +type ExecutionPoint struct { + File string + Line int +} + type Bridge struct { stdin io.Writer stdout *bufio.Reader @@ -22,6 +30,9 @@ type Bridge struct { outputLock *sync.RWMutex output []chan string + executionStopLock *sync.RWMutex + executionStop []chan ExecutionPoint + registry map[string]chan string registryLock *sync.RWMutex @@ -29,18 +40,15 @@ type Bridge struct { callbacksLock *sync.RWMutex callbacks map[string]map[int]func() + + running bool + + path string } func NewBridge(path string) *Bridge { - cmd := exec.Command( - "python", - "-u", - "python/pybug_runtime.py", - path, - ) - return &Bridge{ - cmd: cmd, + path: path, input: make(chan string), registry: make(map[string]chan string), registryLock: &sync.RWMutex{}, @@ -51,16 +59,31 @@ func NewBridge(path string) *Bridge { output: make([]chan string, 0), outputLock: &sync.RWMutex{}, + + executionStop: make([]chan ExecutionPoint, 0), + executionStopLock: &sync.RWMutex{}, + + running: false, } } func (b *Bridge) Start() error { + if b.running { + return ErrAlreadyStarted + } + + b.cmd = exec.Command( + "python", + "-u", + "python/pybug_runtime.py", + b.path, + ) + var err error b.stdin, err = b.cmd.StdinPipe() if err != nil { return err } - // b.cmd.Stdout = os.Stdout reader, err := b.cmd.StdoutPipe() if err != nil { @@ -73,6 +96,7 @@ func (b *Bridge) Start() error { return err } + b.running = true go b.readLoop() go b.writeLoop() @@ -89,7 +113,21 @@ func (b *Bridge) Subscribe() chan string { return c } +func (b *Bridge) SubscribeStopped() chan ExecutionPoint { + b.executionStopLock.Lock() + defer b.executionStopLock.Unlock() + + c := make(chan ExecutionPoint) + b.executionStop = append(b.executionStop, c) + + return c +} + func (b *Bridge) Locals() (map[string]any, error) { + if !b.running { + return nil, ErrNotRunning + } + requestId, cmd := makeCommand(LocalsCommand, map[string]any{}) c := b.sendCommand(requestId, cmd) @@ -110,8 +148,22 @@ func (b *Bridge) Locals() (map[string]any, error) { return vars, nil } +func (b *Bridge) Step() error { + if !b.running { + return ErrNotRunning + } + + _, cmd := makeCommand(StepCommand, nil) + b.sendCommandNoResponse(cmd) + + return nil +} + func (b *Bridge) Breakpoint(file string, line int) error { // Check if breakpoint already exists here + if !b.running { + return ErrNotRunning + } requestId, cmd := makeCommand(BreakCommand, map[string]any{ "file": file, @@ -137,10 +189,15 @@ func (b *Bridge) Breakpoint(file string, line int) error { return nil } -func (b *Bridge) Continue() { +func (b *Bridge) Continue() error { + if !b.running { + return ErrNotRunning + } + _, cmd := makeCommand(ContinueCommand, map[string]any{}) b.sendCommandNoResponse(cmd) + return nil } func (b *Bridge) sendCommandNoResponse(command string) { @@ -165,6 +222,10 @@ func (b *Bridge) writeLoop() { for { cmd := <-b.input + if !b.running { + return + } + slog.Info("Received command", "cmd", cmd) _, err := b.stdin.Write([]byte(cmd + "\n")) @@ -186,6 +247,7 @@ func (b *Bridge) readLoop() { slog.Error("Error occured while reading from stdout", "error", err) continue } else if errors.Is(err, io.EOF) { + b.running = false return } @@ -212,28 +274,64 @@ func (b *Bridge) readLoop() { c <- line } else { - // TODO: set to stopped - if event, ok := msg["event"]; !ok || event != "stopped" { - slog.Warn("received unkown event", "msg", msg) - } - - file := msg["file"].(string) - line, ok := toInt(msg["line"]) - if !ok { - slog.Error("could not convert line to int", "line", msg["line"]) - } - - slog.Info("received stopped event") - b.callbacksLock.RLock() - if callback, ok := b.callbacks[file][line]; ok { - slog.Info("found callback, now running", "file", file, "line", line) - go callback() - } - b.callbacksLock.RUnlock() + b.handleStopped(msg) } } } +func (b *Bridge) handleStopped(msg map[string]any) error { + // TODO: set to stopped + if event, ok := msg["event"]; !ok || event != "stopped" { + slog.Warn("received unkown event", "msg", msg) + return errors.New("unknown event encountered") + } + + file := msg["file"].(string) + line, ok := toInt(msg["line"]) + if !ok { + slog.Error("could not convert line to int", "line", msg["line"]) + return errors.New("could not convert line to int") + } + + slog.Info("received stopped event") + b.callbacksLock.RLock() + defer b.callbacksLock.RUnlock() + + if callback, ok := b.callbacks[file][line]; ok { + slog.Info("found callback, now running", "file", file, "line", line) + go callback() + } + + b.executionStopLock.RLock() + defer b.executionStopLock.RUnlock() + + for _, c := range b.executionStop { + c <- ExecutionPoint{ + File: file, + Line: line, + } + } + + return nil +} + +func (b *Bridge) OnBreakpoint(file string, line int, callback func()) { + b.callbacksLock.Lock() + defer b.callbacksLock.Unlock() + + if f, ok := b.callbacks[file]; ok { + f[line] = callback + } else { + b.callbacks[file] = map[int]func(){ + line: callback, + } + } +} + +func (b *Bridge) Wait() error { + return b.cmd.Wait() +} + func toInt(v any) (int, bool) { switch x := v.(type) { case int: @@ -254,20 +352,3 @@ func toInt(v any) (int, bool) { return 0, false } } - -func (b *Bridge) OnBreakpoint(file string, line int, callback func()) { - b.callbacksLock.Lock() - defer b.callbacksLock.Unlock() - - if f, ok := b.callbacks[file]; ok { - f[line] = callback - } else { - b.callbacks[file] = map[int]func(){ - line: callback, - } - } -} - -func (b *Bridge) Wait() error { - return b.cmd.Wait() -} diff --git a/internal/bridge/protocol.go b/internal/bridge/protocol.go index 997994a..c59d253 100644 --- a/internal/bridge/protocol.go +++ b/internal/bridge/protocol.go @@ -12,6 +12,7 @@ const ( ContinueCommand CommandType = "continue" BreakCommand CommandType = "break" LocalsCommand CommandType = "locals" + StepCommand CommandType = "step" ) func makeCommand(cmd CommandType, values map[string]any) (requestId string, request string) { diff --git a/ui/model.go b/ui/model.go index f6fd95b..a0f4c5c 100644 --- a/ui/model.go +++ b/ui/model.go @@ -16,8 +16,11 @@ type Model struct { width int height int - bridge *bridge.Bridge - listenBridge chan string + bridge *bridge.Bridge + listenBridge chan string + listenBridgeExecutionsStopped chan bridge.ExecutionPoint + + currExecutionPoint bridge.ExecutionPoint messages []string stdoutOutput viewport.Model @@ -29,6 +32,7 @@ type Model struct { func NewModel(b *bridge.Bridge, file string, text string) Model { c := b.Subscribe() + c2 := b.SubscribeStopped() stdoutOutput := viewport.New(0, 0) codeViewer := viewport.New(0, 0) @@ -38,9 +42,14 @@ func NewModel(b *bridge.Bridge, file string, text string) Model { text: text, textLines: len(strings.Split(text, "\n")), - bridge: b, - listenBridge: c, + bridge: b, + listenBridge: c, + listenBridgeExecutionsStopped: c2, + currExecutionPoint: bridge.ExecutionPoint{ + File: file, + Line: 0, + }, breakpoints: make(map[string][]int), messages: make([]string, 0), @@ -57,8 +66,19 @@ func ListenBridge(ch <-chan string) tea.Cmd { } } +func ListenBridgeExecutionsStopped(ch <-chan bridge.ExecutionPoint) tea.Cmd { + return func() tea.Msg { + msg := <-ch + return ExecutionStoppedMsg(msg) + } +} + +type ExecutionStoppedMsg bridge.ExecutionPoint type StdoutMsg string func (m Model) Init() tea.Cmd { - return ListenBridge(m.listenBridge) + return tea.Batch( + ListenBridge(m.listenBridge), + ListenBridgeExecutionsStopped(m.listenBridgeExecutionsStopped), + ) } diff --git a/ui/update.go b/ui/update.go index c429f39..5f5bd79 100644 --- a/ui/update.go +++ b/ui/update.go @@ -1,8 +1,10 @@ package ui import ( + "log/slog" "strings" + "git.pablu.de/pablu/pybug/internal/bridge" tea "github.com/charmbracelet/bubbletea" ) @@ -17,6 +19,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.stdoutOutput.SetContent(strings.Join(m.messages, "")) m.stdoutOutput.GotoBottom() return m, ListenBridge(m.listenBridge) + case ExecutionStoppedMsg: + m.currExecutionPoint = bridge.ExecutionPoint(msg) + return m, ListenBridgeExecutionsStopped(m.listenBridgeExecutionsStopped) } return m, nil @@ -61,8 +66,17 @@ func (m Model) HandleKeyMsg(key tea.KeyMsg) (tea.Model, tea.Cmd) { lineNumber, } } + case "s": + m.bridge.Step() case "c": m.bridge.Continue() + case "r": + m.messages = make([]string, 0) + m.stdoutOutput.SetContent("") + err := m.bridge.Start() + if err != nil { + slog.Error("could not start brige", "error", err) + } } return m, nil diff --git a/ui/view.go b/ui/view.go index 448fd56..05eeeb1 100644 --- a/ui/view.go +++ b/ui/view.go @@ -30,15 +30,20 @@ func (m Model) View() string { for i, line := range lines { breakpoint := " " cursor := " " + executor := " " if slices.Contains(breakpoints, i+1) { breakpoint = "O" } + if i+1 == m.currExecutionPoint.Line { + executor = "!" + } + if i == m.cursor { cursor = ">" } - fmt.Fprintf(&out, "%-4d%s%s %s\n", i+1, breakpoint, cursor, line) + fmt.Fprintf(&out, "%-4d%s%s%s %s\n", i+1, breakpoint, executor, cursor, line) } m.codeViewer.SetContent(out.String())