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:
|
test:
|
||||||
ifeq "$(UNAME)" "Darwin"
|
ifeq "$(UNAME)" "Darwin"
|
||||||
go test $(PREFIX)/command $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line
|
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 -test.v && rm ./proctl.test
|
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
|
else
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
endif
|
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 (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
"os"
|
"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"
|
const version string = "0.5.0.beta"
|
||||||
|
|
||||||
var usage string = `Delve version %s
|
var usage string = `Delve version %s
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
%s
|
%s
|
||||||
|
|
||||||
Invoke with the path to a binary:
|
Invoke with the path to a binary:
|
||||||
|
|
||||||
dlv ./path/to/prog
|
dlv ./path/to/prog
|
||||||
|
|
||||||
or use the following commands:
|
or use the following commands:
|
||||||
run - Build, run, and attach to program
|
run - Build, run, and attach to program
|
||||||
test - Build test binary, run and attach to it
|
test - Build test binary, run and attach to it
|
||||||
@ -28,18 +30,16 @@ or use the following commands:
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.Usage = help
|
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() {
|
func main() {
|
||||||
var printv, printhelp bool
|
var printv, printhelp bool
|
||||||
|
var addr string
|
||||||
|
var logEnabled bool
|
||||||
|
|
||||||
flag.BoolVar(&printv, "v", false, "Print version number and exit.")
|
flag.BoolVar(&printv, "version", false, "Print version number and exit.")
|
||||||
flag.BoolVar(&printhelp, "h", false, "Print help text and exit.")
|
flag.StringVar(&addr, "addr", "localhost:0", "Debugging server listen address.")
|
||||||
|
flag.BoolVar(&logEnabled, "log", false, "Enable debugging server logging.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if flag.NFlag() == 0 && len(flag.Args()) == 0 {
|
if flag.NFlag() == 0 && len(flag.Args()) == 0 {
|
||||||
@ -57,7 +57,80 @@ func main() {
|
|||||||
os.Exit(0)
|
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.
|
// 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/derekparker/delve/proctl"
|
"github.com/derekparker/delve/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCommandDefault(t *testing.T) {
|
func TestCommandDefault(t *testing.T) {
|
||||||
@ -24,8 +24,8 @@ func TestCommandDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCommandReplay(t *testing.T) {
|
func TestCommandReplay(t *testing.T) {
|
||||||
cmds := DebugCommands()
|
cmds := DebugCommands(nil)
|
||||||
cmds.Register("foo", func(p *proctl.DebuggedProcess, args ...string) error { return fmt.Errorf("registered command") }, "foo command")
|
cmds.Register("foo", func(client service.Client, args ...string) error { return fmt.Errorf("registered command") }, "foo command")
|
||||||
cmd := cmds.Find("foo")
|
cmd := cmds.Find("foo")
|
||||||
|
|
||||||
err := cmd(nil)
|
err := cmd(nil)
|
||||||
@ -42,7 +42,7 @@ func TestCommandReplay(t *testing.T) {
|
|||||||
|
|
||||||
func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
|
func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
cmds = DebugCommands()
|
cmds = DebugCommands(nil)
|
||||||
cmd = cmds.Find("")
|
cmd = cmds.Find("")
|
||||||
err = cmd(nil)
|
err = cmd(nil)
|
||||||
)
|
)
|
||||||
@ -51,10 +51,3 @@ func TestCommandReplayWithoutPreviousCommand(t *testing.T) {
|
|||||||
t.Error("Null command not returned", err)
|
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