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
func TestContinue(t *testing.T) {
const listenAddr = "127.0.0.1:40573"

@ -181,14 +181,23 @@ type Config struct {
}
type connection struct {
mu sync.Mutex
closed bool
mu sync.Mutex
closed bool
closedChan chan struct{}
io.ReadWriteCloser
}
func newConnection(conn io.ReadWriteCloser) *connection {
return &connection{ReadWriteCloser: conn, closedChan: make(chan struct{})}
}
func (c *connection) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.closed {
close(c.closedChan)
}
c.closed = true
return c.ReadWriteCloser.Close()
}
@ -335,7 +344,7 @@ func NewSession(conn io.ReadWriteCloser, config *Config, debugger *debugger.Debu
return &Session{
config: config,
id: sessionCount,
conn: &connection{ReadWriteCloser: conn},
conn: newConnection(conn),
stackFrameHandles: newHandlesMap(),
variableHandles: newVariablesHandlesMap(),
args: defaultArgs,
@ -1366,7 +1375,7 @@ func (s *Session) halt() (*api.DebuggerState, error) {
s.config.log.Debug("halting")
// Only send a halt request if the debuggee is running.
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")
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.
func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange *syncflag) {
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 {
s.config.log.Errorf("Error switching goroutines while stepping: %v", err)
// 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,
UnsafeCall: false,
GoroutineID: int64(goid),
}, nil)
}, nil, s.conn.closedChan)
if processExited(state, err) {
s.preTerminatedWG.Wait()
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)
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
}

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

@ -1159,9 +1159,7 @@ func (d *Debugger) IsRunning() bool {
}
// Command handles commands which control the debugger lifecycle
func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}) (*api.DebuggerState, error) {
var err error
func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}, clientStatusCh chan struct{}) (state *api.DebuggerState, err error) {
if command.Name == api.Halt {
// RequestManualStop does not invoke any ptrace syscalls, so it's safe to
// access the process directly.
@ -1338,6 +1336,8 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
bp.Disabled = true
d.amendBreakpoint(bp)
}
d.maybePrintUnattendedBreakpointWarning(state.CurrentThread, clientStatusCh)
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) {
d.targetMutex.Lock()
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 {
return nil, err
}
@ -2426,3 +2429,51 @@ var attachErrorMessage = attachErrorMessageDefault
func attachErrorMessageDefault(pid int, err error) error {
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) {
st, err := s.debugger.Command(command, cb.SetupDoneChan())
st, err := s.debugger.Command(command, cb.SetupDoneChan(), cb.DisconnectChan())
cb.Return(st, err)
}

@ -127,7 +127,7 @@ type CommandOut struct {
// Command interrupts, continues and steps through the program.
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 {
cb.Return(nil, err)
return

@ -8,4 +8,8 @@ type RPCCallback interface {
// asynchronous method has completed setup and the server is ready to
// receive other requests.
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 {
s *ServerImpl
sending *sync.Mutex
codec rpc.ServerCodec
req rpc.Request
setupDone chan struct{}
s *ServerImpl
sending *sync.Mutex
codec rpc.ServerCodec
req rpc.Request
setupDone chan struct{}
disconnectChan chan struct{}
}
var _ service.RPCCallback = &RPCCallback{}
@ -97,7 +98,7 @@ func (s *ServerImpl) Stop() error {
s.listener.Close()
}
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
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) {
clientDisconnectChan := make(chan struct{})
defer func() {
close(clientDisconnectChan)
if !s.config.AcceptMulti && s.config.DisconnectChan != nil {
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)
}
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() {
defer func() {
if ierr := recover(); ierr != nil {
@ -412,9 +415,28 @@ func (cb *RPCCallback) Return(out interface{}, err error) {
outbytes, _ := json.Marshal(out)
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)
}
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{} {
return cb.setupDone
}