Introduce JSON-RPC service

This commit is contained in:
Derek Parker 2015-06-20 22:47:44 -05:00
parent 5642e0a106
commit 687dc4172d
12 changed files with 574 additions and 45 deletions

@ -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

@ -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)
}

@ -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 {

@ -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() {

@ -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)) {

@ -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
}

19
service/config.go Normal file

@ -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
}

@ -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)

186
service/rpc/client.go Normal file

@ -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, &regs)
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)
}

289
service/rpc/server.go Normal file

@ -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
}

6
service/server.go Normal file

@ -0,0 +1,6 @@
package service
type Server interface {
Run() error
Stop(bool) error
}

@ -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) {