pkg/proc,service/*: Supports sending output to clients when running programs remotely (#3253)

* wip: Support sending output when remote debug

* wip: Support local output and remote output

* wip: fix stderr and stdout assignment error

* wip: optimize code

* wip: Only if outputMode is "remote" is the redirected console output

* wip: Redirected debugMode output(Not tested on windows)

* wip: support remote debugging output redirection of windows

* wip: real-time write back output

* wip: support for windows

* wip: fix windows remote debug not output

* wip: fix truncated output redirection

* wip: delete printfln

* wip: use debugger.Config to pass redirect(macOS)

* wip: use debugger.Config to pass redirect(linux)

* wip: Change redirect to a concrete type

* wip: s.wg.wait before sending "terminated"

* wip: add proc/redirect test(darwin and linux)

* Merge branch 'master' of github.com:tttoad/delve into feat-console

* wip: Fix test failure on windows

* fix: undefined: proc.Redirects

* fix: compile failure

* wip: Remove useless code

* fix: filename error

* fix: os.file not close

* test: add server_test.redirect

* fix: Remove 'eol' from end of file

* fix: gdbserial: File not closed in file mode.
(in reality, gdbserial will never use file mode)

* feat: Remove "only-remote". Fix spelling mistakes.

* fix: spelling mistakes

* refactor: redirect

* fix: stdout and stderr are not set to default values

* fix: Restore code logic for rr.openRedirects()

* fix: Optimization Code

* fix: utiltest

* fix: execpt out

* fix: Resource release for redirects

* fix: build failure

* fix: clean->clear

* fix: build failure

* fix: test failure

* fix: Optimization Code

* style: remove useless code

* refactor: namedpipe

* refactor: namedpipe, launch ...

* fix: freebsd compile failure

* fix: proc_darwin compile failure

* style:  remove useless code

* feat: add d.config.Stdxx check on debug.Restart

* style: formatting and adding comments

* style: formatting and adding comments

* feat: add d.config.Stdxx check on debug.Restart

* style: namedpipe->redirector

* style: namedPipe->redirector

---------

Co-authored-by: 李翔 <qian.fu2@amh-group.com>
This commit is contained in:
ttoad 2023-07-05 23:39:01 +08:00 committed by GitHub
parent d963eb1057
commit 53998cbb18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 399 additions and 66 deletions

13
_fixtures/out_redirect.go Normal file

@ -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!")
}

@ -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,
},

@ -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
}
return dflt
}
var f *os.File
f, err = os.Create(path)
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
}
stdout = create(redirects[1], os.Stdout)
if quiet {
return nil
}
return dflt
}
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
}

@ -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)
}

@ -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
}

@ -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)
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
}
stdout = create(redirects[1], os.Stdout)
return dflt
}
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
}

@ -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

@ -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
}

@ -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
}

@ -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
}

@ -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)
}

@ -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:

12
pkg/proc/redirect.go Normal file

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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 {

@ -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
}

@ -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
}

@ -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 {