501 lines
8.7 KiB
Go
501 lines
8.7 KiB
Go
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
|
|
}
|