提交 9a216211 编写于 作者: A aarzilli 提交者: Derek Parker

proc,terminal,service: let headless instances run without connected clients

This pull request makes several changes to delve to allow headless
instancess that are started with the --accept-multiclient flag to
keep running even if there is no connected client. Specifically:

1. Makes a headless instance started with --accept-multiclient quit
    after one of the clients sends a Detach request (previously they
    would never ever quit, which was a bug).
2. Changes proc/gdbserial and proc/native so that they mark the
    Process as exited after they detach, even if they did not kill the
    process during detach. This prevents bugs such as #1231 where we
    attempt to manipulate a target process after we detached from it.
3. On non --accept-multiclient instances do not kill the target
    process unless we started it or the client specifically requests
    it (previously if the client did not Detach before closing the
    connection we would kill the target process unconditionally)
4. Add a -c option to the quit command that detaches from the
    headless server after restarting the target.
5. Change terminal so that, when attached to --accept-multiclient,
    pressing ^C will prompt the user to either disconnect from the
    server or pause the target process. Also extend the exit prompt to
    ask if the user wants to keep the headless server running.

Implements #245, #952, #1159, #1231
上级 818ed0b2
......@@ -163,6 +163,10 @@ Move the current frame down by <m>. The second form runs the command on the give
## exit
Exit the debugger.
exit [-c]
When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.
Aliases: quit q
......
......@@ -488,9 +488,16 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
fmt.Fprint(os.Stderr, "Warning: init file ignored\n")
}
if !Headless && AcceptMulti {
fmt.Fprint(os.Stderr, "Warning accept-multi: ignored\n")
// AcceptMulti won't work in normal (non-headless) mode because we always
// call server.Stop after the terminal client exits.
AcceptMulti = false
}
var server interface {
Run() error
Stop(bool) error
Stop() error
}
disconnectChan := make(chan struct{})
......@@ -541,7 +548,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
case <-ch:
case <-disconnectChan:
}
err = server.Stop(true)
err = server.Stop()
} else {
// Create and start a terminal
client := rpc2.NewClient(listener.Addr().String())
......
......@@ -299,8 +299,8 @@ func (p *Process) Detach(bool) error {
return nil
}
func (p *Process) Exited() bool {
return false
func (p *Process) Valid() (bool, error) {
return true, nil
}
func (p *Process) Common() *proc.CommonProcess {
......
......@@ -24,8 +24,8 @@ const (
// If currentGoroutine is set and thread is stopped at a CALL instruction Disassemble will evaluate the argument of the CALL instruction using the thread's registers
// Be aware that the Bytes field of each returned instruction is a slice of a larger array of size endPC - startPC
func Disassemble(dbp Process, g *G, startPC, endPC uint64) ([]AsmInstruction, error) {
if dbp.Exited() {
return nil, &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return nil, err
}
if g == nil {
ct := dbp.CurrentThread()
......
......@@ -107,8 +107,8 @@ type Process struct {
currentThread *Thread
selectedGoroutine *proc.G
exited bool
ctrlC bool // ctrl-c was sent to stop inferior
exited, detached bool
ctrlC bool // ctrl-c was sent to stop inferior
manualStopRequested bool
......@@ -568,8 +568,14 @@ func (p *Process) Pid() int {
return int(p.conn.pid)
}
func (p *Process) Exited() bool {
return p.exited
func (p *Process) Valid() (bool, error) {
if p.detached {
return false, &proc.ProcessDetachedError{}
}
if p.exited {
return false, &proc.ProcessExitedError{Pid: p.Pid()}
}
return true, nil
}
func (p *Process) ResumeNotify(ch chan<- struct{}) {
......@@ -818,6 +824,7 @@ func (p *Process) Detach(kill bool) error {
<-p.waitChan
p.process = nil
}
p.detached = true
return p.bi.Close()
}
......
......@@ -60,7 +60,10 @@ type Info interface {
// ResumeNotify specifies a channel that will be closed the next time
// ContinueOnce finishes resuming the target.
ResumeNotify(chan<- struct{})
Exited() bool
// Valid returns true if this Process can be used. When it returns false it
// also returns an error describing why the Process is invalid (either
// ProcessExitedError or ProcessDetachedError).
Valid() (bool, error)
BinInfo() *BinaryInfo
// Common returns a struct with fields common to all backends
Common() *CommonProcess
......
......@@ -34,11 +34,12 @@ type Process struct {
firstStart bool
stopMu sync.Mutex
resumeChan chan<- struct{}
exited bool
ptraceChan chan func()
ptraceDoneChan chan interface{}
childProcess bool // this process was launched, not attached to
manualStopRequested bool
exited, detached bool
}
// New returns an initialized Process struct. Before returning,
......@@ -105,14 +106,19 @@ func (dbp *Process) Detach(kill bool) (err error) {
err = killProcess(dbp.pid)
}
})
dbp.bi.Close()
dbp.detached = true
dbp.postExit()
return
}
// Exited returns whether the debugged
// process has exited.
func (dbp *Process) Exited() bool {
return dbp.exited
func (dbp *Process) Valid() (bool, error) {
if dbp.detached {
return false, &proc.ProcessDetachedError{}
}
if dbp.exited {
return false, &proc.ProcessExitedError{Pid: dbp.Pid()}
}
return true, nil
}
func (dbp *Process) ResumeNotify(ch chan<- struct{}) {
......
......@@ -26,6 +26,14 @@ func (pe ProcessExitedError) Error() string {
return fmt.Sprintf("Process %d has exited with status %d", pe.Pid, pe.Status)
}
// ProcessDetachedError indicates that we detached from the target process.
type ProcessDetachedError struct {
}
func (pe ProcessDetachedError) Error() string {
return "detached from the process"
}
// FindFileLocation returns the PC for a given file:line.
// Assumes that `file` is normalized to lower case and '/' on Windows.
func FindFileLocation(p Process, fileName string, lineno int) (uint64, error) {
......@@ -73,8 +81,8 @@ func FindFunctionLocation(p Process, funcName string, firstLine bool, lineOffset
// Next continues execution until the next source line.
func Next(dbp Process) (err error) {
if dbp.Exited() {
return &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return err
}
if dbp.Breakpoints().HasInternalBreakpoints() {
return fmt.Errorf("next while nexting")
......@@ -92,8 +100,8 @@ func Next(dbp Process) (err error) {
// process. It will continue until it hits a breakpoint
// or is otherwise stopped.
func Continue(dbp Process) error {
if dbp.Exited() {
return &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return err
}
for _, thread := range dbp.ThreadList() {
thread.Common().returnValues = nil
......@@ -234,8 +242,8 @@ func pickCurrentThread(dbp Process, trapthread Thread, threads []Thread) error {
// Step will continue until another source line is reached.
// Will step into functions.
func Step(dbp Process) (err error) {
if dbp.Exited() {
return &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return err
}
if dbp.Breakpoints().HasInternalBreakpoints() {
return fmt.Errorf("next while nexting")
......@@ -297,8 +305,8 @@ func andFrameoffCondition(cond ast.Expr, frameoff int64) ast.Expr {
// StepOut will continue until the current goroutine exits the
// function currently being executed or a deferred function is executed
func StepOut(dbp Process) error {
if dbp.Exited() {
return &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return err
}
selg := dbp.SelectedGoroutine()
curthread := dbp.CurrentThread()
......@@ -383,8 +391,8 @@ func StepOut(dbp Process) error {
// GoroutinesInfo returns an array of G structures representing the information
// Delve cares about from the internal runtime G structure.
func GoroutinesInfo(dbp Process) ([]*G, error) {
if dbp.Exited() {
return nil, &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return nil, err
}
if dbp.Common().allGCache != nil {
return dbp.Common().allGCache, nil
......@@ -484,8 +492,8 @@ func FindGoroutine(dbp Process, gid int) (*G, error) {
// ConvertEvalScope returns a new EvalScope in the context of the
// specified goroutine ID and stack frame.
func ConvertEvalScope(dbp Process, gid, frame int) (*EvalScope, error) {
if dbp.Exited() {
return nil, &ProcessExitedError{Pid: dbp.Pid()}
if _, err := dbp.Valid(); err != nil {
return nil, err
}
ct := dbp.CurrentThread()
g, err := FindGoroutine(dbp, gid)
......
......@@ -974,7 +974,7 @@ func TestKill(t *testing.T) {
if err := p.Detach(true); err != nil {
t.Fatal(err)
}
if !p.Exited() {
if valid, _ := p.Valid(); valid {
t.Fatal("expected process to have exited")
}
if runtime.GOOS == "linux" {
......@@ -1036,7 +1036,7 @@ func TestContinueMulti(t *testing.T) {
sayhiCount := 0
for {
err := proc.Continue(p)
if p.Exited() {
if valid, _ := p.Valid(); !valid {
break
}
assertNoError(err, t, "Continue()")
......
......@@ -212,7 +212,11 @@ If regex is specified only package variables with a name matching it will be ret
regs [-a]
Argument -a shows more registers.`},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: `Exit the debugger.
exit [-c]
When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.`},
{aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code.
[goroutine <n>] [frame <m>] list [<linespec>]
......@@ -1669,6 +1673,12 @@ func (ere ExitRequestError) Error() string {
}
func exitCommand(t *Term, ctx callContext, args string) error {
if args == "-c" {
if !t.client.IsMulticlient() {
return errors.New("not connected to an --accept-multiclient server")
}
t.quitContinue = true
}
return ExitRequestError{}
}
......
......@@ -7,6 +7,7 @@ import (
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
......@@ -33,10 +34,31 @@ type Term struct {
dumb bool
stdout io.Writer
InitFile string
// quitContinue is set to true by exitCommand to signal that the process
// should be resumed before quitting.
quitContinue bool
quittingMutex sync.Mutex
quitting bool
}
// New returns a new Term.
func New(client service.Client, conf *config.Config) *Term {
if client != nil && client.IsMulticlient() {
state, _ := client.GetStateNonBlocking()
// The error return of GetState will usually be the ProcessExitedError,
// which we don't care about. If there are other errors they will show up
// later, here we are only concerned about stopping a running target so
// that we can initialize our connection.
if state != nil && state.Running {
_, err := client.Halt()
if err != nil {
fmt.Fprintf(os.Stderr, "could not halt: %v", err)
return nil
}
}
}
cmds := DebugCommands(client)
if conf != nil && conf.Aliases != nil {
cmds.Merge(conf.Aliases)
......@@ -75,22 +97,55 @@ func (t *Term) Close() {
t.line.Close()
}
// Run begins running dlv in the terminal.
func (t *Term) Run() (int, error) {
defer t.Close()
func (t *Term) sigintGuard(ch <-chan os.Signal, multiClient bool) {
for range ch {
if multiClient {
answer, err := t.line.Prompt("Would you like to [s]top the target or [q]uit this client, leaving the target running [s/q]? ")
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
continue
}
answer = strings.TrimSpace(answer)
switch answer {
case "s":
_, err := t.client.Halt()
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
}
case "q":
t.quittingMutex.Lock()
t.quitting = true
t.quittingMutex.Unlock()
err := t.client.Disconnect(false)
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
} else {
t.Close()
}
default:
fmt.Println("only s or q allowed")
}
// Send the debugger a halt command on SIGINT
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT)
go func() {
for range ch {
} else {
fmt.Printf("received SIGINT, stopping process (will not forward signal)\n")
_, err := t.client.Halt()
if err != nil {
fmt.Fprintf(os.Stderr, "%v", err)
}
}
}()
}
}
// Run begins running dlv in the terminal.
func (t *Term) Run() (int, error) {
defer t.Close()
multiClient := t.client.IsMulticlient()
// Send the debugger a halt command on SIGINT
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGINT)
go t.sigintGuard(ch, multiClient)
t.line.SetCompleter(func(line string) (c []string) {
for _, cmd := range t.cmds.cmds {
......@@ -147,6 +202,12 @@ func (t *Term) Run() (int, error) {
if strings.Contains(err.Error(), "exited") {
fmt.Fprintln(os.Stderr, err.Error())
} else {
t.quittingMutex.Lock()
quitting := t.quitting
t.quittingMutex.Unlock()
if quitting {
return t.handleExit()
}
fmt.Fprintf(os.Stderr, "Command failed: %s\n", err)
}
}
......@@ -215,6 +276,22 @@ func (t *Term) promptForInput() (string, error) {
return l, nil
}
func yesno(line *liner.State, question string) (bool, error) {
for {
answer, err := line.Prompt(question)
if err != nil {
return false, err
}
answer = strings.ToLower(strings.TrimSpace(answer))
switch answer {
case "n", "no":
return false, nil
case "y", "yes":
return true, nil
}
}
}
func (t *Term) handleExit() (int, error) {
fullHistoryFile, err := config.GetConfigFilePath(historyFile)
if err != nil {
......@@ -229,22 +306,47 @@ func (t *Term) handleExit() (int, error) {
}
}
t.quittingMutex.Lock()
quitting := t.quitting
t.quittingMutex.Unlock()
if quitting {
return 0, nil
}
s, err := t.client.GetState()
if err != nil {
return 1, err
}
if !s.Exited {
kill := true
if t.client.AttachedToExistingProcess() {
answer, err := t.line.Prompt("Would you like to kill the process? [Y/n] ")
if t.quitContinue {
err := t.client.Disconnect(true)
if err != nil {
return 2, err
}
return 0, nil
}
doDetach := true
if t.client.IsMulticlient() {
answer, err := yesno(t.line, "Would you like to kill the headless instance? [Y/n] ")
if err != nil {
return 2, io.EOF
}
answer = strings.ToLower(strings.TrimSpace(answer))
kill = (answer != "n" && answer != "no")
doDetach = answer
}
if err := t.client.Detach(kill); err != nil {
return 1, err
if doDetach {
kill := true
if t.client.AttachedToExistingProcess() {
answer, err := yesno(t.line, "Would you like to kill the process? [Y/n] ")
if err != nil {
return 2, io.EOF
}
kill = answer
}
if err := t.client.Detach(kill); err != nil {
return 1, err
}
}
}
return 0, nil
......
......@@ -15,6 +15,8 @@ var NotExecutableErr = proc.NotExecutableErr
// DebuggerState represents the current context of the debugger.
type DebuggerState struct {
// Running is true if the process is running and no other information can be collected.
Running bool
// CurrentThread is the currently selected debugger thread.
CurrentThread *Thread `json:"currentThread,omitempty"`
// SelectedGoroutine is the currently selected goroutine
......
......@@ -25,6 +25,8 @@ type Client interface {
// GetState returns the current debugger state.
GetState() (*api.DebuggerState, error)
// GetStateNonBlocking returns the current debugger state, returning immediately if the target is already running.
GetStateNonBlocking() (*api.DebuggerState, error)
// Continue resumes process execution.
Continue() <-chan *api.DebuggerState
......@@ -130,4 +132,11 @@ type Client interface {
// SetReturnValuesLoadConfig sets the load configuration for return values.
SetReturnValuesLoadConfig(*api.LoadConfig)
// IsMulticlien returns true if the headless instance is multiclient.
IsMulticlient() bool
// Disconnect closes the connection to the server without sending a Detach request first.
// If cont is true a continue command will be sent instead.
Disconnect(cont bool) error
}
......@@ -37,6 +37,9 @@ type Debugger struct {
processMutex sync.Mutex
target proc.Process
log *logrus.Entry
running bool
runningMutex sync.Mutex
}
// Config provides the configuration to start a Debugger.
......@@ -219,7 +222,7 @@ func (d *Debugger) Restart(pos string, resetArgs bool, newArgs []string) ([]api.
return nil, proc.NotRecordedErr
}
if !d.target.Exited() {
if valid, _ := d.target.Valid(); valid {
// Ensure the process is in a PTRACE_STOP.
if err := stopProcess(d.ProcessPid()); err != nil {
return nil, err
......@@ -261,15 +264,19 @@ func (d *Debugger) Restart(pos string, resetArgs bool, newArgs []string) ([]api.
}
// State returns the current state of the debugger.
func (d *Debugger) State() (*api.DebuggerState, error) {
func (d *Debugger) State(nowait bool) (*api.DebuggerState, error) {
if d.isRunning() && nowait {
return &api.DebuggerState{Running: true}, nil
}
d.processMutex.Lock()
defer d.processMutex.Unlock()
return d.state(nil)
}
func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error) {
if d.target.Exited() {
return nil, proc.ProcessExitedError{Pid: d.ProcessPid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
var (
......@@ -281,9 +288,14 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error
goroutine = api.ConvertGoroutine(d.target.SelectedGoroutine())
}
exited := false
if _, err := d.target.Valid(); err != nil {
_, exited = err.(*proc.ProcessExitedError)
}
state = &api.DebuggerState{
SelectedGoroutine: goroutine,
Exited: d.target.Exited(),
Exited: exited,
}
for _, thread := range d.target.ThreadList() {
......@@ -478,8 +490,8 @@ func (d *Debugger) Threads() ([]*api.Thread, error) {
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.target.Exited() {
return nil, proc.ProcessExitedError{Pid: d.ProcessPid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
threads := []*api.Thread{}
......@@ -494,8 +506,8 @@ func (d *Debugger) FindThread(id int) (*api.Thread, error) {
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.target.Exited() {
return nil, proc.ProcessExitedError{Pid: d.ProcessPid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
for _, th := range d.target.ThreadList() {
......@@ -506,6 +518,18 @@ func (d *Debugger) FindThread(id int) (*api.Thread, error) {
return nil, nil
}
func (d *Debugger) setRunning(running bool) {
d.runningMutex.Lock()
d.running = running
d.runningMutex.Unlock()
}
func (d *Debugger) isRunning() bool {
d.runningMutex.Lock()
defer d.runningMutex.Unlock()
return d.running
}
// Command handles commands which control the debugger lifecycle
func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, error) {
var err error
......@@ -522,6 +546,9 @@ func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, er
d.processMutex.Lock()
defer d.processMutex.Unlock()
d.setRunning(true)
defer d.setRunning(false)
switch command.Name {
case api.Continue:
d.log.Debug("continuing")
......@@ -864,8 +891,8 @@ func (d *Debugger) Stacktrace(goroutineID, depth int, cfg *proc.LoadConfig) ([]a
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.target.Exited() {
return nil, proc.ProcessExitedError{Pid: d.ProcessPid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
var rawlocs []proc.Stackframe
......@@ -925,8 +952,8 @@ func (d *Debugger) FindLocation(scope api.EvalScope, locStr string) ([]api.Locat
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.target.Exited() {
return nil, &proc.ProcessExitedError{Pid: d.target.Pid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
loc, err := parseLocationSpec(locStr)
......@@ -952,8 +979,8 @@ func (d *Debugger) Disassemble(scope api.EvalScope, startPC, endPC uint64, flavo
d.processMutex.Lock()
defer d.processMutex.Unlock()
if d.target.Exited() {
return nil, &proc.ProcessExitedError{Pid: d.target.Pid()}
if _, err := d.target.Valid(); err != nil {
return nil, err
}
if endPC == 0 {
......
......@@ -29,7 +29,11 @@ func (s *RPCServer) ProcessPid(arg1 interface{}, pid *int) error {
}
func (s *RPCServer) Detach(kill bool, ret *int) error {
return s.debugger.Detach(kill)
err := s.debugger.Detach(kill)
if s.config.DisconnectChan != nil {
close(s.config.DisconnectChan)
}
return err
}
func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error {
......@@ -41,7 +45,7 @@ func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error {
}
func (s *RPCServer) State(arg interface{}, state *api.DebuggerState) error {
st, err := s.debugger.State()
st, err := s.debugger.State(false)
if err != nil {
return err
}
......@@ -154,7 +158,7 @@ func (s *RPCServer) GetThread(id int, thread *api.Thread) error {
}
func (s *RPCServer) ListPackageVars(filter string, variables *[]api.Variable) error {
state, err := s.debugger.State()
state, err := s.debugger.State(false)
if err != nil {
return err
}
......@@ -195,7 +199,7 @@ func (s *RPCServer) ListThreadPackageVars(args *ThreadListArgs, variables *[]api
}
func (s *RPCServer) ListRegisters(arg interface{}, registers *string) error {
state, err := s.debugger.State()
state, err := s.debugger.State(false)
if err != nil {
return err
}
......
......@@ -69,6 +69,12 @@ func (c *RPCClient) GetState() (*api.DebuggerState, error) {
return out.State, err
}
func (c *RPCClient) GetStateNonBlocking() (*api.DebuggerState, error) {
var out StateOut
err := c.call("State", StateIn{NonBlocking: true}, &out)
return out.State, err
}
func (c *RPCClient) Continue() <-chan *api.DebuggerState {
return c.continueDir(api.Continue)
}
......@@ -354,6 +360,20 @@ func (c *RPCClient) SetReturnValuesLoadConfig(cfg *api.LoadConfig) {
c.retValLoadCfg = cfg
}
func (c *RPCClient) IsMulticlient() bool {
var out IsMulticlientOut
c.call("IsMulticlient", IsMulticlientIn{}, &out)
return out.IsMulticlient
}
func (c *RPCClient) Disconnect(cont bool) error {
if cont {
out := new(CommandOut)
c.client.Go("RPCServer.Command", &api.DebuggerCommand{Name: api.Continue, ReturnInfoLoadConfig: c.retValLoadCfg}, &out, nil)
}
return c.client.Close()
}
func (c *RPCClient) call(method string, args, reply interface{}) error {
return c.client.Call("RPCServer."+method, args, reply)
}
......@@ -55,7 +55,12 @@ type DetachOut struct {
// Detach detaches the debugger, optionally killing the process.
func (s *RPCServer) Detach(arg DetachIn, out *DetachOut) error {
return s.debugger.Detach(arg.Kill)
err := s.debugger.Detach(arg.Kill)
if s.config.DisconnectChan != nil {
close(s.config.DisconnectChan)
s.config.DisconnectChan = nil
}
return err
}
type RestartIn struct {
......@@ -85,6 +90,8 @@ func (s *RPCServer) Restart(arg RestartIn, out *RestartOut) error {
}
type StateIn struct {
// If NonBlocking is true State will return immediately even if the target process is running.
NonBlocking bool
}
type StateOut struct {
......@@ -93,7 +100,7 @@ type StateOut struct {
// State returns the current debugger state.
func (s *RPCServer) State(arg StateIn, out *StateOut) error {
st, err := s.debugger.State()
st, err := s.debugger.State(arg.NonBlocking)
if err != nil {
return err
}
......@@ -317,7 +324,7 @@ type ListPackageVarsOut struct {
// ListPackageVars lists all package variables in the context of the current thread.
func (s *RPCServer) ListPackageVars(arg ListPackageVarsIn, out *ListPackageVarsOut) error {
state, err := s.debugger.State()
state, err := s.debugger.State(false)
if err != nil {
return err
}
......@@ -348,7 +355,7 @@ type ListRegistersOut struct {
// ListRegisters lists registers and their values.
func (s *RPCServer) ListRegisters(arg ListRegistersIn, out *ListRegistersOut) error {
if arg.ThreadID == 0 {
state, err := s.debugger.State()
state, err := s.debugger.State(false)
if err != nil {
return err
}
......@@ -632,3 +639,18 @@ type ClearCheckpointOut struct {
func (s *RPCServer) ClearCheckpoint(arg ClearCheckpointIn, out *ClearCheckpointOut) error {
return s.debugger.ClearCheckpoint(arg.ID)
}
type IsMulticlientIn struct {
}
type IsMulticlientOut struct {
// IsMulticlient returns true if the headless instance was started with --accept-multiclient
IsMulticlient bool
}
func (s *RPCServer) IsMulticlient(arg IsMulticlientIn, out *IsMulticlientOut) error {
*out = IsMulticlientOut{
IsMulticlient: s.config.AcceptMulti,
}
return nil
}
......@@ -89,11 +89,12 @@ func NewServer(config *service.Config) *ServerImpl {
}
// Stop stops the JSON-RPC server.
func (s *ServerImpl) Stop(kill bool) error {
func (s *ServerImpl) Stop() error {
if s.config.AcceptMulti {
close(s.stopChan)
s.listener.Close()
}
kill := s.config.AttachPid == 0
return s.debugger.Detach(kill)
}
......@@ -257,6 +258,12 @@ func suitableMethods(rcvr interface{}, methods map[string]*methodType, log *logr
}
func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) {
defer func() {
if !s.config.AcceptMulti && s.config.DisconnectChan != nil {
close(s.config.DisconnectChan)
}
}()
sending := new(sync.Mutex)
codec := jsonrpc.NewServerCodec(conn)
var req rpc.Request
......@@ -342,9 +349,6 @@ func (s *ServerImpl) serveJSONCodec(conn io.ReadWriteCloser) {
}
}
codec.Close()
if !s.config.AcceptMulti && s.config.DisconnectChan != nil {
close(s.config.DisconnectChan)
}
}
// A value sent as a placeholder for the server's response value when the server
......
......@@ -1457,3 +1457,41 @@ func TestClientServer_StepOutReturn(t *testing.T) {
}
})
}
func TestAcceptMulticlient(t *testing.T) {
if testBackend == "rr" {
t.Skip("recording not allowed for TestAcceptMulticlient")
}
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("couldn't start listener: %s\n", err)
}
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
defer listener.Close()
disconnectChan := make(chan struct{})
server := rpccommon.NewServer(&service.Config{
Listener: listener,
ProcessArgs: []string{protest.BuildFixture("testvariables2", 0).Path},
Backend: testBackend,
AcceptMulti: true,
DisconnectChan: disconnectChan,
})
if err := server.Run(); err != nil {
t.Fatal(err)
}
<-disconnectChan
server.Stop()
}()
client1 := rpc2.NewClient(listener.Addr().String())
client1.Disconnect(false)
client2 := rpc2.NewClient(listener.Addr().String())
state := <-client2.Continue()
if state.CurrentThread.Function.Name != "main.main" {
t.Fatalf("bad state after continue: %v\n", state)
}
client2.Detach(true)
<-serverDone
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册