Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Documentation/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,12 @@ Aliases: x
## exit
Exit the debugger.

exit [-c]
exit [-c] [-d]

When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.

Pass -d to detach from the target process, leaving it running, without being prompted whether to kill it.

Aliases: quit q

## frame
Expand Down
15 changes: 12 additions & 3 deletions pkg/terminal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,11 @@ If regex is specified only package variables with a name matching it will be ret
Argument -a shows more registers. Individual registers can also be displayed by 'print' and 'display'. See Documentation/cli/expr.md.`},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: `Exit the debugger.

exit [-c]
exit [-c] [-d]

When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.`},
When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.

Pass -d to detach from the target process, leaving it running, without being prompted whether to kill it.`},
{aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code.

[goroutine <n>] [frame <m>] list [<locspec>]
Expand Down Expand Up @@ -3148,7 +3150,8 @@ func (ere ExitRequestError) Error() string {
}

func exitCommand(t *Term, ctx callContext, args string) error {
if args == "-c" {
switch args {
case "-c":
if !t.client.IsMulticlient() {
return errors.New("not connected to an --accept-multiclient server")
}
Expand All @@ -3167,6 +3170,12 @@ func exitCommand(t *Term, ctx callContext, args string) error {
}
}
t.quitContinue = true
case "-d":
t.detachNoKill = true
case "":
// no arguments
default:
return fmt.Errorf("unknown argument %q to exit", args)
}
return ExitRequestError{}
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/terminal/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ type Term struct {
// should be resumed before quitting.
quitContinue bool

// detachNoKill is set to true by exitCommand when passed -d to signal that
// the target process should be left running, without prompting.
detachNoKill bool

longCommandMu sync.Mutex
longCommandCancelFlag bool

Expand Down Expand Up @@ -593,13 +597,16 @@ func (t *Term) handleExit() (int, error) {

if doDetach {
kill := true
if t.client.AttachedToExistingProcess() {
if t.client.AttachedToExistingProcess() && !t.detachNoKill {
answer, err := yesno(t.line, "Would you like to kill the process? [Y/n] ", "yes")
if err != nil {
return 2, io.EOF
}
kill = answer
}
if t.detachNoKill {
kill = false
}
if err := t.client.Detach(kill); err != nil {
return 1, err
}
Expand Down
53 changes: 53 additions & 0 deletions pkg/terminal/terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import (
"errors"
"net/rpc"
"testing"

"github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api"
"github.com/go-delve/liner"
)

func TestIsErrProcessExited(t *testing.T) {
Expand All @@ -23,3 +27,52 @@ func TestIsErrProcessExited(t *testing.T) {
}
}
}

// fakeExitClient implements just the service.Client methods that handleExit
// needs; the rest are nil and must not be called by the test.
type fakeExitClient struct {
service.Client
multiclient bool
attached bool
detachKill *bool
}

func (c *fakeExitClient) GetState() (*api.DebuggerState, error) { return &api.DebuggerState{}, nil }
func (c *fakeExitClient) IsMulticlient() bool { return c.multiclient }
func (c *fakeExitClient) AttachedToExistingProcess() bool { return c.attached }
func (c *fakeExitClient) Detach(kill bool) error { c.detachKill = &kill; return nil }

func TestExitDetachWithoutKill(t *testing.T) {
oldYesNo := yesno
defer func() { yesno = oldYesNo }()
yesnoCalled := false
yesno = func(_ *liner.State, _, _ string) (bool, error) {
yesnoCalled = true
return true, nil // the interactive default kills the process
}

fc := &fakeExitClient{attached: true}
term := &Term{client: fc}

// "exit -d" records the intent to detach without killing.
if err := exitCommand(term, callContext{}, "-d"); err == nil {
t.Fatal("exitCommand should return an ExitRequestError")
}

status, err := term.handleExit()
if err != nil {
t.Fatalf("handleExit returned error: %v", err)
}
if status != 0 {
t.Errorf("exit status = %d, want 0", status)
}
if yesnoCalled {
t.Error("exit -d must not prompt whether to kill the process")
}
if fc.detachKill == nil {
t.Fatal("Detach was not called")
}
if *fc.detachKill {
t.Error("exit -d must detach without killing the process (kill=false)")
}
}