diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 2a0bd5c8a4..3177afc925 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -326,7 +326,7 @@ func openRedirects(redirects [3]string, foreground bool) (stdin, stdout, stderr return dflt } var f *os.File - f, err = os.Create(path) + f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0o666) if f != nil { toclose = append(toclose, f) } diff --git a/service/dap/server.go b/service/dap/server.go index 94f41e667a..22e5b76cdb 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -273,11 +273,9 @@ const ( maxStringLenInCallRetVars = 1 << 10 // 1024 ) -var ( - // Max number of goroutines that we will return. - // This is a var for testing - maxGoroutines = 1 << 10 -) +// Max number of goroutines that we will return. +// This is a var for testing +var maxGoroutines = 1 << 10 // NewServer creates a new DAP Server. It takes an opened Listener // via config and assumes its ownership. config.DisconnectChan has to be set; @@ -805,7 +803,24 @@ func (s *Session) logToConsole(msg string) { Body: dap.OutputEventBody{ Output: msg + "\n", Category: "console", - }}) + }, + }) +} + +type debugConsoleLogger struct { + session *Session + category string +} + +func (l *debugConsoleLogger) Write(p []byte) (int, error) { + l.session.send(&dap.OutputEvent{ + Event: *newEvent("output"), + Body: dap.OutputEventBody{ + Output: string(p), + Category: l.category, + }, + }) + return len(p), nil } func (s *Session) onInitializeRequest(request *dap.InitializeRequest) { @@ -881,7 +896,7 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) { return } - var args = defaultLaunchConfig // narrow copy for initializing non-zero default values + args := defaultLaunchConfig // narrow copy for initializing non-zero default values if err := unmarshalLaunchAttachArgs(request.Arguments, &args); err != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", fmt.Sprintf("invalid debug configuration - %v", err)) @@ -989,7 +1004,8 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) { Body: dap.OutputEventBody{ Output: fmt.Sprintf("Build Error: %s\n%s (%s)\n", cmd, strings.TrimSpace(string(out)), err.Error()), Category: "stderr", - }}) + }, + }) // Users are used to checking the Debug Console for build errors. // No need to bother them with a visible pop-up. s.sendErrorResponse(request.Request, FailedToLaunch, "Failed to launch", @@ -1028,6 +1044,12 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) { if args.NoDebug { s.mu.Lock() cmd, err := s.newNoDebugProcess(debugbinary, args.Args, s.config.Debugger.WorkingDir) + if args.Console == "internalConsole" { + cmd.Stdout, cmd.Stderr = &debugConsoleLogger{session: s, category: "stdout"}, &debugConsoleLogger{session: s, category: "stderr"} + } + if err == nil { + err = cmd.Start() + } s.mu.Unlock() if err != nil { s.sendShowUserErrorResponse(request.Request, FailedToLaunch, "Failed to launch", err.Error()) @@ -1049,6 +1071,32 @@ func (s *Session) onLaunchRequest(request *dap.LaunchRequest) { return } + func() { + if args.Console == "internalConsole" && runtime.GOOS == "linux" && (args.Backend == "default" || args.Backend == "native") { + redirects, err := generateStdioTempPipes() + if err != nil { + return + } + s.config.Debugger.Redirects[1] = redirects[0] + s.config.Debugger.Redirects[2] = redirects[1] + + stdoutWriter := &debugConsoleLogger{session: s, category: "stdout"} + stderrWriter := &debugConsoleLogger{session: s, category: "stderr"} + f := func(pipePath string, w io.Writer) { + // Blocking until read side of the pipe succeeds, and then delete file + pipe, err := os.Open(pipePath) + os.Remove(pipePath) + if err != nil { + return + } + defer pipe.Close() + io.Copy(w, pipe) + } + go f(redirects[0], stdoutWriter) + go f(redirects[1], stderrWriter) + } + }() + func() { s.mu.Lock() defer s.mu.Unlock() // Make sure to unlock in case of panic that will become internal error @@ -1088,9 +1136,6 @@ func (s *Session) newNoDebugProcess(program string, targetArgs []string, wd stri } 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 { - return nil, err - } s.noDebugProcess = &process{Cmd: cmd, exited: make(chan struct{})} return cmd, nil } @@ -1571,7 +1616,8 @@ func (s *Session) onConfigurationDoneRequest(request *dap.ConfigurationDoneReque func (s *Session) onContinueRequest(request *dap.ContinueRequest, allowNextStateChange chan struct{}) { s.send(&dap.ContinueResponse{ Response: *newResponse(request.Request), - Body: dap.ContinueResponseBody{AllThreadsContinued: true}}) + Body: dap.ContinueResponseBody{AllThreadsContinued: true}, + }) s.runUntilStopAndNotify(api.Continue, allowNextStateChange) } @@ -1635,7 +1681,8 @@ func (s *Session) onThreadsRequest(request *dap.ThreadsRequest) { Body: dap.OutputEventBody{ Output: fmt.Sprintf("Unable to retrieve goroutines: %s\n", err.Error()), Category: "stderr", - }}) + }, + }) } threads = []dap.Thread{{Id: 1, Name: "Dummy"}} } else if len(gs) == 0 { @@ -2463,7 +2510,7 @@ func (s *Session) convertVariableWithOpts(v *proc.Variable, qualifiedNameOrExpr // Some of the types might be fully or partially not loaded based on LoadConfig. // Those that are fully missing (e.g. due to hitting MaxVariableRecurse), can be reloaded in place. - var reloadVariable = func(v *proc.Variable, qualifiedNameOrExpr string) (value string) { + reloadVariable := func(v *proc.Variable, qualifiedNameOrExpr string) (value string) { // We might be loading variables from the frame that's not topmost, so use // frame-independent address-based expression, not fully-qualified name as per // https://github.com/go-delve/delve/blob/master/Documentation/api/ClientHowto.md#looking-into-variables. @@ -3393,6 +3440,7 @@ func newEvent(event string) *dap.Event { const BetterBadAccessError = `invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation] Unable to propagate EXC_BAD_ACCESS signal to target process and panic (see https://github.com/go-delve/delve/issues/852)` + const BetterNextWhileNextingError = `Unable to step while the previous step is interrupted by a breakpoint. Use 'Continue' to resume the original step command.` diff --git a/service/dap/stdio_pipe.go b/service/dap/stdio_pipe.go new file mode 100644 index 0000000000..4a311a722f --- /dev/null +++ b/service/dap/stdio_pipe.go @@ -0,0 +1,13 @@ +//go:build !linux +// +build !linux + +package dap + +import ( + "errors" +) + +func generateStdioTempPipes() (res [2]string, err error) { + err = errors.New("Unimplemented") + return res, err +} diff --git a/service/dap/stdio_pipe_linux.go b/service/dap/stdio_pipe_linux.go new file mode 100644 index 0000000000..8e2b6744aa --- /dev/null +++ b/service/dap/stdio_pipe_linux.go @@ -0,0 +1,33 @@ +//go:build linux +// +build linux + +package dap + +import ( + "crypto/rand" + "encoding/hex" + "os" + "path/filepath" + "syscall" +) + +func generateStdioTempPipes() (res [2]string, err error) { + r := make([]byte, 4) + if _, err := rand.Read(r); err != nil { + return res, err + } + prefix := filepath.Join(os.TempDir(), hex.EncodeToString(r)) + stdoutPath := prefix + "stdout" + stderrPath := prefix + "stderr" + if err := syscall.Mkfifo(stdoutPath, 0o600); err != nil { + return res, err + } + if err := syscall.Mkfifo(stderrPath, 0o600); err != nil { + os.Remove(stdoutPath) + return res, err + } + + res[0] = stdoutPath + res[1] = stderrPath + return res, nil +} diff --git a/service/dap/types.go b/service/dap/types.go index f67a03f66e..4cdfe5af65 100644 --- a/service/dap/types.go +++ b/service/dap/types.go @@ -148,6 +148,12 @@ type LaunchConfig struct { // reference to other environment variables is not supported. Env map[string]*string `json:"env,omitempty"` + // Console specifies the console the user desired to execute with. + // If the user requested `internalConsole` as its terminal, then we + // wish to redirect its stdout/stderr as DAP OutputEvents. + // By default, we do not redirect if it doesn't exist. + Console string `json:"console,omitempty"` + LaunchAttachCommonConfig } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 5b88ef3e05..f4d27dc4f5 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -9,6 +9,7 @@ import ( "fmt" "go/parser" "go/token" + "io" "os" "os/exec" "path/filepath" @@ -139,6 +140,10 @@ type Config struct { // Redirects specifies redirect rules for stdin, stdout and stderr Redirects [3]string + // Writers written which are used instead of Redirects, in case the caller desires to do so + CaptureStdout io.Writer + CaptureStderr io.Writer + // DisableASLR disables ASLR DisableASLR bool } @@ -727,7 +732,6 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, } } createdBp, err := createLogicalBreakpoint(d, requestedBp, &setbp) - if err != nil { return nil, err } @@ -1879,6 +1883,13 @@ func (d *Debugger) FindLocation(goid int64, frame, deferredCall int, locStr stri d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + // TODO(aarzilli): if there is more than one target process all must be + // searched and the addresses returned need to specify which target process + // they belong to. + panic("multiple targets not implemented") + } + if _, err := d.target.Valid(); err != nil { return nil, err } @@ -1899,6 +1910,13 @@ func (d *Debugger) FindLocationSpec(goid int64, frame, deferredCall int, locStr d.targetMutex.Lock() defer d.targetMutex.Unlock() + if len(d.target.Targets()) != 1 { + // TODO(aarzilli): if there is more than one target process all must be + // searched and the addresses returned need to specify which target process + // they belong to. + panic("multiple targets not implemented") + } + if _, err := d.target.Valid(); err != nil { return nil, err } @@ -2024,7 +2042,6 @@ func (d *Debugger) ListDynamicLibraries() []*proc.Image { d.targetMutex.Lock() defer d.targetMutex.Unlock() return d.target.Selected.BinInfo().Images[1:] // skips the first image because it's the executable file - } // ExamineMemory returns the raw memory stored at the given address. @@ -2118,7 +2135,7 @@ func (d *Debugger) DumpStart(dest string) error { d.targetMutex.Lock() // targetMutex will only be unlocked when the dump is done - //TODO(aarzilli): what do we do if the user switches to a different target after starting a dump but before it's finished? + // TODO(aarzilli): what do we do if the user switches to a different target after starting a dump but before it's finished? if !d.target.Selected.CanDump { d.targetMutex.Unlock() @@ -2270,7 +2287,7 @@ func verifyBinaryFormat(exePath string) error { if err != nil { return err } - if (fi.Mode() & 0111) == 0 { + if (fi.Mode() & 0o111) == 0 { return api.ErrNotExecutable } }