diff --git a/Makefile b/Makefile index e596de74..3c1ed8d3 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ ifeq "$(CERT)" "" endif go test $(PREFIX)/terminal $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line go test -c $(PREFIX)/proc && codesign -s $(CERT) ./proc.test && ./proc.test $(TESTFLAGS) && rm ./proc.test - go test -c $(PREFIX)/service/rest && codesign -s $(CERT) ./rest.test && ./rest.test $(TESTFLAGS) && rm ./rest.test + go test -c $(PREFIX)/service/test && codesign -s $(CERT) ./test.test && ./test.test $(TESTFLAGS) && rm ./test.test else go test -v ./... endif @@ -51,7 +51,7 @@ ifeq "$(UNAME)" "Darwin" ifeq "$(CERT)" "" $(error You must provide a CERT env var) endif - go test -c $(PREFIX)/service/rest && codesign -s $(CERT) ./rest.test && ./rest.test -test.run $(RUN) && rm ./rest.test + go test -c $(PREFIX)/service/test && codesign -s $(CERT) ./test.test && ./test.test -test.run $(RUN) && rm ./test.test else go test $(PREFIX)/service/rest -run $(RUN) endif diff --git a/_fixtures/testthreads.go b/_fixtures/testthreads.go index 17dd5461..ba38bab9 100644 --- a/_fixtures/testthreads.go +++ b/_fixtures/testthreads.go @@ -13,7 +13,7 @@ func anotherthread(wg *sync.WaitGroup) { func main() { var wg sync.WaitGroup - for i := 0; i < 100000; i++ { + for i := 0; i < 10; i++ { wg.Add(1) go anotherthread(&wg) } diff --git a/cmd/dlv/main.go b/cmd/dlv/main.go index 8bf73cf0..7702f230 100644 --- a/cmd/dlv/main.go +++ b/cmd/dlv/main.go @@ -12,7 +12,9 @@ import ( sys "golang.org/x/sys/unix" + "github.com/derekparker/delve/service" "github.com/derekparker/delve/service/rest" + "github.com/derekparker/delve/service/rpc" "github.com/derekparker/delve/terminal" ) @@ -38,11 +40,13 @@ func main() { var addr string var logEnabled bool var headless bool + var http bool flag.BoolVar(&printv, "version", false, "Print version number and exit.") flag.StringVar(&addr, "addr", "localhost:0", "Debugging server listen address.") flag.BoolVar(&logEnabled, "log", false, "Enable debugging server logging.") flag.BoolVar(&headless, "headless", false, "Run in headless mode.") + flag.BoolVar(&http, "http", false, "Start HTTP server instead of RPC.") flag.Parse() if flag.NFlag() == 0 && len(flag.Args()) == 0 { @@ -60,12 +64,12 @@ func main() { os.Exit(0) } - status := run(addr, logEnabled, headless) + status := run(addr, logEnabled, headless, http) fmt.Println("[Hope I was of service hunting your bug!]") os.Exit(status) } -func run(addr string, logEnabled, headless bool) int { +func run(addr string, logEnabled, headless, http bool) int { // Collect launch arguments var processArgs []string var attachPid int @@ -121,18 +125,32 @@ func run(addr string, logEnabled, headless bool) int { return 1 } - // Create and start a REST debugger server - server := rest.NewServer(&rest.Config{ - Listener: listener, - ProcessArgs: processArgs, - AttachPid: attachPid, - }, logEnabled) + // Create and start a debugger server + var server service.Server + if http { + server = rest.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: processArgs, + AttachPid: attachPid, + }, logEnabled) + } else { + server = rpc.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: processArgs, + AttachPid: attachPid, + }, logEnabled) + } go server.Run() var status int if !headless { // Create and start a terminal - client := rest.NewClient(listener.Addr().String()) + var client service.Client + if http { + client = rest.NewClient(listener.Addr().String()) + } else { + client = rpc.NewClient(listener.Addr().String()) + } term := terminal.New(client) err, status = term.Run() } else { diff --git a/proc/proc.go b/proc/proc.go index cdc75e25..6f4a90f3 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -101,7 +101,10 @@ func (dbp *Process) Detach(kill bool) (err error) { // Clean up any breakpoints we've set. for _, bp := range dbp.Breakpoints { if bp != nil { - dbp.ClearBreakpoint(bp.Addr) + _, err := dbp.ClearBreakpoint(bp.Addr) + if err != nil { + return err + } } } dbp.execPtraceFunc(func() { diff --git a/proc/proc_test.go b/proc/proc_test.go index 966d2714..4e6aed47 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -3,6 +3,7 @@ package proc import ( "bytes" "encoding/binary" + "os" "path/filepath" "runtime" "testing" @@ -15,7 +16,7 @@ func init() { } func TestMain(m *testing.M) { - protest.RunTestsWithFixtures(m) + os.Exit(protest.RunTestsWithFixtures(m)) } func withTestProcess(name string, t *testing.T, fn func(p *Process, fixture protest.Fixture)) { diff --git a/proc/test/support.go b/proc/test/support.go index e3c184e6..779e714b 100644 --- a/proc/test/support.go +++ b/proc/test/support.go @@ -55,13 +55,12 @@ func BuildFixture(name string) Fixture { // RunTestsWithFixtures will pre-compile test fixtures before running test // methods. Test binaries are deleted before exiting. -func RunTestsWithFixtures(m *testing.M) { +func RunTestsWithFixtures(m *testing.M) int { status := m.Run() // Remove the fixtures. for _, f := range Fixtures { os.Remove(f.Path) } - - os.Exit(status) + return status } diff --git a/service/config.go b/service/config.go new file mode 100644 index 00000000..e90398d9 --- /dev/null +++ b/service/config.go @@ -0,0 +1,19 @@ +package service + +import "net" + +// Config provides the configuration to start a Debugger and expose it with a +// service. +// +// Only one of ProcessArgs or AttachPid should be specified. If ProcessArgs is +// provided, a new process will be launched. Otherwise, the debugger will try +// to attach to an existing process with AttachPid. +type Config struct { + // Listener is used to serve requests. + Listener net.Listener + // ProcessArgs are the arguments to launch a new process. + ProcessArgs []string + // AttachPid is the PID of an existing process to which the debugger should + // attach. + AttachPid int +} diff --git a/service/rest/server.go b/service/rest/server.go index 7e23eb7d..1c461d9a 100644 --- a/service/rest/server.go +++ b/service/rest/server.go @@ -9,6 +9,7 @@ import ( restful "github.com/emicklei/go-restful" + "github.com/derekparker/delve/service" "github.com/derekparker/delve/service/api" "github.com/derekparker/delve/service/debugger" ) @@ -16,31 +17,15 @@ import ( // RESTServer exposes a Debugger via a HTTP REST API. type RESTServer struct { // config is all the information necessary to start the debugger and server. - config *Config + config *service.Config // listener is used to serve HTTP. listener net.Listener // debugger is a debugger service. debugger *debugger.Debugger } -// Config provides the configuration to start a Debugger and expose it with a -// RESTServer. -// -// Only one of ProcessArgs or AttachPid should be specified. If ProcessArgs is -// provided, a new process will be launched. Otherwise, the debugger will try -// to attach to an existing process with AttachPid. -type Config struct { - // Listener is used to serve HTTP. - Listener net.Listener - // ProcessArgs are the arguments to launch a new process. - ProcessArgs []string - // AttachPid is the PID of an existing process to which the debugger should - // attach. - AttachPid int -} - // NewServer creates a new RESTServer. -func NewServer(config *Config, logEnabled bool) *RESTServer { +func NewServer(config *service.Config, logEnabled bool) *RESTServer { log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) if !logEnabled { log.SetOutput(ioutil.Discard) diff --git a/service/rpc/client.go b/service/rpc/client.go new file mode 100644 index 00000000..6168e590 --- /dev/null +++ b/service/rpc/client.go @@ -0,0 +1,186 @@ +package rpc + +import ( + "fmt" + "log" + "net/rpc" + "net/rpc/jsonrpc" + + "github.com/derekparker/delve/service" + "github.com/derekparker/delve/service/api" +) + +// Client is a RPC service.Client. +type RPCClient struct { + addr string + client *rpc.Client +} + +// Ensure the implementation satisfies the interface. +var _ service.Client = &RPCClient{} + +// NewClient creates a new RPCClient. +func NewClient(addr string) *RPCClient { + client, err := jsonrpc.Dial("tcp", addr) + if err != nil { + log.Fatal("dialing:", err) + } + return &RPCClient{ + addr: addr, + client: client, + } +} + +func (c *RPCClient) Detach(kill bool) error { + return c.call("Detach", kill, nil) +} + +func (c *RPCClient) GetState() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("State", nil, state) + return state, err +} + +func (c *RPCClient) Continue() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("Command", &api.DebuggerCommand{Name: api.Continue}, state) + return state, err +} + +func (c *RPCClient) Next() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("Command", &api.DebuggerCommand{Name: api.Next}, state) + return state, err +} + +func (c *RPCClient) Step() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("Command", &api.DebuggerCommand{Name: api.Step}, state) + return state, err +} + +func (c *RPCClient) SwitchThread(threadID int) (*api.DebuggerState, error) { + state := new(api.DebuggerState) + cmd := &api.DebuggerCommand{ + Name: api.SwitchThread, + ThreadID: threadID, + } + err := c.call("Command", cmd, state) + return state, err +} + +func (c *RPCClient) Halt() (*api.DebuggerState, error) { + state := new(api.DebuggerState) + err := c.call("Command", &api.DebuggerCommand{Name: api.Halt}, state) + return state, err +} + +func (c *RPCClient) GetBreakpoint(id int) (*api.Breakpoint, error) { + breakpoint := new(api.Breakpoint) + err := c.call("GetBreakpoint", id, breakpoint) + return breakpoint, err +} + +func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) { + newBreakpoint := new(api.Breakpoint) + err := c.call("CreateBreakpoint", breakPoint, &newBreakpoint) + return newBreakpoint, err +} + +func (c *RPCClient) ListBreakpoints() ([]*api.Breakpoint, error) { + var breakpoints []*api.Breakpoint + err := c.call("ListBreakpoints", nil, &breakpoints) + return breakpoints, err +} + +func (c *RPCClient) ClearBreakpoint(id int) (*api.Breakpoint, error) { + bp := new(api.Breakpoint) + err := c.call("ClearBreakpoint", id, bp) + return bp, err +} + +func (c *RPCClient) ListThreads() ([]*api.Thread, error) { + var threads []*api.Thread + err := c.call("ListThreads", nil, &threads) + return threads, err +} + +func (c *RPCClient) GetThread(id int) (*api.Thread, error) { + thread := new(api.Thread) + err := c.call("GetThread", id, &thread) + return thread, err +} + +func (c *RPCClient) EvalVariable(symbol string) (*api.Variable, error) { + v := new(api.Variable) + err := c.call("EvalSymbol", symbol, v) + return v, err +} + +func (c *RPCClient) EvalVariableFor(threadID int, symbol string) (*api.Variable, error) { + v := new(api.Variable) + err := c.call("EvalThreadSymbol", threadID, v) + return v, err +} + +func (c *RPCClient) ListSources(filter string) ([]string, error) { + var sources []string + err := c.call("ListSources", filter, &sources) + return sources, err +} + +func (c *RPCClient) ListFunctions(filter string) ([]string, error) { + var funcs []string + err := c.call("ListFunctions", filter, &funcs) + return funcs, err +} + +func (c *RPCClient) ListPackageVariables(filter string) ([]api.Variable, error) { + var vars []api.Variable + err := c.call("ListPackageVars", filter, &vars) + return vars, err +} + +func (c *RPCClient) ListPackageVariablesFor(threadID int, filter string) ([]api.Variable, error) { + var vars []api.Variable + err := c.call("ListThreadPackageVars", &ThreadListArgs{Id: threadID, Filter: filter}, &vars) + return vars, err +} + +func (c *RPCClient) ListLocalVariables() ([]api.Variable, error) { + var vars []api.Variable + err := c.call("ListLocalVars", nil, &vars) + return vars, err +} + +func (c *RPCClient) ListRegisters() (string, error) { + var regs string + err := c.call("ListRegisters", nil, ®s) + return regs, err +} + +func (c *RPCClient) ListFunctionArgs() ([]api.Variable, error) { + var vars []api.Variable + err := c.call("ListFunctionArgs", nil, &vars) + return vars, err +} + +func (c *RPCClient) ListGoroutines() ([]*api.Goroutine, error) { + var goroutines []*api.Goroutine + err := c.call("ListGoroutines", nil, &goroutines) + return goroutines, err +} + +func (c *RPCClient) Stacktrace(goroutineId, depth int) ([]*api.Location, error) { + var locations []*api.Location + err := c.call("StacktraceGoroutine", &StacktraceGoroutineArgs{Id: 1, Depth: depth}, &locations) + return locations, err +} + +func (c *RPCClient) url(path string) string { + return fmt.Sprintf("http://%s%s", c.addr, path) +} + +func (c *RPCClient) call(method string, args, reply interface{}) error { + return c.client.Call("RPCServer."+method, args, reply) +} diff --git a/service/rpc/server.go b/service/rpc/server.go new file mode 100644 index 00000000..6bc23d6b --- /dev/null +++ b/service/rpc/server.go @@ -0,0 +1,289 @@ +package rpc + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "net" + grpc "net/rpc" + "net/rpc/jsonrpc" + + "github.com/derekparker/delve/service" + "github.com/derekparker/delve/service/api" + "github.com/derekparker/delve/service/debugger" +) + +type RPCServer struct { + // config is all the information necessary to start the debugger and server. + config *service.Config + // listener is used to serve HTTP. + listener net.Listener + // debugger is a debugger service. + debugger *debugger.Debugger +} + +// NewServer creates a new RPCServer. +func NewServer(config *service.Config, logEnabled bool) *RPCServer { + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + if !logEnabled { + log.SetOutput(ioutil.Discard) + } + + return &RPCServer{ + config: config, + listener: config.Listener, + } +} + +// Stop detaches from the debugger and waits for it to stop. +func (s *RPCServer) Stop(kill bool) error { + return s.debugger.Detach(kill) +} + +// Run starts a debugger and exposes it with an HTTP server. The debugger +// itself can be stopped with the `detach` API. Run blocks until the HTTP +// server stops. +func (s *RPCServer) Run() error { + c, err := s.listener.Accept() + if err != nil { + return err + } + + // Create and start the debugger + if s.debugger, err = debugger.New(&debugger.Config{ + ProcessArgs: s.config.ProcessArgs, + AttachPid: s.config.AttachPid, + }); err != nil { + return err + } + + rpcs := grpc.NewServer() + rpcs.Register(s) + rpcs.ServeCodec(jsonrpc.NewServerCodec(c)) + return nil +} + +func (s *RPCServer) Detach(kill bool, ret *int) error { + return s.debugger.Detach(kill) +} + +func (s *RPCServer) State(arg interface{}, state *api.DebuggerState) error { + st, err := s.debugger.State() + if err != nil { + return err + } + *state = *st + return nil +} + +func (s *RPCServer) Command(command *api.DebuggerCommand, state *api.DebuggerState) error { + st, err := s.debugger.Command(command) + if err != nil { + return err + } + *state = *st + return nil +} + +func (s *RPCServer) GetBreakpoint(id int, breakpoint *api.Breakpoint) error { + bp := s.debugger.FindBreakpoint(id) + if bp == nil { + return fmt.Errorf("no breakpoint with id %d", id) + } + *breakpoint = *bp + return nil +} + +type StacktraceGoroutineArgs struct { + Id, Depth int +} + +func (s *RPCServer) StacktraceGoroutine(args *StacktraceGoroutineArgs, locations *[]api.Location) error { + locs, err := s.debugger.Stacktrace(args.Id, args.Depth) + if err != nil { + return err + } + *locations = locs + return nil +} + +func (s *RPCServer) ListBreakpoints(arg interface{}, breakpoints *[]*api.Breakpoint) error { + *breakpoints = s.debugger.Breakpoints() + return nil +} + +func (s *RPCServer) CreateBreakpoint(bp, newBreakpoint *api.Breakpoint) error { + createdbp, err := s.debugger.CreateBreakpoint(bp) + if err != nil { + return err + } + *newBreakpoint = *createdbp + return nil +} + +func (s *RPCServer) ClearBreakpoint(id int, breakpoint *api.Breakpoint) error { + bp := s.debugger.FindBreakpoint(id) + if bp == nil { + return fmt.Errorf("no breakpoint with id %d", id) + } + deleted, err := s.debugger.ClearBreakpoint(bp) + if err != nil { + return err + } + *breakpoint = *deleted + return nil +} + +func (s *RPCServer) ListThreads(arg interface{}, threads *[]*api.Thread) error { + *threads = s.debugger.Threads() + return nil +} + +func (s *RPCServer) GetThread(id int, thread *api.Thread) error { + t := s.debugger.FindThread(id) + if t == nil { + return fmt.Errorf("no thread with id %d", id) + } + *thread = *t + return nil +} + +func (s *RPCServer) ListPackageVars(filter string, variables *[]api.Variable) error { + state, err := s.debugger.State() + if err != nil { + return err + } + + current := state.CurrentThread + if current == nil { + return fmt.Errorf("no current thread") + } + + vars, err := s.debugger.PackageVariables(current.ID, filter) + if err != nil { + return err + } + *variables = vars + return nil +} + +type ThreadListArgs struct { + Id int + Filter string +} + +func (s *RPCServer) ListThreadPackageVars(args *ThreadListArgs, variables *[]api.Variable) error { + if thread := s.debugger.FindThread(args.Id); thread == nil { + return fmt.Errorf("no thread with id %d", args.Id) + } + + vars, err := s.debugger.PackageVariables(args.Id, args.Filter) + if err != nil { + return err + } + *variables = vars + return nil +} + +func (s *RPCServer) ListRegisters(arg interface{}, registers *string) error { + state, err := s.debugger.State() + if err != nil { + return err + } + + regs, err := s.debugger.Registers(state.CurrentThread.ID) + if err != nil { + return err + } + *registers = regs + return nil +} + +func (s *RPCServer) ListLocalVars(arg interface{}, variables *[]api.Variable) error { + state, err := s.debugger.State() + if err != nil { + return err + } + + vars, err := s.debugger.LocalVariables(state.CurrentThread.ID) + if err != nil { + return err + } + *variables = vars + return nil +} + +func (s *RPCServer) ListFunctionArgs(arg interface{}, variables *[]api.Variable) error { + state, err := s.debugger.State() + if err != nil { + return err + } + + vars, err := s.debugger.FunctionArguments(state.CurrentThread.ID) + if err != nil { + return err + } + *variables = vars + return nil +} + +func (s *RPCServer) EvalSymbol(symbol string, variable *api.Variable) error { + state, err := s.debugger.State() + if err != nil { + return err + } + + current := state.CurrentThread + if current == nil { + return errors.New("no current thread") + } + + v, err := s.debugger.EvalVariableInThread(current.ID, symbol) + if err != nil { + return err + } + *variable = *v + return nil +} + +type ThreadSymbolArgs struct { + Id int + Symbol string +} + +func (s *RPCServer) EvalThreadSymbol(args *ThreadSymbolArgs, variable *api.Variable) error { + v, err := s.debugger.EvalVariableInThread(args.Id, args.Symbol) + if err != nil { + return err + } + *variable = *v + return nil +} + +func (s *RPCServer) ListSources(filter string, sources *[]string) error { + ss, err := s.debugger.Sources(filter) + if err != nil { + return err + } + *sources = ss + return nil +} + +func (s *RPCServer) ListFunctions(filter string, funcs *[]string) error { + fns, err := s.debugger.Functions(filter) + if err != nil { + return err + } + *funcs = fns + return nil +} + +func (s *RPCServer) ListGoroutines(arg interface{}, goroutines *[]*api.Goroutine) error { + gs, err := s.debugger.Goroutines() + if err != nil { + return err + } + *goroutines = gs + return nil +} diff --git a/service/server.go b/service/server.go new file mode 100644 index 00000000..21f907bb --- /dev/null +++ b/service/server.go @@ -0,0 +1,6 @@ +package service + +type Server interface { + Run() error + Stop(bool) error +} diff --git a/service/rest/integration_test.go b/service/test/integration_test.go similarity index 89% rename from service/rest/integration_test.go rename to service/test/integration_test.go index 15c4d054..1678acf1 100644 --- a/service/rest/integration_test.go +++ b/service/test/integration_test.go @@ -1,6 +1,7 @@ -package rest +package servicetest import ( + "fmt" "net" "os" "path/filepath" @@ -8,8 +9,11 @@ import ( "testing" protest "github.com/derekparker/delve/proc/test" + "github.com/derekparker/delve/service" "github.com/derekparker/delve/service/api" + "github.com/derekparker/delve/service/rest" + "github.com/derekparker/delve/service/rpc" ) func init() { @@ -17,7 +21,7 @@ func init() { } func TestMain(m *testing.M) { - protest.RunTestsWithFixtures(m) + os.Exit(protest.RunTestsWithFixtures(m)) } func withTestClient(name string, t *testing.T, fn func(c service.Client)) { @@ -25,14 +29,33 @@ func withTestClient(name string, t *testing.T, fn func(c service.Client)) { if err != nil { t.Fatalf("couldn't start listener: %s\n", err) } - server := NewServer(&Config{ - Listener: listener, - ProcessArgs: []string{protest.BuildFixture(name).Path}, - }, false) - go server.Run() - client := NewClient(listener.Addr().String()) - defer client.Detach(true) - fn(client) + defer listener.Close() + // Test REST service + restService := func() { + fmt.Println("---- RUNNING TEST WITH REST CLIENT ----") + server := rest.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: []string{protest.BuildFixture(name).Path}, + }, false) + go server.Run() + client := rest.NewClient(listener.Addr().String()) + defer client.Detach(true) + fn(client) + } + // Test RPC service + rpcService := func() { + fmt.Println("---- RUNNING TEST WITH RPC CLIENT ----") + server := rpc.NewServer(&service.Config{ + Listener: listener, + ProcessArgs: []string{protest.BuildFixture(name).Path}, + }, false) + go server.Run() + client := rpc.NewClient(listener.Addr().String()) + defer client.Detach(true) + fn(client) + } + rpcService() + restService() } func TestClientServer_exit(t *testing.T) {