diff --git a/Makefile b/Makefile index e8406b1e..14c3608c 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,9 @@ endif test: ifeq "$(UNAME)" "Darwin" - go test $(PREFIX)/command $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line - cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test -test.v && rm ./proctl.test + go test $(PREFIX)/terminal $(PREFIX)/dwarf/frame $(PREFIX)/dwarf/op $(PREFIX)/dwarf/util $(PREFIX)/source $(PREFIX)/dwarf/line + cd proctl && go test -c $(PREFIX)/proctl && codesign -s $(CERT) ./proctl.test && ./proctl.test $(TESTFLAGS) && rm ./proctl.test + cd service/rest && go test -c $(PREFIX)/service/rest && codesign -s $(CERT) ./rest.test && ./rest.test $(TESTFLAGS) && rm ./rest.test else go test -v ./... endif diff --git a/client/cli/cli.go b/client/cli/cli.go deleted file mode 100644 index ab07acb7..00000000 --- a/client/cli/cli.go +++ /dev/null @@ -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:] -} diff --git a/cmd/dlv/main.go b/cmd/dlv/main.go index 6726d307..e26689ff 100644 --- a/cmd/dlv/main.go +++ b/cmd/dlv/main.go @@ -3,23 +3,25 @@ package main import ( "flag" "fmt" + "io/ioutil" + "log" + "net" "os" - "runtime" + "os/exec" + "path/filepath" + "strconv" - "github.com/derekparker/delve/client/cli" + "github.com/derekparker/delve/service/rest" + "github.com/derekparker/delve/terminal" ) const version string = "0.5.0.beta" var usage string = `Delve version %s - flags: %s - Invoke with the path to a binary: - dlv ./path/to/prog - or use the following commands: run - Build, run, and attach to program test - Build test binary, run and attach to it @@ -28,18 +30,16 @@ or use the following commands: func init() { flag.Usage = help - - // We must ensure here that we are running on the same thread during - // the execution of dbg. This is due to the fact that ptrace(2) expects - // all commands after PTRACE_ATTACH to come from the same thread. - runtime.LockOSThread() } func main() { var printv, printhelp bool + var addr string + var logEnabled bool - flag.BoolVar(&printv, "v", false, "Print version number and exit.") - flag.BoolVar(&printhelp, "h", false, "Print help text and exit.") + flag.BoolVar(&printv, "version", false, "Print version number and exit.") + flag.StringVar(&addr, "addr", "localhost:0", "Debugging server listen address.") + flag.BoolVar(&logEnabled, "log", false, "Enable debugging server logging.") flag.Parse() if flag.NFlag() == 0 && len(flag.Args()) == 0 { @@ -57,7 +57,80 @@ func main() { os.Exit(0) } - cli.Run(os.Args[1:]) + log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) + if !logEnabled { + log.SetOutput(ioutil.Discard) + } + + // Collect launch arguments + var processArgs []string + var attachPid int + switch flag.Args()[0] { + case "run": + const debugname = "debug" + cmd := exec.Command("go", "build", "-o", debugname, "-gcflags", "-N -l") + err := cmd.Run() + if err != nil { + fmt.Errorf("Could not compile program: %s\n", err) + os.Exit(1) + } + defer os.Remove(debugname) + + processArgs = append([]string{"./" + debugname}, flag.Args()...) + case "test": + wd, err := os.Getwd() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + base := filepath.Base(wd) + cmd := exec.Command("go", "test", "-c", "-gcflags", "-N -l") + err = cmd.Run() + if err != nil { + fmt.Errorf("Could not compile program: %s\n", err) + os.Exit(1) + } + debugname := "./" + base + ".test" + defer os.Remove(debugname) + + processArgs = append([]string{debugname}, flag.Args()...) + case "attach": + pid, err := strconv.Atoi(flag.Args()[1]) + if err != nil { + fmt.Errorf("Invalid pid: %d", flag.Args()[1]) + os.Exit(1) + } + attachPid = pid + default: + processArgs = flag.Args() + } + + // Make a TCP listener + listener, err := net.Listen("tcp", addr) + if err != nil { + fmt.Printf("couldn't start listener: %s\n", err) + os.Exit(1) + } + + // Create and start a REST debugger server + server := rest.NewServer(&rest.Config{ + Listener: listener, + ProcessArgs: processArgs, + AttachPid: attachPid, + }) + go server.Run() + + // Create and start a terminal + client := rest.NewClient(listener.Addr().String()) + term := terminal.New(client) + err, status := term.Run() + if err != nil { + fmt.Println(err) + } + + // Clean up and exit + fmt.Println("[Hope I was of service hunting your bug!]") + os.Exit(status) } // help prints help text to os.Stderr. diff --git a/service/api/types.go b/service/api/types.go new file mode 100644 index 00000000..e5a31cd1 --- /dev/null +++ b/service/api/types.go @@ -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" +) diff --git a/service/client.go b/service/client.go new file mode 100644 index 00000000..91eb7f28 --- /dev/null +++ b/service/client.go @@ -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) +} diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go new file mode 100644 index 00000000..2b291787 --- /dev/null +++ b/service/debugger/debugger.go @@ -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, + } +} diff --git a/service/rest/client.go b/service/rest/client.go new file mode 100644 index 00000000..37a31496 --- /dev/null +++ b/service/rest/client.go @@ -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 +} diff --git a/service/rest/doc.go b/service/rest/doc.go new file mode 100644 index 00000000..7158e8f1 --- /dev/null +++ b/service/rest/doc.go @@ -0,0 +1,2 @@ +// Package rest provides RESTful HTTP client and server implementations. +package rest diff --git a/service/rest/integration_test.go b/service/rest/integration_test.go new file mode 100644 index 00000000..f950b316 --- /dev/null +++ b/service/rest/integration_test.go @@ -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") + } + }) +} diff --git a/service/rest/server.go b/service/rest/server.go new file mode 100644 index 00000000..761e1a2b --- /dev/null +++ b/service/rest/server.go @@ -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) +} diff --git a/terminal/command.go b/terminal/command.go new file mode 100644 index 00000000..dfa7b6a0 --- /dev/null +++ b/terminal/command.go @@ -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 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 := "" + newThread := "" + 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 ") + } + 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 +} diff --git a/command/command_test.go b/terminal/command_test.go similarity index 66% rename from command/command_test.go rename to terminal/command_test.go index cbbb66b3..d8d20568 100644 --- a/command/command_test.go +++ b/terminal/command_test.go @@ -1,10 +1,10 @@ -package command +package terminal import ( "fmt" "testing" - "github.com/derekparker/delve/proctl" + "github.com/derekparker/delve/service" ) func TestCommandDefault(t *testing.T) { @@ -24,8 +24,8 @@ func TestCommandDefault(t *testing.T) { } func TestCommandReplay(t *testing.T) { - cmds := DebugCommands() - cmds.Register("foo", func(p *proctl.DebuggedProcess, args ...string) error { return fmt.Errorf("registered command") }, "foo command") + cmds := DebugCommands(nil) + cmds.Register("foo", func(client service.Client, args ...string) error { return fmt.Errorf("registered command") }, "foo command") cmd := cmds.Find("foo") err := cmd(nil) @@ -42,7 +42,7 @@ func TestCommandReplay(t *testing.T) { func TestCommandReplayWithoutPreviousCommand(t *testing.T) { var ( - cmds = DebugCommands() + cmds = DebugCommands(nil) cmd = cmds.Find("") err = cmd(nil) ) @@ -51,10 +51,3 @@ func TestCommandReplayWithoutPreviousCommand(t *testing.T) { t.Error("Null command not returned", err) } } - -func TestSwitchThread(t *testing.T) { - err := thread(nil, []string{}...) - if err == nil { - t.Fatal("expected error for empty arg slice") - } -} diff --git a/terminal/terminal.go b/terminal/terminal.go new file mode 100644 index 00000000..f628a8cf --- /dev/null +++ b/terminal/terminal.go @@ -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:] +}