mirror of
https://github.com/go-delve/delve.git
synced 2025-10-31 02:36:18 +08:00
service/dap: support JSON-RPC and DAP on the same port from "dlv debug/exec/test/attach" (#2755)
* Support JSON-RPC and DAP on the same port * Fix test Co-authored-by: Polina Sokolova <polinasok@users.noreply.github.com>
This commit is contained in:
@ -24,8 +24,10 @@ import (
|
|||||||
"github.com/go-delve/delve/pkg/goversion"
|
"github.com/go-delve/delve/pkg/goversion"
|
||||||
protest "github.com/go-delve/delve/pkg/proc/test"
|
protest "github.com/go-delve/delve/pkg/proc/test"
|
||||||
"github.com/go-delve/delve/pkg/terminal"
|
"github.com/go-delve/delve/pkg/terminal"
|
||||||
|
"github.com/go-delve/delve/service/dap"
|
||||||
"github.com/go-delve/delve/service/dap/daptest"
|
"github.com/go-delve/delve/service/dap/daptest"
|
||||||
"github.com/go-delve/delve/service/rpc2"
|
"github.com/go-delve/delve/service/rpc2"
|
||||||
|
godap "github.com/google/go-dap"
|
||||||
"golang.org/x/tools/go/packages"
|
"golang.org/x/tools/go/packages"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -52,6 +54,7 @@ func TestMain(m *testing.M) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func assertNoError(err error, t testing.TB, s string) {
|
func assertNoError(err error, t testing.TB, s string) {
|
||||||
|
t.Helper()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, file, line, _ := runtime.Caller(1)
|
_, file, line, _ := runtime.Caller(1)
|
||||||
fname := filepath.Base(file)
|
fname := filepath.Base(file)
|
||||||
@ -647,8 +650,8 @@ func TestTypecheckRPC(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDap verifies that a dap server can be started and shut down.
|
// TestDAPCmd verifies that a dap server can be started and shut down.
|
||||||
func TestDap(t *testing.T) {
|
func TestDAPCmd(t *testing.T) {
|
||||||
const listenAddr = "127.0.0.1:40575"
|
const listenAddr = "127.0.0.1:40575"
|
||||||
|
|
||||||
dlvbin, tmpdir := getDlvBin(t)
|
dlvbin, tmpdir := getDlvBin(t)
|
||||||
@ -691,8 +694,179 @@ func TestDap(t *testing.T) {
|
|||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDapWithClient tests dlv dap --client-addr can be started and shut down.
|
func newDAPRemoteClient(t *testing.T, addr string) *daptest.Client {
|
||||||
func TestDapWithClient(t *testing.T) {
|
c := daptest.NewClient(addr)
|
||||||
|
c.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
|
||||||
|
c.ExpectInitializedEvent(t)
|
||||||
|
c.ExpectAttachResponse(t)
|
||||||
|
c.ConfigurationDoneRequest()
|
||||||
|
c.ExpectStoppedEvent(t)
|
||||||
|
c.ExpectConfigurationDoneResponse(t)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteDAPClient(t *testing.T) {
|
||||||
|
const listenAddr = "127.0.0.1:40576"
|
||||||
|
|
||||||
|
dlvbin, tmpdir := getDlvBin(t)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
|
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
|
||||||
|
cmd := exec.Command(dlvbin, "debug", "--headless", "--log-output=dap", "--log", "--listen", listenAddr)
|
||||||
|
cmd.Dir = buildtestdir
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
assertNoError(err, t, "stdout pipe")
|
||||||
|
defer stdout.Close()
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
assertNoError(err, t, "stderr pipe")
|
||||||
|
defer stderr.Close()
|
||||||
|
assertNoError(cmd.Start(), t, "start headless instance")
|
||||||
|
|
||||||
|
scanOut := bufio.NewScanner(stdout)
|
||||||
|
scanErr := bufio.NewScanner(stderr)
|
||||||
|
// Wait for the debug server to start
|
||||||
|
scanOut.Scan()
|
||||||
|
t.Log(scanOut.Text())
|
||||||
|
go func() { // Capture logging
|
||||||
|
for scanErr.Scan() {
|
||||||
|
t.Log(scanErr.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := newDAPRemoteClient(t, listenAddr)
|
||||||
|
client.ContinueRequest(1)
|
||||||
|
client.ExpectContinueResponse(t)
|
||||||
|
client.ExpectTerminatedEvent(t)
|
||||||
|
|
||||||
|
client.DisconnectRequest()
|
||||||
|
client.ExpectOutputEventProcessExited(t, 0)
|
||||||
|
client.ExpectOutputEventDetaching(t)
|
||||||
|
client.ExpectDisconnectResponse(t)
|
||||||
|
client.ExpectTerminatedEvent(t)
|
||||||
|
if _, err := client.ReadMessage(); err == nil {
|
||||||
|
t.Error("expected read error upon shutdown")
|
||||||
|
}
|
||||||
|
client.Close()
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func closeDAPRemoteMultiClient(t *testing.T, c *daptest.Client) {
|
||||||
|
c.DisconnectRequest()
|
||||||
|
c.ExpectOutputEventClosingClient(t)
|
||||||
|
c.ExpectDisconnectResponse(t)
|
||||||
|
c.ExpectTerminatedEvent(t)
|
||||||
|
c.Close()
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteDAPClientMulti(t *testing.T) {
|
||||||
|
const listenAddr = "127.0.0.1:40577"
|
||||||
|
|
||||||
|
dlvbin, tmpdir := getDlvBin(t)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
|
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
|
||||||
|
cmd := exec.Command(dlvbin, "debug", "--headless", "--accept-multiclient", "--log-output=debugger", "--log", "--listen", listenAddr)
|
||||||
|
cmd.Dir = buildtestdir
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
assertNoError(err, t, "stdout pipe")
|
||||||
|
defer stdout.Close()
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
assertNoError(err, t, "stderr pipe")
|
||||||
|
defer stderr.Close()
|
||||||
|
assertNoError(cmd.Start(), t, "start headless instance")
|
||||||
|
|
||||||
|
scanOut := bufio.NewScanner(stdout)
|
||||||
|
scanErr := bufio.NewScanner(stderr)
|
||||||
|
// Wait for the debug server to start
|
||||||
|
scanOut.Scan()
|
||||||
|
t.Log(scanOut.Text())
|
||||||
|
go func() { // Capture logging
|
||||||
|
for scanErr.Scan() {
|
||||||
|
t.Log(scanErr.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Client 1 connects and continues to main.main
|
||||||
|
dapclient := newDAPRemoteClient(t, listenAddr)
|
||||||
|
dapclient.SetFunctionBreakpointsRequest([]godap.FunctionBreakpoint{{Name: "main.main"}})
|
||||||
|
dapclient.ExpectSetFunctionBreakpointsResponse(t)
|
||||||
|
dapclient.ContinueRequest(1)
|
||||||
|
dapclient.ExpectContinueResponse(t)
|
||||||
|
dapclient.ExpectStoppedEvent(t)
|
||||||
|
dapclient.CheckStopLocation(t, 1, "main.main", 5)
|
||||||
|
closeDAPRemoteMultiClient(t, dapclient)
|
||||||
|
|
||||||
|
// Client 2 reconnects at main.main and continues to process exit
|
||||||
|
dapclient2 := newDAPRemoteClient(t, listenAddr)
|
||||||
|
dapclient2.CheckStopLocation(t, 1, "main.main", 5)
|
||||||
|
dapclient2.ContinueRequest(1)
|
||||||
|
dapclient2.ExpectContinueResponse(t)
|
||||||
|
dapclient2.ExpectTerminatedEvent(t)
|
||||||
|
closeDAPRemoteMultiClient(t, dapclient2)
|
||||||
|
|
||||||
|
// Attach to exited processs is an error
|
||||||
|
dapclient3 := daptest.NewClient(listenAddr)
|
||||||
|
dapclient3.AttachRequest(map[string]interface{}{"mode": "remote", "stopOnEntry": true})
|
||||||
|
dapclient3.ExpectErrorResponseWith(t, dap.FailedToAttach, `Process \d+ has exited with status 0`, true)
|
||||||
|
closeDAPRemoteMultiClient(t, dapclient3)
|
||||||
|
|
||||||
|
// But rpc clients can still connect and restart
|
||||||
|
rpcclient := rpc2.NewClient(listenAddr)
|
||||||
|
if _, err := rpcclient.Restart(false); err != nil {
|
||||||
|
t.Errorf("error restarting with rpc client: %v", err)
|
||||||
|
}
|
||||||
|
if err := rpcclient.Detach(true); err != nil {
|
||||||
|
t.Fatalf("error detaching from headless instance: %v", err)
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoteDAPClientAfterContinue(t *testing.T) {
|
||||||
|
const listenAddr = "127.0.0.1:40578"
|
||||||
|
|
||||||
|
dlvbin, tmpdir := getDlvBin(t)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
|
||||||
|
fixture := protest.BuildFixture("loopprog", 0)
|
||||||
|
cmd := exec.Command(dlvbin, "exec", fixture.Path, "--headless", "--continue", "--accept-multiclient", "--log-output=debugger,dap", "--log", "--listen", listenAddr)
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
assertNoError(err, t, "stdout pipe")
|
||||||
|
defer stdout.Close()
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
assertNoError(err, t, "stderr pipe")
|
||||||
|
defer stderr.Close()
|
||||||
|
assertNoError(cmd.Start(), t, "start headless instance")
|
||||||
|
|
||||||
|
scanOut := bufio.NewScanner(stdout)
|
||||||
|
scanErr := bufio.NewScanner(stderr)
|
||||||
|
// Wait for the debug server to start
|
||||||
|
scanOut.Scan() // "API server listening...""
|
||||||
|
t.Log(scanOut.Text())
|
||||||
|
// Wait for the program to start
|
||||||
|
scanOut.Scan() // "past main"
|
||||||
|
t.Log(scanOut.Text())
|
||||||
|
|
||||||
|
go func() { // Capture logging
|
||||||
|
for scanErr.Scan() {
|
||||||
|
t.Log(scanErr.Text())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := newDAPRemoteClient(t, listenAddr)
|
||||||
|
c.DisconnectRequestWithKillOption(true)
|
||||||
|
c.ExpectOutputEventDetachingKill(t)
|
||||||
|
c.ExpectDisconnectResponse(t)
|
||||||
|
c.ExpectTerminatedEvent(t)
|
||||||
|
if _, err := c.ReadMessage(); err == nil {
|
||||||
|
t.Error("expected read error upon shutdown")
|
||||||
|
}
|
||||||
|
c.Close()
|
||||||
|
cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDAPCmdWithClient tests dlv dap --client-addr can be started and shut down.
|
||||||
|
func TestDAPCmdWithClient(t *testing.T) {
|
||||||
listener, err := net.Listen("tcp", ":0")
|
listener, err := net.Listen("tcp", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("cannot setup listener required for testing: %v", err)
|
t.Fatalf("cannot setup listener required for testing: %v", err)
|
||||||
|
|||||||
@ -88,11 +88,11 @@ func (c *Client) ExpectVisibleErrorResponse(t *testing.T) *dap.ErrorResponse {
|
|||||||
return er
|
return er
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) expectErrorResponse(t *testing.T, id int, message string) *dap.ErrorResponse {
|
func (c *Client) ExpectErrorResponseWith(t *testing.T, id int, message string, showUser bool) *dap.ErrorResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
er := c.ExpectErrorResponse(t)
|
er := c.ExpectErrorResponse(t)
|
||||||
if er.Body.Error.Id != id || er.Message != message {
|
if matched, _ := regexp.MatchString(message, er.Body.Error.Format); !matched || er.Body.Error.Id != id || er.Body.Error.ShowUser != showUser {
|
||||||
t.Errorf("\ngot %#v\nwant Id=%d Message=%q", er, id, message)
|
t.Errorf("got %#v, want Id=%d Format=%q ShowUser=%v", er, id, message, showUser)
|
||||||
}
|
}
|
||||||
return er
|
return er
|
||||||
}
|
}
|
||||||
@ -129,12 +129,12 @@ func pretty(v interface{}) string {
|
|||||||
|
|
||||||
func (c *Client) ExpectNotYetImplementedErrorResponse(t *testing.T) *dap.ErrorResponse {
|
func (c *Client) ExpectNotYetImplementedErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return c.expectErrorResponse(t, 7777, "Not yet implemented")
|
return c.ExpectErrorResponseWith(t, 7777, "Not yet implemented", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ExpectUnsupportedCommandErrorResponse(t *testing.T) *dap.ErrorResponse {
|
func (c *Client) ExpectUnsupportedCommandErrorResponse(t *testing.T) *dap.ErrorResponse {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
return c.expectErrorResponse(t, 9999, "Unsupported command")
|
return c.ExpectErrorResponseWith(t, 9999, "Unsupported command", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
|
func (c *Client) ExpectOutputEventRegex(t *testing.T, want string) *dap.OutputEvent {
|
||||||
@ -173,6 +173,27 @@ func (c *Client) ExpectOutputEventTerminating(t *testing.T) *dap.OutputEvent {
|
|||||||
return c.ExpectOutputEventRegex(t, `Terminating process [0-9]+\n`)
|
return c.ExpectOutputEventRegex(t, `Terminating process [0-9]+\n`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) ExpectOutputEventClosingClient(t *testing.T) *dap.OutputEvent {
|
||||||
|
t.Helper()
|
||||||
|
return c.ExpectOutputEventRegex(t, `Closing client session, but leaving multi-client DAP server running at .+:[0-9]+\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CheckStopLocation(t *testing.T, thread int, name string, line int) {
|
||||||
|
t.Helper()
|
||||||
|
c.StackTraceRequest(thread, 0, 20)
|
||||||
|
st := c.ExpectStackTraceResponse(t)
|
||||||
|
if len(st.Body.StackFrames) < 1 {
|
||||||
|
t.Errorf("\ngot %#v\nwant len(stackframes) => 1", st)
|
||||||
|
} else {
|
||||||
|
if line != -1 && st.Body.StackFrames[0].Line != line {
|
||||||
|
t.Errorf("\ngot %#v\nwant Line=%d", st, line)
|
||||||
|
}
|
||||||
|
if st.Body.StackFrames[0].Name != name {
|
||||||
|
t.Errorf("\ngot %#v\nwant Name=%q", st, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// InitializeRequest sends an 'initialize' request.
|
// InitializeRequest sends an 'initialize' request.
|
||||||
func (c *Client) InitializeRequest() {
|
func (c *Client) InitializeRequest() {
|
||||||
request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
|
request := &dap.InitializeRequest{Request: *c.newRequest("initialize")}
|
||||||
|
|||||||
@ -166,9 +166,9 @@ type Config struct {
|
|||||||
|
|
||||||
// log is used for structured logging.
|
// log is used for structured logging.
|
||||||
log *logrus.Entry
|
log *logrus.Entry
|
||||||
// stopTriggered is closed when the server is Stop()-ed.
|
// StopTriggered is closed when the server is Stop()-ed.
|
||||||
// Can be used to safeguard against duplicate shutdown sequences.
|
// Can be used to safeguard against duplicate shutdown sequences.
|
||||||
stopTriggered chan struct{}
|
StopTriggered chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type process struct {
|
type process struct {
|
||||||
@ -277,7 +277,7 @@ func NewServer(config *service.Config) *Server {
|
|||||||
config: &Config{
|
config: &Config{
|
||||||
Config: config,
|
Config: config,
|
||||||
log: logger,
|
log: logger,
|
||||||
stopTriggered: make(chan struct{}),
|
StopTriggered: make(chan struct{}),
|
||||||
},
|
},
|
||||||
listener: config.Listener,
|
listener: config.Listener,
|
||||||
}
|
}
|
||||||
@ -286,7 +286,7 @@ func NewServer(config *service.Config) *Server {
|
|||||||
// NewSession creates a new client session that can handle DAP traffic.
|
// NewSession creates a new client session that can handle DAP traffic.
|
||||||
// It takes an open connection and provides a Close() method to shut it
|
// It takes an open connection and provides a Close() method to shut it
|
||||||
// down when the DAP session disconnects or a connection error occurs.
|
// down when the DAP session disconnects or a connection error occurs.
|
||||||
func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
|
func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debugger) *Session {
|
||||||
if config.log == nil {
|
if config.log == nil {
|
||||||
config.log = logflags.DAPLogger()
|
config.log = logflags.DAPLogger()
|
||||||
}
|
}
|
||||||
@ -298,6 +298,7 @@ func NewSession(conn io.ReadWriteCloser, config *Config) *Session {
|
|||||||
variableHandles: newVariablesHandlesMap(),
|
variableHandles: newVariablesHandlesMap(),
|
||||||
args: defaultArgs,
|
args: defaultArgs,
|
||||||
exceptionErr: nil,
|
exceptionErr: nil,
|
||||||
|
debugger: debugger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,11 +329,11 @@ func (s *Session) setLaunchAttachArgs(args LaunchAttachCommonConfig) error {
|
|||||||
// connection. It shuts down the underlying debugger and kills the target
|
// connection. It shuts down the underlying debugger and kills the target
|
||||||
// process if it was launched by it or stops the noDebug process.
|
// process if it was launched by it or stops the noDebug process.
|
||||||
// This method mustn't be called more than once.
|
// This method mustn't be called more than once.
|
||||||
// stopTriggered notifies other goroutines that stop is in progreess.
|
// StopTriggered notifies other goroutines that stop is in progreess.
|
||||||
func (s *Server) Stop() {
|
func (s *Server) Stop() {
|
||||||
s.config.log.Debug("DAP server stopping...")
|
s.config.log.Debug("DAP server stopping...")
|
||||||
defer s.config.log.Debug("DAP server stopped")
|
defer s.config.log.Debug("DAP server stopped")
|
||||||
close(s.config.stopTriggered)
|
close(s.config.StopTriggered)
|
||||||
|
|
||||||
if s.listener != nil {
|
if s.listener != nil {
|
||||||
// If run goroutine is blocked on accept, this will unblock it.
|
// If run goroutine is blocked on accept, this will unblock it.
|
||||||
@ -365,7 +366,7 @@ func (s *Session) Close() {
|
|||||||
}
|
}
|
||||||
// Close client connection last, so other shutdown stages
|
// Close client connection last, so other shutdown stages
|
||||||
// can send client notifications.
|
// can send client notifications.
|
||||||
// Unless Stop() was called after read loop in serveDAPCodec()
|
// Unless Stop() was called after read loop in ServeDAPCodec()
|
||||||
// returned, this will result in a closed connection error
|
// returned, this will result in a closed connection error
|
||||||
// on next read, breaking out the read loop andd
|
// on next read, breaking out the read loop andd
|
||||||
// allowing the run goroutinee to exit.
|
// allowing the run goroutinee to exit.
|
||||||
@ -387,8 +388,8 @@ func (c *Config) triggerServerStop() {
|
|||||||
// -- run goroutine: calls triggerServerStop()
|
// -- run goroutine: calls triggerServerStop()
|
||||||
// -- main goroutine: calls Stop()
|
// -- main goroutine: calls Stop()
|
||||||
// -- main goroutine: Stop() closes client connection (or client closed it)
|
// -- main goroutine: Stop() closes client connection (or client closed it)
|
||||||
// -- run goroutine: serveDAPCodec() gets "closed network connection"
|
// -- run goroutine: ServeDAPCodec() gets "closed network connection"
|
||||||
// -- run goroutine: serveDAPCodec() returns and calls triggerServerStop()
|
// -- run goroutine: ServeDAPCodec() returns and calls triggerServerStop()
|
||||||
if c.DisconnectChan != nil {
|
if c.DisconnectChan != nil {
|
||||||
close(c.DisconnectChan)
|
close(c.DisconnectChan)
|
||||||
c.DisconnectChan = nil
|
c.DisconnectChan = nil
|
||||||
@ -418,7 +419,7 @@ func (s *Server) Run() {
|
|||||||
conn, err := s.listener.Accept() // listener is closed in Stop()
|
conn, err := s.listener.Accept() // listener is closed in Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
select {
|
select {
|
||||||
case <-s.config.stopTriggered:
|
case <-s.config.StopTriggered:
|
||||||
default:
|
default:
|
||||||
s.config.log.Errorf("Error accepting client connection: %s\n", err)
|
s.config.log.Errorf("Error accepting client connection: %s\n", err)
|
||||||
s.config.triggerServerStop()
|
s.config.triggerServerStop()
|
||||||
@ -438,9 +439,9 @@ func (s *Server) Run() {
|
|||||||
|
|
||||||
func (s *Server) runSession(conn io.ReadWriteCloser) {
|
func (s *Server) runSession(conn io.ReadWriteCloser) {
|
||||||
s.sessionMu.Lock()
|
s.sessionMu.Lock()
|
||||||
s.session = NewSession(conn, s.config) // closed in Stop()
|
s.session = NewSession(conn, s.config, nil) // closed in Stop()
|
||||||
s.sessionMu.Unlock()
|
s.sessionMu.Unlock()
|
||||||
s.session.serveDAPCodec()
|
s.session.ServeDAPCodec()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunWithClient is similar to Run but works only with an already established
|
// RunWithClient is similar to Run but works only with an already established
|
||||||
@ -456,10 +457,10 @@ func (s *Server) RunWithClient(conn net.Conn) {
|
|||||||
go s.runSession(conn)
|
go s.runSession(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveDAPCodec reads and decodes requests from the client
|
// ServeDAPCodec reads and decodes requests from the client
|
||||||
// until it encounters an error or EOF, when it sends
|
// until it encounters an error or EOF, when it sends
|
||||||
// a disconnect signal and returns.
|
// a disconnect signal and returns.
|
||||||
func (s *Session) serveDAPCodec() {
|
func (s *Session) ServeDAPCodec() {
|
||||||
// TODO(polina): defer-close conn/session like in serveJSONCodec
|
// TODO(polina): defer-close conn/session like in serveJSONCodec
|
||||||
reader := bufio.NewReader(s.conn)
|
reader := bufio.NewReader(s.conn)
|
||||||
for {
|
for {
|
||||||
@ -475,7 +476,7 @@ func (s *Session) serveDAPCodec() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.config.log.Debug("DAP error: ", err)
|
s.config.log.Debug("DAP error: ", err)
|
||||||
select {
|
select {
|
||||||
case <-s.config.stopTriggered:
|
case <-s.config.StopTriggered:
|
||||||
default:
|
default:
|
||||||
if err != io.EOF { // EOF means client closed connection
|
if err != io.EOF { // EOF means client closed connection
|
||||||
if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
|
if decodeErr, ok := err.(*dap.DecodeProtocolMessageFieldError); ok {
|
||||||
@ -1238,10 +1239,12 @@ func (s *Session) stopDebugSession(killProcess bool) error {
|
|||||||
// halt sends a halt request if the debuggee is running.
|
// halt sends a halt request if the debuggee is running.
|
||||||
// changeStateMu should be held when calling (*Server).halt.
|
// changeStateMu should be held when calling (*Server).halt.
|
||||||
func (s *Session) halt() (*api.DebuggerState, error) {
|
func (s *Session) halt() (*api.DebuggerState, error) {
|
||||||
|
s.config.log.Debug("halting")
|
||||||
// Only send a halt request if the debuggee is running.
|
// Only send a halt request if the debuggee is running.
|
||||||
if s.debugger.IsRunning() {
|
if s.debugger.IsRunning() {
|
||||||
return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil)
|
||||||
}
|
}
|
||||||
|
s.config.log.Debug("process not running")
|
||||||
return s.debugger.State(false)
|
return s.debugger.State(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4598,23 +4598,6 @@ func TestFatalThrowBreakpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyStopLocation(t *testing.T, client *daptest.Client, thread int, name string, line int) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
client.StackTraceRequest(thread, 0, 20)
|
|
||||||
st := client.ExpectStackTraceResponse(t)
|
|
||||||
if len(st.Body.StackFrames) < 1 {
|
|
||||||
t.Errorf("\ngot %#v\nwant len(stackframes) => 1", st)
|
|
||||||
} else {
|
|
||||||
if line != -1 && st.Body.StackFrames[0].Line != line {
|
|
||||||
t.Errorf("\ngot %#v\nwant Line=%d", st, line)
|
|
||||||
}
|
|
||||||
if st.Body.StackFrames[0].Name != name {
|
|
||||||
t.Errorf("\ngot %#v\nwant Name=%q", st, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkStop covers the standard sequence of requests issued by
|
// checkStop covers the standard sequence of requests issued by
|
||||||
// a client at a breakpoint or another non-terminal stop event.
|
// a client at a breakpoint or another non-terminal stop event.
|
||||||
// The details have been tested by other tests,
|
// The details have been tested by other tests,
|
||||||
@ -4625,7 +4608,7 @@ func checkStop(t *testing.T, client *daptest.Client, thread int, fname string, l
|
|||||||
client.ThreadsRequest()
|
client.ThreadsRequest()
|
||||||
client.ExpectThreadsResponse(t)
|
client.ExpectThreadsResponse(t)
|
||||||
|
|
||||||
verifyStopLocation(t, client, thread, fname, line)
|
client.CheckStopLocation(t, thread, fname, line)
|
||||||
|
|
||||||
client.ScopesRequest(1000)
|
client.ScopesRequest(1000)
|
||||||
client.ExpectScopesResponse(t)
|
client.ExpectScopesResponse(t)
|
||||||
@ -5150,7 +5133,7 @@ func TestPauseAndContinue(t *testing.T) {
|
|||||||
fixture.Source, []int{6},
|
fixture.Source, []int{6},
|
||||||
[]onBreakpoint{{
|
[]onBreakpoint{{
|
||||||
execute: func() {
|
execute: func() {
|
||||||
verifyStopLocation(t, client, 1, "main.loop", 6)
|
client.CheckStopLocation(t, 1, "main.loop", 6)
|
||||||
|
|
||||||
// Continue resumes all goroutines, so thread id is ignored
|
// Continue resumes all goroutines, so thread id is ignored
|
||||||
client.ContinueRequest(12345)
|
client.ContinueRequest(12345)
|
||||||
@ -5967,7 +5950,6 @@ func runTestWithDebugger(t *testing.T, dbg *debugger.Debugger, test func(c *dapt
|
|||||||
server, _ := startDAPServer(t, serverStopped)
|
server, _ := startDAPServer(t, serverStopped)
|
||||||
client := daptest.NewClient(server.listener.Addr().String())
|
client := daptest.NewClient(server.listener.Addr().String())
|
||||||
time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
|
time.Sleep(100 * time.Millisecond) // Give time for connection to be set as dap.Session
|
||||||
// TODO(polina): update once the server interface is refactored to take debugger as arg
|
|
||||||
server.sessionMu.Lock()
|
server.sessionMu.Lock()
|
||||||
if server.session == nil {
|
if server.session == nil {
|
||||||
t.Fatal("DAP session is not ready")
|
t.Fatal("DAP session is not ready")
|
||||||
@ -6085,7 +6067,6 @@ func TestAttachRemoteMultiClient(t *testing.T) {
|
|||||||
// DAP server doesn't support accept-multiclient, but we can use this
|
// DAP server doesn't support accept-multiclient, but we can use this
|
||||||
// hack to test the inner connection logic that can be used by a server that does.
|
// hack to test the inner connection logic that can be used by a server that does.
|
||||||
server.session.config.AcceptMulti = true
|
server.session.config.AcceptMulti = true
|
||||||
// TODO(polina): update once the server interface is refactored to take debugger as arg
|
|
||||||
_, server.session.debugger = launchDebuggerWithTargetHalted(t, "increment")
|
_, server.session.debugger = launchDebuggerWithTargetHalted(t, "increment")
|
||||||
server.sessionMu.Unlock()
|
server.sessionMu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package rpccommon
|
package rpccommon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/go-delve/delve/pkg/version"
|
"github.com/go-delve/delve/pkg/version"
|
||||||
"github.com/go-delve/delve/service"
|
"github.com/go-delve/delve/service"
|
||||||
"github.com/go-delve/delve/service/api"
|
"github.com/go-delve/delve/service/api"
|
||||||
|
"github.com/go-delve/delve/service/dap"
|
||||||
"github.com/go-delve/delve/service/debugger"
|
"github.com/go-delve/delve/service/debugger"
|
||||||
"github.com/go-delve/delve/service/internal/sameuser"
|
"github.com/go-delve/delve/service/internal/sameuser"
|
||||||
"github.com/go-delve/delve/service/rpc1"
|
"github.com/go-delve/delve/service/rpc1"
|
||||||
@ -156,7 +158,7 @@ func (s *ServerImpl) Run() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go s.serveJSONCodec(c)
|
go s.serveConnectionDemux(c)
|
||||||
if !s.config.AcceptMulti {
|
if !s.config.AcceptMulti {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -165,6 +167,28 @@ func (s *ServerImpl) Run() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type bufReadWriteCloser struct {
|
||||||
|
*bufio.Reader
|
||||||
|
io.WriteCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerImpl) serveConnectionDemux(c io.ReadWriteCloser) {
|
||||||
|
conn := &bufReadWriteCloser{bufio.NewReader(c), c}
|
||||||
|
b, err := conn.Peek(1)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Warnf("error determining new connection protocol: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if b[0] == 'C' { // C is for DAP's Content-Length
|
||||||
|
s.log.Debugf("serving DAP on new connection")
|
||||||
|
ds := dap.NewSession(conn, &dap.Config{Config: s.config, StopTriggered: s.stopChan}, s.debugger)
|
||||||
|
go ds.ServeDAPCodec()
|
||||||
|
} else {
|
||||||
|
s.log.Debugf("serving JSON-RPC on new connection")
|
||||||
|
go s.serveJSONCodec(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Precompute the reflect type for error. Can't use error directly
|
// Precompute the reflect type for error. Can't use error directly
|
||||||
// because Typeof takes an empty interface value. This is annoying.
|
// because Typeof takes an empty interface value. This is annoying.
|
||||||
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
var typeOfError = reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
|||||||
Reference in New Issue
Block a user