diff --git a/api.go b/api.go index c4a5222dc..02662284e 100644 --- a/api.go +++ b/api.go @@ -250,6 +250,23 @@ func getContainersChanges(srv *Server, version float64, w http.ResponseWriter, r return nil } +func getContainersTop(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if vars == nil { + return fmt.Errorf("Missing parameter") + } + name := vars["name"] + procsStr, err := srv.ContainerTop(name) + if err != nil { + return err + } + b, err := json.Marshal(procsStr) + if err != nil { + return err + } + writeJSON(w, b) + return nil +} + func getContainersJSON(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return err @@ -842,6 +859,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}/export": getContainersExport, "/containers/{name:.*}/changes": getContainersChanges, "/containers/{name:.*}/json": getContainersByName, + "/containers/{name:.*}/top": getContainersTop, }, "POST": { "/auth": postAuth, diff --git a/api_params.go b/api_params.go index b8af690c7..b371ca314 100644 --- a/api_params.go +++ b/api_params.go @@ -26,6 +26,13 @@ type APIInfo struct { SwapLimit bool `json:",omitempty"` } +type APITop struct { + PID string + Tty string + Time string + Cmd string +} + type APIRmi struct { Deleted string `json:",omitempty"` Untagged string `json:",omitempty"` diff --git a/api_test.go b/api_test.go index d111f258c..757004b26 100644 --- a/api_test.go +++ b/api_test.go @@ -436,6 +436,61 @@ func TestGetContainersChanges(t *testing.T) { } } +func TestGetContainersTop(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + builder := NewBuilder(runtime) + + container, err := builder.Create( + &Config{ + Image: GetTestImage(runtime).ID, + Cmd: []string{"/bin/sh", "-c", "sleep 2"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + hostConfig := &HostConfig{} + if err := container.Start(hostConfig); err != nil { + t.Fatal(err) + } + + // Give some time to the process to start + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Errorf("Container should be running") + } + + r := httptest.NewRecorder() + if err := getContainersTop(srv, APIVERSION, r, nil, map[string]string{"name": container.ID}); err != nil { + t.Fatal(err) + } + procs := []APITop{} + if err := json.Unmarshal(r.Body.Bytes(), &procs); err != nil { + t.Fatal(err) + } + + if len(procs) != 2 { + t.Fatalf("Expected 2 processes, found %d.", len(procs)) + } + + if procs[0].Cmd != "sh" && procs[0].Cmd != "busybox" { + t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[0].Cmd) + } + + if procs[1].Cmd != "sh" && procs[1].Cmd != "busybox" { + t.Fatalf("Expected `busybox` or `sh`, found %s.", procs[1].Cmd) + } +} + func TestGetContainersByName(t *testing.T) { runtime, err := newTestRuntime() if err != nil { diff --git a/commands.go b/commands.go index feab55825..c954296a6 100644 --- a/commands.go +++ b/commands.go @@ -89,6 +89,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, + {"top", "Lookup the running processes of a container"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from the docker registry server"}, {"push", "Push an image or a repository to the docker registry server"}, @@ -554,6 +555,33 @@ func (cli *DockerCli) CmdInspect(args ...string) error { return nil } +func (cli *DockerCli) CmdTop(args ...string) error { + cmd := Subcmd("top", "CONTAINER", "Lookup the running processes of a container") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + body, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/top", nil) + if err != nil { + return err + } + var procs []APITop + err = json.Unmarshal(body, &procs) + if err != nil { + return err + } + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, "PID\tTTY\tTIME\tCMD") + for _, proc := range procs { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", proc.PID, proc.Tty, proc.Time, proc.Cmd) + } + w.Flush() + return nil +} + func (cli *DockerCli) CmdPort(args ...string) error { cmd := Subcmd("port", "CONTAINER PRIVATE_PORT", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT") if err := cmd.Parse(args); err != nil { diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 1156f1a7e..183347c23 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -29,6 +29,11 @@ You can still call an old version of the api using /v1.0/images//insert What's new ---------- +Listing processes (/top): + +- List the processes inside a container + + Builder (/build): - Simplify the upload of the build context diff --git a/docs/sources/api/docker_remote_api_v1.3.rst b/docs/sources/api/docker_remote_api_v1.3.rst index b0955ce49..6542c8393 100644 --- a/docs/sources/api/docker_remote_api_v1.3.rst +++ b/docs/sources/api/docker_remote_api_v1.3.rst @@ -220,6 +220,46 @@ Inspect a container :statuscode 500: server error +List processes running inside a container +***************************************** + +.. http:get:: /containers/(id)/top + + List processes running inside the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/top HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "PID":"11935", + "Tty":"pts/2", + "Time":"00:00:00", + "Cmd":"sh" + }, + { + "PID":"12140", + "Tty":"pts/2", + "Time":"00:00:00", + "Cmd":"sleep" + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + Inspect changes on a container's filesystem ******************************************* diff --git a/docs/sources/commandline/cli.rst b/docs/sources/commandline/cli.rst index 118f42f6e..e499b1f09 100644 --- a/docs/sources/commandline/cli.rst +++ b/docs/sources/commandline/cli.rst @@ -52,5 +52,6 @@ Available Commands command/start command/stop command/tag + command/top command/version command/wait diff --git a/docs/sources/commandline/command/top.rst b/docs/sources/commandline/command/top.rst new file mode 100644 index 000000000..bdd35adcf --- /dev/null +++ b/docs/sources/commandline/command/top.rst @@ -0,0 +1,13 @@ +:title: Top Command +:description: Lookup the running processes of a container +:keywords: top, docker, container, documentation + +======================================================= +``top`` -- Lookup the running processes of a container +======================================================= + +:: + + Usage: docker top CONTAINER + + Lookup the running processes of a container diff --git a/server.go b/server.go index f1c090951..193f9e577 100644 --- a/server.go +++ b/server.go @@ -1,6 +1,7 @@ package docker import ( + "bufio" "errors" "fmt" "github.com/dotcloud/docker/auth" @@ -12,6 +13,7 @@ import ( "net/http" "net/url" "os" + "os/exec" "path" "runtime" "strings" @@ -247,6 +249,40 @@ func (srv *Server) ImageHistory(name string) ([]APIHistory, error) { } +func (srv *Server) ContainerTop(name string) ([]APITop, error) { + if container := srv.runtime.Get(name); container != nil { + output, err := exec.Command("lxc-ps", "--name", container.ID).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("Error trying to use lxc-ps: %s (%s)", err, output) + } + var procs []APITop + for i, line := range strings.Split(string(output), "\n") { + if i == 0 || len(line) == 0 { + continue + } + proc := APITop{} + scanner := bufio.NewScanner(strings.NewReader(line)) + scanner.Split(bufio.ScanWords) + if !scanner.Scan() { + return nil, fmt.Errorf("Error trying to use lxc-ps") + } + // no scanner.Text because we skip container id + scanner.Scan() + proc.PID = scanner.Text() + scanner.Scan() + proc.Tty = scanner.Text() + scanner.Scan() + proc.Time = scanner.Text() + scanner.Scan() + proc.Cmd = scanner.Text() + procs = append(procs, proc) + } + return procs, nil + + } + return nil, fmt.Errorf("No such container: %s", name) +} + func (srv *Server) ContainerChanges(name string) ([]Change, error) { if container := srv.runtime.Get(name); container != nil { return container.Changes()