mirror of
				https://github.com/go-delve/delve.git
				synced 2025-10-31 18:57:18 +08:00 
			
		
		
		
	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:
		| @ -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 | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Fata Nugraha
					Fata Nugraha