Browse code

Adding docker exec support in CLI. Fixed a bug in daemon that resulted in accessing of a closed pipe.

Docker-DCO-1.1-Signed-off-by: Vishnu Kannan <vishnuk@google.com> (github: vishh)

Vishnu Kannan authored on 2014/09/16 15:43:43
Showing 9 changed files
... ...
@@ -653,7 +653,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
653 653
 
654 654
 	if *openStdin || *attach {
655 655
 		if tty && cli.isTerminal {
656
-			if err := cli.monitorTtySize(cmd.Arg(0)); err != nil {
656
+			if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
657 657
 				log.Errorf("Error monitoring TTY size: %s", err)
658 658
 			}
659 659
 		}
... ...
@@ -1805,7 +1805,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
1805 1805
 	)
1806 1806
 
1807 1807
 	if tty && cli.isTerminal {
1808
-		if err := cli.monitorTtySize(cmd.Arg(0)); err != nil {
1808
+		if err := cli.monitorTtySize(cmd.Arg(0), false); err != nil {
1809 1809
 			log.Debugf("Error monitoring TTY size: %s", err)
1810 1810
 		}
1811 1811
 	}
... ...
@@ -2136,7 +2136,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
2136 2136
 	}
2137 2137
 
2138 2138
 	if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && cli.isTerminal {
2139
-		if err := cli.monitorTtySize(runResult.Get("Id")); err != nil {
2139
+		if err := cli.monitorTtySize(runResult.Get("Id"), false); err != nil {
2140 2140
 			log.Errorf("Error monitoring TTY size: %s", err)
2141 2141
 		}
2142 2142
 	}
... ...
@@ -2299,3 +2299,101 @@ func (cli *DockerCli) CmdLoad(args ...string) error {
2299 2299
 	}
2300 2300
 	return nil
2301 2301
 }
2302
+
2303
+func (cli *DockerCli) CmdExec(args ...string) error {
2304
+	cmd := cli.Subcmd("exec", "CONTAINER COMMAND [ARG...]", "Run a command in an existing container")
2305
+
2306
+	execConfig, err := runconfig.ParseExec(cmd, args)
2307
+	if err != nil {
2308
+		return err
2309
+	}
2310
+	if execConfig.Container == "" {
2311
+		cmd.Usage()
2312
+		return nil
2313
+	}
2314
+
2315
+	stream, _, err := cli.call("POST", "/containers/"+execConfig.Container+"/exec", execConfig, false)
2316
+	if err != nil {
2317
+		return err
2318
+	}
2319
+
2320
+	var execResult engine.Env
2321
+	if err := execResult.Decode(stream); err != nil {
2322
+		return err
2323
+	}
2324
+
2325
+	execID := execResult.Get("Id")
2326
+
2327
+	if execID == "" {
2328
+		fmt.Fprintf(cli.out, "exec ID empty")
2329
+		return nil
2330
+	}
2331
+
2332
+	if execConfig.Detach {
2333
+		if _, _, err := readBody(cli.call("POST", "/exec/"+execID+"/start", execConfig, false)); err != nil {
2334
+			return err
2335
+		}
2336
+		return nil
2337
+	}
2338
+
2339
+	// Interactive exec requested.
2340
+	var (
2341
+		out, stderr io.Writer
2342
+		in          io.ReadCloser
2343
+		hijacked    = make(chan io.Closer)
2344
+		errCh       chan error
2345
+	)
2346
+
2347
+	// Block the return until the chan gets closed
2348
+	defer func() {
2349
+		log.Debugf("End of CmdExec(), Waiting for hijack to finish.")
2350
+		if _, ok := <-hijacked; ok {
2351
+			log.Errorf("Hijack did not finish (chan still open)")
2352
+		}
2353
+	}()
2354
+
2355
+	if execConfig.AttachStdin {
2356
+		in = cli.in
2357
+	}
2358
+	if execConfig.AttachStdout {
2359
+		out = cli.out
2360
+	}
2361
+	if execConfig.AttachStderr {
2362
+		if execConfig.Tty {
2363
+			stderr = cli.out
2364
+		} else {
2365
+			stderr = cli.err
2366
+		}
2367
+	}
2368
+	errCh = utils.Go(func() error {
2369
+		return cli.hijack("POST", "/exec/"+execID+"/start", execConfig.Tty, in, out, stderr, hijacked, execConfig)
2370
+	})
2371
+
2372
+	// Acknowledge the hijack before starting
2373
+	select {
2374
+	case closer := <-hijacked:
2375
+		// Make sure that hijack gets closed when returning. (result
2376
+		// in closing hijack chan and freeing server's goroutines.
2377
+		if closer != nil {
2378
+			defer closer.Close()
2379
+		}
2380
+	case err := <-errCh:
2381
+		if err != nil {
2382
+			log.Debugf("Error hijack: %s", err)
2383
+			return err
2384
+		}
2385
+	}
2386
+
2387
+	if execConfig.Tty && cli.isTerminal {
2388
+		if err := cli.monitorTtySize(execID, true); err != nil {
2389
+			log.Errorf("Error monitoring TTY size: %s", err)
2390
+		}
2391
+	}
2392
+
2393
+	if err := <-errCh; err != nil {
2394
+		log.Debugf("Error hijack: %s", err)
2395
+		return err
2396
+	}
2397
+
2398
+	return nil
2399
+}
... ...
@@ -115,6 +115,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b
115 115
 		}
