Skip to content

Commit 2cc9fe1

Browse files
authored
Merge pull request #6900 from thaJeztah/cli_stream_cleanups
cli/streams: assorted cleanups
2 parents 38e44e4 + 526dfff commit 2cc9fe1

3 files changed

Lines changed: 120 additions & 68 deletions

File tree

cli/streams/in.go

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,28 @@ package streams
33
import (
44
"errors"
55
"io"
6-
"os"
7-
"runtime"
86

97
"github.com/moby/term"
108
)
119

1210
// In is an input stream to read user input. It implements [io.ReadCloser]
1311
// with additional utilities, such as putting the terminal in raw mode.
1412
type In struct {
15-
commonStream
1613
in io.ReadCloser
14+
cs commonStream
15+
}
16+
17+
// NewIn returns a new [In] from an [io.ReadCloser].
18+
func NewIn(in io.ReadCloser) *In {
19+
return &In{
20+
in: in,
21+
cs: newCommonStream(in),
22+
}
23+
}
24+
25+
// FD returns the file descriptor number for this stream.
26+
func (i *In) FD() uintptr {
27+
return i.cs.fd
1728
}
1829

1930
// Read implements the [io.Reader] interface.
@@ -26,36 +37,37 @@ func (i *In) Close() error {
2637
return i.in.Close()
2738
}
2839

