提交 3e60ae20 编写于 作者: D Derek Parker

*: Add --tty flag for debug / exec

This flag allows users on UNIX systems to set the tty for the program
being debugged by Delve. This is useful for debugging command line
applications which need access to their own TTY, and also for
controlling the output of the debugged programs so that IDEs may open a
dedicated terminal to show the output for the process.
上级 c3a4d726
......@@ -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 && \
......
......@@ -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).
......@@ -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
......
......@@ -20,7 +20,8 @@ dlv exec <path/to/binary>
### 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
......
......@@ -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)
......
......@@ -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
......
......@@ -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")
}
......
......@@ -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
}
......
......@@ -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 {
......
......@@ -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
......
......@@ -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
}
......
......@@ -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
}
......
// +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
}
......@@ -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
......
......@@ -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)
}
......
......@@ -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")
}
......
......@@ -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
}
......@@ -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 {
......
......@@ -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)
}
......
......@@ -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")
}
}
......@@ -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
......
......@@ -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")
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册