cmd/dlv: print out message with stack trace when breakpoint is hit but has no waiting client (#3632)

* Print out message and dump stack on pause

* Fix test

* Move the logic to debugger layer

* Remove unused fields

* Do not use defer to get state

* move channel to connection

* remove lock on isClosed

* Use mutex

* Remove unwanted changes
This commit is contained in:
Fata Nugraha 2024-06-13 02:31:46 +07:00 committed by GitHub
parent 2ec2e831d6
commit 15a9f9d353
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 136 additions and 21 deletions

@ -251,6 +251,35 @@ func TestOutput(t *testing.T) {
} }
} }
// TestUnattendedBreakpoint tests whether dlv will print a message to stderr when the client that sends continue is disconnected
// or not.
func TestUnattendedBreakpoint(t *testing.T) {
const listenAddr = "127.0.0.1:40573"
fixturePath := filepath.Join(protest.FindFixturesDir(), "panic.go")
cmd := exec.Command(getDlvBin(t), "debug", "--continue", "--headless", "--accept-multiclient", "--listen", listenAddr, fixturePath)
stderr, err := cmd.StderrPipe()
assertNoError(err, t, "stdout pipe")
defer stderr.Close()
assertNoError(cmd.Start(), t, "start headless instance")
scan := bufio.NewScanner(stderr)
for scan.Scan() {
t.Log(scan.Text())
if strings.Contains(scan.Text(), "execution is paused because your program is panicking") {
break
}
}
// and detach from and kill the headless instance
client := rpc2.NewClient(listenAddr)
if err := client.Detach(true); err != nil {
t.Fatalf("error detaching from headless instance: %v", err)
}
cmd.Wait()
}
// TestContinue verifies that the debugged executable starts immediately with --continue // TestContinue verifies that the debugged executable starts immediately with --continue
func TestContinue(t *testing.T) { func TestContinue(t *testing.T) {
const listenAddr = "127.0.0.1:40573" const listenAddr = "127.0.0.1:40573"

@ -181,14 +181,23 @@ type Config struct {
} }
type connection struct { type connection struct {
mu sync.Mutex mu sync.Mutex
closed bool closed bool
closedChan chan struct{}
io.ReadWriteCloser io.ReadWriteCloser
} }
func newConnection(conn io.ReadWriteCloser) *connection {
return &connection{ReadWriteCloser: conn, closedChan: make(chan struct{})}
}
func (c *connection) Close() error { func (c *connection) Close() error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if !c.closed {
close(c.closedChan)
}
c.closed = true c.closed = true
return c.ReadWriteCloser.Close() return c.ReadWriteCloser.Close()
} }
@ -335,7 +344,7 @@ func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debu
return &Session{ return &Session{
config: config, config: config,
id: sessionCount, id: sessionCount,
conn: &connection{ReadWriteCloser: conn}, conn: newConnection(conn),
stackFrameHandles: newHandlesMap(), stackFrameHandles: newHandlesMap(),
variableHandles: newVariablesHandlesMap(), variableHandles: newVariablesHandlesMap(),
args: defaultArgs, args: defaultArgs,
@ -1366,7 +1375,7 @@ func (s *Session) halt() (*api.DebuggerState, error) {
s.config.log.Debug("halting") 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, nil)
} }
s.config.log.Debug("process not running") s.config.log.Debug("process not running")
return s.debugger.State(false) return s.debugger.State(false)
@ -2048,7 +2057,7 @@ func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int6
// due to an error, so the server is ready to receive new requests. // due to an error, so the server is ready to receive new requests.
func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange *syncflag) { func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange *syncflag) {
defer allowNextStateChange.raise() defer allowNextStateChange.raise()
_, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil) _, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil, s.conn.closedChan)
if err != nil { if err != nil {
s.config.log.Errorf("Error switching goroutines while stepping: %v", err) s.config.log.Errorf("Error switching goroutines while stepping: %v", err)
// If we encounter an error, we will have to send a stopped event // If we encounter an error, we will have to send a stopped event
@ -2914,7 +2923,7 @@ func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*p
Expr: expr, Expr: expr,
UnsafeCall: false, UnsafeCall: false,
GoroutineID: int64(goid), GoroutineID: int64(goid),
}, nil) }, nil, s.conn.closedChan)
if processExited(state, err) { if processExited(state, err) {
s.preTerminatedWG.Wait() s.preTerminatedWG.Wait()
e := &dap.TerminatedEvent{Event: *newEvent("terminated")} e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
@ -3645,7 +3654,7 @@ func (s *Session) resumeOnce(command string, allowNextStateChange *syncflag) (bo
state, err := s.debugger.State(false) state, err := s.debugger.State(false)
return false, state, err return false, state, err
} }
state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone) state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone, s.conn.closedChan)
return true, state, err return true, state, err
} }