116 116
 		return nil, resp.StatusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
117 117
 	}
118
+
118 119
 	return resp.Body, resp.StatusCode, nil
119 120
 }
120 121
 
... ...
@@ -179,7 +180,7 @@ func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in
179 179
 	return nil
180 180
 }
181 181
 
182
-func (cli *DockerCli) resizeTty(id string) {
182
+func (cli *DockerCli) resizeTty(id string, isExec bool) {
183 183
 	height, width := cli.getTtySize()
184 184
 	if height == 0 && width == 0 {
185 185
 		return
... ...
@@ -187,7 +188,15 @@ func (cli *DockerCli) resizeTty(id string) {
187 187
 	v := url.Values{}
188 188
 	v.Set("h", strconv.Itoa(height))
189 189
 	v.Set("w", strconv.Itoa(width))
190
-	if _, _, err := readBody(cli.call("POST", "/containers/"+id+"/resize?"+v.Encode(), nil, false)); err != nil {
190
+
191
+	path := ""
192
+	if !isExec {
193
+		path = "/containers/" + id + "/resize?"
194
+	} else {
195
+		path = "/exec/" + id + "/resize?"
196
+	}
197
+
198
+	if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, false)); err != nil {
191 199
 		log.Debugf("Error resize: %s", err)
192 200
 	}
193 201
 }
... ...
@@ -226,14 +235,14 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) {
226 226
 	return state.GetBool("Running"), state.GetInt("ExitCode"), nil
227 227
 }
228 228
 
229
-func (cli *DockerCli) monitorTtySize(id string) error {
230
-	cli.resizeTty(id)
229
+func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
230
+	cli.resizeTty(id, isExec)
231 231
 
232 232
 	sigchan := make(chan os.Signal, 1)
233 233
 	gosignal.Notify(sigchan, syscall.SIGWINCH)
234 234
 	go func() {
235 235
 		for _ = range sigchan {
236
-			cli.resizeTty(id)
236
+			cli.resizeTty(id, isExec)
237 237
 		}
238 238
 	}()
239 239
 	return nil
... ...
@@ -663,6 +663,7 @@ func postContainersCreate(eng *engine.Engine, version version.Version, w http.Re
663 663
 	}
664 664
 	out.Set("Id", engine.Tail(stdoutBuffer, 1))
665 665
 	out.SetList("Warnings", outWarnings)
666
+
666 667
 	return writeJSON(w, http.StatusCreated, out)
667 668
 }
668 669
 
... ...
@@ -793,7 +794,7 @@ func postContainersResize(eng *engine.Engine, version version.Version, w http.Re
793 793
 	if vars == nil {
794 794
 		return fmt.Errorf("Missing parameter")
795 795
 	}
796
-	if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w"), r.Form.Get("exec")).Run(); err != nil {
796
+	if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w")).Run(); err != nil {
797 797
 		return err
798 798
 	}
799 799
 	return nil
... ...
@@ -1060,11 +1061,9 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http.
1060 1060
 		job              = eng.Job("execStart", name)
1061 1061
 		errOut io.Writer = os.Stderr
1062 1062
 	)
