Files
buildx/dap/server.go
T
Jonathan A. Sternberg ca9df87014 dap: fix the race condition in the dap unit tests
The context used for serving the dap server was being canceled too early
because it used defer which would initiate at the end of the function
while every other cleanup function used `t.Cleanup` which executes in
its own goroutine.

One possible solution was to move the cancel to the cleanup, but the
context being passed to serve and start doesn't make sense because if it
ever does get canceled, it'll likely cause a similar race condition with
`Stop`.

This removes the context from the methods that were causing this issue
in favor of just relying on the caller calling `Stop` when they are done
with the adapter and server.

This seems to have only affected tests and I don't believe it affected
the actual dap command.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2026-03-10 12:03:35 -05:00

250 lines
5.2 KiB
Go

package dap
import (
"context"
"sync"
"sync/atomic"
"github.com/google/go-dap"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)
var ErrServerStopped = errors.New("dap: server stopped")
type RequestCallback func(c Context, resp dap.ResponseMessage)
type Server struct {
h Handler
mu sync.RWMutex
ch chan dap.Message
eg *errgroup.Group
ctx context.Context
cancel context.CancelCauseFunc
seq atomic.Int64
requests sync.Map
initialized bool
}
func NewServer(h Handler) *Server {
return &Server{h: h}
}
func (s *Server) Serve(conn Conn) error {
writeCh := make(chan dap.Message)
s.ch = writeCh
s.ctx, s.cancel = context.WithCancelCause(context.Background())
// Start an error group to handle server-initiated tasks.
s.eg, _ = errgroup.WithContext(s.ctx)
s.eg.Go(func() error {
<-s.ctx.Done()
return s.ctx.Err()
})
eg, _ := errgroup.WithContext(s.ctx)
eg.Go(func() error {
return s.readLoop(conn)
})
eg.Go(func() error {
return s.writeLoop(conn, writeCh)
})
eg.Go(func() error {
// TODO: reevaluate this logic for shutting down
defer close(writeCh)
err := s.eg.Wait()
s.mu.Lock()
s.ch = nil
s.mu.Unlock()
return err
})
return eg.Wait()
}
func (s *Server) readLoop(conn Conn) error {
for {
m, err := conn.RecvMsg(s.ctx)
if err != nil {
return nil
}
switch m := m.(type) {
case dap.RequestMessage:
if ok := s.dispatchRequest(m); !ok {
return nil
}
case dap.ResponseMessage:
if ok := s.dispatchResponse(m); !ok {
return nil
}
}
}
}
func (s *Server) dispatchRequest(m dap.RequestMessage) bool {
fn := func(c Context) {
rmsg, err := s.handleMessage(c, m)
if err != nil {
rmsg = &dap.Response{}
rmsg.GetResponse().Message = err.Error()
}
rmsg.GetResponse().RequestSeq = m.GetSeq()
rmsg.GetResponse().Command = m.GetRequest().Command
rmsg.GetResponse().Success = err == nil
c.C() <- rmsg
}
return s.Go(fn)
}
func (s *Server) dispatchResponse(m dap.ResponseMessage) bool {
fn := func(c Context) {
reqID := m.GetResponse().RequestSeq
if v, loaded := s.requests.LoadAndDelete(reqID); loaded {
callback := v.(RequestCallback)
s.Go(func(c Context) {
callback(c, m)
})
}
}
return s.Go(fn)
}
func (s *Server) handleMessage(c Context, m dap.Message) (dap.ResponseMessage, error) {
switch req := m.(type) {
case *dap.InitializeRequest:
resp, err := s.handleInitialize(c, req)
if err != nil {
return nil, err
}
return resp, nil
case *dap.LaunchRequest:
return s.h.Launch.Do(c, req)
case *dap.AttachRequest:
return s.h.Attach.Do(c, req)
case *dap.SetBreakpointsRequest:
return s.h.SetBreakpoints.Do(c, req)
case *dap.ConfigurationDoneRequest:
return s.h.ConfigurationDone.Do(c, req)
case *dap.DisconnectRequest:
return s.h.Disconnect.Do(c, req)
case *dap.TerminateRequest:
return s.h.Terminate.Do(c, req)
case *dap.ContinueRequest:
return s.h.Continue.Do(c, req)
case *dap.NextRequest:
return s.h.Next.Do(c, req)
case *dap.StepInRequest:
return s.h.StepIn.Do(c, req)
case *dap.StepOutRequest:
return s.h.StepOut.Do(c, req)
case *dap.RestartRequest:
return s.h.Restart.Do(c, req)
case *dap.ThreadsRequest:
return s.h.Threads.Do(c, req)
case *dap.StackTraceRequest:
return s.h.StackTrace.Do(c, req)
case *dap.ScopesRequest:
return s.h.Scopes.Do(c, req)
case *dap.VariablesRequest:
return s.h.Variables.Do(c, req)
case *dap.EvaluateRequest:
return s.h.Evaluate.Do(c, req)
case *dap.SourceRequest:
return s.h.Source.Do(c, req)
default:
return nil, errors.New("not implemented")
}
}
func (s *Server) handleInitialize(c Context, req *dap.InitializeRequest) (*dap.InitializeResponse, error) {
if s.initialized {
return nil, errors.New("already initialized")
}
resp, err := s.h.Initialize.Do(c, req)
if err != nil {
return nil, err
}
s.initialized = true
return resp, nil
}
func (s *Server) writeLoop(conn Conn, respCh <-chan dap.Message) error {
for m := range respCh {
switch m := m.(type) {
case dap.RequestMessage:
if req := m.GetRequest(); req.Seq == 0 {
req.Seq = int(s.seq.Add(1))
}
m.GetRequest().Type = "request"
case dap.EventMessage:
if event := m.GetEvent(); event.Seq == 0 {
event.Seq = int(s.seq.Add(1))
}
m.GetEvent().Type = "event"
case dap.ResponseMessage:
if resp := m.GetResponse(); resp.Seq == 0 {
resp.Seq = int(s.seq.Add(1))
}
m.GetResponse().Type = "response"
}
if err := conn.SendMsg(m); err != nil {
return err
}
}
return nil
}
func (s *Server) Go(fn func(c Context)) bool {
acquireChannel := func() (chan<- dap.Message, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.ch, s.ch != nil
}
ctx, cancel := context.WithCancelCause(s.ctx)
c := &dispatchContext{
Context: ctx,
srv: s,
}
started := make(chan bool, 1)
s.eg.Go(func() error {
var ok bool
c.ch, ok = acquireChannel()
started <- ok
if c.ch == nil {
return nil
}
defer cancel(context.Canceled)
fn(c)
return nil
})
return <-started
}
func (s *Server) doRequest(c Context, req dap.RequestMessage, callback RequestCallback) {
req.GetRequest().Seq = int(s.seq.Add(1))
s.requests.Store(req.GetRequest().Seq, callback)
c.C() <- req
}
func (s *Server) Stop() {
s.mu.Lock()
s.ch = nil
s.mu.Unlock()
s.cancel(ErrServerStopped)
}