Repository: go-delve/delve
Pull request #3253 (2023-01-19 08:13:48)

Supports sending output to clients when running programs remotely
#3094 
I saw that #3108 has not been updated for a long time, so I committed this PR.


tttoad: I am currently reading the code related to debug mode, and will support debug mode output write-back in this PR.
tttoad: @aarzilli @derekparker  PTAL.
I've tested it in the following environments.
(client: macOS arm64. server: ubuntu arm64 in docker)
(client: macOS arm64. server: macOS arm64)
(client: macOS arm64. server: windows amd64)
It can work.
I should be adding test cases next week when you think it's okay.

tttoad: @aarzilli PTAL. 
tttoad: @aarzilli  PTAL.
tttoad: @aarzilli PTAL. 
The "TeamCity build failed" error doesn't seem to be caused by my changes?
![image](https://user-images.githubusercontent.com/14021602/226182573-fb080f7c-f80a-496d-9b88-912b9bc632cf.png)

https://github.com/go-delve/delve/compare/31dba49..7d441c2

Here it just changes the function name, but it goes from pass to fail.
aarzilli: The windows/arm64 build is flaky, it doesn't have anything to do with you. The rest passed. If you click on "details" you'll see what failed.

tttoad: @aarzilli PTAL.
tttoad: @derekparker PTAL.
mcdoker18: @tttoad Thanks for the implementing this feature!

@aarzilli @derekparker Can you please take a look again?

Patch:
diff --git a/_fixtures/out_redirect.go b/_fixtures/out_redirect.go
new file mode 100644
index 00000000..faa48e92
--- /dev/null
+++ b/_fixtures/out_redirect.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+	"fmt"
+	"os"
+)
+
+func main() {
+	fmt.Println("hello world!")
+	fmt.Fprintf(os.Stdout, "hello world!")
+	fmt.Fprintf(os.Stderr, "hello world!\n")
+	fmt.Fprintf(os.Stderr, "hello world! error!")
+}
diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go
index 47917551..b8d2d7eb 100644
--- a/cmd/dlv/cmds/commands.go
+++ b/cmd/dlv/cmds/commands.go
@@ -20,6 +20,7 @@ import (
 	"github.com/go-delve/delve/pkg/gobuild"
 	"github.com/go-delve/delve/pkg/goversion"
 	"github.com/go-delve/delve/pkg/logflags"
+	"github.com/go-delve/delve/pkg/proc"
 	"github.com/go-delve/delve/pkg/terminal"
 	"github.com/go-delve/delve/pkg/version"
 	"github.com/go-delve/delve/service"
@@ -1015,7 +1016,9 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
 				DebugInfoDirectories: conf.DebugInfoDirectories,
 				CheckGoVersion:       checkGoVersion,
 				TTY:                  tty,
-				Redirects:            redirects,
+				Stdin:                redirects[0],
+				Stdout:               proc.OutputRedirect{Path: redirects[1]},
+				Stderr:               proc.OutputRedirect{Path: redirects[2]},
 				DisableASLR:          disableASLR,
 				RrOnProcessPid:       rrOnProcessPid,
 			},
