
This provides a better error message when the user tries to run dlv debug on a directory that does not contain a main package, when `dlv exec` is used with a source file. Additionally the architecture of the executable is checked as suggested by @alexbrainman in #443. Fixes #509
480 lines
13 KiB
Go
480 lines
13 KiB
Go
package cmds
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"github.com/derekparker/delve/config"
|
|
"github.com/derekparker/delve/service"
|
|
"github.com/derekparker/delve/service/api"
|
|
"github.com/derekparker/delve/service/rpc1"
|
|
"github.com/derekparker/delve/service/rpc2"
|
|
"github.com/derekparker/delve/terminal"
|
|
"github.com/derekparker/delve/version"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// Log is whether to log debug statements.
|
|
Log bool
|
|
// Headless is whether to run without terminal.
|
|
Headless bool
|
|
// ApiVersion is the requested API version while running headless
|
|
ApiVersion int
|
|
// AcceptMulti allows multiple clients to connect to the same server
|
|
AcceptMulti bool
|
|
// Addr is the debugging server listen address.
|
|
Addr string
|
|
// InitFile is the path to initialization file.
|
|
InitFile string
|
|
// BuildFlags is the flags passed during compiler invocation.
|
|
BuildFlags string
|
|
|
|
// RootCommand is the root of the command tree.
|
|
RootCommand *cobra.Command
|
|
|
|
traceAttachPid int
|
|
traceStackDepth int
|
|
|
|
conf *config.Config
|
|
)
|
|
|
|
const (
|
|
debugname = "debug"
|
|
testdebugname = "debug.test"
|
|
)
|
|
|
|
const dlvCommandLongDesc = `Delve is a source level debugger for Go programs.
|
|
|
|
Delve enables you to interact with your program by controlling the execution of the process,
|
|
evaluating variables, and providing information of thread / goroutine state, CPU register state and more.
|
|
|
|
The goal of this tool is to provide a simple yet powerful interface for debugging Go programs.
|
|
`
|
|
|
|
// New returns an initialized command tree.
|
|
func New() *cobra.Command {
|
|
// Config setup and load.
|
|
conf = config.LoadConfig()
|
|
buildFlagsDefault := ""
|
|
if runtime.GOOS == "windows" {
|
|
// Work-around for https://github.com/golang/go/issues/13154
|
|
buildFlagsDefault = "-ldflags=-linkmode internal"
|
|
}
|
|
|
|
// Main dlv root command.
|
|
RootCommand = &cobra.Command{
|
|
Use: "dlv",
|
|
Short: "Delve is a debugger for the Go programming language.",
|
|
Long: dlvCommandLongDesc,
|
|
}
|
|
|
|
RootCommand.PersistentFlags().StringVarP(&Addr, "listen", "l", "localhost:0", "Debugging server listen address.")
|
|
RootCommand.PersistentFlags().BoolVarP(&Log, "log", "", false, "Enable debugging server logging.")
|
|
RootCommand.PersistentFlags().BoolVarP(&Headless, "headless", "", false, "Run debug server only, in headless mode.")
|
|
RootCommand.PersistentFlags().BoolVarP(&AcceptMulti, "accept-multiclient", "", false, "Allows a headless server to accept multiple client connections. Note that the server API is not reentrant and clients will have to coordinate.")
|
|
RootCommand.PersistentFlags().IntVar(&ApiVersion, "api-version", 1, "Selects API version when headless.")
|
|
RootCommand.PersistentFlags().StringVar(&InitFile, "init", "", "Init file, executed by the terminal client.")
|
|
RootCommand.PersistentFlags().StringVar(&BuildFlags, "build-flags", buildFlagsDefault, "Build flags, to be passed to the compiler.")
|
|
|
|
// 'attach' subcommand.
|
|
attachCommand := &cobra.Command{
|
|
Use: "attach pid",
|
|
Short: "Attach to running process and begin debugging.",
|
|
Long: `Attach to an already running process and begin debugging it.
|
|
|
|
This command will cause Delve to take control of an already running process, and
|
|
begin a new debug session. When exiting the debug session you will have the
|
|
option to let the process continue or kill it.
|
|
`,
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("you must provide a PID")
|
|
}
|
|
return nil
|
|
},
|
|
Run: attachCmd,
|
|
}
|
|
RootCommand.AddCommand(attachCommand)
|
|
|
|
// 'connect' subcommand.
|
|
connectCommand := &cobra.Command{
|
|
Use: "connect addr",
|
|
Short: "Connect to a headless debug server.",
|
|
Long: "Connect to a running headless debug server.",
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("you must provide an address as the first argument")
|
|
}
|
|
return nil
|
|
},
|
|
Run: connectCmd,
|
|
}
|
|
RootCommand.AddCommand(connectCommand)
|
|
|
|
// 'debug' subcommand.
|
|
debugCommand := &cobra.Command{
|
|
Use: "debug [package]",
|
|
Short: "Compile and begin debugging main package in current directory, or the package specified.",
|
|
Long: `Compiles your program with optimizations disabled, starts and attaches to it.
|
|
|
|
By default, with no arguments, Delve will compile the 'main' package in the
|
|
current directory, and begin to debug it. Alternatively you can specify a
|
|
package name and Delve will compile that package instead, and begin a new debug
|
|
session.`,
|
|
Run: debugCmd,
|
|
}
|
|
RootCommand.AddCommand(debugCommand)
|
|
|
|
// 'exec' subcommand.
|
|
execCommand := &cobra.Command{
|
|
Use: "exec [./path/to/binary]",
|
|
Short: "Execute a precompiled binary, and begin a debug session.",
|
|
Long: `Execute a precompiled binary and begin a debug session.
|
|
|
|
This command will cause Delve to exec the binary and immediately attach to it to
|
|
begin a new debug session. Please note that if the binary was not compiled with
|
|
optimizations disabled, it may be difficult to properly debug it. Please
|
|
consider compiling debugging binaries with -gcflags="-N -l".`,
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("you must provide a path to a binary")
|
|
}
|
|
return nil
|
|
},
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
os.Exit(execute(0, args, conf, executingExistingFile))
|
|
},
|
|
}
|
|
RootCommand.AddCommand(execCommand)
|
|
|
|
// Deprecated 'run' subcommand.
|
|
runCommand := &cobra.Command{
|
|
Use: "run",
|
|
Short: "Deprecated command. Use 'debug' instead.",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
fmt.Println("This command is deprecated, please use 'debug' instead.")
|
|
os.Exit(0)
|
|
},
|
|
}
|
|
RootCommand.AddCommand(runCommand)
|
|
|
|
// 'test' subcommand.
|
|
testCommand := &cobra.Command{
|
|
Use: "test [package]",
|
|
Short: "Compile test binary and begin debugging program.",
|
|
Long: `Compiles a test binary with optimizations disabled and begins a new debug session.
|
|
|
|
The test command allows you to begin a new debug session in the context of your
|
|
unit tests. By default Delve will debug the tests in the current directory.
|
|
Alternatively you can specify a package name, and Delve will debug the tests in
|
|
that package instead.`,
|
|
Run: testCmd,
|
|
}
|
|
RootCommand.AddCommand(testCommand)
|
|
|
|
// 'trace' subcommand.
|
|
traceCommand := &cobra.Command{
|
|
Use: "trace [package] regexp",
|
|
Short: "Compile and begin tracing program.",
|
|
Long: `Trace program execution.
|
|
|
|
The trace sub command will set a tracepoint on every function matching the
|
|
provided regular expression and output information when tracepoint is hit. This
|
|
is useful if you do not want to begin an entire debug session, but merely want
|
|
to know what functions your process is executing.`,
|
|
Run: traceCmd,
|
|
}
|
|
traceCommand.Flags().IntVarP(&traceAttachPid, "pid", "p", 0, "Pid to attach to.")
|
|
traceCommand.Flags().IntVarP(&traceStackDepth, "stack", "s", 0, "Show stack trace with given depth.")
|
|
RootCommand.AddCommand(traceCommand)
|
|
|
|
// 'version' subcommand.
|
|
versionCommand := &cobra.Command{
|
|
Use: "version",
|
|
Short: "Prints version.",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
fmt.Printf("Delve Debugger\n%s\n", version.DelveVersion)
|
|
},
|
|
}
|
|
RootCommand.AddCommand(versionCommand)
|
|
|
|
return RootCommand
|
|
}
|
|
|
|
func debugCmd(cmd *cobra.Command, args []string) {
|
|
status := func() int {
|
|
var pkg string
|
|
dlvArgs, targetArgs := splitArgs(cmd, args)
|
|
|
|
if len(dlvArgs) > 0 {
|
|
pkg = args[0]
|
|
}
|
|
err := gobuild(debugname, pkg)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
return 1
|
|
}
|
|
fp, err := filepath.Abs("./" + debugname)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
return 1
|
|
}
|
|
defer os.Remove(fp)
|
|
|
|
processArgs := append([]string{"./" + debugname}, targetArgs...)
|
|
return execute(0, processArgs, conf, executingGeneratedFile)
|
|
}()
|
|
os.Exit(status)
|
|
}
|
|
|
|
func traceCmd(cmd *cobra.Command, args []string) {
|
|
status := func() int {
|
|
var regexp string
|
|
var processArgs []string
|
|
|
|
dlvArgs, targetArgs := splitArgs(cmd, args)
|
|
|
|
if traceAttachPid == 0 {
|
|
var pkg string
|
|
switch len(dlvArgs) {
|
|
case 1:
|
|
regexp = args[0]
|
|
case 2:
|
|
pkg = args[0]
|
|
regexp = args[1]
|
|
}
|
|
if err := gobuild(debugname, pkg); err != nil {
|
|
return 1
|
|
}
|
|
defer os.Remove("./" + debugname)
|
|
|
|
processArgs = append([]string{"./" + debugname}, targetArgs...)
|
|
}
|
|
// Make a TCP listener
|
|
listener, err := net.Listen("tcp", Addr)
|
|
if err != nil {
|
|
fmt.Printf("couldn't start listener: %s\n", err)
|
|
return 1
|
|
}
|
|
defer listener.Close()
|
|
|
|
// Create and start a debug server
|
|
server := rpc2.NewServer(&service.Config{
|
|
Listener: listener,
|
|
ProcessArgs: processArgs,
|
|
AttachPid: traceAttachPid,
|
|
}, Log)
|
|
if err := server.Run(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
client := rpc2.NewClient(listener.Addr().String())
|
|
funcs, err := client.ListFunctions(regexp)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
for i := range funcs {
|
|
_, err = client.CreateBreakpoint(&api.Breakpoint{FunctionName: funcs[i], Tracepoint: true, Line: -1, Stacktrace: traceStackDepth, LoadArgs: &terminal.ShortLoadConfig})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
}
|
|
cmds := terminal.DebugCommands(client)
|
|
t := terminal.New(client, nil)
|
|
defer t.Close()
|
|
err = cmds.Call("continue", "", t)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}()
|
|
os.Exit(status)
|
|
}
|
|
|
|
func testCmd(cmd *cobra.Command, args []string) {
|
|
status := func() int {
|
|
var pkg string
|
|
dlvArgs, targetArgs := splitArgs(cmd, args)
|
|
|
|
if len(dlvArgs) > 0 {
|
|
pkg = args[0]
|
|
}
|
|
err := gotestbuild(pkg)
|
|
if err != nil {
|
|
return 1
|
|
}
|
|
defer os.Remove("./" + testdebugname)
|
|
processArgs := append([]string{"./" + testdebugname}, targetArgs...)
|
|
|
|
return execute(0, processArgs, conf, executingOther)
|
|
}()
|
|
os.Exit(status)
|
|
}
|
|
|
|
func attachCmd(cmd *cobra.Command, args []string) {
|
|
pid, err := strconv.Atoi(args[0])
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", args[0])
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(execute(pid, nil, conf, executingOther))
|
|
}
|
|
|
|
func connectCmd(cmd *cobra.Command, args []string) {
|
|
addr := args[0]
|
|
if addr == "" {
|
|
fmt.Fprintf(os.Stderr, "An empty address was provided. You must provide an address as the first argument.\n")
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(connect(addr, conf))
|
|
}
|
|
|
|
func splitArgs(cmd *cobra.Command, args []string) ([]string, []string) {
|
|
if cmd.ArgsLenAtDash() >= 0 {
|
|
return args[:cmd.ArgsLenAtDash()], args[cmd.ArgsLenAtDash():]
|
|
}
|
|
return args, []string{}
|
|
}
|
|
|
|
func connect(addr string, conf *config.Config) int {
|
|
// Create and start a terminal - attach to running instance
|
|
var client service.Client
|
|
client = rpc2.NewClient(addr)
|
|
term := terminal.New(client, conf)
|
|
status, err := term.Run()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
return status
|
|
}
|
|
|
|
type executeKind int
|
|
|
|
const (
|
|
executingExistingFile = executeKind(iota)
|
|
executingGeneratedFile
|
|
executingOther
|
|
)
|
|
|
|
func execute(attachPid int, processArgs []string, conf *config.Config, kind executeKind) int {
|
|
// Make a TCP listener
|
|
listener, err := net.Listen("tcp", Addr)
|
|
if err != nil {
|
|
fmt.Printf("couldn't start listener: %s\n", err)
|
|
return 1
|
|
}
|
|
defer listener.Close()
|
|
|
|
if Headless && (InitFile != "") {
|
|
fmt.Fprintf(os.Stderr, "Warning: init file ignored\n")
|
|
}
|
|
|
|
var server interface {
|
|
Run() error
|
|
Stop(bool) error
|
|
}
|
|
|
|
if !Headless {
|
|
ApiVersion = 2
|
|
}
|
|
|
|
// Create and start a debugger server
|
|
switch ApiVersion {
|
|
case 1:
|
|
server = rpc1.NewServer(&service.Config{
|
|
Listener: listener,
|
|
ProcessArgs: processArgs,
|
|
AttachPid: attachPid,
|
|
AcceptMulti: AcceptMulti,
|
|
}, Log)
|
|
case 2:
|
|
server = rpc2.NewServer(&service.Config{
|
|
Listener: listener,
|
|
ProcessArgs: processArgs,
|
|
AttachPid: attachPid,
|
|
AcceptMulti: AcceptMulti,
|
|
}, Log)
|
|
default:
|
|
fmt.Println("Unknown API version %d", ApiVersion)
|
|
return 1
|
|
}
|
|
|
|
if err := server.Run(); err != nil {
|
|
if err == api.NotExecutableErr {
|
|
switch kind {
|
|
case executingGeneratedFile:
|
|
fmt.Fprintln(os.Stderr, "Can not debug non-main package")
|
|
return 1
|
|
case executingExistingFile:
|
|
fmt.Fprintf(os.Stderr, "%s is not executable\n", processArgs[0])
|
|
return 1
|
|
default:
|
|
// fallthrough
|
|
}
|
|
}
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 1
|
|
}
|
|
|
|
var status int
|
|
if Headless {
|
|
// Print listener address
|
|
fmt.Printf("API server listening at: %s\n", listener.Addr())
|
|
ch := make(chan os.Signal)
|
|
signal.Notify(ch, syscall.SIGINT)
|
|
<-ch
|
|
err = server.Stop(true)
|
|
} else {
|
|
// Create and start a terminal
|
|
var client service.Client
|
|
client = rpc2.NewClient(listener.Addr().String())
|
|
term := terminal.New(client, conf)
|
|
term.InitFile = InitFile
|
|
status, err = term.Run()
|
|
}
|
|
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
}
|
|
|
|
return status
|
|
}
|
|
|
|
func gobuild(debugname, pkg string) error {
|
|
args := []string{"-gcflags", "-N -l", "-o", debugname}
|
|
if BuildFlags != "" {
|
|
args = append(args, BuildFlags)
|
|
}
|
|
args = append(args, pkg)
|
|
return gocommand("build", args...)
|
|
}
|
|
|
|
func gotestbuild(pkg string) error {
|
|
args := []string{"-gcflags", "-N -l", "-c", "-o", testdebugname}
|
|
if BuildFlags != "" {
|
|
args = append(args, BuildFlags)
|
|
}
|
|
args = append(args, pkg)
|
|
return gocommand("test", args...)
|
|
}
|
|
|
|
func gocommand(command string, args ...string) error {
|
|
allargs := []string{command}
|
|
allargs = append(allargs, args...)
|
|
goBuild := exec.Command("go", allargs...)
|
|
goBuild.Stderr = os.Stderr
|
|
return goBuild.Run()
|
|
}
|