1063
-
1064 1063
 	if err := job.DecodeEnv(r.Body); err != nil {
1065 1064
 		return err
1066 1065
 	}
1067
-
1068 1066
 	if !job.GetenvBool("Detach") {
1069 1067
 		// Setting up the streaming http interface.
1070 1068
 		inStream, outStream, err := hijackServer(w)
... ...
@@ -1102,12 +1101,12 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http.
1102 1102
 		errOut = outStream
1103 1103
 	}
1104 1104
 	// Now run the user process in container.
1105
+	job.SetCloseIO(false)
1105 1106
 	if err := job.Run(); err != nil {
1106 1107
 		fmt.Fprintf(errOut, "Error starting exec command in container %s: %s\n", name, err)
1107 1108
 		return err
1108 1109
 	}
1109 1110
 	w.WriteHeader(http.StatusNoContent)
1110
-
1111 1111
 	return nil
1112 1112
 }
1113 1113
 
... ...
@@ -206,7 +206,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
206 206
 		}()
207 207
 	}
208 208
 	if stderr != nil {
209
-		nJobs += 1
209
+		nJobs++
210 210
 		if p, err := streamConfig.StderrPipe(); err != nil {
211 211
 			errors <- err
212 212
 		} else {
... ...
@@ -229,7 +229,6 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
229 229
 				if err != nil {
230 230
 					log.Errorf("attach: stderr: %s", err)
231 231
 				}
232
-				log.Debugf("stdout attach end")
233 232
 				errors <- err
234 233
 			}()
235 234
 		}
