Browse code

Merge pull request #8233 from tiborvass/pr-7658

Fix Interactive container hangs when redirecting stdout

Michael Crosby authored on 2014/09/27 03:31:29
Showing 6 changed files
... ...
@@ -22,10 +22,16 @@ type DockerCli struct {
22 22
 	in         io.ReadCloser
23 23
 	out        io.Writer
24 24
 	err        io.Writer
25
-	isTerminal bool
26
-	terminalFd uintptr
27 25
 	tlsConfig  *tls.Config
28 26
 	scheme     string
27
+	// inFd holds file descriptor of the client's STDIN, if it's a valid file
28
+	inFd uintptr
29
+	// outFd holds file descriptor of the client's STDOUT, if it's a valid file
30
+	outFd uintptr
31
+	// isTerminalIn describes if client's STDIN is a TTY
32
+	isTerminalIn bool
33
+	// isTerminalOut describes if client's STDOUT is a TTY
34
+	isTerminalOut bool
29 35
 }
30 36
 
31 37
 var funcMap = template.FuncMap{
... ...
@@ -94,9 +100,11 @@ func (cli *DockerCli) LoadConfigFile() (err error) {
94 94
 
95 95
 func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig *tls.Config) *DockerCli {
96 96
 	var (
97
-		isTerminal = false
98
-		terminalFd uintptr
99
-		scheme     = "http"
97
+		inFd          uintptr
98
+		outFd         uintptr
99
+		isTerminalIn  = false
100
+		isTerminalOut = false
101
+		scheme        = "http"
100 102
 	)
101 103
 
102 104
 	if tlsConfig != nil {
... ...
@@ -104,24 +112,34 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsC
104 104
 	}
105 105
 
106 106
 	if in != nil {
107
+		if file, ok := in.(*os.File); ok {
108
+			inFd = file.Fd()
109
+			isTerminalIn = term.IsTerminal(inFd)
110
+		}
111
+	}
112
+
113
+	if out != nil {
107 114
 		if file, ok := out.(*os.File); ok {
108
-			terminalFd = file.Fd()
109
-			isTerminal = term.IsTerminal(terminalFd)
115
+			outFd = file.Fd()
116
+			isTerminalOut = term.IsTerminal(outFd)
110 117
 		}
111 118
 	}
112 119
 
113 120
 	if err == nil {
114 121
 		err = out
115 122
 	}
123
+
116 124
 	return &DockerCli{
117
-		proto:      proto,
118
-		addr:       addr,
119
-		in:         in,
120
-		out:        out,
121
-		err:        err,
122
-		isTerminal: isTerminal,
123
-		terminalFd: terminalFd,
124
-		tlsConfig:  tlsConfig,
125
-		scheme:     scheme,
125
+		proto:         proto,
126
+		addr:          addr,
127
+		in:            in,
128
+		out:           out,
129
+		err:           err,
130
+		inFd:          inFd,
131
+		outFd:         outFd,
132
+		isTerminalIn:  isTerminalIn,
133
+		isTerminalOut: isTerminalOut,
134
+		tlsConfig:     tlsConfig,
135
+		scheme:        scheme,
126 136
 	}
127 137
 }
... ...
@@ -277,14 +277,14 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
277 277
 	// the password or email from the config file, so prompt them
278 278
 	if username != authconfig.Username {
279 279
 		if password == "" {
280
-			oldState, _ := term.SaveState(cli.terminalFd)
280
+			oldState, _ := term.SaveState(cli.inFd)
281 281
 			fmt.Fprintf(cli.out, "Password: ")
282
-			term.DisableEcho(cli.terminalFd, oldState)
282
+			term.DisableEcho(cli.inFd, oldState)
283 283
 
284 284
 			password = readInput(cli.in, cli.out)
285 285
 			fmt.Fprint(cli.out, "\n")
286 286
 
287
-			term.RestoreTerminal(cli.terminalFd, oldState)
287
+			term.RestoreTerminal(cli.inFd, oldState)
288 288
 			if password == "" {
289 289
 				return fmt.Errorf("Error : Password Required")
290 290
 			}
... ...
@@ -669,7 +669,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
669 669
 	}
670 670
 
671 671
 	if *openStdin || *attach {
672
-		if tty && cli.isTerminal {
672
+		if tty && cli.isTerminalOut {
673 673
 			if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
674 674
 				log.Errorf("Error monitoring TTY size: %s", err)
675 675
 			}
... ...
@@ -1821,7 +1821,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
1821 1821
 		tty    = config.GetBool("Tty")
1822 1822
 	)
1823 1823
 
1824
-	if tty && cli.isTerminal {
1824
+	if tty && cli.isTerminalOut {
1825 1825
 		if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
1826 1826
 			log.Debugf("Error monitoring TTY size: %s", err)
1827 1827
 		}
... ...
@@ -2241,7 +2241,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
2241 2241
 		return err
2242 2242
 	}
2243 2243
 
2244
-	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminal {
2244
+	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminalOut {
2245 2245
 		if err := cli.monitorTtySize(runResult.Get("Id"), false); err != nil {
2246 2246
 			log.Errorf("Error monitoring TTY size: %s", err)
2247 2247
 		}
... ...
@@ -2490,7 +2490,7 @@ func (cli *DockerCli) CmdExec(args ...string) error {
2490 2490
 		}
2491 2491
 	}
2492 2492
 
2493
-	if execConfig.Tty && cli.isTerminal {
2493
+	if execConfig.Tty && cli.isTerminalIn {
2494 2494
 		if err := cli.monitorTtySize(execID, true); err != nil {
2495 2495
 			log.Errorf("Error monitoring TTY size: %s", err)
2496 2496
 		}
... ...
@@ -69,20 +69,20 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
69 69
 
70 70
 	var oldState *term.State
71 71
 
72
-	if in != nil && setRawTerminal && cli.isTerminal && os.Getenv("NORAW") == "" {
73
-		oldState, err = term.SetRawTerminal(cli.terminalFd)
72
+	if in != nil && setRawTerminal && cli.isTerminalIn && os.Getenv("NORAW") == "" {
73
+		oldState, err = term.SetRawTerminal(cli.inFd)
74 74
 		if err != nil {
75 75
 			return err
76 76
 		}
77
-		defer term.RestoreTerminal(cli.terminalFd, oldState)
77
+		defer term.RestoreTerminal(cli.inFd, oldState)
78 78
 	}
79 79
 
80 80
 	if stdout != nil || stderr != nil {
81 81
 		receiveStdout = utils.Go(func() (err error) {
82 82
 			defer func() {
83 83
 				if in != nil {
84
-					if setRawTerminal && cli.isTerminal {
85
-						term.RestoreTerminal(cli.terminalFd, oldState)
84
+					if setRawTerminal && cli.isTerminalIn {
85
+						term.RestoreTerminal(cli.inFd, oldState)
86 86
 					}
87 87
 					// For some reason this Close call blocks on darwin..
88 88
 					// As the client exists right after, simply discard the close
... ...
@@ -129,7 +129,7 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in io.Rea
129 129
 		}
130 130
 	}
131 131
 
132
-	if !cli.isTerminal {
132
+	if !cli.isTerminalIn {
133 133
 		if err := <-sendStdin; err != nil {
134 134
 			log.Debugf("Error sendStdin: %s", err)
135 135
 			return err
... ...
@@ -168,7 +168,7 @@ func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in
168 168
 	}
169 169
 
170 170
 	if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") || api.MatchesContentType(resp.Header.Get("Content-Type"), "application/x-json-stream") {
171
-		return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.terminalFd, cli.isTerminal)
171
+		return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.outFd, cli.isTerminalOut)
172 172
 	}
173 173
 	if stdout != nil || stderr != nil {
174 174
 		// When TTY is ON, use regular copy
... ...
@@ -252,10 +252,10 @@ func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
252 252
 }
253 253
 
254 254
 func (cli *DockerCli) getTtySize() (int, int) {
255
-	if !cli.isTerminal {
255
+	if !cli.isTerminalOut {
256 256
 		return 0, 0
257 257
 	}
258
-	ws, err := term.GetWinsize(cli.terminalFd)
258
+	ws, err := term.GetWinsize(cli.outFd)
259 259
 	if err != nil {
260 260
 		log.Debugf("Error getting size: %s", err)
261 261
 		if ws == nil {
... ...
@@ -1,12 +1,18 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"bufio"
4 5
 	"fmt"
6
+	"io/ioutil"
7
+	"os"
5 8
 	"os/exec"
6 9
 	"strconv"
7 10
 	"strings"
8 11
 	"testing"
9 12
 	"time"
13
+	"unicode"
14
+
15
+	"github.com/kr/pty"
10 16
 )
11 17
 
12 18
 func TestEventsUntag(t *testing.T) {
... ...
@@ -166,3 +172,46 @@ func TestEventsImageUntagDelete(t *testing.T) {
166 166
 	}
167 167
 	logDone("events - image untag, delete is logged")
168 168
 }
169
+
170
+// #5979
171
+func TestEventsRedirectStdout(t *testing.T) {
172
+
173
+	since := time.Now().Unix()
174
+
175
+	cmd(t, "run", "busybox", "true")
176
+
177
+	defer deleteAllContainers()
178
+
179
+	file, err := ioutil.TempFile("", "")
180
+	if err != nil {
181
+		t.Fatalf("could not create temp file: %v", err)
182
+	}
183
+	defer os.Remove(file.Name())
184
+
185
+	command := fmt.Sprintf("%s events --since=%d --until=%d > %s", dockerBinary, since, time.Now().Unix(), file.Name())
186
+	_, tty, err := pty.Open()
187
+	if err != nil {
188
+		t.Fatalf("Could not open pty: %v", err)
189
+	}
190
+	cmd := exec.Command("sh", "-c", command)
191
+	cmd.Stdin = tty
192
+	cmd.Stdout = tty
193
+	cmd.Stderr = tty
194
+	if err := cmd.Run(); err != nil {
195
+		t.Fatalf("run err for command %q: %v", command, err)
196
+	}
197
+
198
+	scanner := bufio.NewScanner(file)
199
+	for scanner.Scan() {
200
+		for _, c := range scanner.Text() {
201
+			if unicode.IsControl(c) {
202
+				t.Fatalf("found control character %v", []byte(string(c)))
203
+			}
204
+		}
205
+	}
206
+	if err := scanner.Err(); err != nil {
207
+		t.Fatalf("Scan err for command %q: %v", command, err)
208
+	}
209
+
210
+	logDone("events - redirect stdout")
211
+}
... ...
@@ -19,6 +19,7 @@ import (
19 19
 
20 20
 	"github.com/docker/docker/pkg/mount"
21 21
 	"github.com/docker/docker/pkg/networkfs/resolvconf"
22
+	"github.com/kr/pty"
22 23
 )
23 24
 
24 25
 // "test123" should be printed by docker run
... ...
@@ -2182,3 +2183,41 @@ func TestRunExecDir(t *testing.T) {
2182 2182
 
2183 2183
 	logDone("run - check execdriver dir behavior")
2184 2184
 }
2185
+
2186
+// #6509
2187
+func TestRunRedirectStdout(t *testing.T) {
2188
+
2189
+	defer deleteAllContainers()
2190
+
2191
+	checkRedirect := func(command string) {
2192
+		_, tty, err := pty.Open()
2193
+		if err != nil {
2194
+			t.Fatalf("Could not open pty: %v", err)
2195
+		}
2196
+		cmd := exec.Command("sh", "-c", command)
2197
+		cmd.Stdin = tty
2198
+		cmd.Stdout = tty
2199
+		cmd.Stderr = tty
2200
+		ch := make(chan struct{})
2201
+		if err := cmd.Start(); err != nil {
2202
+			t.Fatalf("start err: %v", err)
2203
+		}
2204
+		go func() {
2205
+			if err := cmd.Wait(); err != nil {
2206
+				t.Fatalf("wait err=%v", err)
2207
+			}
2208
+			close(ch)
2209
+		}()
2210
+
2211
+		select {
2212
+		case <-time.After(time.Second):
2213
+			t.Fatal("command timeout")
2214
+		case <-ch:
2215
+		}
2216
+	}
2217
+
2218
+	checkRedirect(dockerBinary + " run -i busybox cat /etc/passwd | grep -q root")
2219
+	checkRedirect(dockerBinary + " run busybox cat /etc/passwd | grep -q root")
2220
+
2221
+	logDone("run - redirect stdout")
2222
+}