diff --git a/Makefile b/Makefile index 3c1ed8d3..e92f26d1 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,6 @@ UNAME = $(shell uname) PREFIX=github.com/derekparker/delve -ifndef $(RUN) - RUN = ".*" -endif - build: go build github.com/derekparker/delve/cmd/dlv ifeq "$(UNAME)" "Darwin" diff --git a/service/client.go b/service/client.go index 4a1bfb59..dae36038 100644 --- a/service/client.go +++ b/service/client.go @@ -7,9 +7,15 @@ import ( // Client represents a debugger service client. All client methods are // synchronous. type Client interface { + // Returns the pid of the process we are debugging. + ProcessPid() int + // Detach detaches the debugger, optionally killing the process. Detach(killProcess bool) error + // Restarts program. + Restart() error + // GetState returns the current debugger state. GetState() (*api.DebuggerState, error) diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 4b675fb3..906c8208 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -8,6 +8,7 @@ import ( "github.com/derekparker/delve/proc" "github.com/derekparker/delve/service/api" + sys "golang.org/x/sys/unix" ) // Debugger service. @@ -61,10 +62,35 @@ func New(config *Config) (*Debugger, error) { return d, nil } +func (d *Debugger) ProcessPid() int { + return d.process.Pid +} + func (d *Debugger) Detach(kill bool) error { return d.process.Detach(kill) } +func (d *Debugger) Restart() error { + if !d.process.Exited() { + if d.process.Running() { + d.process.Halt() + } + // Ensure the process is in a PTRACE_STOP. + if err := sys.Kill(d.ProcessPid(), sys.SIGSTOP); err != nil { + return err + } + if err := d.Detach(true); err != nil { + return err + } + } + p, err := proc.Launch(d.config.ProcessArgs) + if err != nil { + return fmt.Errorf("could not launch process: %s", err) + } + d.process = p + return nil +} + func (d *Debugger) State() (*api.DebuggerState, error) { var ( state *api.DebuggerState diff --git a/service/rpc/client.go b/service/rpc/client.go index 124c6871..297cfd61 100644 --- a/service/rpc/client.go +++ b/service/rpc/client.go @@ -12,8 +12,9 @@ import ( // Client is a RPC service.Client. type RPCClient struct { - addr string - client *rpc.Client + addr string + processPid int + client *rpc.Client } // Ensure the implementation satisfies the interface. @@ -31,10 +32,20 @@ func NewClient(addr string) *RPCClient { } } +func (c *RPCClient) ProcessPid() int { + var pid int + c.call("ProcessPid", nil, &pid) + return pid +} + func (c *RPCClient) Detach(kill bool) error { return c.call("Detach", kill, nil) } +func (c *RPCClient) Restart() error { + return c.call("Restart", nil, nil) +} + func (c *RPCClient) GetState() (*api.DebuggerState, error) { state := new(api.DebuggerState) err := c.call("State", nil, state) diff --git a/service/rpc/server.go b/service/rpc/server.go index f306a604..7f3ac445 100644 --- a/service/rpc/server.go +++ b/service/rpc/server.go @@ -67,10 +67,22 @@ func (s *RPCServer) Run() error { return nil } +func (s *RPCServer) ProcessPid(arg1 interface{}, pid *int) error { + *pid = s.debugger.ProcessPid() + return nil +} + func (s *RPCServer) Detach(kill bool, ret *int) error { return s.debugger.Detach(kill) } +func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error { + if s.config.AttachPid != 0 { + return errors.New("cannot restart process Delve did not create") + } + return s.debugger.Restart() +} + func (s *RPCServer) State(arg interface{}, state *api.DebuggerState) error { st, err := s.debugger.State() if err != nil { diff --git a/service/test/integration_test.go b/service/test/integration_test.go index f31c2db6..866bb109 100644 --- a/service/test/integration_test.go +++ b/service/test/integration_test.go @@ -60,6 +60,69 @@ func TestRunWithInvalidPath(t *testing.T) { } } +func TestRestart_afterExit(t *testing.T) { + withTestClient("continuetestprog", t, func(c service.Client) { + origPid := c.ProcessPid() + state := <-c.Continue() + if !state.Exited { + t.Fatal("expected initial process to have exited") + } + if err := c.Restart(); err != nil { + t.Fatal(err) + } + if c.ProcessPid() == origPid { + t.Fatal("did not spawn new process, has same PID") + } + state = <-c.Continue() + if !state.Exited { + t.Fatal("expected restarted process to have exited") + } + }) +} + +func TestRestart_duringStop(t *testing.T) { + withTestClient("continuetestprog", t, func(c service.Client) { + origPid := c.ProcessPid() + _, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main"}) + if err != nil { + t.Fatal(err) + } + state := <-c.Continue() + if state.Breakpoint == nil { + t.Fatal("did not hit breakpoint") + } + if err := c.Restart(); err != nil { + t.Fatal(err) + } + if c.ProcessPid() == origPid { + t.Fatal("did not spawn new process, has same PID") + } + bps, err := c.ListBreakpoints() + if err != nil { + t.Fatal(err) + } + if len(bps) != 0 { + t.Fatal("breakpoint tabe not cleared") + } + state = <-c.Continue() + if !state.Exited { + t.Fatal("expected process to have exited") + } + }) +} + +func TestRestart_attachPid(t *testing.T) { + // Assert it does not work and returns error. + // We cannot restart a process we did not spawn. + server := rpc.NewServer(&service.Config{ + Listener: nil, + AttachPid: 999, + }, false) + if err := server.Restart(nil, nil); err == nil { + t.Fatal("expected error on restart after attaching to pid but got none") + } +} + func TestClientServer_exit(t *testing.T) { withTestClient("continuetestprog", t, func(c service.Client) { state, err := c.GetState() diff --git a/terminal/command.go b/terminal/command.go index d7ff1ee5..b1b60a5a 100644 --- a/terminal/command.go +++ b/terminal/command.go @@ -48,6 +48,7 @@ func DebugCommands(client service.Client) *Commands { {aliases: []string{"help"}, cmdFn: c.help, helpMsg: "Prints the help message."}, {aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "break
[-stack |-goroutine|]*\nSet break point at the entry point of a function, or at a specific file/line.\nWhen the breakpoint is reached the value of the specified variables will be printed, if -stack is specified the stack trace of the current goroutine will be printed, if -goroutine is specified informations about the current goroutine will be printed. Example: break foo.go:13"}, {aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: "Set tracepoint, takes the same arguments as break"}, + {aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."}, {aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."}, {aliases: []string{"step", "si"}, cmdFn: step, helpMsg: "Single step through program."}, {aliases: []string{"next", "n"}, cmdFn: next, helpMsg: "Step over to next source line."}, @@ -200,6 +201,14 @@ func formatGoroutine(g *api.Goroutine) string { return fmt.Sprintf("%d - %s:%d %s (%#v)\n", g.ID, g.File, g.Line, fname, g.PC) } +func restart(client service.Client, args ...string) error { + if err := client.Restart(); err != nil { + return err + } + fmt.Println("Process restarted with PID", client.ProcessPid()) + return nil +} + func cont(client service.Client, args ...string) error { stateChan := client.Continue() for state := range stateChan { diff --git a/terminal/terminal.go b/terminal/terminal.go index 6f3bb133..a0ee2801 100644 --- a/terminal/terminal.go +++ b/terminal/terminal.go @@ -76,7 +76,7 @@ func (t *Term) Run() (error, int) { cmdstr, err := t.promptForInput() if err != nil { if err == io.EOF { - err, status = handleExit(t.client, t) + return handleExit(t.client, t) } err, status = fmt.Errorf("Prompt for input failed.\n"), 1 break