diff --git a/README.md b/README.md index 66ca656f68..c186d9a063 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,12 @@ docker pull base docker run -i -t base /bin/bash ``` +Detaching from the interactive shell +------------------------------------ +``` +# In order to detach without killing the shell, you can use the escape sequence Ctrl-p + Ctrl-q +# Note: this works only in tty mode (run with -t option). +``` Starting a long-running worker process -------------------------------------- @@ -183,7 +189,9 @@ JOB=$(docker run -d -p 4444 base /bin/nc -l -p 4444) PORT=$(docker port $JOB 4444) # Connect to the public port via the host's public address -echo hello world | nc $(hostname) $PORT +# Please note that because of how routing works connecting to localhost or 127.0.0.1 $PORT will not work. +IP=$(ifconfig eth0 | perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }') +echo hello world | nc $IP $PORT # Verify that the network connection worked echo "Daemon received: $(docker logs $JOB)" diff --git a/commands.go b/commands.go index 0bc5583738..20d6b45c97 100644 --- a/commands.go +++ b/commands.go @@ -18,7 +18,7 @@ import ( "unicode" ) -const VERSION = "0.1.3" +const VERSION = "0.1.4" var GIT_COMMIT string @@ -62,7 +62,7 @@ func (srv *Server) Help() string { } // 'docker login': login / register a user to registry service. -func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { // Read a line on raw terminal with support for simple backspace // sequences and echo. // @@ -113,6 +113,8 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout io.Writer, args ...strin return readStringOnRawTerminal(stdin, stdout, false) } + stdout.SetOptionRawTerminal() + cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server") if err := cmd.Parse(args); err != nil { return nil @@ -417,7 +419,8 @@ func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } -func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdImport(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { + stdout.Flush() cmd := rcli.Subcmd(stdout, "import", "[OPTIONS] URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball") var archive io.Reader var resp *http.Response @@ -464,7 +467,7 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (srv *Server) CmdPush(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdPush(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "push", "NAME", "Push an image or a repository to the registry") if err := cmd.Parse(args); err != nil { return nil @@ -784,7 +787,7 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string return fmt.Errorf("No such container: %s", cmd.Arg(0)) } -func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { cmd := rcli.Subcmd(stdout, "attach", "CONTAINER", "Attach to a running container") if err := cmd.Parse(args); err != nil { return nil @@ -799,6 +802,11 @@ func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...stri return fmt.Errorf("No such container: %s", name) } + if container.Config.Tty { + stdout.SetOptionRawTerminal() + } + // Flush the options to make sure the client sets the raw mode + stdout.Flush() return <-container.Attach(stdin, nil, stdout, stdout) } @@ -870,7 +878,7 @@ func (srv *Server) CmdTag(stdin io.ReadCloser, stdout io.Writer, args ...string) return srv.runtime.repositories.Set(cmd.Arg(1), cmd.Arg(2), cmd.Arg(0), *force) } -func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func (srv *Server) CmdRun(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { config, err := ParseRun(args, stdout) if err != nil { return err @@ -884,6 +892,13 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) return fmt.Errorf("Command not specified") } + if config.Tty { + stdout.SetOptionRawTerminal() + } + // Flush the options to make sure the client sets the raw mode + // or tell the client there is no options + stdout.Flush() + // Create new container container, err := srv.runtime.Create(config) if err != nil { diff --git a/commands_test.go b/commands_test.go index a68aea7a46..30e2579d20 100644 --- a/commands_test.go +++ b/commands_test.go @@ -2,8 +2,8 @@ package docker import ( "bufio" - "bytes" "fmt" + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "strings" @@ -69,15 +69,27 @@ func TestRunHostname(t *testing.T) { srv := &Server{runtime: runtime} - var stdin, stdout bytes.Buffer - setTimeout(t, "CmdRun timed out", 2*time.Second, func() { - if err := srv.CmdRun(ioutil.NopCloser(&stdin), &nopWriteCloser{&stdout}, "-h", "foobar", GetTestImage(runtime).Id, "hostname"); err != nil { + stdin, _ := io.Pipe() + stdout, stdoutPipe := io.Pipe() + + c := make(chan struct{}) + go func() { + if err := srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-h", "foobar", GetTestImage(runtime).Id, "hostname"); err != nil { t.Fatal(err) } - }) - if output := string(stdout.Bytes()); output != "foobar\n" { - t.Fatalf("'hostname' should display '%s', not '%s'", "foobar\n", output) + close(c) + }() + cmdOutput, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) } + if cmdOutput != "foobar\n" { + t.Fatalf("'hostname' should display '%s', not '%s'", "foobar\n", cmdOutput) + } + + setTimeout(t, "CmdRun timed out", 2*time.Second, func() { + <-c + }) } func TestRunExit(t *testing.T) { @@ -93,7 +105,7 @@ func TestRunExit(t *testing.T) { stdout, stdoutPipe := io.Pipe() c1 := make(chan struct{}) go func() { - srv.CmdRun(stdin, stdoutPipe, "-i", GetTestImage(runtime).Id, "/bin/cat") + srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-i", GetTestImage(runtime).Id, "/bin/cat") close(c1) }() @@ -147,7 +159,7 @@ func TestRunDisconnect(t *testing.T) { go func() { // We're simulating a disconnect so the return value doesn't matter. What matters is the // fact that CmdRun returns. - srv.CmdRun(stdin, stdoutPipe, "-i", GetTestImage(runtime).Id, "/bin/cat") + srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-i", GetTestImage(runtime).Id, "/bin/cat") close(c1) }() @@ -179,10 +191,56 @@ func TestRunDisconnect(t *testing.T) { }) } +// Expected behaviour: the process dies when the client disconnects +func TestRunDisconnectTty(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + stdin, stdinPipe := io.Pipe() + stdout, stdoutPipe := io.Pipe() + c1 := make(chan struct{}) + go func() { + // We're simulating a disconnect so the return value doesn't matter. What matters is the + // fact that CmdRun returns. + srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-i", "-t", GetTestImage(runtime).Id, "/bin/cat") + close(c1) + }() + + setTimeout(t, "Read/Write assertion timed out", 2*time.Second, func() { + if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil { + t.Fatal(err) + } + }) + + // Close pipes (simulate disconnect) + if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + t.Fatal(err) + } + + // as the pipes are close, we expect the process to die, + // therefore CmdRun to unblock. Wait for CmdRun + setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() { + <-c1 + }) + + // Client disconnect after run -i should keep stdin out in TTY mode + container := runtime.List()[0] + // Give some time to monitor to do his thing + container.WaitTimeout(500 * time.Millisecond) + if !container.State.Running { + t.Fatalf("/bin/cat should still be running after closing stdin (tty mode)") + } +} + // TestAttachStdin checks attaching to stdin without stdout and stderr. // 'docker run -i -a stdin' should sends the client's stdin to the command, // then detach from it and print the container id. -func TestAttachStdin(t *testing.T) { +func TestRunAttachStdin(t *testing.T) { runtime, err := newTestRuntime() if err != nil { t.Fatal(err) @@ -190,31 +248,41 @@ func TestAttachStdin(t *testing.T) { defer nuke(runtime) srv := &Server{runtime: runtime} - stdinR, stdinW := io.Pipe() - var stdout bytes.Buffer + stdin, stdinPipe := io.Pipe() + stdout, stdoutPipe := io.Pipe() ch := make(chan struct{}) go func() { - srv.CmdRun(stdinR, &stdout, "-i", "-a", "stdin", GetTestImage(runtime).Id, "sh", "-c", "echo hello; cat") + srv.CmdRun(stdin, rcli.NewDockerLocalConn(stdoutPipe), "-i", "-a", "stdin", GetTestImage(runtime).Id, "sh", "-c", "echo hello; cat") close(ch) }() - // Send input to the command, close stdin, wait for CmdRun to return - setTimeout(t, "Read/Write timed out", 2*time.Second, func() { - if _, err := stdinW.Write([]byte("hi there\n")); err != nil { + // Send input to the command, close stdin + setTimeout(t, "Write timed out", 2*time.Second, func() { + if _, err := stdinPipe.Write([]byte("hi there\n")); err != nil { + t.Fatal(err) + } + if err := stdinPipe.Close(); err != nil { t.Fatal(err) } - stdinW.Close() - <-ch }) - // Check output - cmdOutput := string(stdout.Bytes()) container := runtime.List()[0] + + // Check output + cmdOutput, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } if cmdOutput != container.ShortId()+"\n" { t.Fatalf("Wrong output: should be '%s', not '%s'\n", container.ShortId()+"\n", cmdOutput) } + // wait for CmdRun to return + setTimeout(t, "Waiting for CmdRun timed out", 2*time.Second, func() { + <-ch + }) + setTimeout(t, "Waiting for command to exit timed out", 2*time.Second, func() { container.Wait() }) @@ -270,7 +338,7 @@ func TestAttachDisconnect(t *testing.T) { go func() { // We're simulating a disconnect so the return value doesn't matter. What matters is the // fact that CmdAttach returns. - srv.CmdAttach(stdin, stdoutPipe, container.Id) + srv.CmdAttach(stdin, rcli.NewDockerLocalConn(stdoutPipe), container.Id) close(c1) }() diff --git a/container.go b/container.go index 76e9f5a2d7..f180c7559b 100644 --- a/container.go +++ b/container.go @@ -40,11 +40,11 @@ type Container struct { stdin io.ReadCloser stdinPipe io.WriteCloser - ptyStdinMaster io.Closer - ptyStdoutMaster io.Closer - ptyStderrMaster io.Closer + ptyMaster io.Closer runtime *Runtime + + waitLock chan struct{} } type Config struct { @@ -180,63 +180,37 @@ func (container *Container) generateLXCConfig() error { } func (container *Container) startPty() error { - stdoutMaster, stdoutSlave, err := pty.Open() + ptyMaster, ptySlave, err := pty.Open() if err != nil { return err } - container.ptyStdoutMaster = stdoutMaster - container.cmd.Stdout = stdoutSlave - - stderrMaster, stderrSlave, err := pty.Open() - if err != nil { - return err - } - container.ptyStderrMaster = stderrMaster - container.cmd.Stderr = stderrSlave + container.ptyMaster = ptyMaster + container.cmd.Stdout = ptySlave + container.cmd.Stderr = ptySlave // Copy the PTYs to our broadcasters go func() { defer container.stdout.CloseWriters() Debugf("[startPty] Begin of stdout pipe") - io.Copy(container.stdout, stdoutMaster) + io.Copy(container.stdout, ptyMaster) Debugf("[startPty] End of stdout pipe") }() - go func() { - defer container.stderr.CloseWriters() - Debugf("[startPty] Begin of stderr pipe") - io.Copy(container.stderr, stderrMaster) - Debugf("[startPty] End of stderr pipe") - }() - // stdin - var stdinSlave io.ReadCloser if container.Config.OpenStdin { - var stdinMaster io.WriteCloser - stdinMaster, stdinSlave, err = pty.Open() - if err != nil { - return err - } - container.ptyStdinMaster = stdinMaster - container.cmd.Stdin = stdinSlave - // FIXME: The following appears to be broken. - // "cannot set terminal process group (-1): Inappropriate ioctl for device" - // container.cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true} + container.cmd.Stdin = ptySlave + container.cmd.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true} go func() { defer container.stdin.Close() Debugf("[startPty] Begin of stdin pipe") - io.Copy(stdinMaster, container.stdin) + io.Copy(ptyMaster, container.stdin) Debugf("[startPty] End of stdin pipe") }() } if err := container.cmd.Start(); err != nil { return err } - stdoutSlave.Close() - stderrSlave.Close() - if stdinSlave != nil { - stdinSlave.Close() - } + ptySlave.Close() return nil } @@ -278,10 +252,14 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s if cStderr != nil { defer cStderr.Close() } - if container.Config.StdinOnce { + if container.Config.StdinOnce && !container.Config.Tty { defer cStdin.Close() } - _, err := io.Copy(cStdin, stdin) + if container.Config.Tty { + _, err = CopyEscapable(cStdin, stdin) + } else { + _, err = io.Copy(cStdin, stdin) + } if err != nil { Debugf("[error] attach stdin: %s\n", err) } @@ -365,6 +343,9 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s } func (container *Container) Start() error { + container.State.lock() + defer container.State.unlock() + if container.State.Running { return fmt.Errorf("The container %s is already running.", container.Id) } @@ -431,6 +412,9 @@ func (container *Container) Start() error { // FIXME: save state on disk *first*, then converge // this way disk state is used as a journal, eg. we can restore after crash etc. container.State.setRunning(container.cmd.Process.Pid) + + // Init the lock + container.waitLock = make(chan struct{}) container.ToDisk() go container.monitor() return nil @@ -530,19 +514,9 @@ func (container *Container) monitor() { Debugf("%s: Error close stderr: %s", container.Id, err) } - if container.ptyStdinMaster != nil { - if err := container.ptyStdinMaster.Close(); err != nil { - Debugf("%s: Error close pty stdin master: %s", container.Id, err) - } - } - if container.ptyStdoutMaster != nil { - if err := container.ptyStdoutMaster.Close(); err != nil { - Debugf("%s: Error close pty stdout master: %s", container.Id, err) - } - } - if container.ptyStderrMaster != nil { - if err := container.ptyStderrMaster.Close(); err != nil { - Debugf("%s: Error close pty stderr master: %s", container.Id, err) + if container.ptyMaster != nil { + if err := container.ptyMaster.Close(); err != nil { + Debugf("%s: Error closing Pty master: %s", container.Id, err) } } @@ -557,6 +531,10 @@ func (container *Container) monitor() { // Report status back container.State.setStopped(exitCode) + + // Release the lock + close(container.waitLock) + if err := container.ToDisk(); err != nil { // FIXME: there is a race condition here which causes this to fail during the unit tests. // If another goroutine was waiting for Wait() to return before removing the container's root @@ -569,7 +547,7 @@ func (container *Container) monitor() { } func (container *Container) kill() error { - if container.cmd == nil { + if !container.State.Running || container.cmd == nil { return nil } if err := container.cmd.Process.Kill(); err != nil { @@ -581,13 +559,14 @@ func (container *Container) kill() error { } func (container *Container) Kill() error { - if !container.State.Running { - return nil - } + container.State.lock() + defer container.State.unlock() return container.kill() } func (container *Container) Stop() error { + container.State.lock() + defer container.State.unlock() if !container.State.Running { return nil } @@ -596,7 +575,7 @@ func (container *Container) Stop() error { if output, err := exec.Command("lxc-kill", "-n", container.Id, "15").CombinedOutput(); err != nil { log.Print(string(output)) log.Print("Failed to send SIGTERM to the process, force killing") - if err := container.Kill(); err != nil { + if err := container.kill(); err != nil { return err } } @@ -604,7 +583,7 @@ func (container *Container) Stop() error { // 2. Wait for the process to exit on its own if err := container.WaitTimeout(10 * time.Second); err != nil { log.Printf("Container %v failed to exit within 10 seconds of SIGTERM - using the force", container.Id) - if err := container.Kill(); err != nil { + if err := container.kill(); err != nil { return err } } @@ -623,10 +602,7 @@ func (container *Container) Restart() error { // Wait blocks until the container stops running, then returns its exit code. func (container *Container) Wait() int { - - for container.State.Running { - container.State.wait() - } + <-container.waitLock return container.State.ExitCode } diff --git a/container_test.go b/container_test.go index 571a093767..ac47f84bf0 100644 --- a/container_test.go +++ b/container_test.go @@ -267,6 +267,7 @@ func TestStart(t *testing.T) { // Try to avoid the timeoout in destroy. Best effort, don't check error cStdin, _ := container.StdinPipe() cStdin.Close() + container.WaitTimeout(2 * time.Second) } func TestRun(t *testing.T) { diff --git a/docker/docker.go b/docker/docker.go index c9c599954b..1b1c21990d 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -8,7 +8,6 @@ import ( "io" "log" "os" - "os/signal" ) var GIT_COMMIT string @@ -57,29 +56,21 @@ func daemon() error { } func runCommand(args []string) error { - var oldState *term.State - var err error - if term.IsTerminal(int(os.Stdin.Fd())) && os.Getenv("NORAW") == "" { - oldState, err = term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return err - } - defer term.Restore(int(os.Stdin.Fd()), oldState) - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - for _ = range c { - term.Restore(int(os.Stdin.Fd()), oldState) - log.Printf("\nSIGINT received\n") - os.Exit(0) - } - }() - } // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose // CloseWrite(), which we need to cleanly signal that stdin is closed without // closing the connection. // See http://code.google.com/p/go/issues/detail?id=3345 if conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...); err == nil { + options := conn.GetOptions() + if options.RawTerminal && + term.IsTerminal(int(os.Stdin.Fd())) && + os.Getenv("NORAW") == "" { + if oldState, err := rcli.SetRawTerminal(); err != nil { + return err + } else { + defer rcli.RestoreTerminal(oldState) + } + } receiveStdout := docker.Go(func() error { _, err := io.Copy(os.Stdout, conn) return err @@ -104,12 +95,11 @@ func runCommand(args []string) error { if err != nil { return err } - if err := rcli.LocalCall(service, os.Stdin, os.Stdout, args...); err != nil { + dockerConn := rcli.NewDockerLocalConn(os.Stdout) + defer dockerConn.Close() + if err := rcli.LocalCall(service, os.Stdin, dockerConn, args...); err != nil { return err } } - if oldState != nil { - term.Restore(int(os.Stdin.Fd()), oldState) - } return nil } diff --git a/docs/README.md b/docs/README.md index 7a5ee3f8a9..fce7f238fb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -39,4 +39,36 @@ Notes * The index.html and gettingstarted.html files are copied from the source dir to the output dir without modification. So changes to those pages should be made directly in html * For the template the css is compiled from less. When changes are needed they can be compiled using -lessc ``lessc main.less`` or watched using watch-lessc ``watch-lessc -i main.less -o main.css`` \ No newline at end of file +lessc ``lessc main.less`` or watched using watch-lessc ``watch-lessc -i main.less -o main.css`` + + +Guides on using sphinx +---------------------- +* To make links to certain pages create a link target like so: + + ``` + .. _hello_world: + + Hello world + =========== + + This is.. (etc.) + ``` + + The ``_hello_world:`` will make it possible to link to this position (page and marker) from all other pages. + +* Notes, warnings and alarms + + ``` + # a note (use when something is important) + .. note:: + + # a warning (orange) + .. warning:: + + # danger (red, use sparsely) + .. danger:: + +* Code examples + + Start without $, so it's easy to copy and paste. \ No newline at end of file diff --git a/docs/sources/commandline/basics.rst b/docs/sources/commandline/basics.rst index 5da2012da5..8dd8ec9de3 100644 --- a/docs/sources/commandline/basics.rst +++ b/docs/sources/commandline/basics.rst @@ -69,7 +69,8 @@ Expose a service on a TCP port # Connect to the public port via the host's public address # Please note that because of how routing works connecting to localhost or 127.0.0.1 $PORT will not work. - echo hello world | nc $(hostname) $PORT + IP=$(ifconfig eth0 | perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }') + echo hello world | nc $IP $PORT # Verify that the network connection worked echo "Daemon received: $(docker logs $JOB)" diff --git a/docs/sources/examples/example_header.inc b/docs/sources/examples/example_header.inc new file mode 100644 index 0000000000..607421fc13 --- /dev/null +++ b/docs/sources/examples/example_header.inc @@ -0,0 +1,4 @@ + +.. note:: + + This example assumes you have Docker running in daemon mode. For more information please see :ref:`running_examples` diff --git a/docs/sources/examples/hello_world.rst b/docs/sources/examples/hello_world.rst index f7d455f803..1d391f5fb1 100644 --- a/docs/sources/examples/hello_world.rst +++ b/docs/sources/examples/hello_world.rst @@ -6,8 +6,10 @@ Hello World =========== -This is the most basic example available for using Docker. The example assumes you have Docker installed. +.. include:: example_header.inc + +This is the most basic example available for using Docker. Download the base container diff --git a/docs/sources/examples/hello_world_daemon.rst b/docs/sources/examples/hello_world_daemon.rst index 8453952058..7ca251aec8 100644 --- a/docs/sources/examples/hello_world_daemon.rst +++ b/docs/sources/examples/hello_world_daemon.rst @@ -6,6 +6,9 @@ Hello World Daemon ================== + +.. include:: example_header.inc + The most boring daemon ever written. This example assumes you have Docker installed and with the base image already imported ``docker pull base``. @@ -18,7 +21,7 @@ out every second. It will continue to do this until we stop it. CONTAINER_ID=$(docker run -d base /bin/sh -c "while true; do echo hello world; sleep 1; done") -We are going to run a simple hello world daemon in a new container made from the busybox daemon. +We are going to run a simple hello world daemon in a new container made from the base image. - **"docker run -d "** run a command in a new container. We pass "-d" so it runs as a daemon. - **"base"** is the image we want to run the command inside of. diff --git a/docs/sources/examples/index.rst b/docs/sources/examples/index.rst index e01f662002..6a616ec8ff 100644 --- a/docs/sources/examples/index.rst +++ b/docs/sources/examples/index.rst @@ -12,6 +12,7 @@ Contents: .. toctree:: :maxdepth: 1 + running_examples hello_world hello_world_daemon python_web_app diff --git a/docs/sources/examples/python_web_app.rst b/docs/sources/examples/python_web_app.rst index 5f7a07ea1e..7b7edc05e3 100644 --- a/docs/sources/examples/python_web_app.rst +++ b/docs/sources/examples/python_web_app.rst @@ -6,6 +6,9 @@ Building a python web app ========================= + +.. include:: example_header.inc + The goal of this example is to show you how you can author your own docker images using a parent image, making changes to it, and then saving the results as a new image. We will do that by making a simple hello flask web application image. **Steps:** @@ -45,6 +48,11 @@ Save the changed we just made in the container to a new image called "_/builds/g WEB_WORKER=$(docker run -d -p 5000 $BUILD_IMG /usr/local/bin/runapp) +- **"docker run -d "** run a command in a new container. We pass "-d" so it runs as a daemon. + **"-p 5000"* the web app is going to listen on this port, so it must be mapped from the container to the host system. +- **"$BUILD_IMG"** is the image we want to run the command inside of. +- **/usr/local/bin/runapp** is the command which starts the web app. + Use the new image we just created and create a new container with network port 5000, and return the container id and store in the WEB_WORKER variable. .. code-block:: bash @@ -54,6 +62,18 @@ Use the new image we just created and create a new container with network port 5 view the logs for the new container using the WEB_WORKER variable, and if everything worked as planned you should see the line "Running on http://0.0.0.0:5000/" in the log output. +.. code-block:: bash + + WEB_PORT=$(docker port $WEB_WORKER 5000) + +lookup the public-facing port which is NAT-ed store the private port used by the container and store it inside of the WEB_PORT variable. + +.. code-block:: bash + + curl http://`hostname`:$WEB_PORT + Hello world! + +access the web app using curl. If everything worked as planned you should see the line "Hello world!" inside of your console. **Video:** diff --git a/docs/sources/examples/running_examples.rst b/docs/sources/examples/running_examples.rst new file mode 100644 index 0000000000..4042add487 --- /dev/null +++ b/docs/sources/examples/running_examples.rst @@ -0,0 +1,33 @@ +:title: Running the Examples +:description: An overview on how to run the docker examples +:keywords: docker, examples, how to + +.. _running_examples: + +Running The Examples +-------------------- + +There are two ways to run docker, daemon mode and standalone mode. + +When you run the docker command it will first check if there is a docker daemon running in the background it can connect to. + +* If it exists it will use that daemon to run all of the commands. +* If it does not exist docker will run in standalone mode (docker will exit after each command). + +Docker needs to be run from a privileged account (root). + +1. The most common (and recommended) way is to run a docker daemon as root in the background, and then connect to it from the docker client from any account. + + .. code-block:: bash + + # starting docker daemon in the background + sudo docker -d & + + # now you can run docker commands from any account. + docker + +2. Standalone: You need to run every command as root, or using sudo + + .. code-block:: bash + + sudo docker diff --git a/docs/sources/examples/running_ssh_service.rst b/docs/sources/examples/running_ssh_service.rst index 23d2d41c2f..f418b45266 100644 --- a/docs/sources/examples/running_ssh_service.rst +++ b/docs/sources/examples/running_ssh_service.rst @@ -7,7 +7,7 @@ Create an ssh daemon service ============================ - +.. include:: example_header.inc **Video:** diff --git a/docs/theme/docker/static/css/main.css b/docs/theme/docker/static/css/main.css index 1e37fd06c9..a6ef9451ad 100755 --- a/docs/theme/docker/static/css/main.css +++ b/docs/theme/docker/static/css/main.css @@ -82,7 +82,7 @@ h4 { .btn-custom { background-color: #292929 !important; background-repeat: repeat-x; - filter: progid:dximagetransform.microsoft.gradient(startColorstr="#515151", endColorstr="#282828"); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#515151", endColorstr="#282828"); background-image: -khtml-gradient(linear, left top, left bottom, from(#515151), to(#282828)); background-image: -moz-linear-gradient(top, #515151, #282828); background-image: -ms-linear-gradient(top, #515151, #282828); @@ -131,6 +131,27 @@ section.header { margin: 15px 15px 15px 0; border: 2px solid gray; } +.admonition { + padding: 10px; + border: 1px solid grey; + margin-bottom: 10px; + margin-top: 10px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.admonition .admonition-title { + font-weight: bold; +} +.admonition.note { + background-color: #f1ebba; +} +.admonition.warning { + background-color: #eed9af; +} +.admonition.danger { + background-color: #e9bcab; +} /* =================== left navigation ===================== */ diff --git a/docs/theme/docker/static/css/main.less b/docs/theme/docker/static/css/main.less index 100f7b418d..69f53f9e1b 100644 --- a/docs/theme/docker/static/css/main.less +++ b/docs/theme/docker/static/css/main.less @@ -179,7 +179,33 @@ section.header { border: 2px solid gray; } +.admonition { + padding: 10px; + border: 1px solid grey; + margin-bottom: 10px; + margin-top: 10px; + + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.admonition .admonition-title { + font-weight: bold; +} + +.admonition.note { + background-color: rgb(241, 235, 186); +} + +.admonition.warning { + background-color: rgb(238, 217, 175); +} + +.admonition.danger { + background-color: rgb(233, 188, 171); +} /* =================== left navigation diff --git a/network.go b/network.go index f6804cfcd7..9164c1d72e 100644 --- a/network.go +++ b/network.go @@ -111,6 +111,8 @@ func checkRouteOverlaps(dockerNetwork *net.IPNet) error { } func CreateBridgeIface(ifaceName string) error { + // FIXME: try more IP ranges + // FIXME: try bigger ranges! /24 is too small. addrs := []string{"172.16.42.1/24", "10.0.42.1/24", "192.168.42.1/24"} var ifaceAddr string @@ -127,7 +129,7 @@ func CreateBridgeIface(ifaceName string) error { } } if ifaceAddr == "" { - return fmt.Errorf("Impossible to create a bridge. Please create a bridge manually and restart docker with -br ") + return fmt.Errorf("Could not find a free IP address range for interface '%s'. Please configure its address manually and run 'docker -b %s'", ifaceName, ifaceName) } else { Debugf("Creating bridge %s with network %s", ifaceName, ifaceAddr) } diff --git a/rcli/http.go b/rcli/http.go deleted file mode 100644 index 3eeb2c2a97..0000000000 --- a/rcli/http.go +++ /dev/null @@ -1,38 +0,0 @@ -package rcli - -import ( - "fmt" - "net/http" - "net/url" - "path" -) - -// Use this key to encode an RPC call into an URL, -// eg. domain.tld/path/to/method?q=get_user&q=gordon -const ARG_URL_KEY = "q" - -func URLToCall(u *url.URL) (method string, args []string) { - return path.Base(u.Path), u.Query()[ARG_URL_KEY] -} - -func ListenAndServeHTTP(addr string, service Service) error { - return http.ListenAndServe(addr, http.HandlerFunc( - func(w http.ResponseWriter, r *http.Request) { - cmd, args := URLToCall(r.URL) - if err := call(service, r.Body, &AutoFlush{w}, append([]string{cmd}, args...)...); err != nil { - fmt.Fprintln(w, "Error:", err.Error()) - } - })) -} - -type AutoFlush struct { - http.ResponseWriter -} - -func (w *AutoFlush) Write(data []byte) (int, error) { - ret, err := w.ResponseWriter.Write(data) - if flusher, ok := w.ResponseWriter.(http.Flusher); ok { - flusher.Flush() - } - return ret, err -} diff --git a/rcli/tcp.go b/rcli/tcp.go index ff7e191f42..e9dba7f319 100644 --- a/rcli/tcp.go +++ b/rcli/tcp.go @@ -2,6 +2,7 @@ package rcli import ( "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -15,22 +16,109 @@ import ( var DEBUG_FLAG bool = false var CLIENT_SOCKET io.Writer = nil +type DockerTCPConn struct { + conn *net.TCPConn + options *DockerConnOptions + optionsBuf *[]byte + handshaked bool + client bool +} + +func NewDockerTCPConn(conn *net.TCPConn, client bool) *DockerTCPConn { + return &DockerTCPConn{ + conn: conn, + options: &DockerConnOptions{}, + client: client, + } +} + +func (c *DockerTCPConn) SetOptionRawTerminal() { + c.options.RawTerminal = true +} + +func (c *DockerTCPConn) GetOptions() *DockerConnOptions { + if c.client && !c.handshaked { + // Attempt to parse options encoded as a JSON dict and store + // the reminder of what we read from the socket in a buffer. + // + // bufio (and its ReadBytes method) would have been nice here, + // but if json.Unmarshal() fails (which will happen if we speak + // to a version of docker that doesn't send any option), then + // we can't put the data back in it for the next Read(). + c.handshaked = true + buf := make([]byte, 4096) + if n, _ := c.conn.Read(buf); n > 0 { + buf = buf[:n] + if nl := bytes.IndexByte(buf, '\n'); nl != -1 { + if err := json.Unmarshal(buf[:nl], c.options); err == nil { + buf = buf[nl+1:] + } + } + c.optionsBuf = &buf + } + } + + return c.options +} + +func (c *DockerTCPConn) Read(b []byte) (int, error) { + if c.optionsBuf != nil { + // Consume what we buffered in GetOptions() first: + optionsBuf := *c.optionsBuf + optionsBuflen := len(optionsBuf) + copied := copy(b, optionsBuf) + if copied < optionsBuflen { + optionsBuf = optionsBuf[copied:] + c.optionsBuf = &optionsBuf + return copied, nil + } + c.optionsBuf = nil + return copied, nil + } + return c.conn.Read(b) +} + +func (c *DockerTCPConn) Write(b []byte) (int, error) { + optionsLen := 0 + if !c.client && !c.handshaked { + c.handshaked = true + options, _ := json.Marshal(c.options) + options = append(options, '\n') + if optionsLen, err := c.conn.Write(options); err != nil { + return optionsLen, err + } + } + n, err := c.conn.Write(b) + return n + optionsLen, err +} + +func (c *DockerTCPConn) Flush() error { + _, err := c.Write([]byte{}) + return err +} + +func (c *DockerTCPConn) Close() error { return c.conn.Close() } + +func (c *DockerTCPConn) CloseWrite() error { return c.conn.CloseWrite() } + +func (c *DockerTCPConn) CloseRead() error { return c.conn.CloseRead() } + // Connect to a remote endpoint using protocol `proto` and address `addr`, // issue a single call, and return the result. // `proto` may be "tcp", "unix", etc. See the `net` package for available protocols. -func Call(proto, addr string, args ...string) (*net.TCPConn, error) { +func Call(proto, addr string, args ...string) (DockerConn, error) { cmd, err := json.Marshal(args) if err != nil { return nil, err } - conn, err := net.Dial(proto, addr) + conn, err := dialDocker(proto, addr) if err != nil { return nil, err } if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { return nil, err } - return conn.(*net.TCPConn), nil + return conn, nil } // Listen on `addr`, using protocol `proto`, for incoming rcli calls, @@ -46,6 +134,10 @@ func ListenAndServe(proto, addr string, service Service) error { if conn, err := listener.Accept(); err != nil { return err } else { + conn, err := newDockerServerConn(conn) + if err != nil { + return err + } go func() { if DEBUG_FLAG { CLIENT_SOCKET = conn @@ -63,7 +155,7 @@ func ListenAndServe(proto, addr string, service Service) error { // Parse an rcli call on a new connection, and pass it to `service` if it // is valid. -func Serve(conn io.ReadWriter, service Service) error { +func Serve(conn DockerConn, service Service) error { r := bufio.NewReader(conn) var args []string if line, err := r.ReadString('\n'); err != nil { diff --git a/rcli/types.go b/rcli/types.go index 2600fe240d..38f4a8c008 100644 --- a/rcli/types.go +++ b/rcli/types.go @@ -8,15 +8,99 @@ package rcli // are the usual suspects. import ( - "errors" "flag" "fmt" + "github.com/dotcloud/docker/term" "io" "log" + "net" + "os" "reflect" "strings" ) +type DockerConnOptions struct { + RawTerminal bool +} + +type DockerConn interface { + io.ReadWriteCloser + CloseWrite() error + CloseRead() error + GetOptions() *DockerConnOptions + SetOptionRawTerminal() + Flush() error +} + +type DockerLocalConn struct { + writer io.WriteCloser + savedState *term.State +} + +func NewDockerLocalConn(w io.WriteCloser) *DockerLocalConn { + return &DockerLocalConn{ + writer: w, + } +} + +func (c *DockerLocalConn) Read(b []byte) (int, error) { + return 0, fmt.Errorf("DockerLocalConn does not implement Read()") +} + +func (c *DockerLocalConn) Write(b []byte) (int, error) { return c.writer.Write(b) } + +func (c *DockerLocalConn) Close() error { + if c.savedState != nil { + RestoreTerminal(c.savedState) + c.savedState = nil + } + return c.writer.Close() +} + +func (c *DockerLocalConn) Flush() error { return nil } + +func (c *DockerLocalConn) CloseWrite() error { return nil } + +func (c *DockerLocalConn) CloseRead() error { return nil } + +func (c *DockerLocalConn) GetOptions() *DockerConnOptions { return nil } + +func (c *DockerLocalConn) SetOptionRawTerminal() { + if state, err := SetRawTerminal(); err != nil { + if os.Getenv("DEBUG") != "" { + log.Printf("Can't set the terminal in raw mode: %s", err) + } + } else { + c.savedState = state + } +} + +var UnknownDockerProto = fmt.Errorf("Only TCP is actually supported by Docker at the moment") + +func dialDocker(proto string, addr string) (DockerConn, error) { + conn, err := net.Dial(proto, addr) + if err != nil { + return nil, err + } + switch i := conn.(type) { + case *net.TCPConn: + return NewDockerTCPConn(i, true), nil + } + return nil, UnknownDockerProto +} + +func newDockerFromConn(conn net.Conn, client bool) (DockerConn, error) { + switch i := conn.(type) { + case *net.TCPConn: + return NewDockerTCPConn(i, client), nil + } + return nil, UnknownDockerProto +} + +func newDockerServerConn(conn net.Conn) (DockerConn, error) { + return newDockerFromConn(conn, false) +} + type Service interface { Name() string Help() string @@ -26,11 +110,11 @@ type Cmd func(io.ReadCloser, io.Writer, ...string) error type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error // FIXME: For reverse compatibility -func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func call(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { return LocalCall(service, stdin, stdout, args...) } -func LocalCall(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { +func LocalCall(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { if len(args) == 0 { args = []string{"help"} } @@ -49,7 +133,7 @@ func LocalCall(service Service, stdin io.ReadCloser, stdout io.Writer, args ...s if method != nil { return method(stdin, stdout, flags.Args()[1:]...) } - return errors.New("No such command: " + cmd) + return fmt.Errorf("No such command: %s", cmd) } func getMethod(service Service, name string) Cmd { @@ -59,7 +143,7 @@ func getMethod(service Service, name string) Cmd { stdout.Write([]byte(service.Help())) } else { if method := getMethod(service, args[0]); method == nil { - return errors.New("No such command: " + args[0]) + return fmt.Errorf("No such command: %s", args[0]) } else { method(stdin, stdout, "--help") } diff --git a/rcli/utils.go b/rcli/utils.go new file mode 100644 index 0000000000..dbd579ffcd --- /dev/null +++ b/rcli/utils.go @@ -0,0 +1,27 @@ +package rcli + +import ( + "github.com/dotcloud/docker/term" + "os" + "os/signal" +) + +//FIXME: move these function to utils.go (in rcli to avoid import loop) +func SetRawTerminal() (*term.State, error) { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + _ = <-c + term.Restore(int(os.Stdin.Fd()), oldState) + os.Exit(0) + }() + return oldState, err +} + +func RestoreTerminal(state *term.State) { + term.Restore(int(os.Stdin.Fd()), state) +} diff --git a/runtime.go b/runtime.go index 0e5bcbfc69..7971fe4f48 100644 --- a/runtime.go +++ b/runtime.go @@ -116,7 +116,6 @@ func (runtime *Runtime) Load(id string) (*Container, error) { if err := container.FromDisk(); err != nil { return nil, err } - container.State.initLock() if container.Id != id { return container, fmt.Errorf("Container %s is stored at %s", container.Id, id) } @@ -136,6 +135,7 @@ func (runtime *Runtime) Register(container *Container) error { } // FIXME: if the container is supposed to be running but is not, auto restart it? + // if so, then we need to restart monitor and init a new lock // If the container is supposed to be running, make sure of it if container.State.Running { if output, err := exec.Command("lxc-info", "-n", container.Id).CombinedOutput(); err != nil { @@ -150,10 +150,10 @@ func (runtime *Runtime) Register(container *Container) error { } } } + container.State.initLock() container.runtime = runtime - // Setup state lock (formerly in newState() - container.State.initLock() + // Attach to stdout and stderr container.stderr = newWriteBroadcaster() container.stdout = newWriteBroadcaster() diff --git a/runtime_test.go b/runtime_test.go index 3cdcbe3b39..9ab8b9b1e7 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -1,6 +1,7 @@ package docker import ( + "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "os" @@ -77,7 +78,7 @@ func init() { runtime: runtime, } // Retrieve the Image - if err := srv.CmdPull(os.Stdin, os.Stdout, unitTestImageName); err != nil { + if err := srv.CmdPull(os.Stdin, rcli.NewDockerLocalConn(os.Stdout), unitTestImageName); err != nil { panic(err) } } @@ -314,7 +315,7 @@ func TestRestore(t *testing.T) { // Simulate a crash/manual quit of dockerd: process dies, states stays 'Running' cStdin, _ := container2.StdinPipe() cStdin.Close() - if err := container2.WaitTimeout(time.Second); err != nil { + if err := container2.WaitTimeout(2 * time.Second); err != nil { t.Fatal(err) } container2.State.Running = true @@ -358,4 +359,5 @@ func TestRestore(t *testing.T) { if err := container3.Run(); err != nil { t.Fatal(err) } + container2.State.Running = false } diff --git a/state.go b/state.go index bf325a3eac..2ca7130921 100644 --- a/state.go +++ b/state.go @@ -11,9 +11,7 @@ type State struct { Pid int ExitCode int StartedAt time.Time - - stateChangeLock *sync.Mutex - stateChangeCond *sync.Cond + l *sync.Mutex } // String returns a human-readable description of the state @@ -29,31 +27,22 @@ func (s *State) setRunning(pid int) { s.ExitCode = 0 s.Pid = pid s.StartedAt = time.Now() - s.broadcast() } func (s *State) setStopped(exitCode int) { s.Running = false s.Pid = 0 s.ExitCode = exitCode - s.broadcast() } func (s *State) initLock() { - if s.stateChangeLock == nil { - s.stateChangeLock = &sync.Mutex{} - s.stateChangeCond = sync.NewCond(s.stateChangeLock) - } + s.l = &sync.Mutex{} } -func (s *State) broadcast() { - s.stateChangeLock.Lock() - s.stateChangeCond.Broadcast() - s.stateChangeLock.Unlock() +func (s *State) lock() { + s.l.Lock() } -func (s *State) wait() { - s.stateChangeLock.Lock() - s.stateChangeCond.Wait() - s.stateChangeLock.Unlock() +func (s *State) unlock() { + s.l.Unlock() } diff --git a/term/termios_linux.go b/term/termios_linux.go index 5275ba87fb..92f21edde2 100644 --- a/term/termios_linux.go +++ b/term/termios_linux.go @@ -15,7 +15,8 @@ void MakeRaw(int fd) { ioctl(fd, TCGETS, &t); t.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - t.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN); + t.c_oflag &= ~OPOST; + t.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG); t.c_cflag &= ~(CSIZE | PARENB); t.c_cflag |= CS8; diff --git a/utils.go b/utils.go index 5ee84239b1..68e12b20bd 100644 --- a/utils.go +++ b/utils.go @@ -341,3 +341,46 @@ func TruncateId(id string) string { } return id[:shortLen] } + +// Code c/c from io.Copy() modified to handle escape sequence +func CopyEscapable(dst io.Writer, src io.ReadCloser) (written int64, err error) { + buf := make([]byte, 32*1024) + for { + nr, er := src.Read(buf) + if nr > 0 { + // ---- Docker addition + // char 16 is C-p + if nr == 1 && buf[0] == 16 { + nr, er = src.Read(buf) + // char 17 is C-q + if nr == 1 && buf[0] == 17 { + if err := src.Close(); err != nil { + return 0, err + } + return 0, io.EOF + } + } + // ---- End of docker + nw, ew := dst.Write(buf[0:nr]) + if nw > 0 { + written += int64(nw) + } + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er == io.EOF { + break + } + if er != nil { + err = er + break + } + } + return written, err +}