package bridge import ( "context" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "sync" "sync/atomic" ) 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 { path string lifecycleMu sync.Mutex started bool finished bool closing bool failure error exitErr error cmd *exec.Cmd stdin io.WriteCloser protocol io.ReadCloser encoder *json.Encoder decoder *json.Decoder writeMu sync.Mutex nextID atomic.Uint64 pendingMu sync.Mutex pending map[uint64]chan Message events chan Message output chan Output done chan struct{} finishOnce sync.Once } type OutputStream string const ( StdoutStream OutputStream = "stdout" StderrStream OutputStream = "stderr" ) type Output struct { Stream OutputStream Text string } func NewBridge(path string) *Bridge { return &Bridge{ path: path, pending: make(map[uint64]chan Message), events: make(chan Message, 32), output: make(chan Output, 128), done: make(chan struct{}), } } func (b *Bridge) Events() <-chan Message { return b.events } func (b *Bridge) Output() <-chan Output { return b.output } func (b *Bridge) Done() <-chan struct{} { return b.done } func (b *Bridge) Start() error { b.lifecycleMu.Lock() defer b.lifecycleMu.Unlock() if b.started { return ErrAlreadyStarted } protocolReader, protocolWriter, err := os.Pipe() if err != nil { return fmt.Errorf("create protocol pipe: %w", err) } cmd := exec.Command( "python", "-u", "python/pybug_runtime.py", b.path, ) cmd.ExtraFiles = []*os.File{protocolWriter} cmd.Env = append(os.Environ(), "PYBUG_PROTOCOL_FD=3") stdin, err := cmd.StdinPipe() if err != nil { protocolWriter.Close() protocolReader.Close() return fmt.Errorf("create command pipe: %w", err) } stdout, err := cmd.StdoutPipe() if err != nil { stdin.Close() protocolReader.Close() protocolWriter.Close() return fmt.Errorf("create stdout pipe: %w", err) } stderr, err := cmd.StderrPipe() if err != nil { stdout.Close() protocolReader.Close() protocolWriter.Close() return fmt.Errorf("create stderr pipe: %w", err) } if err := cmd.Start(); err != nil { stdin.Close() stdout.Close() stderr.Close() protocolWriter.Close() protocolReader.Close() return fmt.Errorf("start debugger runtime: %w", err) } if err := protocolWriter.Close(); err != nil { cmd.Process.Kill() stdin.Close() stdout.Close() stderr.Close() protocolWriter.Close() protocolReader.Close() return fmt.Errorf("close parent protocol writer: %w", err) } b.cmd = cmd b.stdin = stdin b.protocol = protocolReader b.encoder = json.NewEncoder(stdin) b.decoder = json.NewDecoder(protocolReader) b.started = true go b.readProtocol() go b.readOutput(stdout, StdoutStream) go b.readOutput(stderr, StderrStream) go b.waitForProcess() return nil } func (b *Bridge) ensureRunning() error { b.lifecycleMu.Lock() defer b.lifecycleMu.Unlock() if !b.started || b.finished { return ErrNotRunning } return nil } func (b *Bridge) writeMessage(msg Message) error { b.writeMu.Lock() defer b.writeMu.Unlock() if err := b.encoder.Encode(msg); err != nil { return fmt.Errorf("write protocol message: %w", err) } return nil } func (b *Bridge) request( ctx context.Context, name string, body any, ) (Message, error) { if err := b.ensureRunning(); err != nil { return Message{}, err } id := b.nextID.Add(1) response := make(chan Message, 1) b.pendingMu.Lock() b.pending[id] = response b.pendingMu.Unlock() defer func() { b.pendingMu.Lock() delete(b.pending, id) b.pendingMu.Unlock() }() msg, err := newMessage(Request, &id, name, body) if err != nil { return Message{}, fmt.Errorf("create %q request: %w", name, err) } if err := msg.Validate(); err != nil { return Message{}, fmt.Errorf("validate %q request: %w", name, err) } if err := b.writeMessage(msg); err != nil { return Message{}, err } select { case response := <-response: if response.Name != name { return Message{}, fmt.Errorf("response name mismatch: requested %q, received %q", name, response.Name) } if response.Error != nil { return Message{}, response.Error } return response, nil case <-ctx.Done(): return Message{}, ctx.Err() case <-b.done: return Message{}, ErrNotRunning } } func (b *Bridge) deliverResponse(msg Message) { id := *msg.RequestId b.pendingMu.Lock() response, ok := b.pending[id] b.pendingMu.Unlock() if !ok { return } select { case response <- msg: case <-b.done: default: } } func (b *Bridge) readProtocol() { for { var msg Message if err := b.decoder.Decode(&msg); err != nil { if errors.Is(err, io.EOF) { return } select { case <-b.done: return default: } b.abort(fmt.Errorf("decode protocol message: %w", err)) return } if err := msg.Validate(); err != nil { b.abort(fmt.Errorf("invalid protocol message: %w", err)) return } switch msg.Type { case Response: b.deliverResponse(msg) case Event: select { case b.events <- msg: case <-b.done: return } default: b.abort(fmt.Errorf("unexpected message type from runtime: %q", msg.Type)) return } } } func (b *Bridge) readOutput(reader io.ReadCloser, stream OutputStream) { defer reader.Close() buffer := make([]byte, 4096) for { n, err := reader.Read(buffer) if n > 0 { output := Output{ Stream: stream, Text: string(buffer[:n]), } select { case b.output <- output: case <-b.done: return } } if err != nil { if !errors.Is(err, io.EOF) { b.abort(fmt.Errorf("read target %s: %w", stream, err)) } return } } } func (b *Bridge) waitForProcess() { err := b.cmd.Wait() b.finish(err) } func (b *Bridge) abort(err error) { b.lifecycleMu.Lock() if b.failure == nil { b.failure = err } var process *os.Process if b.cmd != nil { process = b.cmd.Process } finished := b.finished b.lifecycleMu.Unlock() if !finished && process != nil { _ = process.Kill() } } func (b *Bridge) finish(waitErr error) { b.finishOnce.Do(func() { b.lifecycleMu.Lock() b.finished = true if b.failure != nil { b.exitErr = b.failure } else if !b.closing { b.exitErr = waitErr } stdin := b.stdin protocol := b.protocol b.lifecycleMu.Unlock() if stdin != nil { _ = stdin.Close() } if protocol != nil { _ = protocol.Close() } close(b.done) }) } func (b *Bridge) Wait(ctx context.Context) error { b.lifecycleMu.Lock() started := b.started b.lifecycleMu.Unlock() if !started { return ErrNotRunning } select { case <-b.done: return b.processError() case <-ctx.Done(): return ctx.Err() } } func (b *Bridge) Close(ctx context.Context) error { b.lifecycleMu.Lock() if !b.started { b.lifecycleMu.Unlock() return ErrNotRunning } if b.finished { b.lifecycleMu.Unlock() return b.processError() } b.closing = true process := b.cmd.Process b.lifecycleMu.Unlock() if process != nil { err := process.Kill() if err != nil && !errors.Is(err, os.ErrProcessDone) { return fmt.Errorf("kill debugger runtime: %w", err) } } return b.Wait(ctx) } func (b *Bridge) processError() error { b.lifecycleMu.Lock() defer b.lifecycleMu.Unlock() return b.exitErr } func (b *Bridge) Locals(ctx context.Context) (LocalsResponse, error) { msg, err := b.request(ctx, "locals/get", nil) if err != nil { return LocalsResponse{}, err } var result LocalsResponse if err := json.Unmarshal(msg.Body, &result); err != nil { return LocalsResponse{}, fmt.Errorf("decode locals response: %w", err) } return result, nil } func (b *Bridge) Continue(ctx context.Context) error { _, err := b.request(ctx, "execution/continue", nil) return err } func (b *Bridge) StepInto(ctx context.Context) error { _, err := b.request(ctx, "execution/step-into", nil) return err } func (b *Bridge) StepOver(ctx context.Context) error { _, err := b.request(ctx, "execution/step-over", nil) return err } func (b *Bridge) SetBreakpoint(ctx context.Context, file string, line int) (SetBreakpointResponse, error) { msg, err := b.request( ctx, "breakpoint/set", SetBreakpointRequest{ File: file, Line: line, }, ) if err != nil { return SetBreakpointResponse{}, err } var response SetBreakpointResponse if err := json.Unmarshal(msg.Body, &response); err != nil { return SetBreakpointResponse{}, fmt.Errorf( "decode breakpoint response: %w", err, ) } return response, nil } func (b *Bridge) RemoveBreakpoint( ctx context.Context, file string, line int, ) error { _, err := b.request( ctx, "breakpoint/remove", RemoveBreakpointRequest{ File: file, Line: line, }, ) return err }