dap: add debug adapter implementation

Adds a simple implementation of the debug adapter that supports the very
basics of a debug adapter.

It supports the launch request, the configuration done request, the
creation of threads, stopping, resuming, and disconnecting from server.

It does not support custom breakpoints, stack traces, or variable
inspection yet. These are planned to be added in the future.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-06-30 10:51:20 -05:00
parent 43e2f27cac
commit 42599a7d49
27 changed files with 4034 additions and 213 deletions
+109
View File
@@ -0,0 +1,109 @@
package dap
import (
"bufio"
"context"
"io"
"sync"
"github.com/google/go-dap"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
type Conn interface {
SendMsg(m dap.Message) error
RecvMsg(ctx context.Context) (dap.Message, error)
io.Closer
}
type conn struct {
recvCh <-chan dap.Message
sendCh chan<- dap.Message
ctx context.Context
cancel context.CancelCauseFunc
eg *errgroup.Group
once sync.Once
}
func NewConn(rd io.Reader, wr io.Writer) Conn {
recvCh := make(chan dap.Message, 100)
sendCh := make(chan dap.Message, 100)
errCh := make(chan error, 1)
// Reader input may never close so this is an orphaned goroutine.
// It's ok if it does actually close but not necessary for the
// proper functioning of this connection.
//
// The reason this might not close is because stdin close is controlled
// by the OS and can't be closed from within the program.
go func() {
defer close(errCh)
defer close(recvCh)
rd := bufio.NewReader(rd)
for {
m, err := dap.ReadProtocolMessage(rd)
if err != nil {
if !errors.Is(err, io.EOF) {
// TODO: not actually using this yet
errCh <- err
}
return
}
recvCh <- m
}
}()
eg, _ := errgroup.WithContext(context.Background())
eg.Go(func() error {
for m := range sendCh {
if err := dap.WriteProtocolMessage(wr, m); err != nil {
return err
}
}
return nil
})
ctx, cancel := context.WithCancelCause(context.Background())
return &conn{
recvCh: recvCh,
sendCh: sendCh,
ctx: ctx,
cancel: cancel,
eg: eg,
}
}
func (c *conn) SendMsg(m dap.Message) error {
select {
case c.sendCh <- m:
return nil
default:
return errors.New("send channel full")
}
}
func (c *conn) RecvMsg(ctx context.Context) (dap.Message, error) {
select {
case m, ok := <-c.recvCh:
if !ok {
return nil, io.EOF
}
return m, nil
case <-ctx.Done():
return nil, context.Cause(ctx)
case <-c.ctx.Done():
return nil, c.ctx.Err()
}
}
func (c *conn) Close() error {
c.cancel(context.Canceled)
c.once.Do(func() {
close(c.sendCh)
})
return c.eg.Wait()
}