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:
parent
288248d048
commit
2954e03a20
5
Makefile
5
Makefile
@ -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:]
|
||||
}
|
101
cmd/dlv/main.go
101
cmd/dlv/main.go
@ -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
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
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)
|
||||
}
|
512
service/debugger/debugger.go
Normal file
512
service/debugger/debugger.go
Normal file
@ -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
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
2
service/rest/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package rest provides RESTful HTTP client and server implementations.
|
||||
package rest
|
299
service/rest/integration_test.go
Normal file
299
service/rest/integration_test.go
Normal file
@ -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
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
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
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:]
|
||||
}
|
Loading…
Reference in New Issue
Block a user