Skip to content

Commit c786a8e

Browse files
committed
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 <[email protected]> (github: vishh)
1 parent 3903038 commit c786a8e

9 files changed

Lines changed: 208 additions & 35 deletions

File tree

api/client/commands.go

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
653653

654654
if *openStdin || *attach {
655655
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 {
657657
log.Errorf("Error monitoring TTY size: %s", err)
658658
}
659659
}
@@ -1805,7 +1805,7 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
18051805
)
18061806

18071807
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 {
18091809
log.Debugf("Error monitoring TTY size: %s", err)
18101810
}
18111811
}
@@ -2136,7 +2136,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
21362136
}
21372137

21382138
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 {
21402140
log.Errorf("Error monitoring TTY size: %s", err)
21412141
}
21422142
}
@@ -2299,3 +2299,101 @@ func (cli *DockerCli) CmdLoad(args ...string) error {
22992299
}
23002300
return nil
23012301
}
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+
}

api/client/utils.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ func (cli *DockerCli) call(method, path string, data interface{}, passAuthInfo b
115115
}
116116
return nil, resp.StatusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
117117
}
118+
118119
return resp.Body, resp.StatusCode, nil
119120
}
120121

@@ -179,15 +180,23 @@ func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in
179180
return nil
180181
}
181182

182-
func (cli *DockerCli) resizeTty(id string) {
183+
func (cli *DockerCli) resizeTty(id string, isExec bool) {
183184
height, width := cli.getTtySize()
184185
if height == 0 && width == 0 {
185186
return
186187
}
187188
v := url.Values{}
188189
v.Set("h", strconv.Itoa(height))
189190
v.Set("w", strconv.Itoa(width))
190-
if _, _, err := readBody(cli.call("POST", "/containers/"+id+"/resize?"+v.Encode(), nil, false)); err != nil {
191+
192+
path := ""
193+
if !isExec {
194+
path = "/containers/" + id + "/resize?"
195+
} else {
196+
path = "/exec/" + id + "/resize?"
197+
}
198+
199+
if _, _, err := readBody(cli.call("POST", path+v.Encode(), nil, false)); err != nil {
191200
log.Debugf("Error resize: %s", err)
192201
}
193202
}
@@ -226,14 +235,14 @@ func getExitCode(cli *DockerCli, containerId string) (bool, int, error) {
226235
return state.GetBool("Running"), state.GetInt("ExitCode"), nil
227236
}
228237

229-
func (cli *DockerCli) monitorTtySize(id string) error {
230-
cli.resizeTty(id)
238+
func (cli *DockerCli) monitorTtySize(id string, isExec bool) error {
239+
cli.resizeTty(id, isExec)
231240

232241
sigchan := make(chan os.Signal, 1)
233242
gosignal.Notify(sigchan, syscall.SIGWINCH)
234243
go func() {
235244
for _ = range sigchan {
236-
cli.resizeTty(id)
245+
cli.resizeTty(id, isExec)
237246
}
238247
}()
239248
return nil

api/server/server.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,7 @@ func postContainersCreate(eng *engine.Engine, version version.Version, w http.Re
663663
}
664664
out.Set("Id", engine.Tail(stdoutBuffer, 1))
665665
out.SetList("Warnings", outWarnings)
666+
666667
return writeJSON(w, http.StatusCreated, out)
667668
}
668669

@@ -793,7 +794,7 @@ func postContainersResize(eng *engine.Engine, version version.Version, w http.Re
793794
if vars == nil {
794795
return fmt.Errorf("Missing parameter")
795796
}
796-
if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w"), r.Form.Get("exec")).Run(); err != nil {
797+
if err := eng.Job("resize", vars["name"], r.Form.Get("h"), r.Form.Get("w")).Run(); err != nil {
797798
return err
798799
}
799800
return nil
@@ -1060,11 +1061,9 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http.
10601061
job = eng.Job("execStart", name)
10611062
errOut io.Writer = os.Stderr
10621063
)
1063-
10641064
if err := job.DecodeEnv(r.Body); err != nil {
10651065
return err
10661066
}
1067-
10681067
if !job.GetenvBool("Detach") {
10691068
// Setting up the streaming http interface.
10701069
inStream, outStream, err := hijackServer(w)
@@ -1102,12 +1101,12 @@ func postContainerExecStart(eng *engine.Engine, version version.Version, w http.
11021101
errOut = outStream
11031102
}
11041103
// Now run the user process in container.
1104+
job.SetCloseIO(false)
11051105
if err := job.Run(); err != nil {
11061106
fmt.Fprintf(errOut, "Error starting exec command in container %s: %s\n", name, err)
11071107
return err
11081108
}
11091109
w.WriteHeader(http.StatusNoContent)
1110-
11111110
return nil
11121111
}
11131112

daemon/attach.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
206206
}()
207207
}
208208
if stderr != nil {
209-
nJobs += 1
209+
nJobs++
210210
if p, err := streamConfig.StderrPipe(); err != nil {
211211
errors <- err
212212
} else {
@@ -229,7 +229,6 @@ func (daemon *Daemon) Attach(streamConfig *StreamConfig, openStdin, stdinOnce, t
229229
if err != nil {
230230
log.Errorf("attach: stderr: %s", err)
231231
}
232-
log.Debugf("stdout attach end")
233232
errors <- err
234233
}()
235234
}

daemon/exec.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
type execConfig struct {
2121
sync.Mutex
2222
ID string
23-
Running bool
23+
Running bool
2424
ProcessConfig execdriver.ProcessConfig
2525
StreamConfig
2626
OpenStdin bool
@@ -130,7 +130,7 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status {
130130
StreamConfig: StreamConfig{},
131131
ProcessConfig: processConfig,
132132
Container: container,
133-
Running: false,
133+
Running: false,
134134
}
135135

136136
d.registerExecCommand(execConfig)
@@ -141,8 +141,8 @@ func (d *Daemon) ContainerExecCreate(job *engine.Job) engine.Status {
141141
}
142142

143143
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)
146146
}
147147

148148
var (
@@ -165,11 +165,11 @@ func (d *Daemon) ContainerExecStart(job *engine.Job) engine.Status {
165165
}
166166
execConfig.Running = true
167167
}()
168-
169168
if err != nil {
170169
return job.Error(err)
171170
}
172171

172+
log.Debugf("starting exec command %s in container %s", execConfig.ID, execConfig.Container.ID)
173173
container := execConfig.Container
174174

175175
if execConfig.OpenStdin {

docs/man/docker-exec.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
% DOCKER(1) Docker User Manuals
2+
% Docker Community
3+
% SEPT 2014
4+
# NAME
5+
docker-exec - Run a command in an existing container
6+
7+
# SYNOPSIS
8+
**docker exec**
9+
[**-d**|**--detach**[=*false*]]
10+
[**-i**|**--interactive**[=*false*]]
11+
[**-t**|**--tty**[=*false*]]
12+
CONTAINER COMMAND [ARG...]
13+
14+
# DESCRIPTION
15+
16+
Run a process in an existing container. The existing CONTAINER needs to be active.
17+
18+
# Options
19+
20+
**-d**, **--detach**=*true*|*false*
21+
Detached mode. This runs the new process in the background.
22+
23+
**-i**, **--interactive**=*true*|*false*
24+
When set to true, keep STDIN open even if not attached. The default is false.
25+
26+
**-t**, **--tty**=*true*|*false*
27+
When set to true Docker can allocate a pseudo-tty and attach to the standard
28+
input of the process. This can be used, for example, to run a throwaway
29+
interactive shell. The default value is false.

docs/sources/reference/commandline/cli.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,36 @@ It is even useful to cherry-pick particular tags of an image repository
12951295

12961296
$ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy
12971297

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+
12981328
## search
12991329

13001330
Search [Docker Hub](https://hub.docker.com) for images

engine/engine.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ func (eng *Engine) commands() []string {
115115
// This function mimics `Command` from the standard os/exec package.
116116
func (eng *Engine) Job(name string, args ...string) *Job {
117117
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,
125126
}
126127
if eng.Logging {
127128
job.Stderr.Add(ioutils.NopWriteCloser(eng.Stderr))

0 commit comments

Comments
 (0)