Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supports sending output to clients when running programs remotely #3253

Merged
merged 61 commits into from
Jul 5, 2023

Conversation

tttoad
Copy link
Contributor

@tttoad tttoad commented Jan 19, 2023

#3094
I saw that #3108 has not been updated for a long time, so I committed this PR.

@tttoad
Copy link
Contributor Author

tttoad commented Jan 19, 2023

I am currently reading the code related to debug mode, and will support debug mode output write-back in this PR.

Copy link
Member

@aarzilli aarzilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be supported on debugged processes as well, it would be weird to support this only when nodebug (I imagine this is just because it's a WIP). Also it needs a test. The tests are in service/dap/server_test.go.

service/dap/server.go Outdated Show resolved Hide resolved
service/dap/server.go Outdated Show resolved Hide resolved
service/dap/server.go Outdated Show resolved Hide resolved
service/dap/server.go Outdated Show resolved Hide resolved
service/dap/server.go Outdated Show resolved Hide resolved
wg := &sync.WaitGroup{}
readerFunc := func(reader io.Reader, category string, wg *sync.WaitGroup) {
// Display one line back after outputting one line
var scanner = bufio.NewScanner(reader)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect it's a bad idea to add a layer of line buffering here, I think you should read directly from the reader into a buffer and send whatever you read down.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output of the program may take a long time... I've thought about using a timed refresh and a fixed buffer size to achieve this, or maybe that's a better idea.

s.send(&dap.OutputEvent{
Event: *newEvent("output"),
Body: dap.OutputEventBody{
Output: fmt.Sprintln(out),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fmt.Sprintln doesn't do anything here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scanner.Text() will drop n, so I need to add it back.

service/dap/server.go Show resolved Hide resolved
@tttoad
Copy link
Contributor Author

tttoad commented Feb 2, 2023

@aarzilli @derekparker PTAL.
I've tested it in the following environments.
(client: macOS arm64. server: ubuntu arm64 in docker)
(client: macOS arm64. server: macOS arm64)
(client: macOS arm64. server: windows amd64)
It can work.
I should be adding test cases next week when you think it's okay.

)

var (
redirectMap = &RedirectStroe{rs: sync.Map{}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry but passing around pipes by stuffing them in a global variable is not acceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only in windows system need to use the pipeline...
Using debugger.Config to pass the pipeline will change the definition of the native.Launch function.
Is this acceptable to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Also this will eventually need tests, both the nodebug path and the normal path. And Debugger.Restart needs to return an error in case pipe redirects are used (it would be even better if it worked correctly instead of erroring but that's hard and dap doesn't call Restart anyway).

Copy link
Member

@aarzilli aarzilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still doesn't have tests, also the (*Debugger).Restart function needs to return an error if it is called with pipe redirects.
Also it doesn't build.

@@ -0,0 +1,36 @@
package util
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file shouldn't be used anymore and should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK.

var ErrorNotImplemented = errors.New("not implemented")

// Redirect
type Redirect interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a concrete type, put it in pkg/proc, not in its own package, make it describe a single redirect rather than three redirects in one. Do not repeat the type name in the name of its methods. For example, it should be (*Redirect).Path not (*Redirect).RedirectPath.

Restart is a word, it shouldn't be spelled ReStart.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it describe a single redirect rather than three redirects in one

Still TBD.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean WriterFiles [3]*os.File -> StdinWriter *os.File, StdoutWriter *os.File, StderrWriter *os.File

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should just be a string/io.Writer pair:

type OutputRedirect struct {
     Path string
     Writer io.Writer
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

type OutputRedirect struct {
	Path string
	File *os.File
}

Launch(.....,redirect [3]OutputRedirect) 

I can't use io.Writer or io.WriteColser, because in gdbserial/rr.go it needs *os.File.
In service/service.go, the data will be read from this structure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aarzilli Need a reply.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you suggested is ok.

@@ -786,6 +807,13 @@ func (s *Session) handleRequest(request dap.Message) {
}

func (s *Session) send(message dap.Message) {
if event, ok := message.(*dap.OutputEvent); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't hijack the send method to do other things, it should be done by the caller of this method, or maybe in a method that both waits for the readers to finish and then sends the terminated event.

service/dap/server.go Show resolved Hide resolved
@@ -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, redirects [3]proc.OutputRedirect) (run func() (string, error), stop func() error, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think that this and other should be replaced by stdin string, stdout, stderr proc.OutputRedirect. They are not three of the same thing anymore, stdin is treated differently and there is no path for it becoming similar to the other two. Also stdin is definitely not an output.

return oor.rd.Close()
}

func NamedPipe() (reader [2]io.ReadCloser, output [3]OutputRedirect, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be NamedPipe() (io.ReadClsoer, OutputRedirect, error). It's called NamedPipe not TwoNamedPipes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@@ -5926,6 +5927,120 @@ func TestStacktraceExtlinkMac(t *testing.T) {
})
}

func testGenRedirect(t *testing.T, fixture protest.Fixture, expectStdout string, expectStderr string, errChan chan error) (redirect [3]proc.OutputRedirect, cancelFunc func(), err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this functionality is adequately tested throught the dap layer and there is no need for this test here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

stderr = bytes.NewBufferString("")
)

TerminnatedPoint:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

terminatedPoint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

defer os.Remove(oor.p)

// try from "init" to "close".
if !atomic.CompareAndSwapInt32(&oor.status, 0, 2) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awful, there has to be a better way. At minimum it should use a mutex instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be enough to start the readers only after the target process has started.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can not work because rr.go (at minimum) would deadlock. However all we need to know is that the fifo has been opened for writing at least once, which we can do by unconditionally opening the fifo with os.O_WRONLY|syscall.O_NONBLOCK. This should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.O_WRONLY|syscall.O_NONBLOCK can solve the blocking problem. But, until the pipe is opened in write mode, the read pipe will return null.

https://go.dev/play/p/hKYuUNNg868

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant this:

func (oor *openOnRead) Close() error {
	defer os.Remove(oor.p)
	fh, _ := os.OpenFile(oor.p, os.O_WRONLY|syscall.O_NONBLOCK, 0)
	if fh != nil {
		fh.Close()
	}
	return oor.rd.Close()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Member

@aarzilli aarzilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a couple of minor changes, otherwise it's OK.

Also, in (*Debugger).Restart after the canRestart check but before the call to detach add this:

if !resetArgs && (d.config.Stdout.File != nil || d.config.Stderr.File != nil) {
	return nil, ErrCanNotRestart
	
}

stderr = bytes.NewBufferString("")
)

terminnatedPoint:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

terminatedPoint

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@@ -63,11 +63,13 @@ 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) {
toclose := []*os.File{}
func openRedirects(stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use stdin, stdout, stderr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdin, stdout, stderr is in conflict with the returned parameter name.

Copy link
Member

@aarzilli aarzilli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks.

Copy link
Member

@derekparker derekparker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the review delay, just got back from some time off.

func openRedirects(redirects [3]string, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
toclose := []*os.File{}
func openRedirects(stdinPath string, stdoutOR proc.OutputRedirect, stderrOR proc.OutputRedirect, quiet bool) (stdin, stdout, stderr *os.File, closefn func(), err error) {
var (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this var () block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

pkg/proc/namedpipe_other.go Outdated Show resolved Hide resolved
pkg/proc/namedpipe_other.go Outdated Show resolved Hide resolved
return oor.rd.Close()
}

func NamedPipe() (reader io.ReadCloser, output OutputRedirect, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this file and the Windows variant, why not simply use os.Pipe across the board?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, calling this a "named pipe" seems disingenuous as the caller cannot provide a name, so it's essentially the same as calling os.Pipe but with wrapped return values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pkg/proc/gdbserial/gdbserver.go
The file path needs to be used in the gdbserver.LLDBLaunch(). I can't get the file path from os.Pipe.
It is called NamedPipe because it uses the Named Pipe mode. But this seems disingenuous in the namedpipe_other file. Maybe we can call it Redirector?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok that makes sense. Yes, I like the name Redirector better, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derekparker PTAL.

pkg/proc/redirect.go Show resolved Hide resolved
@tttoad
Copy link
Contributor Author

tttoad commented May 10, 2023

@derekparker PTAL.

@mcdoker18
Copy link

@tttoad Thanks for the implementing this feature!

@aarzilli @derekparker Can you please take a look again?

@tttoad tttoad requested a review from derekparker June 29, 2023 14:51
Copy link
Member

@derekparker derekparker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@derekparker derekparker merged commit 53998cb into go-delve:master Jul 5, 2023
1 check passed
@@ -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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It needs to document what this is for and what values are acceptable. Please find how other string type fields were documented (e.g. Mode) and provide similar level of details.

n, err := reader.Read(out[:])
if err != nil {
if errors.Is(io.EOF, err) {
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we flush out the remaining bytes buffered in out (n>0) before returning if any?
From io.Reader document, it's possible n>0 and err != nil.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. Thanks for your guidance.

return nil, err
}
} else {
cmd.Stdout, cmd.Stderr = os.Stdin, os.Stderr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr

hyangah added a commit to hyangah/delve that referenced this pull request Oct 6, 2023
Fixes bugs introduced in v1.21.1

* Avoid dropping the last bytes from stderr/stdout when Read returns an
  error. (Read returns n>0). And skip sending Output event if Read
  returns n==0.

* Fix the bug that drops all stdout in the existing noDebug mode.

For go-delve#3253
derekparker pushed a commit that referenced this pull request Oct 9, 2023
Fixes bugs introduced in v1.21.1

* Avoid dropping the last bytes from stderr/stdout when Read returns an
  error. (Read returns n>0). And skip sending Output event if Read
  returns n==0.

* Fix the bug that drops all stdout in the existing noDebug mode.

For #3253
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants