diff --git a/_fixtures/out_redirect.go b/_fixtures/out_redirect.go new file mode 100644 index 0000000000..faa48e926f --- /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 4791755107..b8d2d7eb72 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 364191241f..d04f42a1fc 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/gdbserial/rr_test.go b/pkg/proc/gdbserial/rr_test.go index 9c96280f65..3eb97119c6 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/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go index b60ee32d54..616fe869eb 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 c886202b99..da5d88650e 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 83eb2dbc7d..7a2bbb9b57 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 51f4889658..23f64ed6cc 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 a05ecb24f7..b61e663d51 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 318533e83b..9cbf326430 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/proc_linux_test.go b/pkg/proc/proc_linux_test.go index d10e17ff4c..f105ce9148 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 5df428a560..ec12303f69 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/pkg/proc/redirect.go b/pkg/proc/redirect.go new file mode 100644 index 0000000000..278d8853ff --- /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 0000000000..2203eb71f8 --- /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 0000000000..edd74a7ac3 --- /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 8e8e193659..75a06e98b2 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/server_test.go b/service/dap/server_test.go index b2a0d53ea5..5618100ad3 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/dap/types.go b/service/dap/types.go index 5876ae6c78..aa5bbab65b 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 8c22db2cab..b666bb7439 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 } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index a6c3b45e3f..6b03a852bd 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 {