proc,terminal: Ensure correct exit status (#2543)

Ensure that any command executed after the process we are trying to
debug prints a correct and consistent exit status.

Previously the exit code was being lost after the first time we printed
that a process has exited. Additionally, certain commands would print
the PID of the process and other would not. This change makes everything
more correct and consistent.
This commit is contained in:
Derek Parker
2021-06-22 04:35:13 -07:00
committed by GitHub
parent d3f4a8d443
commit 42ecbd4413
7 changed files with 49 additions and 8 deletions

View File

@ -26,6 +26,10 @@ type Process interface {
// the `proc` package. // the `proc` package.
// This is temporary and in support of an ongoing refactor. // This is temporary and in support of an ongoing refactor.
type ProcessInternal interface { type ProcessInternal interface {
// 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
// ErrProcessExited or ErrProcessDetached).
Valid() (bool, error)
// Restart restarts the recording from the specified position, or from the // Restart restarts the recording from the specified position, or from the
// last checkpoint if pos == "". // last checkpoint if pos == "".
// If pos starts with 'c' it's a checkpoint ID, otherwise it's an event // If pos starts with 'c' it's a checkpoint ID, otherwise it's an event
@ -87,10 +91,6 @@ type Info interface {
// ResumeNotify specifies a channel that will be closed the next time // ResumeNotify specifies a channel that will be closed the next time
// ContinueOnce finishes resuming the target. // ContinueOnce finishes resuming the target.
ResumeNotify(chan<- struct{}) ResumeNotify(chan<- struct{})
// 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
// ErrProcessExited or ErrProcessDetached).
Valid() (bool, error)
BinInfo() *BinaryInfo BinInfo() *BinaryInfo
EntryPoint() (uint64, error) EntryPoint() (uint64, error)

View File

@ -62,6 +62,10 @@ type Target struct {
// This must be cleared whenever the target is resumed. // This must be cleared whenever the target is resumed.
gcache goroutineCache gcache goroutineCache
iscgo *bool iscgo *bool
// exitStatus is the exit status of the process we are debugging.
// Saved here to relay to any future commands.
exitStatus int
} }
// ErrProcessExited indicates that the process has exited and contains both // ErrProcessExited indicates that the process has exited and contains both
@ -203,6 +207,20 @@ func (t *Target) IsCgo() bool {
return false return false
} }
// 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
// ErrProcessExited or ErrProcessDetached).
func (t *Target) Valid() (bool, error) {
ok, err := t.proc.Valid()
if !ok && err != nil {
if pe, ok := err.(ErrProcessExited); ok {
pe.Status = t.exitStatus
err = pe
}
}
return ok, err
}
// SupportsFunctionCalls returns whether or not the backend supports // SupportsFunctionCalls returns whether or not the backend supports
// calling functions during a debug session. // calling functions during a debug session.
// Currently only non-recorded processes running on AMD64 support // Currently only non-recorded processes running on AMD64 support

View File

@ -83,6 +83,9 @@ func (dbp *Target) Continue() error {
dbp.selectedGoroutine, _ = GetG(curth) dbp.selectedGoroutine, _ = GetG(curth)
} }
} }
if pe, ok := err.(ErrProcessExited); ok {
dbp.exitStatus = pe.Status
}
return err return err
} }
if dbp.StopReason == StopLaunched { if dbp.StopReason == StopLaunched {

View File

@ -1308,7 +1308,7 @@ func scopePrefixSwitch(t *Term, ctx callContext) error {
func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, error) { func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, error) {
if err == nil && state.Exited { if err == nil && state.Exited {
return nil, fmt.Errorf("Process has exited with status %d", state.ExitStatus) return nil, fmt.Errorf("Process %d has exited with status %d", state.Pid, state.ExitStatus)
} }
return state, err return state, err
} }

View File

@ -17,6 +17,8 @@ var ErrNotExecutable = errors.New("not an executable file")
// DebuggerState represents the current context of the debugger. // DebuggerState represents the current context of the debugger.
type DebuggerState struct { type DebuggerState struct {
// PID of the process we are debugging.
Pid int
// Running is true if the process is running and no other information can be collected. // Running is true if the process is running and no other information can be collected.
Running bool Running bool
// Recording is true if the process is currently being recorded and no other // Recording is true if the process is currently being recorded and no other

View File

@ -1189,11 +1189,12 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
} }
if err != nil { if err != nil {
if exitedErr, exited := err.(proc.ErrProcessExited); command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread && exited { if pe, ok := err.(proc.ErrProcessExited); ok && command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread {
state := &api.DebuggerState{} state := &api.DebuggerState{}
state.Pid = d.target.Pid()
state.Exited = true state.Exited = true
state.ExitStatus = exitedErr.Status state.ExitStatus = pe.Status
state.Err = errors.New(exitedErr.Error()) state.Err = pe
return state, nil return state, nil
} }
return nil, err return nil, err

View File

@ -1773,6 +1773,23 @@ func TestClientServerConsistentExit(t *testing.T) {
if state.ExitStatus != 2 { if state.ExitStatus != 2 {
t.Fatalf("Process exit status is not 2, got: %v", state.ExitStatus) t.Fatalf("Process exit status is not 2, got: %v", state.ExitStatus)
} }
// Ensure future commands also return the correct exit status.
// Previously there was a bug where the command which prompted the
// process to exit (continue, next, etc...) would return the corrent
// exit status but subsequent commands would return an incorrect exit
// status of 0. To test this we simply repeat the 'next' command and
// ensure we get the correct response again.
state, err = c.Next()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if !state.Exited {
t.Fatal("Second process state is not exited")
}
if state.ExitStatus != 2 {
t.Fatalf("Second process exit status is not 2, got: %v", state.ExitStatus)
}
}) })
} }