Skip to content

Commit f6ff3f3

Browse files
committed
client: close client.Std{err,out} if they are not os.Std{err,out}
cpu clients are implemented in the client package. The cpu command uses this package. As in os/exec, code can call client.Command(); followed by Client.Start() and client.Wait(), or use client.Run(), which combines Start and Wait. The client struct has io.WriteCloser for its Stdout and Stderr. The question of how these gets closed is important. Normally, when a cpu process runs, kernel process exit handling performs a close on stdout and stderr. User code need not call close. When the client package is used as part of a program that does not exit, as in a test, code might set the client struct Stderr and Stdout to a pipe, to avoid littering test output with cpu command output. In this case, the pipe reader will hang until the Stderr and Stdout are closed, as no process exits. It is not safe to just blindly close these files; users might unintentionally close Stdout, leading to confusing results. The actual IO is handled by two io.Copy goroutines started in cpu.Start, one for Stdout, one for Stderr. These two goroutines exit when the remote process closes the ssh Session Std{err,out}. At that point, code could decide whether to close the Std{err,out}. An ideal time to do this check is when the io.Copy exits. This change tests the type of client.Std{err,out}; IF they are an *os.File, AND if their Fd() matches the Fd() of os.Std{err,out}, respectively, then they are left alone; otherwise, they are closed. This seems a safe test. It has been tested both in a test and interactively and the behavior is sensible. Signed-off-by: Ronald G Minnich <rminnich@gmail.com>
1 parent 5c713f2 commit f6ff3f3

File tree

1 file changed

+22
-2
lines changed

1 file changed

+22
-2
lines changed

client/client.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ type Cmd struct {
6161
SessionOut io.Reader
6262
SessionErr io.Reader
6363
Stdin io.Reader
64-
Stdout io.Writer
65-
Stderr io.Writer
64+
Stdout io.WriteCloser
65+
Stderr io.WriteCloser
6666
Row int
6767
Col int
6868
hasTTY bool // Set if we have a TTY
@@ -102,6 +102,13 @@ func (c *Cmd) Listen(n, addr string) (net.Listener, error) {
102102
return c.client.Listen(n, addr)
103103
}
104104

105+
func sameFD(w io.WriteCloser, std *os.File) bool {
106+
if file, ok := w.(*os.File); ok {
107+
return file.Fd() == std.Fd()
108+
}
109+
return false
110+
}
111+
105112
// Command implements exec.Command. The required parameter is a host.
106113
// The args arg args to $SHELL. If there are no args, then starting $SHELL
107114
// is assumed.
@@ -512,6 +519,7 @@ func (c *Cmd) Start() error {
512519
}
513520
go c.TTYIn(c.session, c.SessionIn, c.Stdin)
514521
} else {
522+
verbose("Setup batch input")
515523
go func() {
516524
if _, err := io.Copy(c.SessionIn, c.Stdin); err != nil && !errors.Is(err, io.EOF) {
517525
log.Printf("copying stdin: %v", err)
@@ -522,14 +530,26 @@ func (c *Cmd) Start() error {
522530
}()
523531
}
524532
go func() {
533+
verbose("set up copying to c.Stdout %s", c.Stdout)
525534
if _, err := io.Copy(c.Stdout, c.SessionOut); err != nil && !errors.Is(err, io.EOF) {
526535
log.Printf("copying stdout: %v", err)
527536
}
537+
538+
// If the file is NOT stdout, close it.
539+
// This is needed when programmers have
540+
// set c.Stdout to be some other WriteCloser, e.g. a pipe.
541+
if !sameFD(c.Stdout, os.Stdout) {
542+
c.Stdout.Close()
543+
}
528544
}()
529545
go func() {
546+
verbose("set up copying to c.Stderr %s", c.Stderr)
530547
if _, err := io.Copy(c.Stderr, c.SessionErr); err != nil && !errors.Is(err, io.EOF) {
531548
log.Printf("copying stderr: %v", err)
532549
}
550+
if !sameFD(c.Stdout, os.Stderr) {
551+
c.Stderr.Close()
552+
}
533553
}()
534554

535555
return nil

0 commit comments

Comments
 (0)