Introduce client/server separation

Refactor to introduce client/server separation, including a typed
client API and a HTTP REST server implementation.

Refactor the terminal to be an API consumer.
This commit is contained in:
Dan Mace 2015-03-20 17:11:11 -04:00 committed by Derek Parker
parent 288248d048
commit 2954e03a20
13 changed files with 2418 additions and 240 deletions

@ -15,8 +15,9 @@ endif
test:
ifeq "$(UNAME)" "Darwin"
go test $(PREFIX)/command $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line
cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test -test.v && rm ./proctl.test
go test $(PREFIX)/terminal $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line
cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test $(TESTFLAGS) && rm ./proctl.test
cd service/rest && go test -c $(PREFIX)/service/rest && codesign -s $(CERT) ./rest.test && ./rest.test $(TESTFLAGS) && rm ./rest.test
else
go test -v ./...
endif

@ -1,212 +0,0 @@
package cli
import (
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
sys "golang.org/x/sys/unix"
"github.com/derekparker/delve/command"
"github.com/derekparker/delve/proctl"
"github.com/peterh/liner"
)
const historyFile string = ".dbg_history"
func Run(args []string) {
var (
dbp *proctl.DebuggedProcess
err error
t = &Term{prompt: "(dlv) ", line: liner.NewLiner()}
)
defer t.line.Close()
switch args[0] {
case "run":
const debugname = "debug"
cmd := exec.Command("go", "build", "-o", debugname, "-gcflags", "-N -l")
err := cmd.Run()
if err != nil {
t.die(1, "Could not compile program:", err)
}
defer os.Remove(debugname)
dbp, err = proctl.Launch(append([]string{"./" + debugname}, args...))
if err != nil {
t.die(1, "Could not launch program:", err)
}
case "test":
wd, err := os.Getwd()
if err != nil {
t.die(1, err)
}
base := filepath.Base(wd)
cmd := exec.Command("go", "test", "-c", "-gcflags", "-N -l")
err = cmd.Run()
if err != nil {
t.die(1, "Could not compile program:", err)
}
debugname := "./" + base + ".test"
defer os.Remove(debugname)
dbp, err = proctl.Launch(append([]string{debugname}, args...))
if err != nil {
t.die(1, "Could not launch program:", err)
}
case "attach":
pid, err := strconv.Atoi(args[1])
if err != nil {
t.die(1, "Invalid pid", args[1])
}
dbp, err = proctl.Attach(pid)
if err != nil {
t.die(1, "Could not attach to process:", err)
}
default:
dbp, err = proctl.Launch(args)
if err != nil {
t.die(1, "Could not launch program:", err)
}
}
ch := make(chan os.Signal)
signal.Notify(ch, sys.SIGINT)
go func() {
for _ = range ch {
if dbp.Running() {
dbp.RequestManualStop()
}
}
}()
cmds := command.DebugCommands()
f, err := os.Open(historyFile)
if err != nil {
f, _ = os.Create(historyFile)
}
t.line.ReadHistory(f)
f.Close()
fmt.Println("Type 'help' for list of commands.")
for {
cmdstr, err := t.promptForInput()
if err != nil {
if err == io.EOF {
handleExit(dbp, t, 0)
}
t.die(1, "Prompt for input failed.\n")
}
cmdstr, args := parseCommand(cmdstr)
if cmdstr == "exit" {
handleExit(dbp, t, 0)
}
if dbp.Exited() && cmdstr != "help" {
fmt.Fprintf(os.Stderr, "Process has already exited.\n")
continue
}
cmd := cmds.Find(cmdstr)
if err := cmd(dbp, args...); err != nil {
switch err.(type) {
case proctl.ProcessExitedError:
pe := err.(proctl.ProcessExitedError)
fmt.Fprintf(os.Stderr, "Process exited with status %d\n", pe.Status)
default:
fmt.Fprintf(os.Stderr, "Command failed: %s\n", err)
}
}
}
}
func handleExit(dbp *proctl.DebuggedProcess, t *Term, status int) {
if f, err := os.OpenFile(historyFile, os.O_RDWR, 0666); err == nil {
_, err := t.line.WriteHistory(f)
if err != nil {
fmt.Println("readline history error: ", err)
}
f.Close()
}
if !dbp.Exited() {
for _, bp := range dbp.HWBreakPoints {
if bp == nil {
continue
}
if _, err := dbp.Clear(bp.Addr); err != nil {
fmt.Printf("Can't clear breakpoint @%x: %s\n", bp.Addr, err)
}
}
for pc := range dbp.BreakPoints {
if _, err := dbp.Clear(pc); err != nil {
fmt.Printf("Can't clear breakpoint @%x: %s\n", pc, err)
}
}
answer, err := t.line.Prompt("Would you like to kill the process? [y/n]")
if err != nil {
t.die(2, io.EOF)
}
answer = strings.TrimSuffix(answer, "\n")
fmt.Println("Detaching from process...")
err = sys.PtraceDetach(dbp.Process.Pid)
if err != nil {
t.die(2, "Could not detach", err)
}
if answer == "y" {
fmt.Println("Killing process", dbp.Process.Pid)
err := dbp.Process.Kill()
if err != nil {
fmt.Println("Could not kill process", err)
}
}
}
t.die(status, "Hope I was of service hunting your bug!")
}
type Term struct {
prompt string
line *liner.State
}
func (t *Term) die(status int, args ...interface{}) {
if t.line != nil {
t.line.Close()
}
fmt.Fprint(os.Stderr, args)
fmt.Fprint(os.Stderr, "\n")
os.Exit(status)
}
func (t *Term) promptForInput() (string, error) {
l, err := t.line.Prompt(t.prompt)
if err != nil {
return "", err
}
l = strings.TrimSuffix(l, "\n")
if l != "" {
t.line.AppendHistory(l)
}
return l, nil
}
func parseCommand(cmdstr string) (string, []string) {
vals := strings.Split(cmdstr, " ")
return vals[0], vals[1:]
}