@ -6736,7 +6736,7 @@ func launchDebuggerWithTargetRunning(t *testing.T, fixture string) (*protest.Fix
var err error var err error
go func() { go func() {
t.Helper() t.Helper()
_, err = dbg.Command(&api.DebuggerCommand{Name: api.Continue}, running) _, err = dbg.Command(&api.DebuggerCommand{Name: api.Continue}, running, nil)
select { select {
case <-running: case <-running:
default: default:
@ -6934,7 +6934,7 @@ func (s *MultiClientCloseServerMock) stop(t *testing.T) {
// they are part of dap.Session. // they are part of dap.Session.
// We must take it down manually as if we are in rpccommon::ServerImpl::Stop. // We must take it down manually as if we are in rpccommon::ServerImpl::Stop.
if s.debugger.IsRunning() { if s.debugger.IsRunning() {
s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil) s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil)
} }
s.debugger.Detach(true) s.debugger.Detach(true)
} }

@ -1159,9 +1159,7 @@ func (d *Debugger) IsRunning() bool {
} }
// Command handles commands which control the debugger lifecycle // Command handles commands which control the debugger lifecycle
func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}) (*api.DebuggerState, error) { func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}, clientStatusCh chan struct{}) (state *api.DebuggerState, err error) {
var err error
if command.Name == api.Halt { if command.Name == api.Halt {
// RequestManualStop does not invoke any ptrace syscalls, so it's safe to // RequestManualStop does not invoke any ptrace syscalls, so it's safe to
// access the process directly. // access the process directly.
@ -1338,6 +1336,8 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
bp.Disabled = true bp.Disabled = true
d.amendBreakpoint(bp) d.amendBreakpoint(bp)
} }
d.maybePrintUnattendedBreakpointWarning(state.CurrentThread, clientStatusCh)
return state, err return state, err
} }
@ -1785,7 +1785,10 @@ func (d *Debugger) GroupGoroutines(gs []*proc.G, group *api.GoroutineGroupingOpt
func (d *Debugger) Stacktrace(goroutineID int64, depth int, opts api.StacktraceOptions) ([]proc.Stackframe, error) { func (d *Debugger) Stacktrace(goroutineID int64, depth int, opts api.StacktraceOptions) ([]proc.Stackframe, error) {
d.targetMutex.Lock() d.targetMutex.Lock()
defer d.targetMutex.Unlock() defer d.targetMutex.Unlock()
return d.stacktrace(goroutineID, depth, opts)
}
func (d *Debugger) stacktrace(goroutineID int64, depth int, opts api.StacktraceOptions) ([]proc.Stackframe, error) {
if _, err := d.target.Valid(); err != nil { if _, err := d.target.Valid(); err != nil {
return nil, err return nil, err
} }
@ -2426,3 +2429,51 @@ var attachErrorMessage = attachErrorMessageDefault
func attachErrorMessageDefault(pid int, err error) error { func attachErrorMessageDefault(pid int, err error) error {
return fmt.Errorf("could not attach to pid %d: %s", pid, err) return fmt.Errorf("could not attach to pid %d: %s", pid, err)
} }
func (d *Debugger) maybePrintUnattendedBreakpointWarning(currentThread *api.Thread, clientStatusCh <-chan struct{}) {
select {
case <-clientStatusCh:
// the channel will be closed if the client that sends the command has left
// i.e. closed the connection.
default:
return
}
const defaultStackTraceDepth = 50
frames, err := d.stacktrace(currentThread.GoroutineID, defaultStackTraceDepth, 0)
if err != nil {
fmt.Fprintln(os.Stderr, "err", err)
return
}
apiFrames, err := d.convertStacktrace(frames, nil)
if err != nil {
fmt.Fprintln(os.Stderr, "err", err)
return
}
bp := currentThread.Breakpoint
if bp == nil {
fmt.Fprintln(os.Stderr, "bp", bp)
return
}
switch bp.Name {
case proc.FatalThrow, proc.UnrecoveredPanic:
fmt.Fprintln(os.Stderr, "\n** execution is paused because your program is panicking **")
default:
fmt.Fprintln(os.Stderr, "\n** execution is paused because a breakpoint is hit **")
}
fmt.Fprintf(os.Stderr, "To continue the execution please connect your client to the debugger.")
fmt.Fprintln(os.Stderr, "\nStack trace:")
formatPathFunc := func(s string) string {
return s
}
includeFunc := func(f api.Stackframe) bool {
// todo(fata): do not include the final panic/fatal function if bp.Name is fatalthrow/panic
return true
}
api.PrintStack(formatPathFunc, os.Stderr, apiFrames, "", false, api.StackTraceColors{}, includeFunc)
}

@ -56,7 +56,7 @@ func (s *RPCServer) State(arg interface{}, state *api.DebuggerState) error {
} }
func (s *RPCServer) Command(command *api.DebuggerCommand, cb service.RPCCallback) { func (s *RPCServer) Command(command *api.DebuggerCommand, cb service.RPCCallback) {
st, err := s.debugger.Command(command, cb.SetupDoneChan()) st, err := s.debugger.Command(command, cb.SetupDoneChan(), cb.DisconnectChan())
cb.Return(st, err) cb.Return(st, err)
} }

@ -127,7 +127,7 @@ type CommandOut struct {
// Command interrupts, continues and steps through the program. // Command interrupts, continues and steps through the program.
func (s *RPCServer) Command(command api.DebuggerCommand, cb service.RPCCallback) { func (s *RPCServer) Command(command api.DebuggerCommand, cb service.RPCCallback) {
st, err := s.debugger.Command(&command, cb.SetupDoneChan()) st, err := s.debugger.Command(&command, cb.SetupDoneChan(), cb.DisconnectChan())
if err != nil { if err != nil {
cb.Return(nil, err) cb.Return(nil, err)
return return

@ -8,4 +8,8 @@ type RPCCallback interface {
// asynchronous method has completed setup and the server is ready to // asynchronous method has completed setup and the server is ready to
// receive other requests. // receive other requests.
SetupDoneChan() chan struct{} SetupDoneChan() chan struct{}
// DisconnectChan returns a channel that should be clised to signal that
// the client that initially issued the command has been disconnected.
DisconnectChan() chan struct{}
} }

@ -48,11 +48,12 @@ type ServerImpl struct {
} }
type RPCCallback struct { type RPCCallback struct {
s *ServerImpl s *ServerImpl
sending *sync.Mutex sending *sync.Mutex
codec rpc.ServerCodec codec rpc.ServerCodec
req rpc.Request req rpc.Request
setupDone chan struct{} setupDone chan struct{}
disconnectChan chan struct{}
} }
var _ service.RPCCallback = &RPCCallback{} var _ service.RPCCallback = &RPCCallback{}
@ -97,7 +98,7 @@ func (s *ServerImpl) Stop() error {
s.listener.Close() s.listener.Close()
} }
if s.debugger.IsRunning() { if s.debugger.IsRunning() {
s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil) s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil)
} }
kill := s.config.Debugger.AttachPid == 0 kill := s.config.Debugger.AttachPid == 0
return s.debugger.Detach(kill) return s.debugger.Detach(kill)
@ -277,7 +278,9 @@ func suitableMethods(rcvr interface{}, methods map[string]*methodType, log logfl
} }
func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) { func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) {
clientDisconnectChan := make(chan struct{})
defer func() { defer func() {
close(clientDisconnectChan)
if !s.config.AcceptMulti && s.config.DisconnectChan != nil { if !s.config.AcceptMulti && s.config.DisconnectChan != nil {
close(s.config.DisconnectChan) close(s.config.DisconnectChan)
} }
@ -361,7 +364,7 @@ func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) {
s.log.Debugf("(async %d) <- %s(%T%s)", req.Seq, req.ServiceMethod, argv.Interface(), argvbytes) s.log.Debugf("(async %d) <- %s(%T%s)", req.Seq, req.ServiceMethod, argv.Interface(), argvbytes)
} }
function := mtype.method.Func function := mtype.method.Func
ctl := &RPCCallback{s, sending, codec, req, make(chan struct{})} ctl := &RPCCallback{s, sending, codec, req, make(chan struct{}), clientDisconnectChan}
go func() { go func() {
defer func() { defer func() {
if ierr := recover(); ierr != nil { if ierr := recover(); ierr != nil {
@ -412,9 +415,28 @@ func (cb *RPCCallback) Return(out interface{}, err error) {
outbytes, _ := json.Marshal(out) outbytes, _ := json.Marshal(out)
cb.s.log.Debugf("(async %d) -> %T%s error: %q", cb.req.Seq, out, outbytes, errmsg) cb.s.log.Debugf("(async %d) -> %T%s error: %q", cb.req.Seq, out, outbytes, errmsg)
} }
if cb.hasDisconnected() {
return
}
cb.s.sendResponse(cb.sending, &cb.req, &resp, out, cb.codec, errmsg) cb.s.sendResponse(cb.sending, &cb.req, &resp, out, cb.codec, errmsg)
} }
func (cb *RPCCallback) DisconnectChan() chan struct{} {
return cb.disconnectChan
}
func (cb *RPCCallback) hasDisconnected() bool {
select {
case <-cb.disconnectChan:
return true
default:
}
return false
}
func (cb *RPCCallback) SetupDoneChan() chan struct{} { func (cb *RPCCallback) SetupDoneChan() chan struct{} {
return cb.setupDone return cb.setupDone
} }