Browse code

pass extra file to child process as status handler

When stdout/stderr is closed prematurely, the proxy's writes to stdout/stderr
(i.e. `log.Errorf/log.Printf`) will returns with EPIPE error, and go runtime
will terminate the proxy when stdout/stderr writes trigger 10 EPIPE errors.

instead of using stdout/stderr as the status handler, we pass an extra file to
the child process and write `0\n` or `1\nerror message` to it and close it
after. This allow the child process to handle stdout/stderr as normal.

Docker-DCO-1.1-Signed-off-by: Daniel, Dao Quang Minh <dqminh89@gmail.com> (github: dqminh)

Daniel, Dao Quang Minh authored on 2014/10/08 21:41:26
Showing 2 changed files
... ...
@@ -36,16 +36,18 @@ type proxyCommand struct {
36 36
 
37 37
 // execProxy is the reexec function that is registered to start the userland proxies
38 38
 func execProxy() {
39
+	f := os.NewFile(3, "signal-parent")
39 40
 	host, container := parseHostContainerAddrs()
40 41
 
41 42
 	p, err := proxy.NewProxy(host, container)
42 43
 	if err != nil {
43
-		os.Stdout.WriteString("1\n")
44
-		fmt.Fprint(os.Stderr, err)
44
+		fmt.Fprintf(f, "1\n%s", err)
45
+		f.Close()
45 46
 		os.Exit(1)
46 47
 	}
47 48
 	go handleStopSignals(p)
48
-	os.Stdout.WriteString("0\n")
49
+	fmt.Fprint(f, "0\n")
50
+	f.Close()
49 51
 
50 52
 	// Run will block until the proxy stops
51 53
 	p.Run()
... ...
@@ -111,27 +113,24 @@ func NewProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.
111 111
 }
112 112
 
113 113
 func (p *proxyCommand) Start() error {
114
-	stdout, err := p.cmd.StdoutPipe()
114
+	r, w, err := os.Pipe()
115 115
 	if err != nil {
116
-		return err
117
-	}
118
-	defer stdout.Close()
119
-	stderr, err := p.cmd.StderrPipe()
120
-	if err != nil {
121
-		return err
116
+		return fmt.Errorf("proxy unable to open os.Pipe %s", err)
122 117
 	}
123
-	defer stderr.Close()
118
+	defer r.Close()
119
+	p.cmd.ExtraFiles = []*os.File{w}
124 120
 	if err := p.cmd.Start(); err != nil {
125 121
 		return err
126 122
 	}
123
+	w.Close()
127 124
 
128 125
 	errchan := make(chan error, 1)
129 126
 	go func() {
130 127
 		buf := make([]byte, 2)
131
-		stdout.Read(buf)
128
+		r.Read(buf)
132 129
 
133 130
 		if string(buf) != "0\n" {
134
-			errStr, _ := ioutil.ReadAll(stderr)
131
+			errStr, _ := ioutil.ReadAll(r)
135 132
 			errchan <- fmt.Errorf("Error starting userland proxy: %s", errStr)
136 133
 			return
137 134
 		}
... ...
@@ -2093,6 +2093,39 @@ func TestRunPortInUse(t *testing.T) {
2093 2093
 	logDone("run - fail if port already in use")
2094 2094
 }
2095 2095
 
2096
+// https://github.com/docker/docker/issues/8428
2097
+func TestRunPortProxy(t *testing.T) {
2098
+	defer deleteAllContainers()
2099
+
2100
+	port := "12345"
2101
+	cmd := exec.Command(dockerBinary, "run", "-p", port+":80", "busybox", "true")
2102
+
2103
+	out, _, err := runCommandWithOutput(cmd)
2104
+	if err != nil {
2105
+		t.Fatalf("Failed to run and bind port %s, output: %s, error: %s", port, out, err)
2106
+	}
2107
+
2108
+	// connect for 10 times here. This will trigger 10 EPIPES in the child
2109
+	// process and kill it when it writes to a closed stdout/stderr
2110
+	for i := 0; i < 10; i++ {
2111
+		net.Dial("tcp", fmt.Sprintf("0.0.0.0:%s", port))
2112
+	}
2113
+
2114
+	listPs := exec.Command("sh", "-c", "ps ax | grep docker")
2115
+	out, _, err = runCommandWithOutput(listPs)
2116
+	if err != nil {
2117
+		t.Errorf("list docker process failed with output %s, error %s", out, err)
2118
+	}
2119
+	if strings.Contains(out, "docker <defunct>") {
2120
+		t.Errorf("Unexpected defunct docker process")
2121
+	}
2122
+	if !strings.Contains(out, "docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 12345") {
2123
+		t.Errorf("Failed to find docker-proxy process, got %s", out)
2124
+	}
2125
+
2126
+	logDone("run - proxy should work with unavailable port")
2127
+}
2128
+
2096 2129
 // Regression test for #7792
2097 2130
 func TestRunMountOrdering(t *testing.T) {
2098 2131
 	tmpDir, err := ioutil.TempDir("", "docker_nested_mount_test")