diff --git a/.travis.yml b/.travis.yml index 21b4f8eb..03119174 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,7 +58,11 @@ jobs: script: >- if [ $TRAVIS_OS_NAME = "linux" ] && [ $go_32_version ]; then docker pull i386/centos:7; - docker run -v $(pwd):/delve --privileged i386/centos:7 /bin/bash -c "set -x && \ + docker run \ + -v $(pwd):/delve \ + --env TRAVIS=true \ + --privileged i386/centos:7 \ + /bin/bash -c "set -x && \ cd delve && \ yum -y update && yum -y upgrade && \ yum -y install wget make git gcc && \ diff --git a/Documentation/faq.md b/Documentation/faq.md index 9b8874eb..54f10957 100644 --- a/Documentation/faq.md +++ b/Documentation/faq.md @@ -35,3 +35,17 @@ dlv exec --headless --continue --listen :4040 --accept-multiclient /path/to/exec ``` Note that the connection to Delve is unauthenticated and will allow arbitrary remote code execution: *do not do this in production*. + +#### How can I use Delve to debug a CLI application? + +There are three good ways to go about this + +1. Run your CLI application in a separate terminal and then attach to it via `dlv attach`. + +1. Run Delve in headless mode via `dlv debug --headless` and then connect to it from +another terminal. This will place the process in the foreground and allow it to access +the terminal TTY. + +1. Assign the process its own TTY. This can be done on UNIX systems via the `--tty` flag for the +`dlv debug` and `dlv exec` commands. For the best experience, you should create your own PTY and +assign it as the TTY. This can be done via [ptyme](https://github.com/derekparker/ptyme). diff --git a/Documentation/usage/dlv_debug.md b/Documentation/usage/dlv_debug.md index d3242475..d00dee1b 100644 --- a/Documentation/usage/dlv_debug.md +++ b/Documentation/usage/dlv_debug.md @@ -21,6 +21,7 @@ dlv debug [package] ``` --continue Continue the debugged process on start. --output string Output path for the binary. (default "./__debug_bin") + --tty string TTY to use for the target program ``` ### Options inherited from parent commands diff --git a/Documentation/usage/dlv_exec.md b/Documentation/usage/dlv_exec.md index e9711b98..2b3d123e 100644 --- a/Documentation/usage/dlv_exec.md +++ b/Documentation/usage/dlv_exec.md @@ -20,7 +20,8 @@ dlv exec ### Options ``` - --continue Continue the debugged process on start. + --continue Continue the debugged process on start. + --tty string TTY to use for the target program ``` ### Options inherited from parent commands diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index 3fade4f3..ca6ac1ed 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -52,6 +52,8 @@ var ( // checkLocalConnUser is true if the debugger should check that local // connections come from the same user that started the headless server checkLocalConnUser bool + // tty is used to provide an alternate TTY for the program you wish to debug. + tty string // backend selection backend string @@ -184,6 +186,7 @@ session.`, } debugCommand.Flags().String("output", "./__debug_bin", "Output path for the binary.") debugCommand.Flags().BoolVar(&continueOnStart, "continue", false, "Continue the debugged process on start.") + debugCommand.Flags().StringVar(&tty, "tty", "", "TTY to use for the target program") rootCommand.AddCommand(debugCommand) // 'exec' subcommand. @@ -207,6 +210,7 @@ or later, -gcflags="-N -l" on earlier versions of Go.`, os.Exit(execute(0, args, conf, "", executingExistingFile)) }, } + execCommand.Flags().StringVar(&tty, "tty", "", "TTY to use for the target program") execCommand.Flags().BoolVar(&continueOnStart, "continue", false, "Continue the debugged process on start.") rootCommand.AddCommand(execCommand) @@ -398,10 +402,11 @@ func dapCmd(cmd *cobra.Command, args []string) { server := dap.NewServer(&service.Config{ Listener: listener, Backend: backend, - Foreground: true, // always headless + Foreground: (headless && tty == ""), DebugInfoDirectories: conf.DebugInfoDirectories, CheckGoVersion: checkGoVersion, DisconnectChan: disconnectChan, + TTY: tty, }) defer server.Stop() @@ -741,11 +746,12 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile WorkingDir: workingDir, Backend: backend, CoreFile: coreFile, - Foreground: headless, + Foreground: (headless && tty == ""), DebugInfoDirectories: conf.DebugInfoDirectories, CheckGoVersion: checkGoVersion, CheckLocalConnUser: checkLocalConnUser, DisconnectChan: disconnectChan, + TTY: tty, }) default: fmt.Printf("Unknown API version: %d\n", apiVersion) diff --git a/go.mod b/go.mod index 7ded24d4..76275bc9 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.11 require ( github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5 github.com/cpuguy83/go-md2man v1.0.10 // indirect + github.com/creack/pty v1.1.9 github.com/google/go-dap v0.2.0 - github.com/cpuguy83/go-md2man v1.0.8 // indirect github.com/hashicorp/golang-lru v0.5.4 github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561 diff --git a/go.sum b/go.sum index fde29856..51983b18 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6IC github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 225eeef0..642b7580 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -345,7 +345,7 @@ func getLdEnvVars() []string { // LLDBLaunch starts an instance of lldb-server and connects to it, asking // it to launch the specified target program with the specified arguments // (cmd) on the specified directory wd. -func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (*proc.Target, error) { +func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string) (*proc.Target, error) { if runtime.GOOS == "windows" { return nil, ErrUnsupportedOS } @@ -374,6 +374,9 @@ func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string if foreground { args = append(args, "--stdio-path", "/dev/tty") } + if tty != "" { + args = append(args, "--stdio-path", tty) + } if logflags.LLDBServerOutput() { args = append(args, "-g", "-l", "stdout") } diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go index 2f006f3b..5ea4dc8a 100644 --- a/pkg/proc/native/nonative_darwin.go +++ b/pkg/proc/native/nonative_darwin.go @@ -12,12 +12,12 @@ import ( var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation") // Launch returns ErrNativeBackendDisabled. -func Launch(cmd []string, wd string, foreground bool, _ []string) (*proc.Target, error) { +func Launch(_ []string, _ string, _ bool, _ []string, _ string) (*proc.Target, error) { return nil, ErrNativeBackendDisabled } // Attach returns ErrNativeBackendDisabled. -func Attach(pid int, _ []string) (*proc.Target, error) { +func Attach(_ int, _ []string) (*proc.Target, error) { return nil, ErrNativeBackendDisabled } diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 1fe3454a..18869198 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -2,6 +2,7 @@ package native import ( "go/ast" + "os" "runtime" "sync" @@ -34,6 +35,10 @@ type nativeProcess struct { childProcess bool // this process was launched, not attached to manualStopRequested bool + // Controlling terminal file descriptor for + // this process. + ctty *os.File + exited, detached bool } @@ -343,6 +348,9 @@ func (dbp *nativeProcess) postExit() { close(dbp.ptraceChan) close(dbp.ptraceDoneChan) dbp.bi.Close() + if dbp.ctty != nil { + dbp.ctty.Close() + } } func (dbp *nativeProcess) writeSoftwareBreakpoint(thread *nativeThread, addr uint64) error { diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go index aa0b7655..95636462 100644 --- a/pkg/proc/native/proc_darwin.go +++ b/pkg/proc/native/proc_darwin.go @@ -37,7 +37,7 @@ type osProcessDetails struct { // custom fork/exec process in order to take advantage of // PT_SIGEXC on Darwin which will turn Unix signals into // Mach exceptions. -func Launch(cmd []string, wd string, foreground bool, _ []string) (*proc.Target, error) { +func Launch(cmd []string, wd string, foreground bool, _ []string, _ string) (*proc.Target, error) { argv0Go, err := filepath.Abs(cmd[0]) if err != nil { return nil, err diff --git a/pkg/proc/native/proc_freebsd.go b/pkg/proc/native/proc_freebsd.go index b491b8f9..c925cc81 100644 --- a/pkg/proc/native/proc_freebsd.go +++ b/pkg/proc/native/proc_freebsd.go @@ -43,7 +43,7 @@ type osProcessDetails struct { // to be supplied to that process. `wd` is working directory of the program. // If the DWARF information cannot be found in the binary, Delve will look // for external debug files in the directories passed in. -func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (*proc.Target, error) { +func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string) (*proc.Target, error) { var ( process *exec.Cmd err error @@ -66,6 +66,12 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (* signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN) process.Stdin = os.Stdin } + if tty != "" { + dbp.ctty, err = attachProcessToTTY(process, tty) + if err != nil { + return + } + } if wd != "" { process.Dir = wd } diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go index 3aa0c56a..c1b53cc0 100644 --- a/pkg/proc/native/proc_linux.go +++ b/pkg/proc/native/proc_linux.go @@ -49,7 +49,7 @@ type osProcessDetails struct { // to be supplied to that process. `wd` is working directory of the program. // If the DWARF information cannot be found in the binary, Delve will look // for external debug files in the directories passed in. -func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (*proc.Target, error) { +func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string) (*proc.Target, error) { var ( process *exec.Cmd err error @@ -67,11 +67,21 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string) (* process.Args = cmd process.Stdout = os.Stdout process.Stderr = os.Stderr - process.SysProcAttr = &syscall.SysProcAttr{Ptrace: true, Setpgid: true, Foreground: foreground} + process.SysProcAttr = &syscall.SysProcAttr{ + Ptrace: true, + Setpgid: true, + Foreground: foreground, + } if foreground { signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN) process.Stdin = os.Stdin } + if tty != "" { + dbp.ctty, err = attachProcessToTTY(process, tty) + if err != nil { + return + } + } if wd != "" { process.Dir = wd } diff --git a/pkg/proc/native/proc_unix.go b/pkg/proc/native/proc_unix.go new file mode 100644 index 00000000..2ca69f3a --- /dev/null +++ b/pkg/proc/native/proc_unix.go @@ -0,0 +1,30 @@ +// +build !windows + +package native + +import ( + "fmt" + "os" + "os/exec" + + isatty "github.com/mattn/go-isatty" +) + +func attachProcessToTTY(process *exec.Cmd, tty string) (*os.File, error) { + f, err := os.OpenFile(tty, os.O_RDWR, 0) + if err != nil { + return nil, err + } + if !isatty.IsTerminal(f.Fd()) { + f.Close() + return nil, fmt.Errorf("%s is not a terminal", f.Name()) + } + process.Stdin = f + process.Stdout = f + process.Stderr = f + process.SysProcAttr.Setpgid = false + process.SysProcAttr.Setsid = true + process.SysProcAttr.Setctty = true + + return f, nil +} diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go index 4585eb62..1e04e0d2 100644 --- a/pkg/proc/native/proc_windows.go +++ b/pkg/proc/native/proc_windows.go @@ -20,7 +20,7 @@ type osProcessDetails struct { } // Launch creates and begins debugging a new process. -func Launch(cmd []string, wd string, foreground bool, _ []string) (*proc.Target, error) { +func Launch(cmd []string, wd string, foreground bool, _ []string, _ string) (*proc.Target, error) { argv0Go, err := filepath.Abs(cmd[0]) if err != nil { return nil, err diff --git a/pkg/proc/proc_linux_test.go b/pkg/proc/proc_linux_test.go index 530bcadb..6ea02e0f 100644 --- a/pkg/proc/proc_linux_test.go +++ b/pkg/proc/proc_linux_test.go @@ -14,7 +14,7 @@ func TestLoadingExternalDebugInfo(t *testing.T) { fixture := protest.BuildFixture("locationsprog", 0) defer os.Remove(fixture.Path) stripAndCopyDebugInfo(fixture, t) - p, err := native.Launch(append([]string{fixture.Path}, ""), "", false, []string{filepath.Dir(fixture.Path)}) + p, err := native.Launch(append([]string{fixture.Path}, ""), "", false, []string{filepath.Dir(fixture.Path)}, "") if err != nil { t.Fatal(err) } diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index b8c374bb..09082737 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -67,9 +67,9 @@ func withTestProcessArgs(name string, t testing.TB, wd string, args []string, bu switch testBackend { case "native": - p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false, []string{}) + p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false, []string{}, "") case "lldb": - p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false, []string{}) + p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false, []string{}, "") case "rr": protest.MustHaveRecordingAllowed(t) t.Log("recording") @@ -2065,9 +2065,9 @@ func TestUnsupportedArch(t *testing.T) { switch testBackend { case "native": - p, err = native.Launch([]string{outfile}, ".", false, []string{}) + p, err = native.Launch([]string{outfile}, ".", false, []string{}, "") case "lldb": - p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false, []string{}) + p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false, []string{}, "") default: t.Skip("test not valid for this backend") } diff --git a/service/config.go b/service/config.go index 11fc31a0..7ff4a5a3 100644 --- a/service/config.go +++ b/service/config.go @@ -50,4 +50,8 @@ type Config struct { // DisconnectChan will be closed by the server when the client disconnects DisconnectChan chan<- struct{} + + // TTY is passed along to the target process on creation. Used to specify a + // TTY for that process. + TTY string } diff --git a/service/dap/server.go b/service/dap/server.go index a7262a8e..fadbee78 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -368,6 +368,7 @@ func (s *Server) onLaunchRequest(request *dap.LaunchRequest) { Foreground: s.config.Foreground, DebugInfoDirectories: s.config.DebugInfoDirectories, CheckGoVersion: s.config.CheckGoVersion, + TTY: s.config.TTY, } var err error if s.debugger, err = debugger.New(config, s.config.ProcessArgs); err != nil { diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index a12e4f82..cec918a4 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -91,6 +91,10 @@ type Config struct { // used to compile the executable and refuse to work on incompatible // versions. CheckGoVersion bool + + // TTY is passed along to the target process on creation. Used to specify a + // TTY for that process. + TTY string } // New creates a new Debugger. ProcessArgs specify the commandline arguments for the @@ -195,9 +199,9 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) } switch d.config.Backend { case "native": - return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories) + return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY) case "lldb": - return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories)) + return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY)) case "rr": if d.target != nil { // restart should not call us if the backend is 'rr' @@ -239,9 +243,9 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.Target, error) case "default": if runtime.GOOS == "darwin" { - return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories)) + return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY)) } - return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories) + return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY) default: return nil, fmt.Errorf("unknown backend %q", d.config.Backend) } diff --git a/service/debugger/debugger_unix_test.go b/service/debugger/debugger_unix_test.go index 2bea8f8f..caa12c96 100644 --- a/service/debugger/debugger_unix_test.go +++ b/service/debugger/debugger_unix_test.go @@ -3,18 +3,25 @@ package debugger import ( + "bytes" "fmt" "os" + "os/exec" "path/filepath" "runtime" "testing" + "github.com/creack/pty" "github.com/go-delve/delve/pkg/gobuild" protest "github.com/go-delve/delve/pkg/proc/test" "github.com/go-delve/delve/service/api" ) func TestDebugger_LaunchNoExecutablePerm(t *testing.T) { + defer func() { + os.Setenv("GOOS", runtime.GOOS) + os.Setenv("GOARCH", runtime.GOARCH) + }() fixturesDir := protest.FindFixturesDir() buildtestdir := filepath.Join(fixturesDir, "buildtest") debugname := "debug" @@ -29,10 +36,10 @@ func TestDebugger_LaunchNoExecutablePerm(t *testing.T) { } os.Setenv("GOOS", switchOS[runtime.GOOS]) exepath := filepath.Join(buildtestdir, debugname) + defer os.Remove(exepath) if err := gobuild.GoBuild(debugname, []string{buildtestdir}, fmt.Sprintf("-o %s", exepath)); err != nil { t.Fatalf("go build error %v", err) } - defer os.Remove(exepath) if err := os.Chmod(exepath, 0644); err != nil { t.Fatal(err) } @@ -45,3 +52,46 @@ func TestDebugger_LaunchNoExecutablePerm(t *testing.T) { t.Fatalf("expected error \"%s\" got \"%v\"", api.ErrNotExecutable, err) } } + +func TestDebugger_LaunchWithTTY(t *testing.T) { + if os.Getenv("TRAVIS") == "true" { + if _, err := exec.LookPath("lsof"); err != nil { + t.Skip("skipping test in CI, system does not contain lsof") + } + } + // Ensure no env meddling is leftover from previous tests. + os.Setenv("GOOS", runtime.GOOS) + os.Setenv("GOARCH", runtime.GOARCH) + + p, tty, err := pty.Open() + if err != nil { + t.Fatal(err) + } + defer p.Close() + defer tty.Close() + + fixturesDir := protest.FindFixturesDir() + buildtestdir := filepath.Join(fixturesDir, "buildtest") + debugname := "debugtty" + exepath := filepath.Join(buildtestdir, debugname) + if err := gobuild.GoBuild(debugname, []string{buildtestdir}, fmt.Sprintf("-o %s", exepath)); err != nil { + t.Fatalf("go build error %v", err) + } + defer os.Remove(exepath) + var backend string + protest.DefaultTestBackend(&backend) + conf := &Config{TTY: tty.Name(), Backend: backend} + pArgs := []string{exepath} + d, err := New(conf, pArgs) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("lsof", "-p", fmt.Sprintf("%d", d.ProcessPid())) + result, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(result, []byte(tty.Name())) { + t.Fatal("process open file list does not contain expected tty") + } +} diff --git a/service/rpccommon/server.go b/service/rpccommon/server.go index bde3bfc9..b07ece3e 100644 --- a/service/rpccommon/server.go +++ b/service/rpccommon/server.go @@ -116,6 +116,7 @@ func (s *ServerImpl) Run() error { Foreground: s.config.Foreground, DebugInfoDirectories: s.config.DebugInfoDirectories, CheckGoVersion: s.config.CheckGoVersion, + TTY: s.config.TTY, }, s.config.ProcessArgs); err != nil { return err diff --git a/service/test/variables_test.go b/service/test/variables_test.go index 4e1943b5..5d000e4a 100644 --- a/service/test/variables_test.go +++ b/service/test/variables_test.go @@ -131,9 +131,9 @@ func withTestProcessArgs(name string, t *testing.T, wd string, args []string, bu var tracedir string switch testBackend { case "native": - p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false, []string{}) + p, err = native.Launch(append([]string{fixture.Path}, args...), wd, false, []string{}, "") case "lldb": - p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false, []string{}) + p, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, false, []string{}, "") case "rr": protest.MustHaveRecordingAllowed(t) t.Log("recording")