... ...
@@ -20,7 +20,7 @@ import (
20 20
 type execConfig struct {
21 21
 	sync.Mutex
22 22
 	ID            string
23
-	Running bool
23
+	Running       bool
24 24
 	ProcessConfig execdriver.ProcessConfig
25 25
 	StreamConfig
26 26
 	OpenStdin  bool
... ...
@@ -130,7 +130,7 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status {
130 130
 		StreamConfig:  StreamConfig{},
131 131
 		ProcessConfig: processConfig,
132 132
 		Container:     container,
133
-		Running: false,
133
+		Running:       false,
134 134
 	}
135 135
 
136 136
 	d.registerExecCommand(execConfig)
... ...
@@ -141,8 +141,8 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status {
141 141
 }
142 142
 
143 143
 func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
144
-	if len(job.Args) != 2 {
145
-		return job.Errorf("Usage: %s [options] container exec", job.Name)
144
+	if len(job.Args) != 1 {
145
+		return job.Errorf("Usage: %s [options] exec", job.Name)
146 146
 	}
147 147
 
148 148
 	var (
... ...
@@ -165,11 +165,11 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
165 165
 		}
166 166
 		execConfig.Running = true
167 167
 	}()
168
-
169 168
 	if err != nil {
170 169
 		return job.Error(err)
171 170
 	}
172 171
 
172
+	log.Debugf("starting exec command %s in container %s", execConfig.ID, execConfig.Container.ID)
173 173
 	container := execConfig.Container
174 174
 
175 175
 	if execConfig.OpenStdin {
176 176
new file mode 100644
... ...
@@ -0,0 +1,29 @@
0
+% DOCKER(1) Docker User Manuals
1
+% Docker Community
2
+% SEPT 2014
3
+# NAME
4
+docker-exec - Run a command in an existing container
5
+
6
+# SYNOPSIS
7
+**docker exec**
8
+[**-d**|**--detach**[=*false*]]
9
+[**-i**|**--interactive**[=*false*]]
10
+[**-t**|**--tty**[=*false*]]
11
+ CONTAINER COMMAND [ARG...]
12
+
13
+# DESCRIPTION
14
+
15
+Run a process in an existing container. The existing CONTAINER needs to be active.
16
+
17
+# Options
18
+
19
+**-d**, **--detach**=*true*|*false*
20
+   Detached mode. This runs the new process in the background.
21
+
22
+**-i**, **--interactive**=*true*|*false*
23
+   When set to true, keep STDIN open even if not attached. The default is false.
24
+
25
+**-t**, **--tty**=*true*|*false*
26
+   When set to true Docker can allocate a pseudo-tty and attach to the standard
27
+input of the process. This can be used, for example, to run a throwaway
28
+interactive shell. The default value is false.
... ...
@@ -1295,6 +1295,36 @@ It is even useful to cherry-pick particular tags of an image repository
1295 1295
 
1296 1296
    $ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy
1297 1297
 
1298
+## exec
1299
+
1300
+    Usage: docker exec CONTAINER COMMAND [ARG...]
1301
+
1302
+    Run a command in an existing container
1303
+
1304
+      -d, --detach=false         Detached mode: run the process in the background and exit
1305
+      -i, --interactive=false    Keep STDIN open even if not attached
1306
+      -t, --tty=false            Allocate a pseudo-TTY
1307
+
1308
+The `docker exec` command runs a user specified command as a new process in an existing
1309
+user specified container. The container needs to be active.
1310
+
1311
+The `docker exec` command will typically be used after `docker run`.
1312
+
1313
+### Examples:
1314
+
1315
+    $ sudo docker run --name ubuntu_bash --rm -i -t ubuntu bash
1316
+
1317
+This will create a container named 'ubuntu_bash' and start a bash session.
1318
+
1319
+    $ sudo docker exec -d ubuntu_bash touch /tmp/execWorks
1320
+
1321
+This will create a new file '/tmp/execWorks' inside the existing and active container
1322
+'ubuntu_bash', in the background.
1323
+
1324
+    $ sudo docker exec ubuntu_bash -it bash
1325
+
1326
+This will create a new bash session in the container 'ubuntu_bash'.
1327
+
1298 1328
 ## search
1299 1329
 
1300 1330
 Search [Docker Hub](https://hub.docker.com) for images
... ...
@@ -115,13 +115,14 @@ func (eng *Engine) commands() []string {
115 115
 // This function mimics `Command` from the standard os/exec package.
116 116
 func (eng *Engine) Job(name string, args ...string) *Job {
117 117
 	job := &Job{
118
-		Eng:    eng,
119
-		Name:   name,
120
-		Args:   args,
121
-		Stdin:  NewInput(),
122
-		Stdout: NewOutput(),
123
-		Stderr: NewOutput(),
124
-		env:    &Env{},
118
+		Eng:     eng,
119
+		Name:    name,
120
+		Args:    args,
121
+		Stdin:   NewInput(),
122
+		Stdout:  NewOutput(),
123
+		Stderr:  NewOutput(),
124
+		env:     &Env{},
125
+		closeIO: true,
125 126
 	}
126 127
 	if eng.Logging {
127 128
 		job.Stderr.Add(ioutils.NopWriteCloser(eng.Stderr))
... ...
@@ -31,6 +31,7 @@ type Job struct {
31 31
 	handler Handler
32 32
 	status  Status
33 33
 	end     time.Time
34
+	closeIO bool
34 35
 }
35 36
 
36 37
 type Status int
... ...
@@ -78,19 +79,22 @@ func (job *Job) Run() error {
78 78
 		job.status = job.handler(job)
79 79
 		job.end = time.Now()
80 80
 	}
81
-	// Wait for all background tasks to complete
82
-	if err := job.Stdout.Close(); err != nil {
83
-		return err
84
-	}
85
-	if err := job.Stderr.Close(); err != nil {
86
-		return err
87
-	}
88
-	if err := job.Stdin.Close(); err != nil {
89
-		return err
81
+	if job.closeIO {
82
+		// Wait for all background tasks to complete
83
+		if err := job.Stdout.Close(); err != nil {
84
+			return err
85
+		}
86
+		if err := job.Stderr.Close(); err != nil {
87
+			return err
88
+		}
89
+		if err := job.Stdin.Close(); err != nil {
90
+			return err
91
+		}
90 92
 	}
91 93
 	if job.status != 0 {
92 94
 		return fmt.Errorf("%s", Tail(errorMessage, 1))
93 95
 	}
96
+
94 97
 	return nil
95 98
 }
96 99
 
... ...
@@ -228,3 +232,7 @@ func (job *Job) Error(err error) Status {
228 228
 func (job *Job) StatusCode() int {
229 229
 	return int(job.status)
230 230
 }
231
+
232
+func (job *Job) SetCloseIO(val bool) {
233
+	job.closeIO = val
234
+}