40+
// IsTerminal returns whether this stream is connected to a terminal.
41+
func (i *In) IsTerminal() bool {
42+
return i.cs.isTerminal()
43+
}
44+
2945
// SetRawTerminal sets raw mode on the input terminal. It is a no-op if In
3046
// is not a TTY, or if the "NORAW" environment variable is set to a non-empty
3147
// value.
32-
func (i *In) SetRawTerminal() (err error) {
33-
if !i.isTerminal || os.Getenv("NORAW") != "" {
34-
return nil
35-
}
36-
i.state, err = term.SetRawTerminal(i.fd)
37-
return err
48+
func (i *In) SetRawTerminal() error {
49+
return i.cs.setRawTerminal(term.SetRawTerminal)
50+
}
51+
52+
// RestoreTerminal restores the terminal state if SetRawTerminal succeeded earlier.
53+
func (i *In) RestoreTerminal() {
54+
i.cs.restoreTerminal()
3855
}
3956

40-
// CheckTty checks if we are trying to attach to a container TTY
41-
// from a non-TTY client input stream, and if so, returns an error.
57+
// CheckTty reports an error when stdin is requested for a TTY-enabled
58+
// container, but the client stdin is not itself a terminal (for example,
59+
// when input is piped or redirected).
4260
func (i *In) CheckTty(attachStdin, ttyMode bool) error {
43-
// In order to attach to a container tty, input stream for the client must
44-
// be a tty itself: redirecting or piping the client standard input is
45-
// incompatible with `docker run -t`, `docker exec -t` or `docker attach`.
46-
if ttyMode && attachStdin && !i.isTerminal {
47-
const eText = "the input device is not a TTY"
48-
if runtime.GOOS == "windows" {
49-
return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'")
50-
}
51-
return errors.New(eText)
61+
// TODO(thaJeztah): consider inlining this code and deprecating the method.
62+
if !ttyMode || !attachStdin || i.cs.isTerminal() {
63+
return nil
5264
}
53-
return nil
65+
return errors.New("cannot attach stdin to a TTY-enabled container because stdin is not a terminal")
5466
}
5567

56-
// NewIn returns a new [In] from an [io.ReadCloser].
57-
func NewIn(in io.ReadCloser) *In {
58-
i := &In{in: in}
59-
i.fd, i.isTerminal = term.GetFdInfo(in)
60-
return i
68+
// SetIsTerminal overrides whether a terminal is connected. It is used to
69+
// override this property in unit-tests, and should not be depended on for
70+
// other purposes.
71+
func (i *In) SetIsTerminal(isTerminal bool) {
72+
i.cs.setIsTerminal(isTerminal)
6173
}

cli/streams/out.go

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,57 +2,65 @@ package streams
22

33
import (
44
"io"
5-
"os"
65

76
"github.com/moby/term"
8-
"github.com/sirupsen/logrus"
97
)
108

119
// Out is an output stream to write normal program output. It implements
1210
// an [io.Writer], with additional utilities for detecting whether a terminal
1311
// is connected, getting the TTY size, and putting the terminal in raw mode.
1412
type Out struct {
15-
commonStream
1613
out io.Writer
14+
cs commonStream
1715
}
1816

17+
// NewOut returns a new [Out] from an [io.Writer].
18+
func NewOut(out io.Writer) *Out {
19+
return &Out{
20+
out: out,
21+
cs: newCommonStream(out),
22+
}
23+
}
24+
25+
// FD returns the file descriptor number for this stream.
26+
func (o *Out) FD() uintptr {
27+
return o.cs.FD()
28+
}
29+
30+
// Write writes to the output stream.
1931
func (o *Out) Write(p []byte) (int, error) {
2032
return o.out.Write(p)
2133
}
2234

35+
// IsTerminal returns whether this stream is connected to a terminal.
36+
func (o *Out) IsTerminal() bool {
37+
return o.cs.isTerminal()
38+
}
39+
2340
// SetRawTerminal puts the output of the terminal connected to the stream
2441
// into raw mode.
2542
//
2643
// On UNIX, this does nothing. On Windows, it disables LF -> CRLF/ translation.
2744
// It is a no-op if Out is not a TTY, or if the "NORAW" environment variable is
2845
// set to a non-empty value.
29-
func (o *Out) SetRawTerminal() (err error) {
30-
if !o.isTerminal || os.Getenv("NORAW") != "" {
31-
return nil
32-
}
33-
o.state, err = term.SetRawTerminalOutput(o.fd)
34-
return err
46+
func (o *Out) SetRawTerminal() error {
47+
return o.cs.setRawTerminal(term.SetRawTerminalOutput)
48+
}
49+
50+
// RestoreTerminal restores the terminal state if SetRawTerminal succeeded earlier.
51+
func (o *Out) RestoreTerminal() {
52+
o.cs.restoreTerminal()
3553
}
3654

3755
// GetTtySize returns the height and width in characters of the TTY, or
3856
// zero for both if no TTY is connected.
3957
func (o *Out) GetTtySize() (height uint, width uint) {
40-
if !o.isTerminal {
41-
return 0, 0
42-
}
43-
ws, err := term.GetWinsize(o.fd)
44-
if err != nil {
45-
logrus.WithError(err).Debug("Error getting TTY size")
46-
if ws == nil {
47-
return 0, 0
48-
}
49-
}
50-
return uint(ws.Height), uint(ws.Width)
58+
return o.cs.terminalSize()
5159
}
5260

53-
// NewOut returns a new [Out] from an [io.Writer].
54-
func NewOut(out io.Writer) *Out {
55-
o := &Out{out: out}
56-
o.fd, o.isTerminal = term.GetFdInfo(out)
57-
return o
61+
// SetIsTerminal overrides whether a terminal is connected. It is used to
62+
// override this property in unit-tests, and should not be depended on for
63+
// other purposes.
64+
func (o *Out) SetIsTerminal(isTerminal bool) {
65+
o.cs.setIsTerminal(isTerminal)
5866
}

cli/streams/stream.go

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,67 @@
1+
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
2+
//go:build go1.25
3+
14
package streams
25

36
import (
7+
"os"
8+
49
"github.com/moby/term"
10+
"github.com/sirupsen/logrus"
511
)
612

13+
func newCommonStream(stream any) commonStream {
14+
fd, tty := term.GetFdInfo(stream)
15+
return commonStream{
16+
fd: fd,
17+
tty: tty,
18+
}
19+
}
20+
721
type commonStream struct {
8-
fd uintptr
9-
isTerminal bool
10-
state *term.State
22+
fd uintptr
23+
tty bool
24+
state *term.State
1125
}
1226

1327
// FD returns the file descriptor number for this stream.
14-
func (s *commonStream) FD() uintptr {
15-
return s.fd
16-
}
28+
func (s *commonStream) FD() uintptr { return s.fd }
1729

18-
// IsTerminal returns true if this stream is connected to a terminal.
19-
func (s *commonStream) IsTerminal() bool {
20-
return s.isTerminal
21-
}
30+
// isTerminal returns whether this stream is connected to a terminal.
31+
func (s *commonStream) isTerminal() bool { return s.tty }
2232

23-
// RestoreTerminal restores normal mode to the terminal.
24-
func (s *commonStream) RestoreTerminal() {
33+
// setIsTerminal overrides whether a terminal is connected for testing.
34+
func (s *commonStream) setIsTerminal(isTerminal bool) { s.tty = isTerminal }
35+
36+
// restoreTerminal restores the terminal state if SetRawTerminal succeeded earlier.
37+
func (s *commonStream) restoreTerminal() {
2538
if s.state != nil {
2639
_ = term.RestoreTerminal(s.fd, s.state)
2740
}
2841
}
2942

30-
// SetIsTerminal overrides whether a terminal is connected. It is used to
31-
// override this property in unit-tests, and should not be depended on for
32-
// other purposes.
33-
func (s *commonStream) SetIsTerminal(isTerminal bool) {
34-
s.isTerminal = isTerminal
43+
func (s *commonStream) setRawTerminal(setter func(uintptr) (*term.State, error)) error {
44+
if !s.tty || os.Getenv("NORAW") != "" {
45+
return nil
46+
}
47+
state, err := setter(s.fd)
48+
if err != nil {
49+
return err
50+
}
51+
s.state = state
52+
return nil
53+
}
54+
55+
func (s *commonStream) terminalSize() (height uint, width uint) {
56+
if !s.tty {
57+
return 0, 0
58+
}
59+
ws, err := term.GetWinsize(s.fd)
60+
if err != nil {
61+
logrus.WithError(err).Debug("Error getting TTY size")
62+
if ws == nil {
63+
return 0, 0
64+
}
65+
}
66+
return uint(ws.Height), uint(ws.Width)
3567
}

0 commit comments

Comments
 (0)