-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Conversation
I am currently reading the code related to debug mode, and will support debug mode output write-back in this PR. |
There was a problem hiding this 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
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
service/dap/server.go
Outdated
s.send(&dap.OutputEvent{ | ||
Event: *newEvent("output"), | ||
Body: dap.OutputEventBody{ | ||
Output: fmt.Sprintln(out), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@aarzilli @derekparker PTAL. |
pkg/util/redirect.go
Outdated
) | ||
|
||
var ( | ||
redirectMap = &RedirectStroe{rs: sync.Map{}} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this 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.
pkg/util/redirect.go
Outdated
@@ -0,0 +1,36 @@ | |||
package util |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK.
pkg/proc/redirect/redirect.go
Outdated
var ErrorNotImplemented = errors.New("not implemented") | ||
|
||
// Redirect | ||
type Redirect interface { |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aarzilli Need a reply.
There was a problem hiding this comment.
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.
service/dap/server.go
Outdated
@@ -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 { |
There was a problem hiding this comment.
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.
pkg/proc/gdbserial/rr.go
Outdated
@@ -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) { |
There was a problem hiding this comment.
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.
pkg/proc/namedpipe_other.go
Outdated
return oor.rd.Close() | ||
} | ||
|
||
func NamedPipe() (reader [2]io.ReadCloser, output [3]OutputRedirect, err error) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
pkg/proc/proc_test.go
Outdated
@@ -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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
service/dap/server_test.go
Outdated
stderr = bytes.NewBufferString("") | ||
) | ||
|
||
TerminnatedPoint: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
terminatedPoint
There was a problem hiding this comment.
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
defer os.Remove(oor.p) | ||
|
||
// try from "init" to "close". | ||
if !atomic.CompareAndSwapInt32(&oor.status, 0, 2) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
There was a problem hiding this 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
}
service/dap/server_test.go
Outdated
stderr = bytes.NewBufferString("") | ||
) | ||
|
||
terminnatedPoint: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
terminatedPoint
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks.
There was a problem hiding this 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.
pkg/proc/gdbserial/rr.go
Outdated
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 ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
return oor.rd.Close() | ||
} | ||
|
||
func NamedPipe() (reader io.ReadCloser, output OutputRedirect, err error) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@derekparker PTAL.
@derekparker PTAL. |
@tttoad Thanks for the implementing this feature! @aarzilli @derekparker Can you please take a look again? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@@ -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"` |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
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
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
#3094
I saw that #3108 has not been updated for a long time, so I committed this PR.