diff --git a/cmd/commands/dlv/dlv.go b/cmd/commands/dlv/dlv.go index d4ab2b4..3db4690 100644 --- a/cmd/commands/dlv/dlv.go +++ b/cmd/commands/dlv/dlv.go @@ -1 +1,16 @@ +// Copyright 2017 bee authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package dlv ... package dlv diff --git a/cmd/commands/dlv/dlv_amd64.go b/cmd/commands/dlv/dlv_amd64.go index eb58909..de2d3ae 100644 --- a/cmd/commands/dlv/dlv_amd64.go +++ b/cmd/commands/dlv/dlv_amd64.go @@ -23,6 +23,8 @@ import ( "net" "os" "path/filepath" + "strings" + "time" "github.com/beego/bee/cmd/commands" "github.com/beego/bee/cmd/commands/version" @@ -32,11 +34,12 @@ import ( "github.com/derekparker/delve/service/rpc2" "github.com/derekparker/delve/service/rpccommon" "github.com/derekparker/delve/terminal" + "github.com/fsnotify/fsnotify" ) var cmdDlv = &commands.Command{ CustomFlags: true, - UsageLine: "dlv [-package=\"\"] [-port=8181]", + UsageLine: "dlv [-package=\"\"] [-port=8181] [-verbose=false]", Short: "Start a debugging session using Delve", Long: `dlv command start a debugging session using debugging tool Delve. @@ -50,43 +53,89 @@ var cmdDlv = &commands.Command{ var ( packageName string + verbose bool port int ) func init() { fs := flag.NewFlagSet("dlv", flag.ContinueOnError) - fs.IntVar(&port, "port", 8181, "Port to listen to for clients") fs.StringVar(&packageName, "package", "", "The package to debug (Must have a main package)") + fs.BoolVar(&verbose, "verbose", false, "Enable verbose mode") + fs.IntVar(&port, "port", 8181, "Port to listen to for clients") cmdDlv.Flag = *fs commands.AvailableCommands = append(commands.AvailableCommands, cmdDlv) } func runDlv(cmd *commands.Command, args []string) int { if err := cmd.Flag.Parse(args); err != nil { - beeLogger.Log.Fatalf("Error parsing flags: %v", err.Error()) + beeLogger.Log.Fatalf("Error while parsing flags: %v", err.Error()) } - debugname := "debug" - addr := fmt.Sprintf("127.0.0.1:%d", port) - return runDelve(addr, debugname) + var ( + addr = fmt.Sprintf("127.0.0.1:%d", port) + paths = make([]string, 0) + notifyChan = make(chan int) + ) + + if err := loadPathsToWatch(&paths); err != nil { + beeLogger.Log.Fatalf("Error while loading paths to watch: %v", err.Error()) + } + go startWatcher(paths, notifyChan) + return startDelveDebugger(addr, notifyChan) } -// runDelve runs the Delve debugger server -func runDelve(addr, debugname string) int { - beeLogger.Log.Info("Starting Delve Debugger...") - - err := gobuild(debugname, packageName) - if err != nil { - beeLogger.Log.Fatalf("%v", err) +// buildDebug builds a debug binary in the current working directory +func buildDebug() (string, error) { + args := []string{"-gcflags", "-N -l", "-o", "debug"} + args = append(args, utils.SplitQuotedFields("-ldflags='-linkmode internal'")...) + args = append(args, packageName) + if err := utils.GoCommand("build", args...); err != nil { + return "", err } - fp, err := filepath.Abs("./" + debugname) + fp, err := filepath.Abs("./debug") if err != nil { - beeLogger.Log.Fatalf("%v", err) + return "", err + } + return fp, nil +} + +// loadPathsToWatch loads the paths that needs to be watched for changes +func loadPathsToWatch(paths *[]string) error { + directory, err := os.Getwd() + if err != nil { + return err + } + filepath.Walk(directory, func(path string, info os.FileInfo, _ error) error { + if strings.HasSuffix(info.Name(), "docs") { + return filepath.SkipDir + } + if strings.HasSuffix(info.Name(), "swagger") { + return filepath.SkipDir + } + if strings.HasSuffix(info.Name(), "vendor") { + return filepath.SkipDir + } + + if filepath.Ext(info.Name()) == ".go" { + *paths = append(*paths, path) + } + return nil + }) + return nil +} + +// startDelveDebugger starts the Delve debugger server +func startDelveDebugger(addr string, ch chan int) int { + beeLogger.Log.Info("Starting Delve Debugger...") + + fp, err := buildDebug() + if err != nil { + beeLogger.Log.Fatalf("Error while building debug binary: %v", err) } defer os.Remove(fp) - abs, err := filepath.Abs(debugname) + abs, err := filepath.Abs("./debug") if err != nil { beeLogger.Log.Fatalf("%v", err) } @@ -103,7 +152,7 @@ func runDelve(addr, debugname string) int { AcceptMulti: true, AttachPid: 0, APIVersion: 2, - WorkingDir: "./", + WorkingDir: ".", ProcessArgs: []string{abs}, }, false) if err := server.Run(); err != nil { @@ -112,26 +161,87 @@ func runDelve(addr, debugname string) int { // Start the Delve client REPL client := rpc2.NewClient(addr) - term := terminal.New(client, nil) + // Make sure the client is restarted when new changes are introduced + go func() { + for { + if val := <-ch; val == 0 { + if _, err := client.Restart(); err != nil { + utils.Notify("Error while restarting the client: "+err.Error(), "bee") + } else { + if verbose { + utils.Notify("Delve Debugger Restarted", "bee") + } + } + } + } + }() + // Create the terminal and connect it to the client debugger + term := terminal.New(client, nil) status, err := term.Run() if err != nil { beeLogger.Log.Fatalf("Could not start Delve REPL: %v", err) } - defer term.Close() - // Stop and kill the debugger server once - // user quits the REPL + // Stop and kill the debugger server once user quits the REPL if err := server.Stop(true); err != nil { beeLogger.Log.Fatalf("Could not stop Delve server: %v", err) } return status } -// gobuild runs the "go build" command on the specified package -func gobuild(debugname, pkg string) error { - args := []string{"-gcflags", "-N -l", "-o", debugname} - args = append(args, utils.SplitQuotedFields("-ldflags='-linkmode internal'")...) - args = append(args, pkg) - return utils.GoCommand("build", args...) +var eventsModTime = make(map[string]int64) + +// startWatcher starts the fsnotify watcher on the passed paths +func startWatcher(paths []string, ch chan int) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + beeLogger.Log.Fatalf("Could not start the watcher: %v", err) + } + defer watcher.Close() + + // Feed the paths to the watcher + for _, path := range paths { + if err := watcher.Add(path); err != nil { + beeLogger.Log.Fatalf("Could not set a watch on path: %v", err) + } + } + + for { + select { + case evt := <-watcher.Events: + build := true + if filepath.Ext(evt.Name) != ".go" { + continue + } + + mt := utils.GetFileModTime(evt.Name) + if t := eventsModTime[evt.Name]; mt == t { + build = false + } + eventsModTime[evt.Name] = mt + + if build { + go func() { + if verbose { + utils.Notify("Rebuilding application with the new changes", "bee") + } + + // Wait 1s before re-build until there is no file change + scheduleTime := time.Now().Add(1 * time.Second) + time.Sleep(scheduleTime.Sub(time.Now())) + _, err := buildDebug() + if err != nil { + utils.Notify("Build Failed: "+err.Error(), "bee") + } else { + ch <- 0 // Notify listeners + } + }() + } + case err := <-watcher.Errors: + if err != nil { + ch <- -1 + } + } + } }