cmd,proc,terminal,debugger: Support default file descriptor redirects

Adds features to support default file descriptor redirects for the
target process:

1. A new command line flag '--redirect' and '-r' are added to specify
   file redirects for the target process
2. New syntax is added to the 'restart' command to specify file
   redirects.
3. Interactive instances will check if stdin/stdout and stderr are
   terminals and print a helpful error message if they aren't.
This commit is contained in:
aarzilli 2020-08-21 16:14:02 +02:00 committed by Alessandro Arzilli
parent f90134eb4d
commit 7555d1c063
43 changed files with 766 additions and 271 deletions

@ -414,15 +414,21 @@ For recorded targets the command takes the following forms:
restart resets ot the start of the recording
restart [checkpoint] resets the recording to the given checkpoint
restart -r [newargv...] re-records the target process
restart -r [newargv...] [redirects...] re-records the target process
For live targets the command takes the following forms:
restart [newargv...] restarts the process
restart [newargv...] [redirects...] restarts the process
If newargv is omitted the process is restarted (or re-recorded) with the same argument vector.
If -noargs is specified instead, the argument vector is cleared.
A list of file redirections can be specified after the new argument list to override the redirections defined using the '--redirect' command line option. A syntax similar to Unix shells is used:
<input.txt redirects the standard input of the target process from input.txt
>output.txt redirects the standard output of the target process to output.txt
2>error.txt redirects the standard error of the target process to error.txt
Aliases: r

