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
This commit is contained in:
parent
818ed0b2d0
commit
9a216211d3
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user