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:
parent
d963eb1057
commit
53998cbb18
13
_fixtures/out_redirect.go
Normal file
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
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
|
||||
}
|
59
pkg/proc/redirector_other.go
Normal file
59
pkg/proc/redirector_other.go
Normal 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
|
||||
}
|
15
pkg/proc/redirector_windows.go
Normal file
15
pkg/proc/redirector_windows.go
Normal file
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user