@ -51,7 +51,7 @@ threads() | Equivalent to API call [ListThreads](https://godoc.org/github.com/go
types(Filter) | Equivalent to API call [ListTypes](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListTypes)
process_pid() | Equivalent to API call [ProcessPid](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ProcessPid)
recorded() | Equivalent to API call [Recorded](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Recorded)
restart(Position, ResetArgs, NewArgs, Rerecord, Rebuild) | Equivalent to API call [Restart](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Restart)
restart(Position, ResetArgs, NewArgs, Rerecord, Rebuild, NewRedirects) | Equivalent to API call [Restart](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Restart)
set_expr(Scope, Symbol, Value) | Equivalent to API call [Set](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Set)
stacktrace(Id, Depth, Full, Defers, Opts, Cfg) | Equivalent to API call [Stacktrace](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Stacktrace)
state(NonBlocking) | Equivalent to API call [State](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.State)

@ -20,6 +20,7 @@ Pass flags to the program you are debugging using `--`, for example:
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -31,6 +32,7 @@ Pass flags to the program you are debugging using `--`, for example:
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -26,6 +26,7 @@ dlv attach pid [executable]
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -37,6 +38,7 @@ dlv attach pid [executable]
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -19,6 +19,7 @@ are:
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -30,6 +31,7 @@ are:
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -15,6 +15,7 @@ dlv connect addr
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -26,6 +27,7 @@ dlv connect addr
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -21,6 +21,7 @@ dlv core <executable> <core>
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -32,6 +33,7 @@ dlv core <executable> <core>
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -22,6 +22,7 @@ dlv dap
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -33,6 +34,7 @@ dlv dap
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -28,6 +28,7 @@ dlv debug [package]
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -39,6 +40,7 @@ dlv debug [package]
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -28,6 +28,7 @@ dlv exec <path/to/binary>
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -39,6 +40,7 @@ dlv exec <path/to/binary>
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -34,6 +34,7 @@ and dap modes.
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -45,6 +46,7 @@ and dap modes.
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -0,0 +1,43 @@
## dlv redirect
Help about file redirection.
### Synopsis
The standard file descriptors of the target process can be controlled using the '-r' and '--tty' arguments.
The --tty argument allows redirecting all standard descriptors to a terminal, specified as an argument to --tty.
The syntax for '-r' argument is:
-r [source:]destination
Where source is one of 'stdin', 'stdout' or 'stderr' and destination is the path to a file. If the source is omitted stdin is used implicitly.
File redirects can also be changed using the 'restart' command.
### Options inherited from parent commands
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
--check-go-version Checks that the version of Go in use is compatible with Delve. (default true)
--headless Run debug server only, in headless mode.
--init string Init file, executed by the terminal client.
-l, --listen string Debugging server listen address. (default "127.0.0.1:0")
--log Enable debugging server logging.
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```
### SEE ALSO
* [dlv](dlv.md) - Delve is a debugger for the Go programming language.

@ -19,6 +19,7 @@ dlv replay [trace directory]
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -30,6 +31,7 @@ dlv replay [trace directory]
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -15,6 +15,7 @@ dlv run
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -26,6 +27,7 @@ dlv run
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -26,6 +26,7 @@ dlv test [package]
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -37,6 +38,7 @@ dlv test [package]
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -33,6 +33,7 @@ dlv trace [package] regexp
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -44,6 +45,7 @@ dlv trace [package] regexp
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -15,6 +15,7 @@ dlv version
```
--accept-multiclient Allows a headless server to accept multiple client connections.
--allow-non-terminal-interactive Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr
--api-version int Selects API version when headless. New clients should use v2. Can be reset via RPCServer.SetApiVersion. See Documentation/api/json-rpc/README.md. (default 1)
--backend string Backend selection (see 'dlv help backend'). (default "default")
--build-flags string Build flags, to be passed to the compiler.
@ -26,6 +27,7 @@ dlv version
--log-dest string Writes logs to the specified file or file descriptor (see 'dlv help log').
--log-output string Comma separated list of components that should produce debug output (see 'dlv help log')
--only-same-user Only connections from the same user that started this instance of Delve are allowed to connect. (default true)
-r, --redirect stringArray Specifies redirect rules for target process (see 'dlv help redirect')
--wd string Working directory for running the program.
```

@ -0,0 +1 @@
Redirect test

13
_fixtures/redirect.go Normal file

@ -0,0 +1,13 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"time"
)
func main() {
buf, _ := ioutil.ReadAll(os.Stdin)
fmt.Fprintf(os.Stdout, "%s %v\n", buf, time.Now())
}

59
cmd/dlv/cmds/cmds_test.go Normal file

@ -0,0 +1,59 @@
package cmds
import (
"testing"
)
func TestParseRedirects(t *testing.T) {
testCases := []struct {
in []string
tgt [3]string
tgterr string
}{
{
[]string{"one.txt"},
[3]string{"one.txt", "", ""},
"",
},
{
[]string{"one.txt", "two.txt"},
[3]string{},
"redirect error: stdin redirected twice",
},
{
[]string{"stdout:one.txt"},
[3]string{"", "one.txt", ""},
"",
},
{
[]string{"stdout:one.txt", "stderr:two.txt", "stdin:three.txt"},
[3]string{"three.txt", "one.txt", "two.txt"},
"",
},
{
[]string{"stdout:one.txt", "stderr:two.txt", "three.txt"},
[3]string{"three.txt", "one.txt", "two.txt"},
"",
},
}
for _, tc := range testCases {
t.Logf("input: %q", tc.in)
out, err := parseRedirects(tc.in)
t.Logf("output: %q error %v", out, err)
if tc.tgterr != "" {
if err == nil {
t.Errorf("Expected error %q, got output %q", tc.tgterr, out)
} else if errstr := err.Error(); errstr != tc.tgterr {
t.Errorf("Expected error %q, got error %q", tc.tgterr, errstr)
}
} else {
for i := range tc.tgt {
if tc.tgt[i] != out[i] {
t.Errorf("Expected %q, got %q (mismatch at index %d)", tc.tgt, out, i)
break
}
}
}
}
}

@ -26,6 +26,7 @@ import (
"github.com/go-delve/delve/service/debugger"
"github.com/go-delve/delve/service/rpc2"
"github.com/go-delve/delve/service/rpccommon"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
)
@ -74,6 +75,11 @@ var (
traceTestBinary bool
traceStackDepth int
// redirect specifications for target process
redirects []string
allowNonTerminalInteractive bool
conf *config.Config
)
@ -123,6 +129,8 @@ func New(docCall bool) *cobra.Command {
rootCommand.PersistentFlags().BoolVarP(&checkGoVersion, "check-go-version", "", true, "Checks that the version of Go in use is compatible with Delve.")
rootCommand.PersistentFlags().BoolVarP(&checkLocalConnUser, "only-same-user", "", true, "Only connections from the same user that started this instance of Delve are allowed to connect.")
rootCommand.PersistentFlags().StringVar(&backend, "backend", "default", `Backend selection (see 'dlv help backend').`)
rootCommand.PersistentFlags().StringArrayVarP(&redirects, "redirect", "r", []string{}, "Specifies redirect rules for target process (see 'dlv help redirect')")
rootCommand.PersistentFlags().BoolVar(&allowNonTerminalInteractive, "allow-non-terminal-interactive", false, "Allows interactive sessions of Delve that don't have a terminal as stdin, stdout and stderr")
// 'attach' subcommand.
attachCommand := &cobra.Command{
@ -358,6 +366,23 @@ otherwise as a file path.
This option will also redirect the "server listening at" message in headless
and dap modes.
`,
})
rootCommand.AddCommand(&cobra.Command{
Use: "redirect",
Short: "Help about file redirection.",
Long: `The standard file descriptors of the target process can be controlled using the '-r' and '--tty' arguments.
The --tty argument allows redirecting all standard descriptors to a terminal, specified as an argument to --tty.
The syntax for '-r' argument is:
-r [source:]destination
Where source is one of 'stdin', 'stdout' or 'stderr' and destination is the path to a file. If the source is omitted stdin is used implicitly.
File redirects can also be changed using the 'restart' command.
`,
})
@ -745,9 +770,34 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
acceptMulti = false
}
if !headless && !allowNonTerminalInteractive {
for _, f := range []struct {
name string
file *os.File
}{{"Stdin", os.Stdin}, {"Stdout", os.Stdout}, {"Stderr", os.Stderr}} {
if f.file == nil {
continue
}
if !isatty.IsTerminal(f.file.Fd()) {
fmt.Fprintf(os.Stderr, "%s is not a terminal, use '-r' to specify redirects for the target process or --allow-non-terminal-interactive=true if you really want to specify a redirect for Delve\n", f.name)
return 1
}
}
}
if len(redirects) > 0 && tty != "" {
fmt.Fprintf(os.Stderr, "Can not use -r and --tty together\n")
return 1
}
redirects, err := parseRedirects(redirects)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return 1
}
var listener net.Listener
var clientConn net.Conn
var err error
// Make a TCP listener
if headless {
@ -791,6 +841,7 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
Redirects: redirects,
},
})
default:
@ -832,3 +883,24 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
return connect(listener.Addr().String(), clientConn, conf, kind)
}
func parseRedirects(redirects []string) ([3]string, error) {
r := [3]string{}
names := [3]string{"stdin", "stdout", "stderr"}
for _, redirect := range redirects {
idx := 0
for i, name := range names {
pfx := name + ":"
if strings.HasPrefix(redirect, pfx) {
idx = i
redirect = redirect[len(pfx):]
break
}
}
if r[idx] != "" {
return r, fmt.Errorf("redirect error: %s redirected twice", names[idx])
}
r[idx] = redirect
}
return r, nil
}