@ -3,23 +3,25 @@ package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"runtime"
"os/exec"
"path/filepath"
"strconv"
"github.com/derekparker/delve/client/cli"
"github.com/derekparker/delve/service/rest"
"github.com/derekparker/delve/terminal"
)
const version string = "0.5.0.beta"
var usage string = `Delve version %s
flags:
%s
Invoke with the path to a binary:
dlv ./path/to/prog
or use the following commands:
run - Build, run, and attach to program
test - Build test binary, run and attach to it
@ -28,18 +30,16 @@ or use the following commands:
func init() {
flag.Usage = help
// We must ensure here that we are running on the same thread during
// the execution of dbg. This is due to the fact that ptrace(2) expects
// all commands after PTRACE_ATTACH to come from the same thread.
runtime.LockOSThread()
}
func main() {
var printv, printhelp bool
var addr string
var logEnabled bool
flag.BoolVar(&printv, "v", false, "Print version number and exit.")
flag.BoolVar(&printhelp, "h", false, "Print help text and exit.")
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.Parse()
if flag.NFlag() == 0 && len(flag.Args()) == 0 {
@ -57,7 +57,80 @@ func main() {
os.Exit(0)
}
cli.Run(os.Args[1:])
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
if !logEnabled {
log.SetOutput(ioutil.Discard)
}
// Collect launch arguments
var processArgs []string
var attachPid int
switch flag.Args()[0] {
case "run":
const debugname = "debug"
cmd := exec.Command("go", "build", "-o", debugname, "-gcflags", "-N -l")
err := cmd.Run()
if err != nil {
fmt.Errorf("Could not compile program: %s\n", err)
os.Exit(1)
}
defer os.Remove(debugname)
processArgs = append([]string{"./" + debugname}, flag.Args()...)
case "test":
wd, err := os.Getwd()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
base := filepath.Base(wd)
cmd := exec.Command("go", "test", "-c", "-gcflags", "-N -l")
err = cmd.Run()
if err != nil {
fmt.Errorf("Could not compile program: %s\n", err)
os.Exit(1)
}
debugname := "./" + base + ".test"
defer os.Remove(debugname)
processArgs = append([]string{debugname}, flag.Args()...)
case "attach":
pid, err := strconv.Atoi(flag.Args()[1])
if err != nil {
fmt.Errorf("Invalid pid: %d", flag.Args()[1])
os.Exit(1)
}
attachPid = pid
default:
processArgs = flag.Args()
}
// Make a TCP listener
listener, err := net.Listen("tcp", addr)
if err != nil {
fmt.Printf("couldn't start listener: %s\n", err)
os.Exit(1)
}
// Create and start a REST debugger server
server := rest.NewServer(&rest.Config{
Listener: listener,
ProcessArgs: processArgs,
AttachPid: attachPid,
})
go server.Run()
// Create and start a terminal
client := rest.NewClient(listener.Addr().String())
term := terminal.New(client)
err, status := term.Run()
if err != nil {
fmt.Println(err)
}
// Clean up and exit
fmt.Println("[Hope I was of service hunting your bug!]")
os.Exit(status)
}
// help prints help text to os.Stderr.

99
service/api/types.go Normal file

@ -0,0 +1,99 @@
package api
// DebuggerState represents the current context of the debugger.
type DebuggerState struct {
// BreakPoint is the current breakpoint at which the debugged process is
// suspended, and may be empty if the process is not suspended.
BreakPoint *BreakPoint `json:"breakPoint,omitempty"`
// CurrentThread is the currently selected debugger thread.
CurrentThread *Thread `json:"currentThread,omitempty"`
// Exited indicates whether the debugged process has exited.
Exited bool `json:"exited"`
}
// BreakPoint addresses a location at which process execution may be
// suspended.
type BreakPoint struct {
// ID is a unique identifier for the breakpoint.
ID int `json:"id"`
// Addr is the address of the breakpoint.
Addr uint64 `json:"addr"`
// File is the source file for the breakpoint.
File string `json:"file"`
// Line is a line in File for the breakpoint.
Line int `json:"line"`
// FunctionName is the name of the function at the current breakpoint, and
// may not always be available.
FunctionName string `json:"functionName,omitempty"`
}
// Thread is a thread within the debugged process.
type Thread struct {
// ID is a unique identifier for the thread.
ID int `json:"id"`
// PC is the current program counter for the thread.
PC uint64 `json:"pc"`
// File is the file for the program counter.
File string `json:"file"`
// Line is the line number for the program counter.
Line int `json:"line"`
// Function is function information at the program counter. May be nil.
Function *Function `json:"function,omitempty"`
}
// Function represents thread-scoped function information.
type Function struct {
// Name is the function name.
Name string `json:"name"`
Value uint64 `json:"value"`
Type byte `json:"type"`
GoType uint64 `json:"goType"`
// Args are the function arguments in a thread context.
Args []Variable `json:"args"`
// Locals are the thread local variables.
Locals []Variable `json:"locals"`
}
// Variable describes a variable.
type Variable struct {
Name string `json:"name"`
Value string `json:"value"`
Type string `json:"type"`
}
// Goroutine represents the information relevant to Delve from the runtime's
// internal G structure.
type Goroutine struct {
// ID is a unique identifier for the goroutine.
ID int `json:"id"`
// PC is the current program counter for the goroutine.
PC uint64 `json:"pc"`
// File is the file for the program counter.
File string `json:"file"`
// Line is the line number for the program counter.
Line int `json:"line"`
// Function is function information at the program counter. May be nil.
Function *Function `json:"function,omitempty"`
}
// DebuggerCommand is a command which changes the debugger's execution state.
type DebuggerCommand struct {
// Name is the command to run.
Name string `json:"name"`
// ThreadID is used to specify which thread to use with the SwitchThread
// command.
ThreadID int `json:"threadID,omitempty"`
}
const (
// Continue resumes process execution.
Continue = "continue"
// Step continues for a single instruction, entering function calls.
Step = "step"
// Next continues to the next source line, not entering function calls.
Next = "next"
// SwitchThread switches the debugger's current thread context.
SwitchThread = "switchThread"
// Halt suspends the process.
Halt = "halt"
)

57
service/client.go Normal file

@ -0,0 +1,57 @@
package service
import (
"github.com/derekparker/delve/service/api"
)
// Client represents a debugger service client. All client methods are
// synchronous.
type Client interface {
// Detach detaches the debugger, optionally killing the process.
Detach(killProcess bool) error
// GetState returns the current debugger state.
GetState() (*api.DebuggerState, error)
// Continue resumes process execution.
Continue() (*api.DebuggerState, error)
// Next continues to the next source line, not entering function calls.
Next() (*api.DebuggerState, error)
// Step continues to the next source line, entering function calls.
Step() (*api.DebuggerState, error)
// SwitchThread switches the current thread context.
SwitchThread(threadID int) (*api.DebuggerState, error)
// Halt suspends the process.
Halt() (*api.DebuggerState, error)
// GetBreakPoint gets a breakpoint by ID.
GetBreakPoint(id int) (*api.BreakPoint, error)
// CreateBreakPoint creates a new breakpoint.
CreateBreakPoint(*api.BreakPoint) (*api.BreakPoint, error)
// ListBreakPoints gets all breakpoints.
ListBreakPoints() ([]*api.BreakPoint, error)
// ClearBreakPoint deletes a breakpoint by ID.
ClearBreakPoint(id int) (*api.BreakPoint, error)
// ListThreads lists all threads.
ListThreads() ([]*api.Thread, error)
// GetThread gets a thread by its ID.
GetThread(id int) (*api.Thread, error)
// ListPackageVariables lists all package variables in the context of the current thread.
ListPackageVariables(filter string) ([]api.Variable, error)
// EvalSymbol returns a variable in the context of the current thread.
EvalSymbol(symbol string) (*api.Variable, error)
// ListPackageVariablesFor lists all package variables in the context of a thread.
ListPackageVariablesFor(threadID int, filter string) ([]api.Variable, error)
// EvalSymbolFor returns a variable in the context of the specified thread.
EvalSymbolFor(threadID int, symbol string) (*api.Variable, error)
// ListSources lists all source files in the process matching filter.
ListSources(filter string) ([]string, error)
// ListFunctions lists all functions in the process matching filter.
ListFunctions(filter string) ([]string, error)
// ListGoroutines lists all goroutines.
ListGoroutines() ([]*api.Goroutine, error)
}

@ -0,0 +1,512 @@
package debugger
import (
"fmt"
"log"
"regexp"
"runtime"
sys "golang.org/x/sys/unix"
"github.com/derekparker/delve/proctl"
"github.com/derekparker/delve/service/api"
)
// Debugger provides a thread-safe DebuggedProcess service. Instances of
// Debugger can be exposed by other services.
type Debugger struct {
config *Config
process *proctl.DebuggedProcess
processOps chan func(*proctl.DebuggedProcess)
stop chan stopSignal
running bool
}
// Config provides the configuration to start a Debugger.
//
// 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 {
// 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
}
// stopSignal is used to stop the debugger.
type stopSignal struct {
// KillProcess indicates whether to kill the debugee following detachment.
KillProcess bool
}
// New creates a new Debugger.
func New(config *Config) *Debugger {
debugger := &Debugger{
processOps: make(chan func(*proctl.DebuggedProcess)),
config: config,
stop: make(chan stopSignal),
}
return debugger
}
// withProcess facilitates thread-safe access to the DebuggedProcess. Most
// interaction with DebuggedProcess should occur via calls to withProcess[1],
// and the functions placed on the processOps channel should be consumed and
// executed from the same thread as the DebuggedProcess.
//
// This is convenient because it allows things like HTTP handlers in
// goroutines to work with the DebuggedProcess with synchronous semantics.
//
// [1] There are some exceptional cases where direct access is okay; for
// instance, when performing an operation like halt which merely sends a
// signal to the process rather than performing something like a ptrace
// operation.
func (d *Debugger) withProcess(f func(*proctl.DebuggedProcess) error) error {
if !d.running {
return fmt.Errorf("debugger isn't running")
}
result := make(chan error)
d.processOps <- func(proc *proctl.DebuggedProcess) {
result <- f(proc)
}
return <-result
}
// Run starts debugging a process until Detach is called.
func (d *Debugger) Run() error {
// We must ensure here that we are running on the same thread during
// the execution of dbg. This is due to the fact that ptrace(2) expects
// all commands after PTRACE_ATTACH to come from the same thread.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
d.running = true
defer func() { d.running = false }()
// Create the process by either attaching or launching.
if d.config.AttachPid > 0 {
log.Printf("attaching to pid %d", d.config.AttachPid)
p, err := proctl.Attach(d.config.AttachPid)
if err != nil {
return fmt.Errorf("couldn't attach to pid %d: %s", d.config.AttachPid, err)
}
d.process = p
} else {
log.Printf("launching process with args: %v", d.config.ProcessArgs)
p, err := proctl.Launch(d.config.ProcessArgs)
if err != nil {
return fmt.Errorf("couldn't launch process: %s", err)
}
d.process = p
}
// Handle access to the process from the current thread.
log.Print("debugger started")
for {
select {
case f := <-d.processOps:
// Execute the function
f(d.process)
case s := <-d.stop:
// Handle shutdown
log.Print("debugger is stopping")
// Clear breakpoints
bps := []*proctl.BreakPoint{}
for _, bp := range d.process.BreakPoints {
if bp != nil {
bps = append(bps, bp)
}
}
for _, bp := range d.process.HWBreakPoints {
if bp != nil {
bps = append(bps, bp)
}
}
for _, bp := range bps {
_, err := d.process.Clear(bp.Addr)
if err != nil {
log.Printf("warning: couldn't clear breakpoint @ %#v: %s", bp.Addr, err)
} else {
log.Printf("cleared breakpoint @ %#v", bp.Addr)
}
}
// Detach
if !d.process.Exited() {
if err := sys.PtraceDetach(d.process.Pid); err == nil {
log.Print("detached from process")
} else {
log.Printf("couldn't detach from process: %s", err)
}
}
// Kill the process if requested
if s.KillProcess {
if err := d.process.Process.Kill(); err == nil {
log.Print("killed process")
} else {
log.Printf("couldn't kill process: %s", err)
}
}
return nil
}
}
}
// Detach stops the debugger.
func (d *Debugger) Detach(kill bool) error {
if !d.running {
return fmt.Errorf("debugger isn't running")
}
d.stop <- stopSignal{KillProcess: kill}
return nil
}
func (d *Debugger) State() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
var thread *api.Thread
th := p.CurrentThread
if th != nil {
thread = convertThread(th)
}
var breakpoint *api.BreakPoint
bp := p.CurrentBreakpoint()
if bp != nil {
breakpoint = convertBreakPoint(bp)
}
state = &api.DebuggerState{
BreakPoint: breakpoint,
CurrentThread: thread,
Exited: p.Exited(),
}
return nil
})
return state, err
}
func (d *Debugger) CreateBreakPoint(requestedBp *api.BreakPoint) (*api.BreakPoint, error) {
var createdBp *api.BreakPoint
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
var loc string
switch {
case len(requestedBp.File) > 0:
loc = fmt.Sprintf("%s:%d", requestedBp.File, requestedBp.Line)
case len(requestedBp.FunctionName) > 0:
loc = requestedBp.FunctionName
default:
return fmt.Errorf("no file or function name specified")
}
bp, breakError := p.BreakByLocation(loc)
if breakError != nil {
return breakError
}
createdBp = convertBreakPoint(bp)
log.Printf("created breakpoint: %#v", createdBp)
return nil
})
return createdBp, err
}
func (d *Debugger) ClearBreakPoint(requestedBp *api.BreakPoint) (*api.BreakPoint, error) {
var clearedBp *api.BreakPoint
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
bp, err := p.Clear(requestedBp.Addr)
if err != nil {
return fmt.Errorf("Can't clear breakpoint @%x: %s", requestedBp.Addr, err)
}
clearedBp = convertBreakPoint(bp)
log.Printf("cleared breakpoint: %#v", clearedBp)
return nil
})
return clearedBp, err
}
func (d *Debugger) BreakPoints() []*api.BreakPoint {
bps := []*api.BreakPoint{}
d.withProcess(func(p *proctl.DebuggedProcess) error {
for _, bp := range p.HWBreakPoints {
if bp == nil {
continue
}
bps = append(bps, convertBreakPoint(bp))
}
for _, bp := range p.BreakPoints {
if bp.Temp {
continue
}
bps = append(bps, convertBreakPoint(bp))
}
return nil
})
return bps
}
func (d *Debugger) FindBreakPoint(id int) *api.BreakPoint {
for _, bp := range d.BreakPoints() {
if bp.ID == id {
return bp
}
}
return nil
}
func (d *Debugger) Threads() []*api.Thread {
threads := []*api.Thread{}
d.withProcess(func(p *proctl.DebuggedProcess) error {
for _, th := range p.Threads {
threads = append(threads, convertThread(th))
}
return nil
})
return threads
}
func (d *Debugger) FindThread(id int) *api.Thread {
for _, thread := range d.Threads() {
if thread.ID == id {
return thread
}
}
return nil
}
// Command handles commands which control the debugger lifecycle. Like other
// debugger operations, these are executed one at a time as part of the
// process operation pipeline.
//
// The one exception is the Halt command, which can be executed concurrently
// with any operation.
func (d *Debugger) Command(command *api.DebuggerCommand) (*api.DebuggerState, error) {
var err error
switch command.Name {
case api.Continue:
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
log.Print("continuing")
e := p.Continue()
return e
})
case api.Next:
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
log.Print("nexting")
return p.Next()
})
case api.Step:
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
log.Print("stepping")
return p.Step()
})
case api.SwitchThread:
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
log.Printf("switching to thread %d", command.ThreadID)
return p.SwitchThread(command.ThreadID)
})
case api.Halt:
// RequestManualStop does not invoke any ptrace syscalls, so it's safe to
// access the process directly.
log.Print("halting")
err = d.process.RequestManualStop()
}
if err != nil {
// Only report the error if it's not a process exit.
if _, exited := err.(proctl.ProcessExitedError); !exited {
return nil, err
}
}
return d.State()
}
func (d *Debugger) Sources(filter string) ([]string, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
files := []string{}
d.withProcess(func(p *proctl.DebuggedProcess) error {
for f := range p.Sources() {
if regex.Match([]byte(f)) {
files = append(files, f)
}
}
return nil
})
return files, nil
}
func (d *Debugger) Functions(filter string) ([]string, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
funcs := []string{}
d.withProcess(func(p *proctl.DebuggedProcess) error {
for _, f := range p.Funcs() {
if f.Sym != nil && regex.Match([]byte(f.Name)) {
funcs = append(funcs, f.Name)
}
}
return nil
})
return funcs, nil
}
func (d *Debugger) PackageVariables(threadID int, filter string) ([]api.Variable, error) {
regex, err := regexp.Compile(filter)
if err != nil {
return nil, fmt.Errorf("invalid filter argument: %s", err.Error())
}
vars := []api.Variable{}
err = d.withProcess(func(p *proctl.DebuggedProcess) error {
thread, found := p.Threads[threadID]
if !found {
return fmt.Errorf("couldn't find thread %d", threadID)
}
pv, err := thread.PackageVariables()
if err != nil {
return err
}
for _, v := range pv {
if regex.Match([]byte(v.Name)) {
vars = append(vars, convertVar(v))
}
}
return nil
})
return vars, err
}
func (d *Debugger) EvalSymbolInThread(threadID int, symbol string) (*api.Variable, error) {
var variable *api.Variable
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
thread, found := p.Threads[threadID]
if !found {
return fmt.Errorf("couldn't find thread %d", threadID)
}
v, err := thread.EvalSymbol(symbol)
if err != nil {
return err
}
converted := convertVar(v)
variable = &converted
return nil
})
return variable, err
}
func (d *Debugger) Goroutines() ([]*api.Goroutine, error) {
goroutines := []*api.Goroutine{}
err := d.withProcess(func(p *proctl.DebuggedProcess) error {
gs, err := p.GoroutinesInfo()
if err != nil {
return err
}
for _, g := range gs {
goroutines = append(goroutines, convertGoroutine(g))
}
return nil
})
return goroutines, err
}
// convertBreakPoint converts an internal breakpoint to an API BreakPoint.
func convertBreakPoint(bp *proctl.BreakPoint) *api.BreakPoint {
return &api.BreakPoint{
ID: bp.ID,
FunctionName: bp.FunctionName,
File: bp.File,
Line: bp.Line,
Addr: bp.Addr,
}
}
// convertThread converts an internal thread to an API Thread.
func convertThread(th *proctl.ThreadContext) *api.Thread {
var function *api.Function
file, line := "", 0
pc, err := th.PC()
if err == nil {
f, l, fn := th.Process.PCToLine(pc)
file = f
line = l
if fn != nil {
function = &api.Function{
Name: fn.Name,
Type: fn.Type,
Value: fn.Value,
GoType: fn.GoType,
Args: []api.Variable{},
Locals: []api.Variable{},
}
if vars, err := th.LocalVariables(); err == nil {
for _, v := range vars {
function.Locals = append(function.Locals, convertVar(v))
}
} else {
log.Printf("error getting locals for function at %s:%d: %s", file, line, err)
}
if vars, err := th.FunctionArguments(); err == nil {
for _, v := range vars {
function.Args = append(function.Args, convertVar(v))
}
} else {
log.Printf("error getting args for function at %s:%d: %s", file, line, err)
}
}
}
return &api.Thread{
ID: th.Id,
PC: pc,
File: file,
Line: line,
Function: function,
}
}
// convertVar converts an internal variable to an API Variable.
func convertVar(v *proctl.Variable) api.Variable {
return api.Variable{
Name: v.Name,
Value: v.Value,
Type: v.Type,
}
}
// convertGoroutine converts an internal Goroutine to an API Goroutine.
func convertGoroutine(g *proctl.G) *api.Goroutine {
var function *api.Function
if g.Func != nil {
function = &api.Function{
Name: g.Func.Name,
Type: g.Func.Type,
Value: g.Func.Value,
GoType: g.Func.GoType,
}
}
return &api.Goroutine{
ID: g.Id,
PC: g.PC,
File: g.File,
Line: g.Line,
Function: function,
}
}

364
service/rest/client.go Normal file

@ -0,0 +1,364 @@
package rest
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
)
// Client is a REST service.Client.
type RESTClient struct {
addr string
httpClient *http.Client
}
// Ensure the implementation satisfies the interface.
var _ service.Client = &RESTClient{}
// NewClient creates a new RESTClient.
func NewClient(addr string) *RESTClient {
return &RESTClient{
addr: addr,
httpClient: &http.Client{},
}
}
func (c *RESTClient) Detach(killProcess bool) error {
params := [][]string{{"kill", strconv.FormatBool(killProcess)}}
err := c.doGET("/detach", nil, params...)
if err != nil {
return err
}
return nil
}
func (c *RESTClient) GetState() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doGET("/state", &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) Continue() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doPOST("/command", &api.DebuggerCommand{Name: api.Continue}, &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) Next() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doPOST("/command", &api.DebuggerCommand{Name: api.Next}, &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) Step() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doPOST("/command", &api.DebuggerCommand{Name: api.Step}, &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) SwitchThread(threadID int) (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doPOST("/command", &api.DebuggerCommand{
Name: api.SwitchThread,
ThreadID: threadID,
}, &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) Halt() (*api.DebuggerState, error) {
var state *api.DebuggerState
err := c.doPOST("/command", &api.DebuggerCommand{Name: api.Halt}, &state)
if err != nil {
return nil, err
}
return state, nil
}
func (c *RESTClient) GetBreakPoint(id int) (*api.BreakPoint, error) {
var breakPoint *api.BreakPoint
err := c.doGET(fmt.Sprintf("/breakpoints/%d", id), &breakPoint)
if err != nil {
return nil, err
}
return breakPoint, nil
}
func (c *RESTClient) CreateBreakPoint(breakPoint *api.BreakPoint) (*api.BreakPoint, error) {
var newBreakPoint *api.BreakPoint
err := c.doPOST("/breakpoints", breakPoint, &newBreakPoint)
if err != nil {
return nil, err
}
return newBreakPoint, nil
}
func (c *RESTClient) ListBreakPoints() ([]*api.BreakPoint, error) {
var breakPoints []*api.BreakPoint
err := c.doGET("/breakpoints", &breakPoints)
if err != nil {
return nil, err
}
return breakPoints, nil
}
func (c *RESTClient) ClearBreakPoint(id int) (*api.BreakPoint, error) {
var breakPoint *api.BreakPoint
err := c.doDELETE(fmt.Sprintf("/breakpoints/%d", id), &breakPoint)
if err != nil {
return nil, err
}
return breakPoint, nil
}
func (c *RESTClient) ListThreads() ([]*api.Thread, error) {
var threads []*api.Thread
err := c.doGET("/threads", &threads)
if err != nil {
return nil, err
}
return threads, nil
}
func (c *RESTClient) GetThread(id int) (*api.Thread, error) {
var thread *api.Thread
err := c.doGET(fmt.Sprintf("/threads/%d", id), &thread)
if err != nil {
return nil, err
}
return thread, nil
}
func (c *RESTClient) EvalSymbol(symbol string) (*api.Variable, error) {
var v *api.Variable
err := c.doGET(fmt.Sprintf("/eval/%s", symbol), &v)
if err != nil {
return nil, err
}
return v, nil
}
func (c *RESTClient) EvalSymbolFor(threadID int, symbol string) (*api.Variable, error) {
var v *api.Variable
err := c.doGET(fmt.Sprintf("/threads/%d/eval/%s", threadID, symbol), &v)
if err != nil {
return nil, err
}
return v, nil
}
func (c *RESTClient) ListSources(filter string) ([]string, error) {
params := [][]string{}
if len(filter) > 0 {
params = append(params, []string{"filter", filter})
}
var sources []string
err := c.doGET("/sources", &sources, params...)
if err != nil {
return nil, err
}
return sources, nil
}
func (c *RESTClient) ListFunctions(filter string) ([]string, error) {
params := [][]string{}
if len(filter) > 0 {
params = append(params, []string{"filter", filter})
}
var funcs []string
err := c.doGET("/functions", &funcs, params...)
if err != nil {
return nil, err
}
return funcs, nil
}
func (c *RESTClient) ListPackageVariables(filter string) ([]api.Variable, error) {
params := [][]string{}
if len(filter) > 0 {
params = append(params, []string{"filter", filter})
}
var vars []api.Variable
err := c.doGET(fmt.Sprintf("/vars"), &vars, params...)
if err != nil {
return nil, err
}
return vars, nil
}
func (c *RESTClient) ListPackageVariablesFor(threadID int, filter string) ([]api.Variable, error) {
params := [][]string{}
if len(filter) > 0 {
params = append(params, []string{"filter", filter})
}
var vars []api.Variable
err := c.doGET(fmt.Sprintf("/threads/%d/vars", threadID), &vars, params...)
if err != nil {
return nil, err
}
return vars, nil
}
func (c *RESTClient) ListGoroutines() ([]*api.Goroutine, error) {
var goroutines []*api.Goroutine
err := c.doGET("/goroutines", &goroutines)
if err != nil {
return nil, err
}
return goroutines, nil
}
// TODO: how do we use http.Client with a UNIX socket URI?
func (c *RESTClient) url(path string) string {
return fmt.Sprintf("http://%s%s", c.addr, path)
}
// doGET performs an HTTP GET to path and stores the resulting API object in
// obj. Query parameters are passed as an array of 2-element string arrays
// representing key-value pairs.
func (c *RESTClient) doGET(path string, obj interface{}, params ...[]string) error {
url, err := url.Parse(c.url(path))
if err != nil {
return err
}
// Add any supplied query parameters to the URL
q := url.Query()
for _, p := range params {
q.Set(p[0], p[1])
}
url.RawQuery = q.Encode()
// Create the request
req, err := http.NewRequest("GET", url.String(), nil)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
// Execute the request
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Extract error text and return
if resp.StatusCode != http.StatusOK {
contents, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s: %s", resp.Status, contents)
}
// Decode result object
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&obj)
if err != nil {
return err
}
return nil
}
// doPOST performs an HTTP POST to path, sending 'out' as the body and storing
// the resulting API object to 'in'.
func (c *RESTClient) doPOST(path string, out interface{}, in interface{}) error {
jsonString, err := json.Marshal(out)
if err != nil {
return err
}
req, err := http.NewRequest("POST", c.url(path), bytes.NewBuffer(jsonString))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
contents, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s: %s", resp.Status, contents)
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&in)
if err != nil {
return err
}
return nil
}
// doDELETE performs an HTTP DELETE to path, storing the resulting API object
// to 'obj'.
func (c *RESTClient) doDELETE(path string, obj interface{}) error {
req, err := http.NewRequest("DELETE", c.url(path), nil)
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
contents, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s: %s", resp.Status, contents)
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&obj)
if err != nil {
return err
}
return nil
}
// doPUT performs an HTTP PUT to path, sending 'out' as the body and storing
// the resulting API object to 'in'.
func (c *RESTClient) doPUT(path string, out interface{}, in interface{}) error {
jsonString, err := json.Marshal(out)
if err != nil {
return err
}
req, err := http.NewRequest("PUT", c.url(path), bytes.NewBuffer(jsonString))
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
contents, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("%s: %s", resp.Status, contents)
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&in)
if err != nil {
return err
}
return nil
}

2
service/rest/doc.go Normal file

@ -0,0 +1,2 @@
// Package rest provides RESTful HTTP client and server implementations.
package rest

@ -0,0 +1,299 @@
package rest
import (
"crypto/rand"
"encoding/hex"
"net"
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
)
const (
continuetestprog = "../../_fixtures/continuetestprog"
testprog = "../../_fixtures/testprog"
testnextprog = "../../_fixtures/testnextprog"
testthreads = "../../_fixtures/testthreads"
)
func withTestClient(name string, t *testing.T, fn func(c service.Client)) {
// Make a (good enough) random temporary file name
r := make([]byte, 4)
rand.Read(r)
file := filepath.Join(os.TempDir(), filepath.Base(name)+hex.EncodeToString(r))
// Build the test binary
if err := exec.Command("go", "build", "-gcflags=-N -l", "-o", file, name+".go").Run(); err != nil {
t.Fatalf("Could not compile %s due to %s", name, err)
}
t.Logf("Compiled test binary %s", file)
defer os.Remove(file)
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("couldn't start listener: %s\n", err)
}
server := NewServer(&Config{
Listener: listener,
ProcessArgs: []string{file},
})
go server.Run()
client := NewClient(listener.Addr().String())
defer client.Detach(true)
fn(client)
}
func TestClientServer_exit(t *testing.T) {
withTestClient(continuetestprog, t, func(c service.Client) {
state, err := c.GetState()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if e, a := false, state.Exited; e != a {
t.Fatalf("Expected exited %v, got %v", e, a)
}
state, err = c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v, state: %#v", err, state)
}
if state.CurrentThread == nil {
t.Fatalf("Expected CurrentThread")
}
if e, a := true, state.Exited; e != a {
t.Fatalf("Expected exited %v, got %v", e, a)
}
})
}
func TestClientServer_step(t *testing.T) {
withTestClient(testprog, t, func(c service.Client) {
_, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: "main.helloworld"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
stateBefore, err := c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
stateAfter, err := c.Step()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if before, after := stateBefore.CurrentThread.PC, stateAfter.CurrentThread.PC; before >= after {
t.Errorf("Expected %#v to be greater than %#v", before, after)
}
})
}
//func TestClientServer_next(t *testing.T) {
type nextTest struct {
begin, end int
}
func testnext(testcases []nextTest, initialLocation string, t *testing.T) {
fp, err := filepath.Abs(testnextprog)
if err != nil {
t.Fatal(err)
}
fp = fp + ".go"
withTestClient(testnextprog, t, func(c service.Client) {
bp, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: initialLocation})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
state, err := c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
_, err = c.ClearBreakPoint(bp.ID)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
for _, tc := range testcases {
if state.CurrentThread.Line != tc.begin {
t.Fatalf("Program not stopped at correct spot expected %d was %s:%d", tc.begin, filepath.Base(fp), state.CurrentThread.Line)
}
t.Logf("Next for scenario %#v", tc)
state, err = c.Next()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if state.CurrentThread.Line != tc.end {
t.Fatalf("Program did not continue to correct next location expected %d was %s:%d", tc.end, filepath.Base(fp), state.CurrentThread.Line)
}
}
})
}
func TestNextGeneral(t *testing.T) {
testcases := []nextTest{
{17, 19},
{19, 20},
{20, 23},
{23, 24},
{24, 26},
{26, 31},
{31, 23},
{23, 24},
{24, 26},
{26, 31},
{31, 23},
{23, 24},
{24, 26},
{26, 27},
{27, 34},
}
testnext(testcases, "main.testnext", t)
}
func TestNextGoroutine(t *testing.T) {
testcases := []nextTest{
{46, 47},
{47, 42},
}
testnext(testcases, "main.testgoroutine", t)
}
func TestNextFunctionReturn(t *testing.T) {
testcases := []nextTest{
{13, 14},
{14, 35},
}
testnext(testcases, "main.helloworld", t)
}
func TestClientServer_breakpointInMainThread(t *testing.T) {
withTestClient(testprog, t, func(c service.Client) {
bp, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: "main.helloworld"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
state, err := c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v, state: %#v", err, state)
}
pc := state.CurrentThread.PC
if pc-1 != bp.Addr && pc != bp.Addr {
f, l := state.CurrentThread.File, state.CurrentThread.Line
t.Fatalf("Break not respected:\nPC:%#v %s:%d\nFN:%#v \n", pc, f, l, bp.Addr)
}
})
}
func TestClientServer_breakpointInSeparateGoroutine(t *testing.T) {
withTestClient(testthreads, t, func(c service.Client) {
_, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: "main.anotherthread"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
state, err := c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v, state: %#v", err, state)
}
f, l := state.CurrentThread.File, state.CurrentThread.Line
if f != "testthreads.go" && l != 8 {
t.Fatal("Program did not hit breakpoint")
}
})
}
func TestClientServer_breakAtNonexistentPoint(t *testing.T) {
withTestClient(testprog, t, func(c service.Client) {
_, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: "nowhere"})
if err == nil {
t.Fatal("Should not be able to break at non existent function")
}
})
}
func TestClientServer_clearBreakpoint(t *testing.T) {
withTestClient(testprog, t, func(c service.Client) {
bp, err := c.CreateBreakPoint(&api.BreakPoint{FunctionName: "main.sleepytime"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
bps, err := c.ListBreakPoints()
if e, a := 1, len(bps); e != a {
t.Fatalf("Expected breakpoint count %d, got %d", e, a)
}
deleted, err := c.ClearBreakPoint(bp.ID)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if deleted.ID != bp.ID {
t.Fatalf("Expected deleted breakpoint ID %v, got %v", bp.ID, deleted.ID)
}
bps, err = c.ListBreakPoints()
if e, a := 0, len(bps); e != a {
t.Fatalf("Expected breakpoint count %d, got %d", e, a)
}
})
}
func TestClientServer_switchThread(t *testing.T) {
withTestClient(testnextprog, t, func(c service.Client) {
// With invalid thread id
_, err := c.SwitchThread(-1)
if err == nil {
t.Fatal("Expected error for invalid thread id")
}
_, err = c.CreateBreakPoint(&api.BreakPoint{FunctionName: "main.main"})
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
state, err := c.Continue()
if err != nil {
t.Fatalf("Unexpected error: %v, state: %#v", err, state)
}
var nt int
ct := state.CurrentThread.ID
threads, err := c.ListThreads()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
for _, th := range threads {
if th.ID != ct {
nt = th.ID
break
}
}
if nt == 0 {
t.Fatal("could not find thread to switch to")
}
// With valid thread id
state, err = c.SwitchThread(nt)
if err != nil {
t.Fatal(err)
}
if state.CurrentThread.ID != nt {
t.Fatal("Did not switch threads")
}
})
}

385
service/rest/server.go Normal file

@ -0,0 +1,385 @@
package rest
import (
"log"
"net"
"net/http"
"strconv"
restful "github.com/emicklei/go-restful"
"github.com/derekparker/delve/service/api"
"github.com/derekparker/delve/service/debugger"
)
// 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
// listener is used to serve HTTP.
listener net.Listener
// debugger is a debugger service.
debugger *debugger.Debugger
// debuggerStopped is used to detect shutdown of the debugger service.
debuggerStopped chan error
}
// 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) *RESTServer {
return &RESTServer{
config: config,
listener: config.Listener,
debuggerStopped: make(chan error),
}
}
// 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 *RESTServer) Run() error {
// Create and start the debugger
s.debugger = debugger.New(&debugger.Config{
ProcessArgs: s.config.ProcessArgs,
AttachPid: s.config.AttachPid,
})
go func() {
err := s.debugger.Run()
if err != nil {
log.Printf("debugger stopped with error: %s", err)
}
s.debuggerStopped <- err
}()
// Set up the HTTP server
container := restful.NewContainer()
ws := new(restful.WebService)
ws.
Path("").
Consumes(restful.MIME_JSON).
Produces(restful.MIME_JSON).
Route(ws.GET("/state").To(s.getState)).
Route(ws.GET("/breakpoints").To(s.listBreakPoints)).
Route(ws.GET("/breakpoints/{breakpoint-id}").To(s.getBreakPoint)).
Route(ws.POST("/breakpoints").To(s.createBreakPoint)).
Route(ws.DELETE("/breakpoints/{breakpoint-id}").To(s.clearBreakPoint)).
Route(ws.GET("/threads").To(s.listThreads)).
Route(ws.GET("/threads/{thread-id}").To(s.getThread)).
Route(ws.GET("/threads/{thread-id}/vars").To(s.listThreadPackageVars)).
Route(ws.GET("/threads/{thread-id}/eval/{symbol}").To(s.evalThreadSymbol)).
Route(ws.GET("/goroutines").To(s.listGoroutines)).
Route(ws.POST("/command").To(s.doCommand)).
Route(ws.GET("/sources").To(s.listSources)).
Route(ws.GET("/functions").To(s.listFunctions)).
Route(ws.GET("/vars").To(s.listPackageVars)).
Route(ws.GET("/eval/{symbol}").To(s.evalSymbol)).
// TODO: GET might be the wrong verb for this
Route(ws.GET("/detach").To(s.detach))
container.Add(ws)
// Start the HTTP server
log.Printf("server listening on %s", s.listener.Addr())
return http.Serve(s.listener, container)
}
func (s *RESTServer) Stop(kill bool) error {
return s.debugger.Detach(kill)
}
// writeError writes a simple error response.
func writeError(response *restful.Response, statusCode int, message string) {
response.AddHeader("Content-Type", "text/plain")
response.WriteErrorString(statusCode, message)
}
// detach stops the debugger and waits for it to shut down before returning an
// OK response. Clients expect this to be a synchronous call.
func (s *RESTServer) detach(request *restful.Request, response *restful.Response) {
kill, err := strconv.ParseBool(request.QueryParameter("kill"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid kill parameter")
return
}
err = s.debugger.Detach(kill)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
err = <-s.debuggerStopped
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
}
func (s *RESTServer) getState(request *restful.Request, response *restful.Response) {
state, err := s.debugger.State()
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteEntity(state)
}
func (s *RESTServer) doCommand(request *restful.Request, response *restful.Response) {
command := new(api.DebuggerCommand)
err := request.ReadEntity(command)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
state, err := s.debugger.Command(command)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusCreated)
response.WriteEntity(state)
}
func (s *RESTServer) getBreakPoint(request *restful.Request, response *restful.Response) {
id, err := strconv.Atoi(request.PathParameter("breakpoint-id"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid breakpoint id")
return
}
found := s.debugger.FindBreakPoint(id)
if found == nil {
writeError(response, http.StatusNotFound, "breakpoint not found")
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(found)
}
func (s *RESTServer) listBreakPoints(request *restful.Request, response *restful.Response) {
response.WriteEntity(s.debugger.BreakPoints())
}
func (s *RESTServer) createBreakPoint(request *restful.Request, response *restful.Response) {
incomingBp := new(api.BreakPoint)
err := request.ReadEntity(incomingBp)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
if len(incomingBp.File) == 0 && len(incomingBp.FunctionName) == 0 {
writeError(response, http.StatusBadRequest, "no file or function name provided")
return
}
createdbp, err := s.debugger.CreateBreakPoint(incomingBp)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusCreated)
response.WriteEntity(createdbp)
}
func (s *RESTServer) clearBreakPoint(request *restful.Request, response *restful.Response) {
id, err := strconv.Atoi(request.PathParameter("breakpoint-id"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid breakpoint id")
return
}
found := s.debugger.FindBreakPoint(id)
if found == nil {
writeError(response, http.StatusNotFound, "breakpoint not found")
return
}
deleted, err := s.debugger.ClearBreakPoint(found)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(deleted)
}
func (s *RESTServer) listThreads(request *restful.Request, response *restful.Response) {
response.WriteEntity(s.debugger.Threads())
}
func (s *RESTServer) getThread(request *restful.Request, response *restful.Response) {
id, err := strconv.Atoi(request.PathParameter("thread-id"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid thread id")
return
}
found := s.debugger.FindThread(id)
if found == nil {
writeError(response, http.StatusNotFound, "thread not found")
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(found)
}
func (s *RESTServer) listPackageVars(request *restful.Request, response *restful.Response) {
state, err := s.debugger.State()
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
current := state.CurrentThread
if current == nil {
writeError(response, http.StatusBadRequest, "no current thread")
return
}
filter := request.QueryParameter("filter")
vars, err := s.debugger.PackageVariables(current.ID, filter)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(vars)
}
func (s *RESTServer) listThreadPackageVars(request *restful.Request, response *restful.Response) {
id, err := strconv.Atoi(request.PathParameter("thread-id"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid thread id")
return
}
if found := s.debugger.FindThread(id); found == nil {
writeError(response, http.StatusNotFound, "thread not found")
return
}
filter := request.QueryParameter("filter")
vars, err := s.debugger.PackageVariables(id, filter)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(vars)
}
func (s *RESTServer) evalSymbol(request *restful.Request, response *restful.Response) {
symbol := request.PathParameter("symbol")
if len(symbol) == 0 {
writeError(response, http.StatusBadRequest, "invalid symbol")
return
}
state, err := s.debugger.State()
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
current := state.CurrentThread
if current == nil {
writeError(response, http.StatusBadRequest, "no current thread")
return
}
v, err := s.debugger.EvalSymbolInThread(current.ID, symbol)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(v)
}
func (s *RESTServer) evalThreadSymbol(request *restful.Request, response *restful.Response) {
id, err := strconv.Atoi(request.PathParameter("thread-id"))
if err != nil {
writeError(response, http.StatusBadRequest, "invalid thread id")
return
}
if found := s.debugger.FindThread(id); found == nil {
writeError(response, http.StatusNotFound, "thread not found")
return
}
symbol := request.PathParameter("symbol")
if len(symbol) == 0 {
writeError(response, http.StatusNotFound, "invalid symbol")
return
}
v, err := s.debugger.EvalSymbolInThread(id, symbol)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(v)
}
func (s *RESTServer) listSources(request *restful.Request, response *restful.Response) {
filter := request.QueryParameter("filter")
sources, err := s.debugger.Sources(filter)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(sources)
}
func (s *RESTServer) listFunctions(request *restful.Request, response *restful.Response) {
filter := request.QueryParameter("filter")
funcs, err := s.debugger.Functions(filter)
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(funcs)
}
func (s *RESTServer) listGoroutines(request *restful.Request, response *restful.Response) {
gs, err := s.debugger.Goroutines()
if err != nil {
writeError(response, http.StatusInternalServerError, err.Error())
return
}
response.WriteHeader(http.StatusOK)
response.WriteEntity(gs)
}

470
terminal/command.go Normal file

@ -0,0 +1,470 @@
// Package command implements functions for responding to user
// input and dispatching to appropriate backend commands.
package terminal
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
)
type cmdfunc func(client service.Client, args ...string) error
type command struct {
aliases []string
helpMsg string
cmdFn cmdfunc
}
// Returns true if the command string matches one of the aliases for this command
func (c command) match(cmdstr string) bool {
for _, v := range c.aliases {
if v == cmdstr {
return true
}
}
return false
}
type Commands struct {
cmds []command
lastCmd cmdfunc
client service.Client
}
// Returns a Commands struct with default commands defined.
func DebugCommands(client service.Client) *Commands {
c := &Commands{client: client}
c.cmds = []command{
{aliases: []string{"help"}, cmdFn: c.help, helpMsg: "Prints the help message."},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "Set break point at the entry point of a function, or at a specific file/line. Example: break foo.go:13"},
{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."},
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
{aliases: []string{"thread", "t"}, cmdFn: thread, helpMsg: "Switch to the specified thread."},
{aliases: []string{"clear"}, cmdFn: clear, helpMsg: "Deletes breakpoint."},
{aliases: []string{"clearall"}, cmdFn: clearAll, helpMsg: "Deletes all breakpoints."},
{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: "Print out info for every goroutine."},
{aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."},
{aliases: []string{"print", "p"}, cmdFn: printVar, helpMsg: "Evaluate a variable."},
{aliases: []string{"info"}, cmdFn: info, helpMsg: "Provides info about args, funcs, locals, sources, or vars."},
{aliases: []string{"exit"}, cmdFn: nullCommand, helpMsg: "Exit the debugger."},
}
return c
}
// Register custom commands. Expects cf to be a func of type cmdfunc,
// returning only an error.
func (c *Commands) Register(cmdstr string, cf cmdfunc, helpMsg string) {
for _, v := range c.cmds {
if v.match(cmdstr) {
v.cmdFn = cf
return
}
}
c.cmds = append(c.cmds, command{aliases: []string{cmdstr}, cmdFn: cf, helpMsg: helpMsg})
}
// Find will look up the command function for the given command input.
// If it cannot find the command it will defualt to noCmdAvailable().
// If the command is an empty string it will replay the last command.
func (c *Commands) Find(cmdstr string) cmdfunc {
// If <enter> use last command, if there was one.
if cmdstr == "" {
if c.lastCmd != nil {
return c.lastCmd
}
return nullCommand
}
for _, v := range c.cmds {
if v.match(cmdstr) {
c.lastCmd = v.cmdFn
return v.cmdFn
}
}
return noCmdAvailable
}
func CommandFunc(fn func() error) cmdfunc {
return func(client service.Client, args ...string) error {
return fn()
}
}
func noCmdAvailable(client service.Client, args ...string) error {
return fmt.Errorf("command not available")
}
func nullCommand(client service.Client, args ...string) error {
return nil
}
func (c *Commands) help(client service.Client, args ...string) error {
fmt.Println("The following commands are available:")
for _, cmd := range c.cmds {
fmt.Printf("\t%s - %s\n", strings.Join(cmd.aliases, "|"), cmd.helpMsg)
}
return nil
}
func threads(client service.Client, args ...string) error {
threads, err := client.ListThreads()
if err != nil {
return err
}
state, err := client.GetState()
if err != nil {
return err
}
for _, th := range threads {
prefix := " "
if state.CurrentThread != nil && state.CurrentThread.ID == th.ID {
prefix = "* "
}
if th.Function != nil {
fmt.Printf("%sThread %d at %#v %s:%d %s\n",
prefix, th.ID, th.PC, th.File,
th.Line, th.Function.Name)
} else {
fmt.Printf("%sThread %d at %s:%d\n", prefix, th.ID, th.File, th.Line)
}
}
return nil
}
func thread(client service.Client, args ...string) error {
tid, err := strconv.Atoi(args[0])
if err != nil {
return err
}
oldState, err := client.GetState()
if err != nil {
return err
}
newState, err := client.SwitchThread(tid)
if err != nil {
return err
}
oldThread := "<none>"
newThread := "<none>"
if oldState.CurrentThread != nil {
oldThread = strconv.Itoa(oldState.CurrentThread.ID)
}
if newState.CurrentThread != nil {
newThread = strconv.Itoa(newState.CurrentThread.ID)
}
fmt.Printf("Switched from %s to %s\n", oldThread, newThread)
return nil
}
func goroutines(client service.Client, args ...string) error {
gs, err := client.ListGoroutines()
if err != nil {
return err
}
fmt.Printf("[%d goroutines]\n", len(gs))
for _, g := range gs {
var fname string
if g.Function != nil {
fname = g.Function.Name
}
fmt.Printf("Goroutine %d - %s:%d %s\n", g.ID, g.File, g.Line, fname)
}
return nil
}
func cont(client service.Client, args ...string) error {
state, err := client.Continue()
if err != nil {
return err
}
printcontext(state)
return nil
}
func step(client service.Client, args ...string) error {
state, err := client.Step()
if err != nil {
return err
}
printcontext(state)
return nil
}
func next(client service.Client, args ...string) error {
state, err := client.Next()
if err != nil {
return err
}
printcontext(state)
return nil
}
func clear(client service.Client, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
id, err := strconv.Atoi(args[0])
if err != nil {
return err
}
bp, err := client.ClearBreakPoint(id)
if err != nil {
return err
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, bp.File, bp.Line)
return nil
}
func clearAll(client service.Client, args ...string) error {
breakPoints, err := client.ListBreakPoints()
if err != nil {
return err
}
for _, bp := range breakPoints {
_, err := client.ClearBreakPoint(bp.ID)
if err != nil {
fmt.Printf("Couldn't delete breakpoint %d at %#v %s:%d: %s\n", bp.ID, bp.Addr, bp.File, bp.Line, err)
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, bp.File, bp.Line)
}
return nil
}
type ById []*api.BreakPoint
func (a ById) Len() int { return len(a) }
func (a ById) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ById) Less(i, j int) bool { return a[i].ID < a[j].ID }
func breakpoints(client service.Client, args ...string) error {
breakPoints, err := client.ListBreakPoints()
if err != nil {
return err
}
sort.Sort(ById(breakPoints))
for _, bp := range breakPoints {
fmt.Printf("Breakpoint %d at %#v %s:%d\n", bp.ID, bp.Addr, bp.File, bp.Line)
}
return nil
}
func breakpoint(client service.Client, args ...string) error {
if len(args) != 1 {
return fmt.Errorf("argument must be either a function name or <file:line>")
}
requestedBp := &api.BreakPoint{}
tokens := strings.Split(args[0], ":")
switch {
case len(tokens) == 1:
requestedBp.FunctionName = args[0]
case len(tokens) == 2:
file := tokens[0]
line, err := strconv.Atoi(tokens[1])
if err != nil {
return err
}
requestedBp.File = file
requestedBp.Line = line
default:
return fmt.Errorf("invalid line reference")
}
bp, err := client.CreateBreakPoint(requestedBp)
if err != nil {
return err
}
fmt.Printf("Breakpoint %d set at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, bp.File, bp.Line)
return nil
}
func printVar(client service.Client, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
val, err := client.EvalSymbol(args[0])
if err != nil {
return err
}
fmt.Println(val.Value)
return nil
}
func filterVariables(vars []api.Variable, filter *regexp.Regexp) []string {
data := make([]string, 0, len(vars))
for _, v := range vars {
if filter == nil || filter.Match([]byte(v.Name)) {
data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value))
}
}
return data
}
func info(client service.Client, args ...string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments. expected info type [regex].")
}
// Allow for optional regex
var filter *regexp.Regexp
if len(args) >= 2 {
var err error
if filter, err = regexp.Compile(args[1]); err != nil {
return fmt.Errorf("invalid filter argument: %s", err.Error())
}
}
var data []string
switch args[0] {
case "sources":
regex := ""
if len(args) >= 2 && len(args[1]) > 0 {
regex = args[1]
}
sources, err := client.ListSources(regex)
if err != nil {
return err
}
data = sources
case "funcs":
regex := ""
if len(args) >= 2 && len(args[1]) > 0 {
regex = args[1]
}
funcs, err := client.ListFunctions(regex)
if err != nil {
return err
}
data = funcs
case "args":
state, err := client.GetState()
if err != nil {
return err
}
if state.CurrentThread == nil || state.CurrentThread.Function == nil {
return nil
}
data = filterVariables(state.CurrentThread.Function.Args, filter)
case "locals":
state, err := client.GetState()
if err != nil {
return err
}
if state.CurrentThread == nil || state.CurrentThread.Function == nil {
return nil
}
data = filterVariables(state.CurrentThread.Function.Locals, filter)
case "vars":
regex := ""
if len(args) >= 2 && len(args[1]) > 0 {
regex = args[1]
}
vars, err := client.ListPackageVariables(regex)
if err != nil {
return err
}
for _, v := range vars {
data = append(data, fmt.Sprintf("%s = %s", v.Name, v.Value))
}
default:
return fmt.Errorf("unsupported info type, must be args, funcs, locals, sources, or vars")
}
// sort and output data
sort.Sort(sort.StringSlice(data))
for _, d := range data {
fmt.Println(d)
}
return nil
}
func printcontext(state *api.DebuggerState) error {
if state.CurrentThread == nil {
fmt.Println("No current thread available")
return nil
}
if len(state.CurrentThread.File) == 0 {
fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC)
fmt.Printf("\033[34m=>\033[0m no source available\n")
return nil
}
var context []string
fn := ""
if state.CurrentThread.Function != nil {
fn = state.CurrentThread.Function.Name
}
fmt.Printf("current loc: %s %s:%d\n", fn, state.CurrentThread.File, state.CurrentThread.Line)
file, err := os.Open(state.CurrentThread.File)
if err != nil {
return err
}
defer file.Close()
buf := bufio.NewReader(file)
l := state.CurrentThread.Line
for i := 1; i < l-5; i++ {
_, err := buf.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
}
for i := l - 5; i <= l+5; i++ {
line, err := buf.ReadString('\n')
if err != nil {
if err != io.EOF {
return err
}
if err == io.EOF {
break
}
}
arrow := " "
if i == l {
arrow = "=>"
}
context = append(context, fmt.Sprintf("\033[34m%s %d\033[0m: %s", arrow, i, line))
}
fmt.Println(strings.Join(context, ""))
return nil
}

@ -1,10 +1,10 @@
package command
package terminal
import (
"fmt"
"testing"
"github.com/derekparker/delve/proctl"
"github.com/derekparker/delve/service"
)
func TestCommandDefault(t *testing.T) {
@ -24,8 +24,8 @@ func TestCommandDefault(t *testing.T) {
}
func TestCommandReplay(t *testing.T) {
cmds := DebugCommands()
cmds.Register("foo", func(p *proctl.DebuggedProcess, args ...string) error { return fmt.Errorf("registered command") }, "foo command")
cmds := DebugCommands(nil)
cmds.Register("foo", func(client service.Client, args ...string) error { return fmt.Errorf("registered command") }, "foo command")
cmd := cmds.Find("foo")
err := cmd(nil)
@ -42,7 +42,7 @@ func TestCommandReplay(t *testing.T) {
func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
var (
cmds = DebugCommands()
cmds = DebugCommands(nil)
cmd = cmds.Find("")
err = cmd(nil)
)
@ -51,10 +51,3 @@ func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
t.Error("Null command not returned", err)
}
}
func TestSwitchThread(t *testing.T) {
err := thread(nil, []string{}...)
if err == nil {
t.Fatal("expected error for empty arg slice")
}
}

135
terminal/terminal.go Normal file

@ -0,0 +1,135 @@
package terminal
import (
"fmt"
"io"
"os"
"os/signal"
"strings"
"github.com/peterh/liner"
sys "golang.org/x/sys/unix"
"github.com/derekparker/delve/proctl"
"github.com/derekparker/delve/service"
)
const historyFile string = ".dbg_history"
type Term struct {
client service.Client
prompt string
line *liner.State
}
func New(client service.Client) *Term {
return &Term{
prompt: "(dlv) ",
line: liner.NewLiner(),
client: client,
}
}
func (t *Term) promptForInput() (string, error) {
l, err := t.line.Prompt(t.prompt)
if err != nil {
return "", err
}
l = strings.TrimSuffix(l, "\n")
if l != "" {
t.line.AppendHistory(l)
}
return l, nil
}
func (t *Term) Run() (error, int) {
defer t.line.Close()
// Send the debugger a halt command on SIGINT
ch := make(chan os.Signal)
signal.Notify(ch, sys.SIGINT)
go func() {
for range ch {
_, err := t.client.Halt()
if err != nil {
fmt.Println(err)
}
}
}()
cmds := DebugCommands(t.client)
f, err := os.Open(historyFile)
if err != nil {
f, _ = os.Create(historyFile)
}
t.line.ReadHistory(f)
f.Close()
fmt.Println("Type 'help' for list of commands.")
var status int
for {
cmdstr, err := t.promptForInput()
if len(cmdstr) == 0 {
continue
}
if err != nil {
if err == io.EOF {
err, status = handleExit(t.client, t)
}
err, status = fmt.Errorf("Prompt for input failed.\n"), 1
break
}
cmdstr, args := parseCommand(cmdstr)
if cmdstr == "exit" {
err, status = handleExit(t.client, t)
break
}
cmd := cmds.Find(cmdstr)
if err := cmd(t.client, args...); err != nil {
switch err.(type) {
case proctl.ProcessExitedError:
pe := err.(proctl.ProcessExitedError)
fmt.Fprintf(os.Stderr, "Process exited with status %d\n", pe.Status)
default:
fmt.Fprintf(os.Stderr, "Command failed: %s\n", err)
}
}
}
return nil, status
}
func handleExit(client service.Client, t *Term) (error, int) {
if f, err := os.OpenFile(historyFile, os.O_RDWR, 0666); err == nil {
_, err := t.line.WriteHistory(f)
if err != nil {
fmt.Println("readline history error: ", err)
}
f.Close()
}
answer, err := t.line.Prompt("Would you like to kill the process? [y/n] ")
if err != nil {
return io.EOF, 2
}
answer = strings.TrimSuffix(answer, "\n")
kill := (answer == "y")
err = client.Detach(kill)
if err != nil {
return err, 1
}
return nil, 0
}
func parseCommand(cmdstr string) (string, []string) {
vals := strings.Split(cmdstr, " ")
return vals[0], vals[1:]
}