diff --git a/pkg/proc/gdbserial/rr.go b/pkg/proc/gdbserial/rr.go
index 36419124..d04f42a1 100644
--- a/pkg/proc/gdbserial/rr.go
+++ b/pkg/proc/gdbserial/rr.go
@@ -20,7 +20,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, redirects [3]string) (run func() (string, error), stop func() error, err error) {
+func RecordAsync(cmd []string, wd string, quiet bool, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (run func() (string, error), stop func() error, err error) {
 	if err := checkRRAvailable(); err != nil {
 		return nil, nil, err
 	}
@@ -35,7 +35,7 @@ func RecordAsync(cmd []string, wd string, quiet bool, redirects [3]string) (run
 	args = append(args, cmd...)
 	rrcmd := exec.Command("rr", args...)
 	var closefn func()
-	rrcmd.Stdin, rrcmd.Stdout, rrcmd.Stderr, closefn, err = openRedirects(redirects, quiet)
+	rrcmd.Stdin, rrcmd.Stdout, rrcmd.Stderr, closefn, err = openRedirects(stdin, stdout, stderr, quiet)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -63,11 +63,11 @@ func RecordAsync(cmd []string, wd string, quiet bool, redirects [3]string) (run
 	return run, stop, nil
 }
 
-func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
+func openRedirects(stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
 	toclose := []*os.File{}
 
-	if redirects[0] != "" {
-		stdin, err = os.Open(redirects[0])
+	if stdinPath != "" {
+		stdin, err = os.Open(stdinPath)
 		if err != nil {
 			return nil, nil, nil, nil, err
 		}
@@ -76,27 +76,33 @@ func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.F
 		stdin = os.Stdin
 	}
 
-	create := func(path string, dflt *os.File) *os.File {
-		if path == "" {
-			if quiet {
-				return nil
+	create := func(redirect proc.OutputRedirect, dflt *os.File) (f *os.File) {
+		if redirect.Path != "" {
+			f, err = os.Create(redirect.Path)
+			if f != nil {
+				toclose = append(toclose, f)
 			}
-			return dflt
+
+			return f
+		} else if redirect.File != nil {
+			toclose = append(toclose, redirect.File)
+
+			return redirect.File
 		}
-		var f *os.File
-		f, err = os.Create(path)
-		if f != nil {
-			toclose = append(toclose, f)
+
+		if quiet {
+			return nil
 		}
-		return f
+
+		return dflt
 	}
 
-	stdout = create(redirects[1], os.Stdout)
+	stdout = create(stdoutOR, os.Stdout)
 	if err != nil {
 		return nil, nil, nil, nil, err
 	}
 
-	stderr = create(redirects[2], os.Stderr)
+	stderr = create(stderrOR, os.Stderr)
 	if err != nil {
 		return nil, nil, nil, nil, err
 	}
@@ -112,8 +118,8 @@ func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.F
 
 // 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, redirects [3]string) (tracedir string, err error) {
-	run, _, err := RecordAsync(cmd, wd, quiet, redirects)
+func Record(cmd []string, wd string, quiet bool, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (tracedir string, err error) {
+	run, _, err := RecordAsync(cmd, wd, quiet, stdin, stdout, stderr)
 	if err != nil {
 		return "", err
 	}
@@ -288,8 +294,8 @@ func rrParseGdbCommand(line string) rrInit {
 }
 
 // RecordAndReplay acts like calling Record and then Replay.
-func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, redirects [3]string) (*proc.TargetGroup, string, error) {
-	tracedir, err := Record(cmd, wd, quiet, redirects)
+func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string, stdin string, stdout proc.OutputRedirect, stderr proc.OutputRedirect) (*proc.TargetGroup, string, error) {
+	tracedir, err := Record(cmd, wd, quiet, stdin, stdout, stderr)
 	if tracedir == "" {
 		return nil, "", err
 	}
diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go
index b60ee32d..616fe869 100644
--- a/pkg/proc/native/nonative_darwin.go
+++ b/pkg/proc/native/nonative_darwin.go
@@ -16,7 +16,7 @@ import (
 var ErrNativeBackendDisabled = errors.New("native backend disabled during compilation")
 
 // Launch returns ErrNativeBackendDisabled.
-func Launch(_ []string, _ string, _ proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.TargetGroup, error) {
+func Launch(_ []string, _ string, _ proc.LaunchFlags, _ []string, _ string, _ string, _ proc.OutputRedirect, _ proc.OutputRedirect) (*proc.TargetGroup, error) {
 	return nil, ErrNativeBackendDisabled
 }
 
diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go
index c886202b..da5d8865 100644
--- a/pkg/proc/native/proc.go
+++ b/pkg/proc/native/proc.go
@@ -376,11 +376,11 @@ func (dbp *nativeProcess) writeSoftwareBreakpoint(thread *nativeThread, addr uin
 	return err
 }
 
-func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
+func openRedirects(stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect, foreground bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
 	toclose := []*os.File{}
 
-	if redirects[0] != "" {
-		stdin, err = os.Open(redirects[0])
+	if stdinPath != "" {
+		stdin, err = os.Open(stdinPath)
 		if err != nil {
 			return nil, nil, nil, nil, err
 		}
@@ -389,24 +389,29 @@ func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr
 		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)
+	create := func(redirect proc.OutputRedirect, dflt *os.File) (f *os.File) {
+		if redirect.Path != "" {
+			f, err = os.Create(redirect.Path)
+			if f != nil {
+				toclose = append(toclose, f)
+			}
+
+			return f
+		} else if redirect.File != nil {
+			toclose = append(toclose, redirect.File)
+
+			return redirect.File
 		}
-		return f
+
+		return dflt
 	}
 
-	stdout = create(redirects[1], os.Stdout)
+	stdout = create(stdoutOR, os.Stdout)
 	if err != nil {
 		return nil, nil, nil, nil, err
 	}
 
-	stderr = create(redirects[2], os.Stderr)
+	stderr = create(stderrOR, os.Stderr)
 	if err != nil {
 		return nil, nil, nil, nil, err
 	}
diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go
index 83eb2dbc..7a2bbb9b 100644
--- a/pkg/proc/native/proc_darwin.go
+++ b/pkg/proc/native/proc_darwin.go
@@ -42,7 +42,7 @@ func (os *osProcessDetails) Close() {}
 // 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, flags proc.LaunchFlags, _ []string, _ string, _ [3]string) (*proc.TargetGroup, error) {
+func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, _ string, _ proc.OutputRedirect, _ proc.OutputRedirect) (*proc.TargetGroup, 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 51f48896..23f64ed6 100644
--- a/pkg/proc/native/proc_freebsd.go
+++ b/pkg/proc/native/proc_freebsd.go
@@ -55,7 +55,7 @@ func (os *osProcessDetails) Close() {}
 // 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, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.TargetGroup, error) {
+func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect) (*proc.TargetGroup, error) {
 	var (
 		process *exec.Cmd
 		err     error
@@ -63,7 +63,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
 
 	foreground := flags&proc.LaunchForeground != 0
 
-	stdin, stdout, stderr, closefn, err := openRedirects(redirects, foreground)
+	stdin, stdout, stderr, closefn, err := openRedirects(stdinPath, stdoutOR, stderrOR, foreground)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go
index a05ecb24..b61e663d 100644
--- a/pkg/proc/native/proc_linux.go
+++ b/pkg/proc/native/proc_linux.go
@@ -62,7 +62,7 @@ func (os *osProcessDetails) Close() {
 // 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, flags proc.LaunchFlags, debugInfoDirs []string, tty string, redirects [3]string) (*proc.TargetGroup, error) {
+func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []string, tty string, stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect) (*proc.TargetGroup, error) {
 	var (
 		process *exec.Cmd
 		err     error
@@ -70,7 +70,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
 
 	foreground := flags&proc.LaunchForeground != 0
 
-	stdin, stdout, stderr, closefn, err := openRedirects(redirects, foreground)
+	stdin, stdout, stderr, closefn, err := openRedirects(stdinPath, stdoutOR, stderrOR, foreground)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go
index 318533e8..9cbf3264 100644
--- a/pkg/proc/native/proc_windows.go
+++ b/pkg/proc/native/proc_windows.go
@@ -25,12 +25,12 @@ type osProcessDetails struct {
 func (os *osProcessDetails) Close() {}
 
 // Launch creates and begins debugging a new process.
-func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, redirects [3]string) (*proc.TargetGroup, error) {
+func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ string, stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect) (*proc.TargetGroup, error) {
 	argv0Go := cmd[0]
 
 	env := proc.DisableAsyncPreemptEnv()
 
-	stdin, stdout, stderr, closefn, err := openRedirects(redirects, true)
+	stdin, stdout, stderr, closefn, err := openRedirects(stdinPath, stdoutOR, stderrOR, true)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/proc/redirect.go b/pkg/proc/redirect.go
new file mode 100644
index 00000000..278d8853
--- /dev/null
+++ b/pkg/proc/redirect.go
@@ -0,0 +1,12 @@
+package proc
+
+import "os"
+
+// OutputRedirect Specifies where the target program output will be redirected to.
+// Only one of "Path" and "File" should be set.
+type OutputRedirect struct {
+	// Path File path.
+	Path string
+	// File Redirect file.
+	File *os.File
+}
diff --git a/pkg/proc/redirector_other.go b/pkg/proc/redirector_other.go
new file mode 100644
index 00000000..2203eb71
--- /dev/null
+++ b/pkg/proc/redirector_other.go
@@ -0,0 +1,59 @@
+//go:build !windows
+// +build !windows
+
+package proc
+
+import (
+	"crypto/rand"
+	"encoding/hex"
+	"io"
+	"os"
+	"path/filepath"
+	"syscall"
+)
+
+type openOnRead struct {
+	path string
+	rd   io.ReadCloser
+}
+
+func (oor *openOnRead) Read(p []byte) (n int, err error) {
+	if oor.rd != nil {
+		return oor.rd.Read(p)
+	}
+
+	fh, err := os.OpenFile(oor.path, os.O_RDONLY, os.ModeNamedPipe)
+	if err != nil {
+		return 0, err
+	}
+
+	oor.rd = fh
+	return oor.rd.Read(p)
+}
+
+func (oor *openOnRead) Close() error {
+	defer os.Remove(oor.path)
+
+	fh, _ := os.OpenFile(oor.path, os.O_WRONLY|syscall.O_NONBLOCK, 0)
+	if fh != nil {
+		fh.Close()
+	}
+
+	return oor.rd.Close()
+}
+
+func Redirector() (reader io.ReadCloser, output OutputRedirect, err error) {
+	r := make([]byte, 4)
+	if _, err = rand.Read(r); err != nil {
+		return reader, output, err
+	}
+
+	var path = filepath.Join(os.TempDir(), hex.EncodeToString(r))
+
+	if err = syscall.Mkfifo(path, 0o600); err != nil {
+		_ = os.Remove(path)
+		return reader, output, err
+	}
+
+	return &openOnRead{path: path}, OutputRedirect{Path: path}, nil
+}
diff --git a/pkg/proc/redirector_windows.go b/pkg/proc/redirector_windows.go
new file mode 100644
index 00000000..edd74a7a
--- /dev/null
+++ b/pkg/proc/redirector_windows.go
@@ -0,0 +1,15 @@
+//go:build windows
+// +build windows
+
+package proc
+
+import (
+	"io"
+	"os"
+)
+
+func Redirector() (reader io.ReadCloser, output OutputRedirect, err error) {
+	reader, output.File, err = os.Pipe()
+
+	return reader, output, err
+}
diff --git a/service/dap/server.go b/service/dap/server.go
index 8e8e1936..75a06e98 100644
--- a/service/dap/server.go
+++ b/service/dap/server.go
@@ -157,6 +157,15 @@ type Session struct {
 	// changeStateMu must be held for a request to protect itself from another goroutine
 	// changing the state of the running process at the same time.
 	changeStateMu sync.Mutex
+
+	// stdoutReader the programs's stdout.
+	stdoutReader io.ReadCloser
+
+	// stderrReader the program's stderr.
+	stderrReader io.ReadCloser
+
+	// preTerminatedWG the WaitGroup that needs to wait before sending a terminated event.
+	preTerminatedWG sync.WaitGroup
 }
 
 // Config is all the information needed to start the debugger, handle
@@ -969,7 +978,7 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 
 		var cmd string
 		var out []byte
-		var err error
+
 		switch args.Mode {
 		case "debug":
 			cmd, out, err = gobuild.GoBuildCombinedOutput(args.Output, []string{args.Program}, args.BuildFlags)
@@ -1020,9 +1029,51 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 	argsToLog.Cwd, _ = filepath.Abs(args.Cwd)
 	s.config.log.Debugf("launching binary '%s' with config: %s", debugbinary, prettyPrint(argsToLog))
 
+	var redirected = false
+	switch args.OutputMode {
+	case "remote":
+		redirected = true
+	case "local", "":
+		// noting
+	default:
+		s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch",
+			fmt.Sprintf("invalid debug configuration - unsupported 'outputMode' attribute %q", args.OutputMode))
+		return
+	}
+
+	redirectedFunc := func(stdoutReader io.ReadCloser, stderrReader io.ReadCloser) {
+		runReadFunc := func(reader io.ReadCloser, category string) {
+			defer s.preTerminatedWG.Done()
+			defer reader.Close()
+			// Read output from `reader` and send to client
+			var out [1024]byte
+			for {
+				n, err := reader.Read(out[:])
+				if err != nil {
+					if errors.Is(io.EOF, err) {
+						return
+					}
+					s.config.log.Errorf("failed read by %s - %v ", category, err)
+					return
+				}
+				outs := string(out[:n])
+				s.send(&dap.OutputEvent{
+					Event: *newEvent("output"),
+					Body: dap.OutputEventBody{
+						Output:   outs,
+						Category: category,
+					}})
+			}
+		}
+
+		s.preTerminatedWG.Add(2)
+		go runReadFunc(stdoutReader, "stdout")
+		go runReadFunc(stderrReader, "stderr")
+	}
+
 	if args.NoDebug {
 		s.mu.Lock()
-		cmd, err := s.newNoDebugProcess(debugbinary, args.Args, s.config.Debugger.WorkingDir)
+		cmd, err := s.newNoDebugProcess(debugbinary, args.Args, s.config.Debugger.WorkingDir, redirected)
 		s.mu.Unlock()
 		if err != nil {
 			s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
@@ -1034,9 +1085,14 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 
 		// Start the program on a different goroutine, so we can listen for disconnect request.
 		go func() {
+			if redirected {
+				redirectedFunc(s.stdoutReader, s.stderrReader)
+			}
+
 			if err := cmd.Wait(); err != nil {
 				s.config.log.Debugf("program exited with error: %v", err)
 			}
+
 			close(s.noDebugProcess.exited)
 			s.logToConsole(proc.ErrProcessExited{Pid: cmd.ProcessState.Pid(), Status: cmd.ProcessState.ExitCode()}.Error())
 			s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
@@ -1044,6 +1100,35 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 		return
 	}
 
+	var clear func()
+	if redirected {
+		var (
+			readers         [2]io.ReadCloser
+			outputRedirects [2]proc.OutputRedirect
+		)
+
+		for i := 0; i < 2; i++ {
+			readers[i], outputRedirects[i], err = proc.Redirector()
+			if err != nil {
+				s.sendShowUserErrorResponse(request.Request, InternalError, "Internal Error",
+					fmt.Sprintf("failed to generate stdio pipes - %v", err))
+				return
+			}
+		}
+
+		s.config.Debugger.Stdout = outputRedirects[0]
+		s.config.Debugger.Stderr = outputRedirects[1]
+
+		redirectedFunc(readers[0], readers[1])
+		clear = func() {
+			for index := range readers {
+				if closeErr := readers[index].Close(); closeErr != nil {
+					s.config.log.Warnf("failed to clear redirects - %v", closeErr)
+				}
+			}
+		}
+	}
+
 	func() {
 		s.mu.Lock()
 		defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error
@@ -1054,6 +1139,9 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) {
 			gobuild.Remove(s.binaryToRemove)
 		}
 		s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error())
+		if redirected {
+			clear()
+		}
 		return
 	}
 	// Enable StepBack controls on supported backends
@@ -1080,15 +1168,30 @@ func (s *Session) getPackageDir(pkg string) string {
 
 // newNoDebugProcess is called from onLaunchRequest (run goroutine) and
 // requires holding mu lock. It prepares process exec.Cmd to be started.
-func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd string) (*exec.Cmd, error) {
+func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd string, redirected bool) (cmd *exec.Cmd, err error) {
 	if s.noDebugProcess != nil {
 		return nil, fmt.Errorf("another launch request is in progress")
 	}
-	cmd := exec.Command(program, targetArgs...)
-	cmd.Stdout, cmd.Stderr, cmd.Stdin, cmd.Dir = os.Stdout, os.Stderr, os.Stdin, wd
-	if err := cmd.Start(); err != nil {
+
+	cmd = exec.Command(program, targetArgs...)
+	cmd.Stdin, cmd.Dir = os.Stdin, wd
+
+	if redirected {
+		if s.stderrReader, err = cmd.StderrPipe(); err != nil {
+			return nil, err
+		}
+
+		if s.stdoutReader, err = cmd.StdoutPipe(); err != nil {
+			return nil, err
+		}
+	} else {
+		cmd.Stdout, cmd.Stderr = os.Stdin, os.Stderr
+	}
+
+	if err = cmd.Start(); err != nil {
 		return nil, err
 	}
+
 	s.noDebugProcess = &process{Cmd: cmd, exited: make(chan struct{})}
 	return cmd, nil
 }
@@ -1135,9 +1238,11 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 		status := "halted"
 		if s.isRunningCmd() {
 			status = "running"
-		} else if s, err := s.debugger.State(false); processExited(s, err) {
+		} else if state, err := s.debugger.State(false); processExited(state, err) {
 			status = "exited"
+			s.preTerminatedWG.Wait()
 		}
+
 		s.logToConsole(fmt.Sprintf("Closing client session, but leaving multi-client DAP server at %s with debuggee %s", s.config.Listener.Addr().String(), status))
 		s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
@@ -1171,6 +1276,7 @@ func (s *Session) onDisconnectRequest(request *dap.DisconnectRequest) {
 	} else {
 		s.send(&dap.DisconnectResponse{Response: *newResponse(request.Request)})
 	}
+	s.preTerminatedWG.Wait()
 	// The debugging session has ended, so we send a terminated event.
 	s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 }
@@ -2724,6 +2830,7 @@ func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*p
 		GoroutineID:          int64(goid),
 	}, nil)
 	if processExited(state, err) {
+		s.preTerminatedWG.Wait()
 		e := &dap.TerminatedEvent{Event: *newEvent("terminated")}
 		s.send(e)
 		return nil, nil, errors.New("terminated")
@@ -3470,6 +3577,7 @@ func (s *Session) runUntilStopAndNotify(command string, allowNextStateChange cha
 	}
 
 	if processExited(state, err) {
+		s.preTerminatedWG.Wait()
 		s.send(&dap.TerminatedEvent{Event: *newEvent("terminated")})
 		return
 	}
diff --git a/service/dap/types.go b/service/dap/types.go
index 5876ae6c..aa5bbab6 100644
--- a/service/dap/types.go
+++ b/service/dap/types.go
@@ -148,6 +148,9 @@ type LaunchConfig struct {
 	// reference to other environment variables is not supported.
 	Env map[string]*string `json:"env,omitempty"`
 
+	// The output mode specifies how to handle the program's output.
+	OutputMode string `json:"outputMode,omitempty"`
+
 	LaunchAttachCommonConfig
 }
 
diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go
index 8c22db2c..b666bb74 100644
--- a/service/debugger/debugger.go
+++ b/service/debugger/debugger.go
@@ -136,8 +136,14 @@ 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
+	// Stdin Redirect file path for stdin
+	Stdin string
+
+	// Redirects specifies redirect rules for stdout
+	Stdout proc.OutputRedirect
+
+	// Redirects specifies redirect rules for stderr
+	Stderr proc.OutputRedirect
 
 	// DisableASLR disables ASLR
 	DisableASLR bool
@@ -259,16 +265,16 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.TargetGroup, e
 
 	switch d.config.Backend {
 	case "native":
-		return native.Launch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects)
+		return native.Launch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Stdin, d.config.Stdout, d.config.Stderr)
 	case "lldb":
-		return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects))
+		return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, [3]string{d.config.Stdin, d.config.Stdout.Path, d.config.Stderr.Path}))
 	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, d.config.Redirects)
+		run, stop, err := gdbserial.RecordAsync(processArgs, wd, false, d.config.Stdin, d.config.Stdout, d.config.Stderr)
 		if err != nil {
 			return nil, err
 		}
@@ -303,9 +309,9 @@ func (d *Debugger) Launch(processArgs []string, wd string) (*proc.TargetGroup, e
 
 	case "default":
 		if runtime.GOOS == "darwin" {
-			return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects))
+			return betterGdbserialLaunchError(gdbserial.LLDBLaunch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, [3]string{d.config.Stdin, d.config.Stdout.Path, d.config.Stderr.Path}))
 		}
-		return native.Launch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Redirects)
+		return native.Launch(processArgs, wd, launchFlags, d.config.DebugInfoDirectories, d.config.TTY, d.config.Stdin, d.config.Stdout, d.config.Stderr)
 	default:
 		return nil, fmt.Errorf("unknown backend %q", d.config.Backend)
 	}
@@ -472,12 +478,19 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
 		return nil, ErrCanNotRestart
 	}
 
+	if !resetArgs && (d.config.Stdout.File != nil || d.config.Stderr.File != nil) {
+		return nil, ErrCanNotRestart
+
+	}
+
 	if err := d.detach(true); err != nil {
 		return nil, err
 	}
 	if resetArgs {
 		d.processArgs = append([]string{d.processArgs[0]}, newArgs...)
-		d.config.Redirects = newRedirects
+		d.config.Stdin = newRedirects[0]
+		d.config.Stdout = proc.OutputRedirect{Path: newRedirects[1]}
+		d.config.Stderr = proc.OutputRedirect{Path: newRedirects[2]}
 	}
 	var grp *proc.TargetGroup
 	var err error
@@ -501,7 +514,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, d.config.Redirects)
+		run, stop, err2 := gdbserial.RecordAsync(d.processArgs, d.config.WorkingDir, false, d.config.Stdin, d.config.Stdout, d.config.Stderr)
 		if err2 != nil {
 			return nil, err2
 		}


Test patch:
diff --git a/pkg/proc/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go
index 9c96280f..3eb97119 100644
--- a/pkg/proc/gdbserial/rr_test.go
+++ b/pkg/proc/gdbserial/rr_test.go
@@ -30,7 +30,7 @@ func withTestRecording(name string, t testing.TB, fn func(grp *proc.TargetGroup,
 		t.Skip("test skipped, rr not found")
 	}
 	t.Log("recording")
-	grp, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}, [3]string{})
+	grp, tracedir, err := gdbserial.RecordAndReplay([]string{fixture.Path}, ".", true, []string{}, "", proc.OutputRedirect{}, proc.OutputRedirect{})
 	if err != nil {
 		t.Fatal("Launch():", err)
 	}
diff --git a/pkg/proc/proc_linux_test.go b/pkg/proc/proc_linux_test.go
index d10e17ff..f105ce91 100644
--- a/pkg/proc/proc_linux_test.go
+++ b/pkg/proc/proc_linux_test.go
@@ -6,6 +6,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	"github.com/go-delve/delve/pkg/proc"
 	"github.com/go-delve/delve/pkg/proc/native"
 	protest "github.com/go-delve/delve/pkg/proc/test"
 )
@@ -14,7 +15,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}, ""), "", 0, []string{filepath.Dir(fixture.Path)}, "", [3]string{})
+	p, err := native.Launch(append([]string{fixture.Path}, ""), "", 0, []string{filepath.Dir(fixture.Path)}, "", "", proc.OutputRedirect{}, proc.OutputRedirect{})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go
index 5df428a5..ec12303f 100644
--- a/pkg/proc/proc_test.go
+++ b/pkg/proc/proc_test.go
@@ -103,13 +103,13 @@ func withTestProcessArgs(name string, t testing.TB, wd string, args []string, bu
 
 	switch testBackend {
 	case "native":
-		grp, err = native.Launch(append([]string{fixture.Path}, args...), wd, 0, []string{}, "", [3]string{})
+		grp, err = native.Launch(append([]string{fixture.Path}, args...), wd, 0, []string{}, "", "", proc.OutputRedirect{}, proc.OutputRedirect{})
 	case "lldb":
 		grp, err = gdbserial.LLDBLaunch(append([]string{fixture.Path}, args...), wd, 0, []string{}, "", [3]string{})
 	case "rr":
 		protest.MustHaveRecordingAllowed(t)
 		t.Log("recording")
-		grp, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{}, [3]string{})
+		grp, tracedir, err = gdbserial.RecordAndReplay(append([]string{fixture.Path}, args...), wd, true, []string{}, "", proc.OutputRedirect{}, proc.OutputRedirect{})
 		t.Logf("replaying %q", tracedir)
 	default:
 		t.Fatal("unknown backend")
@@ -2247,7 +2247,7 @@ func TestUnsupportedArch(t *testing.T) {
 
 	switch testBackend {
 	case "native":
-		p, err = native.Launch([]string{outfile}, ".", 0, []string{}, "", [3]string{})
+		p, err = native.Launch([]string{outfile}, ".", 0, []string{}, "", "", proc.OutputRedirect{}, proc.OutputRedirect{})
 	case "lldb":
 		p, err = gdbserial.LLDBLaunch([]string{outfile}, ".", 0, []string{}, "", [3]string{})
 	default:
diff --git a/service/dap/server_test.go b/service/dap/server_test.go
index b2a0d53e..5618100a 100644
--- a/service/dap/server_test.go
+++ b/service/dap/server_test.go
@@ -2,6 +2,7 @@ package dap
 
 import (
 	"bufio"
+	"bytes"
 	"flag"
 	"fmt"
 	"io"
@@ -7366,6 +7367,98 @@ func TestDisassembleCgo(t *testing.T) {
 		protest.AllNonOptimized, true)
 }
 
+
+func TestRedirect(t *testing.T) {
+	runTest(t, "out_redirect", func(client *daptest.Client, fixture protest.Fixture) {
+		// 1 >> initialize, << initialize
+		client.InitializeRequest()
+		initResp := client.ExpectInitializeResponseAndCapabilities(t)
+		if initResp.Seq != 0 || initResp.RequestSeq != 1 {
+			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=1", initResp)
+		}
+
+		// 2 >> launch, << initialized, << launch
+		client.LaunchRequestWithArgs(map[string]interface{}{
+			"request":    "launch",
+			"mode":       "debug",
+			"program":    fixture.Source,
+			"outputMode": "remote",
+		})
+		initEvent := client.ExpectInitializedEvent(t)
+		if initEvent.Seq != 0 {
+			t.Errorf("\ngot %#v\nwant Seq=0", initEvent)
+		}
+		launchResp := client.ExpectLaunchResponse(t)
+		if launchResp.Seq != 0 || launchResp.RequestSeq != 2 {
+			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=2", launchResp)
+		}
+
+		// 5 >> configurationDone, << stopped, << configurationDone
+		client.ConfigurationDoneRequest()
+
+		cdResp := client.ExpectConfigurationDoneResponse(t)
+		if cdResp.Seq != 0 || cdResp.RequestSeq != 3 {
+			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=5", cdResp)
+		}
+
+		// 6 << output, << terminated
+		var (
+			stdout = bytes.NewBufferString("")
+			stderr = bytes.NewBufferString("")
+		)
+
+	terminatedPoint:
+		for {
+			message := client.ExpectMessage(t)
+			switch m := message.(type) {
+			case *dap.OutputEvent:
+				switch m.Body.Category {
+				case "stdout":
+					stdout.WriteString(m.Body.Output)
+				case "stderr":
+					stderr.WriteString(m.Body.Output)
+				default:
+					t.Errorf("\ngot %#v\nwant Category='stdout' or 'stderr'", m)
+				}
+			case *dap.TerminatedEvent:
+				break terminatedPoint
+			default:
+				t.Errorf("\n got %#v, want *dap.OutputEvent or *dap.TerminateResponse", m)
+			}
+		}
+
+		var (
+			expectStdout = "hello world!\nhello world!"
+			expectStderr = "hello world!\nhello world! error!"
+		)
+
+		// check output
+		if expectStdout != stdout.String() {
+			t.Errorf("\n got stdout: len:%d\n%s\nwant: len:%d\n%s", stdout.Len(), stdout.String(), len(expectStdout), string(expectStdout))
+		}
+
+		if expectStderr != stderr.String() {
+			t.Errorf("\n got stderr: len:%d \n%s\nwant: len:%d\n%s", stderr.Len(), stderr.String(), len(expectStderr), string(expectStderr))
+		}
+
+		// 7 >> disconnect, << disconnect
+		client.DisconnectRequest()
+		oep := client.ExpectOutputEventProcessExited(t, 0)
+		if oep.Seq != 0 || oep.Body.Category != "console" {
+			t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oep)
+		}
+		oed := client.ExpectOutputEventDetaching(t)
+		if oed.Seq != 0 || oed.Body.Category != "console" {
+			t.Errorf("\ngot %#v\nwant Seq=0 Category='console'", oed)
+		}
+		dResp := client.ExpectDisconnectResponse(t)
+		if dResp.Seq != 0 || dResp.RequestSeq != 4 {
+			t.Errorf("\ngot %#v\nwant Seq=0, RequestSeq=43", dResp)
+		}
+		client.ExpectTerminatedEvent(t)
+	})
+}
+
 // Helper functions for checking ErrorMessage field values.
 
 func checkErrorMessageId(er *dap.ErrorMessage, id int) bool {
diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go
index a6c3b45e..6b03a852 100644
--- a/service/test/integration2_test.go
+++ b/service/test/integration2_test.go
@@ -85,7 +85,9 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redir
 			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,
+			Stdin:          redirects[0],
+			Stdout:         proc.OutputRedirect{Path: redirects[1]},
+			Stderr:         proc.OutputRedirect{Path: redirects[2]},
 		},
 	})
 	if err := server.Run(); err != nil {