@ -121,7 +121,7 @@ func testOutput(t *testing.T, dlvbin, output string, delveCmds []string) (stdout
var stdoutBuf, stderrBuf bytes.Buffer
buildtestdir := filepath.Join(protest.FindFixturesDir(), "buildtest")
c := []string{dlvbin, "debug"}
c := []string{dlvbin, "debug", "--allow-non-terminal-interactive=true"}
debugbin := filepath.Join(buildtestdir, "__debug_bin")
if output != "" {
c = append(c, "--output", output)
@ -734,7 +734,7 @@ func TestDlvTestChdir(t *testing.T) {
defer os.RemoveAll(tmpdir)
fixtures := protest.FindFixturesDir()
cmd := exec.Command(dlvbin, "test", filepath.Join(fixtures, "buildtest"), "--", "-test.v")
cmd := exec.Command(dlvbin, "--allow-non-terminal-interactive=true", "test", filepath.Join(fixtures, "buildtest"), "--", "-test.v")
cmd.Stdin = strings.NewReader("continue\nexit\n")
out, err := cmd.CombinedOutput()
if err != nil {

@ -342,7 +342,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, tty string) (*proc.Target, error) {
func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) {
if runtime.GOOS == "windows" {
return nil, ErrUnsupportedOS
}
@ -371,12 +371,32 @@ func LLDBLaunch(cmd []string, wd string, foreground bool, debugInfoDirs []string
ldEnvVars := getLdEnvVars()
args := make([]string, 0, len(cmd)+4+len(ldEnvVars))
args = append(args, ldEnvVars...)
if foreground {
args = append(args, "--stdio-path", "/dev/tty")
}
if tty != "" {
args = append(args, "--stdio-path", tty)
} else {
found := [3]bool{}
names := [3]string{"stdin", "stdout", "stderr"}
for i := range redirects {
if redirects[i] != "" {
found[i] = true
args = append(args, fmt.Sprintf("--%s-path", names[i]), redirects[i])
}
}
if foreground {
if !found[0] && !found[1] && !found[2] {
args = append(args, "--stdio-path", "/dev/tty")
} else {
for i := range found {
if !found[i] {
args = append(args, fmt.Sprintf("--%s-path", names[i]), "/dev/tty")
}
}
}
}
}
if logflags.LLDBServerOutput() {
args = append(args, "-g", "-l", "stdout")
}

@ -21,7 +21,7 @@ import (
// program. Returns a run function which will actually record the program, a
// stop function which will prematurely terminate the recording of the
// program.
func RecordAsync(cmd []string, wd string, quiet bool) (run func() (string, error), stop func() error, err error) {
func RecordAsync(cmd []string, wd string, quiet bool, redirects [3]string) (run func() (string, error), stop func() error, err error) {
if err := checkRRAvailabe(); err != nil {
return nil, nil, err
}
@ -35,10 +35,10 @@ func RecordAsync(cmd []string, wd string, quiet bool) (run func() (string, error
args = append(args, "record", "--print-trace-dir=3")
args = append(args, cmd...)
rrcmd := exec.Command("rr", args...)
rrcmd.Stdin = os.Stdin
if !quiet {
rrcmd.Stdout = os.Stdout
rrcmd.Stderr = os.Stderr
var closefn func()
rrcmd.Stdin, rrcmd.Stdout, rrcmd.Stderr, closefn, err = openRedirects(redirects, quiet)
if err != nil {
return nil, nil, err
}
rrcmd.ExtraFiles = []*os.File{wfd}
rrcmd.Dir = wd
@ -51,6 +51,7 @@ func RecordAsync(cmd []string, wd string, quiet bool) (run func() (string, error
run = func() (string, error) {
err := rrcmd.Run()
closefn()
_ = wfd.Close()
tracedir := <-tracedirChan
return tracedir, err
@ -63,10 +64,57 @@ func RecordAsync(cmd []string, wd string, quiet bool) (run func() (string, error
return run, stop, nil
}
func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
toclose := []*os.File{}
if redirects[0] != "" {
stdin, err = os.Open(redirects[0])
if err != nil {
return nil, nil, nil, nil, err
}
toclose = append(toclose, stdin)
} else {
stdin = os.Stdin
}
create := func(path string, dflt *os.File) *os.File {
if path == "" {
if quiet {
return nil
}
return dflt
}
var f *os.File
f, err = os.Create(path)
if f != nil {
toclose = append(toclose, f)
}
return f
}
stdout = create(redirects[1], os.Stdout)
if err != nil {
return nil, nil, nil, nil, err
}
stderr = create(redirects[2], os.Stderr)
if err != nil {
return nil, nil, nil, nil, err
}
closefn = func() {
for _, f := range toclose {
_ = f.Close()
}
}
return stdin, stdout, stderr, closefn, nil
}
// Record uses rr to record the execution of the specified program and
// returns the trace directory's path.
func Record(cmd []string, wd string, quiet bool) (tracedir string, err error) {
run, _, err := RecordAsync(cmd, wd, quiet)
func Record(cmd []string, wd string, quiet bool, redirects [3]string) (tracedir string, err error) {
run, _, err := RecordAsync(cmd, wd, quiet, redirects)
if err != nil {
return "", err
}
@ -286,8 +334,8 @@ func splitQuotedFields(in string) []string {
}
// RecordAndReplay acts like calling Record and then Replay.
func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string) (*proc.Target, string, error) {
tracedir, err := Record(cmd, wd, quiet)
func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, redirects [3]string) (*proc.Target, string, error) {
tracedir, err := Record(cmd, wd, quiet, redirects)
if tracedir == "" {
return nil, "", err
}

@ -30,7 +30,7 @@ func withTestRecording(name string, t testing.TB, fn func(t *proc.Target, fixtur
t.Skip("test skipped, rr not found")
}
t.Log("recording")
p, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{})
p, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}, [3]string{})
if err != nil {
t.Fatal("Launch():", err)
}

@ -12,7 +12,7 @@ import (
var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation")
// Launch returns ErrNativeBackendDisabled.
func Launch(_ []string, _ string, _ bool, _ []string, _ string) (*proc.Target, error) {
func Launch(_ []string, _ string, _ bool, _ []string, _ string, _ [3]string) (*proc.Target, error) {
return nil, ErrNativeBackendDisabled
}

@ -320,3 +320,47 @@ func (dbp *nativeProcess) writeSoftwareBreakpoint(thread *nativeThread, addr uin
_, err := thread.WriteMemory(uintptr(addr), dbp.bi.Arch.BreakpointInstruction())
return err
}
func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
toclose := []*os.File{}
if redirects[0] != "" {
stdin, err = os.Open(redirects[0])
if err != nil {
return nil, nil, nil, nil, err
}
toclose = append(toclose, stdin)
} else if foreground {
stdin = os.Stdin
}
create := func(path string, dflt *os.File) *os.File {
if path == "" {
return dflt
}
var f *os.File
f, err = os.Create(path)
if f != nil {
toclose = append(toclose, f)
}
return f
}
stdout = create(redirects[1], os.Stdout)
if err != nil {
return nil, nil, nil, nil, err
}
stderr = create(redirects[2], os.Stderr)
if err != nil {
return nil, nil, nil, nil, err
}
closefn = func() {
for _, f := range toclose {
_ = f.Close()
}
}
return stdin, stdout, stderr, closefn, nil
}

@ -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, _ string) (*proc.Target, error) {
func Launch(cmd []string, wd string, foreground bool, _ []string, _ string, _ [3]string) (*proc.Target, error) {
argv0Go, err := filepath.Abs(cmd[0])
if err != nil {
return nil, err

@ -6,7 +6,6 @@ package native
import "C"
import (
"fmt"
"os"
"os/exec"
"os/signal"
"strings"
@ -43,13 +42,18 @@ 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, tty string) (*proc.Target, error) {
func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) {
var (
process *exec.Cmd
err error
)
if !isatty.IsTerminal(os.Stdin.Fd()) {
stdin, stdout, stderr, closefn, err := openRedirects(redirects, foreground)
if err != nil {
return nil, err
}
if stdin == nil || !isatty.IsTerminal(stdin.Fd()) {
// exec.(*Process).Start will fail if we try to send a process to
// foreground but we are not attached to a terminal.
foreground = false
@ -64,13 +68,13 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tt
dbp.execPtraceFunc(func() {
process = exec.Command(cmd[0])
process.Args = cmd
process.Stdout = os.Stdout
process.Stderr = os.Stderr
process.Stdin = stdin
process.Stdout = stdout
process.Stderr = stderr
process.SysProcAttr = &syscall.SysProcAttr{Ptrace: true, Setpgid: true, Foreground: foreground}
process.Env = proc.DisableAsyncPreemptEnv()
if foreground {
signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN)
process.Stdin = os.Stdin
}
if tty != "" {
dbp.ctty, err = attachProcessToTTY(process, tty)
@ -83,6 +87,7 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tt
}
err = process.Start()
})
closefn()
if err != nil {
return nil, err
}

@ -49,13 +49,18 @@ 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, tty string) (*proc.Target, error) {
func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tty string, redirects [3]string) (*proc.Target, error) {
var (
process *exec.Cmd
err error
)
if !isatty.IsTerminal(os.Stdin.Fd()) {
stdin, stdout, stderr, closefn, err := openRedirects(redirects, foreground)
if err != nil {
return nil, err
}
if stdin == nil || !isatty.IsTerminal(stdin.Fd()) {
// exec.(*Process).Start will fail if we try to send a process to
// foreground but we are not attached to a terminal.
foreground = false
@ -70,8 +75,9 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tt
dbp.execPtraceFunc(func() {
process = exec.Command(cmd[0])
process.Args = cmd
process.Stdout = os.Stdout
process.Stderr = os.Stderr
process.Stdin = stdin
process.Stdout = stdout
process.Stderr = stderr
process.SysProcAttr = &syscall.SysProcAttr{
Ptrace: true,
Setpgid: true,
@ -79,7 +85,6 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tt
}
if foreground {
signal.Ignore(syscall.SIGTTOU, syscall.SIGTTIN)
process.Stdin = os.Stdin
}
if tty != "" {
dbp.ctty, err = attachProcessToTTY(process, tty)
@ -92,6 +97,7 @@ func Launch(cmd []string, wd string, foreground bool, debugInfoDirs []string, tt
}
err = process.Start()
})
closefn()
if err != nil {
return nil, err
}

@ -21,7 +21,7 @@ type osProcessDetails struct {
}
// Launch creates and begins debugging a new process.
func Launch(cmd []string, wd string, foreground bool, _ []string, _ string) (*proc.Target, error) {
func Launch(cmd []string, wd string, foreground bool, _ []string, _ string, redirects [3]string) (*proc.Target, error) {
argv0Go, err := filepath.Abs(cmd[0])
if err != nil {
return nil, err
@ -29,12 +29,17 @@ func Launch(cmd []string, wd string, foreground bool, _ []string, _ string) (*pr
env := proc.DisableAsyncPreemptEnv()
stdin, stdout, stderr, closefn, err := openRedirects(redirects, true)
if err != nil {
return nil, err
}
var p *os.Process
dbp := newProcess(0)
dbp.execPtraceFunc(func() {
attr := &os.ProcAttr{
Dir: wd,
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
Files: []*os.File{stdin, stdout, stderr},
Sys: &syscall.SysProcAttr{
CreationFlags: _DEBUG_ONLY_THIS_PROCESS,
},
@ -42,6 +47,7 @@ func Launch(cmd []string, wd string, foreground bool, _ []string, _ string) (*pr
}
p, err = os.StartProcess(argv0Go, cmd, attr)
})
closefn()
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)}, "", [3]string{})
if err != nil {
t.Fatal(err)
}

@ -69,13 +69,13 @@ 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{}, "", [3]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{}, "", [3]string{})
case "rr":
protest.MustHaveRecordingAllowed(t)
t.Log("recording")
p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{})
p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{}, [3]string{})
t.Logf("replaying %q", tracedir)
default:
t.Fatal("unknown backend")
@ -2102,9 +2102,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{}, "", [3]string{})
case "lldb":
p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false, []string{}, "")
p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", false, []string{}, "", [3]string{})
default:
t.Skip("test not valid for this backend")
}

@ -134,14 +134,20 @@ For recorded targets the command takes the following forms:
restart resets ot the start of the recording
restart [checkpoint] resets the recording to the given checkpoint
restart -r [newargv...] re-records the target process
restart -r [newargv...] [redirects...] re-records the target process
For live targets the command takes the following forms:
restart [newargv...] restarts the process
restart [newargv...] [redirects...] restarts the process
If newargv is omitted the process is restarted (or re-recorded) with the same argument vector.
If -noargs is specified instead, the argument vector is cleared.
A list of file redirections can be specified after the new argument list to override the redirections defined using the '--redirect' command line option. A syntax similar to Unix shells is used:
<input.txt redirects the standard input of the target process from input.txt
>output.txt redirects the standard output of the target process to output.txt
2>error.txt redirects the standard error of the target process to error.txt
`},
{aliases: []string{"rebuild"}, group: runCmds, cmdFn: c.rebuild, allowedPrefixes: revPrefix, helpMsg: "Rebuild the target executable and restarts it. It does not work if the executable was not built by delve."},
{aliases: []string{"continue", "c"}, group: runCmds, cmdFn: c.cont, allowedPrefixes: revPrefix, helpMsg: "Run until breakpoint or program termination."},
@ -971,6 +977,7 @@ func restartRecorded(t *Term, ctx callContext, args string) error {
rerecord := false
resetArgs := false
newArgv := []string{}
newRedirects := [3]string{}
restartPos := ""
if len(v) > 0 {
@ -978,7 +985,7 @@ func restartRecorded(t *Term, ctx callContext, args string) error {
rerecord = true
if len(v) == 2 {
var err error
resetArgs, newArgv, err = parseNewArgv(v[1])
resetArgs, newArgv, newRedirects, err = parseNewArgv(v[1])
if err != nil {
return err
}
@ -991,7 +998,7 @@ func restartRecorded(t *Term, ctx callContext, args string) error {
}
}
if err := restartIntl(t, rerecord, restartPos, resetArgs, newArgv); err != nil {
if err := restartIntl(t, rerecord, restartPos, resetArgs, newArgv, newRedirects); err != nil {
return err
}
@ -1015,12 +1022,12 @@ func parseOptionalCount(arg string) (int64, error) {
}
func restartLive(t *Term, ctx callContext, args string) error {
resetArgs, newArgv, err := parseNewArgv(args)
resetArgs, newArgv, newRedirects, err := parseNewArgv(args)
if err != nil {
return err
}
if err := restartIntl(t, false, "", resetArgs, newArgv); err != nil {
if err := restartIntl(t, false, "", resetArgs, newArgv, newRedirects); err != nil {
return err
}
@ -1028,8 +1035,8 @@ func restartLive(t *Term, ctx callContext, args string) error {
return nil
}
func restartIntl(t *Term, rerecord bool, restartPos string, resetArgs bool, newArgv []string) error {
discarded, err := t.client.RestartFrom(rerecord, restartPos, resetArgs, newArgv, false)
func restartIntl(t *Term, rerecord bool, restartPos string, resetArgs bool, newArgv []string, newRedirects [3]string) error {
discarded, err := t.client.RestartFrom(rerecord, restartPos, resetArgs, newArgv, newRedirects, false)
if err != nil {
return err
}
@ -1039,9 +1046,9 @@ func restartIntl(t *Term, rerecord bool, restartPos string, resetArgs bool, newA
return nil
}
func parseNewArgv(args string) (resetArgs bool, newArgv []string, err error) {
func parseNewArgv(args string) (resetArgs bool, newArgv []string, newRedirects [3]string, err error) {
if args == "" {
return false, nil, nil
return false, nil, [3]string{}, nil
}
v, err := argv.Argv(args,
func(s string) (string, error) {
@ -1049,22 +1056,58 @@ func parseNewArgv(args string) (resetArgs bool, newArgv []string, err error) {
},
nil)
if err != nil {
return false, nil, err
return false, nil, [3]string{}, err
}
if len(v) != 1 {
return false, nil, fmt.Errorf("Illegal commandline '%s'", args)
return false, nil, [3]string{}, fmt.Errorf("illegal commandline '%s'", args)
}
w := v[0]
if len(w) == 0 {
return false, nil, nil
return false, nil, [3]string{}, nil
}
if w[0] == "-noargs" {
if len(w) > 1 {
return false, nil, fmt.Errorf("Too many arguments to restart")
return false, nil, [3]string{}, fmt.Errorf("too many arguments to restart")
}
return true, nil, nil
return true, nil, [3]string{}, nil
}
return true, w, nil
redirs := [3]string{}
for len(w) > 0 {
var found bool
var err error
w, found, err = parseOneRedirect(w, &redirs)
if err != nil {
return false, nil, [3]string{}, err
}
if !found {
break
}
}
return true, w, redirs, nil
}
func parseOneRedirect(w []string, redirs *[3]string) ([]string, bool, error) {
prefixes := []string{"<", ">", "2>"}
names := []string{"stdin", "stdout", "stderr"}
if len(w) >= 2 {
for _, prefix := range prefixes {
if w[len(w)-2] == prefix {
w[len(w)-2] += w[len(w)-1]
w = w[:len(w)-1]
break
}
}
}
for i, prefix := range prefixes {
if strings.HasPrefix(w[len(w)-1], prefix) {
if redirs[i] != "" {
return nil, false, fmt.Errorf("redirect error: %s redirected twice", names[i])
}
redirs[i] = w[len(w)-1][len(prefix):]
return w[:len(w)-1], true, nil
}
}
return w, false, nil
}
func printcontextNoState(t *Term) {

@ -1138,3 +1138,50 @@ func TestPrintCastToInterface(t *testing.T) {
t.Logf("%q", out)
})
}
func TestParseNewArgv(t *testing.T) {
testCases := []struct {
in string
tgtargs string
tgtredir string
tgterr string
}{
{"-noargs", "", " | | ", ""},
{"-noargs arg1", "", "", "too many arguments to restart"},
{"arg1 arg2", "arg1 | arg2", " | | ", ""},
{"arg1 arg2 <input.txt", "arg1 | arg2", "input.txt | | ", ""},
{"arg1 arg2 < input.txt", "arg1 | arg2", "input.txt | | ", ""},
{"<input.txt", "", "input.txt | | ", ""},
{"< input.txt", "", "input.txt | | ", ""},
{"arg1 < input.txt > output.txt 2> error.txt", "arg1", "input.txt | output.txt | error.txt", ""},
{"< input.txt > output.txt 2> error.txt", "", "input.txt | output.txt | error.txt", ""},
{"arg1 <input.txt >output.txt 2>error.txt", "arg1", "input.txt | output.txt | error.txt", ""},
{"<input.txt >output.txt 2>error.txt", "", "input.txt | output.txt | error.txt", ""},
{"<input.txt <input2.txt", "", "", "redirect error: stdin redirected twice"},
}
for _, tc := range testCases {
resetArgs, newArgv, newRedirects, err := parseNewArgv(tc.in)
t.Logf("%q -> %q %q %v\n", tc.in, newArgv, newRedirects, err)
if tc.tgterr != "" {
if err == nil {
t.Errorf("Expected error %q, got no error", tc.tgterr)
} else if errstr := err.Error(); errstr != tc.tgterr {
t.Errorf("Expected error %q, got error %q", tc.tgterr, errstr)
}
} else {
if !resetArgs {
t.Errorf("parse error, resetArgs is false")
continue
}
argvstr := strings.Join(newArgv, " | ")
if argvstr != tc.tgtargs {
t.Errorf("Expected new arguments %q, got %q", tc.tgtargs, argvstr)
}
redirstr := strings.Join(newRedirects[:], " | ")
if redirstr != tc.tgtredir {
t.Errorf("Expected new redirects %q, got %q", tc.tgtredir, redirstr)
}
}
}
}

@ -1097,6 +1097,12 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
return starlark.None, decorateError(thread, err)
}
}
if len(args) > 5 && args[5] != starlark.None {
err := unmarshalStarlarkValue(args[5], &rpcArgs.NewRedirects, "NewRedirects")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
@ -1110,6 +1116,8 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Rerecord, "Rerecord")
case "Rebuild":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Rebuild, "Rebuild")
case "NewRedirects":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.NewRedirects, "NewRedirects")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}

@ -21,7 +21,7 @@ type Client interface {
// Restarts program. Set true if you want to rebuild the process we are debugging.
Restart(rebuild bool) ([]api.DiscardedBreakpoint, error)
// Restarts program from the specified position.
RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, rebuild bool) ([]api.DiscardedBreakpoint, error)
RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error)
// GetState returns the current debugger state.
GetState() (*api.DebuggerState, error)

@ -117,6 +117,9 @@ type Config struct {
// ExecuteKind contains the kind of the executed program.
ExecuteKind ExecuteKind
// Redirects specifies redirect rules for stdin, stdout and stderr
Redirects [3]string
}
// New creates a new Debugger. ProcessArgs specify the commandline arguments for the
@ -221,16 +224,16 @@ 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, d.config.TTY)
return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects)
case "lldb":
return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY))
return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects))
case "rr":
if d.target != nil {
// restart should not call us if the backend is 'rr'
panic("internal error: call to Launch with rr backend and target already exists")
}
run, stop, err := gdbserial.RecordAsync(processArgs, wd, false)
run, stop, err := gdbserial.RecordAsync(processArgs, wd, false, d.config.Redirects)
if err != nil {
return nil, err
}
@ -265,9 +268,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, d.config.TTY))
return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects))
}
return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY)
return native.Launch(processArgs, wd, d.config.Foreground, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects)
default:
return nil, fmt.Errorf("unknown backend %q", d.config.Backend)
}
@ -409,7 +412,7 @@ func (d *Debugger) detach(kill bool) error {
// If the target process is a recording it will restart it from the given
// position. If pos starts with 'c' it's a checkpoint ID, otherwise it's an
// event number. If resetArgs is true, newArgs will replace the process args.
func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
@ -437,6 +440,7 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
}
if resetArgs {
d.processArgs = append([]string{d.processArgs[0]}, newArgs...)
d.config.Redirects = newRedirects
}
var p *proc.Target
var err error
@ -460,7 +464,7 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
}
if recorded {
run, stop, err2 := gdbserial.RecordAsync(d.processArgs, d.config.WorkingDir, false)
run, stop, err2 := gdbserial.RecordAsync(d.processArgs, d.config.WorkingDir, false, d.config.Redirects)
if err2 != nil {
return nil, err2
}

@ -46,7 +46,7 @@ func (s *RPCServer) Restart(arg1 interface{}, arg2 *int) error {
if s.config.Debugger.AttachPid != 0 {
return errors.New("cannot restart process Delve did not create")
}
_, err := s.debugger.Restart(false, "", false, nil, false)
_, err := s.debugger.Restart(false, "", false, nil, [3]string{}, false)
return err
}

@ -62,13 +62,13 @@ func (c *RPCClient) Detach(kill bool) error {
func (c *RPCClient) Restart(rebuild bool) ([]api.DiscardedBreakpoint, error) {
out := new(RestartOut)
err := c.call("Restart", RestartIn{"", false, nil, false, rebuild}, out)
err := c.call("Restart", RestartIn{"", false, nil, false, rebuild, [3]string{}}, out)
return out.DiscardedBreakpoints, err
}
func (c *RPCClient) RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
func (c *RPCClient) RestartFrom(rerecord bool, pos string, resetArgs bool, newArgs []string, newRedirects [3]string, rebuild bool) ([]api.DiscardedBreakpoint, error) {
out := new(RestartOut)
err := c.call("Restart", RestartIn{pos, resetArgs, newArgs, rerecord, rebuild}, out)
err := c.call("Restart", RestartIn{pos, resetArgs, newArgs, rerecord, rebuild, newRedirects}, out)
return out.DiscardedBreakpoints, err
}

@ -68,7 +68,7 @@ type RestartIn struct {
// otherwise it's an event number. Only valid for recorded targets.
Position string
// ResetArgs tell whether NewArgs should take effect.
// ResetArgs tell whether NewArgs and NewRedirects should take effect.
ResetArgs bool
// NewArgs are arguments to launch a new process. They replace only the
// argv[1] and later. Argv[0] cannot be changed.
@ -79,6 +79,8 @@ type RestartIn struct {
// When Rebuild is set the process will be build again
Rebuild bool
NewRedirects [3]string
}
type RestartOut struct {
@ -93,7 +95,7 @@ func (s *RPCServer) Restart(arg RestartIn, cb service.RPCCallback) {
}
var out RestartOut
var err error
out.DiscardedBreakpoints, err = s.debugger.Restart(arg.Rerecord, arg.Position, arg.ResetArgs, arg.NewArgs, arg.Rebuild)
out.DiscardedBreakpoints, err = s.debugger.Restart(arg.Rerecord, arg.Position, arg.ResetArgs, arg.NewArgs, arg.NewRedirects, arg.Rebuild)
cb.Return(out, err)
}

@ -54,12 +54,12 @@ func TestMain(m *testing.M) {
}
func withTestClient2(name string, t *testing.T, fn func(c service.Client)) {
withTestClient2Extended(name, t, 0, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended(name, t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
fn(c)
})
}
func startServer(name string, buildFlags protest.BuildFlags, t *testing.T) (clientConn net.Conn, fixture protest.Fixture) {
func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redirects [3]string) (clientConn net.Conn, fixture protest.Fixture) {
if testBackend == "rr" {
protest.MustHaveRecordingAllowed(t)
}
@ -69,6 +69,11 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T) (clie
buildFlags |= protest.BuildModePIE
}
fixture = protest.BuildFixture(name, buildFlags)
for i := range redirects {
if redirects[i] != "" {
redirects[i] = filepath.Join(fixture.BuildDir, redirects[i])
}
}
server := rpccommon.NewServer(&service.Config{
Listener: listener,
ProcessArgs: []string{fixture.Path},
@ -78,6 +83,7 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T) (clie
Packages: []string{fixture.Source},
BuildFlags: "", // build flags can be an empty string here because the only test that uses it, does not set special flags.
ExecuteKind: debugger.ExecutingGeneratedFile,
Redirects: redirects,
},
})
if err := server.Run(); err != nil {
@ -86,8 +92,8 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T) (clie
return clientConn, fixture
}
func withTestClient2Extended(name string, t *testing.T, buildFlags protest.BuildFlags, fn func(c service.Client, fixture protest.Fixture)) {
clientConn, fixture := startServer(name, buildFlags, t)
func withTestClient2Extended(name string, t *testing.T, buildFlags protest.BuildFlags, redirects [3]string, fn func(c service.Client, fixture protest.Fixture)) {
clientConn, fixture := startServer(name, buildFlags, t, redirects)
client := rpc2.NewClientFromConn(clientConn)
defer func() {
client.Detach(true)
@ -223,7 +229,7 @@ func TestRestart_rebuild(t *testing.T) {
// In the original fixture file the env var tested for is SOMEVAR.
os.Setenv("SOMEVAR", "bah")
withTestClient2Extended("testenv", t, 0, func(c service.Client, f protest.Fixture) {
withTestClient2Extended("testenv", t, 0, [3]string{}, func(c service.Client, f protest.Fixture) {
<-c.Continue()
var1, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "x", normalLoadConfig)
@ -772,7 +778,7 @@ func TestClientServer_FindLocations(t *testing.T) {
findLocationHelper(t, c, "main.stacktraceme", false, 1, stacktracemeAddr)
})
withTestClient2Extended("locationsUpperCase", t, 0, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("locationsUpperCase", t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
// Upper case
findLocationHelper(t, c, "locationsUpperCase.go:6", false, 1, 0)
@ -1895,7 +1901,7 @@ func (c *brokenRPCClient) call(method string, args, reply interface{}) error {
}
func TestUnknownMethodCall(t *testing.T) {
clientConn, _ := startServer("continuetestprog", 0, t)
clientConn, _ := startServer("continuetestprog", 0, t, [3]string{})
client := &brokenRPCClient{jsonrpc.NewClient(clientConn)}
client.call("SetApiVersion", api.SetAPIVersionIn{APIVersion: 2}, &api.SetAPIVersionOut{})
defer client.Detach(true)
@ -1950,7 +1956,7 @@ func TestRerecord(t *testing.T) {
t0 := gett()
_, err = c.RestartFrom(false, "", false, nil, false)
_, err = c.RestartFrom(false, "", false, nil, [3]string{}, false)
assertNoError(err, t, "First restart")
t1 := gett()
@ -1960,7 +1966,7 @@ func TestRerecord(t *testing.T) {
time.Sleep(2 * time.Second) // make sure that we're not running inside the same second
_, err = c.RestartFrom(true, "", false, nil, false)
_, err = c.RestartFrom(true, "", false, nil, [3]string{}, false)
assertNoError(err, t, "Second restart")
t2 := gett()
@ -2025,7 +2031,7 @@ func TestStopRecording(t *testing.T) {
// try rerecording
go func() {
c.RestartFrom(true, "", false, nil, false)
c.RestartFrom(true, "", false, nil, [3]string{}, false)
}()
time.Sleep(time.Second) // hopefully the re-recording started...
@ -2039,7 +2045,7 @@ func TestClearLogicalBreakpoint(t *testing.T) {
// Clearing a logical breakpoint should clear all associated physical
// breakpoints.
// Issue #1955.
withTestClient2Extended("testinline", t, protest.EnableInlining, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("testinline", t, protest.EnableInlining, [3]string{}, func(c service.Client, fixture protest.Fixture) {
bp, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.inlineThis"})
assertNoError(err, t, "CreateBreakpoint()")
t.Logf("breakpoint set at %#v", bp.Addrs)
@ -2058,3 +2064,37 @@ func TestClearLogicalBreakpoint(t *testing.T) {
}
})
}
func TestRedirects(t *testing.T) {
const (
infile = "redirect-input.txt"
outfile = "redirect-output.txt"
)
protest.AllowRecording(t)
withTestClient2Extended("redirect", t, 0, [3]string{infile, outfile, ""}, func(c service.Client, fixture protest.Fixture) {
outpath := filepath.Join(fixture.BuildDir, outfile)
<-c.Continue()
buf, err := ioutil.ReadFile(outpath)
assertNoError(err, t, "Reading output file")
t.Logf("output %q", buf)
if !strings.HasPrefix(string(buf), "Redirect test") {
t.Fatalf("Wrong output %q", string(buf))
}
os.Remove(outpath)
if testBackend != "rr" {
_, err = c.Restart(false)
assertNoError(err, t, "Restart")
<-c.Continue()
buf2, err := ioutil.ReadFile(outpath)
t.Logf("output %q", buf2)
assertNoError(err, t, "Reading output file (second time)")
if !strings.HasPrefix(string(buf2), "Redirect test") {
t.Fatalf("Wrong output %q", string(buf2))
}
if string(buf2) == string(buf) {
t.Fatalf("Expected output change got %q and %q", string(buf), string(buf2))
}
os.Remove(outpath)
}
})
}

@ -131,13 +131,13 @@ 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{}, "", [3]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{}, "", [3]string{})
case "rr":
protest.MustHaveRecordingAllowed(t)
t.Log("recording")
p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{})
p, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{}, [3]string{})
t.Logf("replaying %q", tracedir)
default:
t.Fatalf("unknown backend %q", testBackend)