diff --git a/main.go b/main.go index 0e7b2ad58ca..acf098c2889 100644 --- a/main.go +++ b/main.go @@ -173,6 +173,8 @@ func newApp() *cli.App { versionCommand, // Inspect inspectCommand, + // stats + topCommand, // Management containerCommand, imageCommand, diff --git a/top.go b/top.go index a3210d9eb9d..ab4770d19c2 100644 --- a/top.go +++ b/top.go @@ -17,28 +17,72 @@ package main import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + "github.com/containerd/containerd" "github.com/pkg/errors" "github.com/urfave/cli/v2" - "regexp" ) +// ContainerTopOKBody OK response to ContainerTop operation +type ContainerTopOKBody struct { + + // Each process running in the container, where each is process + // is an array of values corresponding to the titles. + // + // Required: true + Processes [][]string `json:"Processes"` + + // The ps column titles + // Required: true + Titles []string `json:"Titles"` +} + var topCommand = &cli.Command{ - Name: "top", - Usage: "docker top CONTAINER", - ArgsUsage: "CONTAINER [ps OPTIONS]", - Action: topAction, - BashComplete: topBashComplete, + Name: "top", + Usage: "Display the running processes of a container", + ArgsUsage: "CONTAINER [ps OPTIONS]", + Action: topAction, + //BashComplete: topBashComplete, } func topAction(clicontext *cli.Context) error { if clicontext.NArg() < 1 { return errors.Errorf("requires at least 1 argument") } - //TODO + return containerTop(clicontext, clicontext.Args().First(), strings.Join(clicontext.Args().Tail(), " ")) } -//function from moby/moby +//function from moby/moby/daemon/top_unix.go +func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) { + // Make sure number of fields equals number of header titles + // merging "overhanging" fields + process := fields[:len(procList.Titles)-1] + process = append(process, strings.Join(fields[len(procList.Titles)-1:], " ")) + procList.Processes = append(procList.Processes, process) +} + +//function from moby/moby/daemon/top_unix.go +// psPidsArg converts a slice of PIDs to a string consisting +// of comma-separated list of PIDs prepended by "-q". +// For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3". +func psPidsArg(pids []uint32) string { + b := []byte{'-', 'q'} + for i, p := range pids { + b = strconv.AppendUint(b, uint64(p), 10) + if i < len(pids)-1 { + b = append(b, ',') + } + } + return string(b) +} + +//function from moby/moby/daemon/top_unix.go func validatePSArgs(psArgs string) error { // NOTE: \\s does not detect unicode whitespaces. // So we use fieldsASCII instead of strings.Fields in parsePSOutput. @@ -57,57 +101,137 @@ func validatePSArgs(psArgs string) error { return nil } -//function from moby/moby +//function from moby/moby/daemon/top_unix.go +// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces +func fieldsASCII(s string) []string { + fn := func(r rune) bool { + switch r { + case '\t', '\n', '\f', '\r', ' ': + return true + } + return false + } + return strings.FieldsFunc(s, fn) +} + +//function from moby/moby/daemon/top_unix.go +func hasPid(procs []uint32, pid int) bool { + for _, p := range procs { + if int(p) == pid { + return true + } + } + return false +} + +//function from moby/moby/daemon/top_unix.go +func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) { + procList := &ContainerTopOKBody{} + + lines := strings.Split(string(output), "\n") + procList.Titles = fieldsASCII(lines[0]) + + pidIndex := -1 + for i, name := range procList.Titles { + if name == "PID" { + pidIndex = i + break + } + } + if pidIndex == -1 { + return nil, fmt.Errorf("Couldn't find PID field in ps output") + } + + // loop through the output and extract the PID from each line + // fixing #30580, be able to display thread line also when "m" option used + // in "docker top" client command + preContainedPidFlag := false + for _, line := range lines[1:] { + if len(line) == 0 { + continue + } + fields := fieldsASCII(line) + + var ( + p int + err error + ) + + if fields[pidIndex] == "-" { + if preContainedPidFlag { + appendProcess2ProcList(procList, fields) + } + continue + } + p, err = strconv.Atoi(fields[pidIndex]) + if err != nil { + return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err) + } + + if hasPid(procs, p) { + preContainedPidFlag = true + appendProcess2ProcList(procList, fields) + continue + } + preContainedPidFlag = false + } + return procList, nil +} + +// function inspired from moby/moby/daemon/top_unix.go // ContainerTop lists the processes running inside of the given // container by calling ps with the given args, or with the flags // "-ef" if no args are given. An error is returned if the container // is not found, or is not running, or if there are any problems // running ps, or parsing the output. -func (daemon *Daemon) ContainerTop(clicontext *cli.Context, name string, psArgs string) (*container.ContainerTopOKBody, error) { +func containerTop(clicontext *cli.Context, name string, psArgs string) error { if psArgs == "" { psArgs = "-ef" } if err := validatePSArgs(psArgs); err != nil { - return nil, err + return err } - client, ctx, cancel, err := newClient(clicontext) - if err != nil { - return err - } - defer cancel() + client, ctx, cancel, err := newClient(clicontext) + if err != nil { + return err + } + defer cancel() - container, err := client.LoadContainer(ctx, name) - if err != nil { - return err - } + container, err := client.LoadContainer(ctx, name) + if err != nil { + return err + } task, err := container.Task(ctx, nil) - if err != nil { - return err - } + if err != nil { + return err + } - status, err := task.Status(ctx) - if err != nil { - return err - } + status, err := task.Status(ctx) + if err != nil { + return err + } if status.Status != containerd.Running { - return nil, errNotRunning(ctr.ID) + return nil } - if ctr.IsRestarting() { - return nil, errContainerIsRestarting(ctr.ID) - } + //TO DO handle restarting case: wait for container to restart and then launch top command - procs, err := containerd.ListPids(context.Background(), ctr.ID) + procs, err := task.Pids(ctx) if err != nil { - return nil, err + return err + } + + var psList []uint32 + for _, ps := range procs { + psList = append(psList, ps.Pid) } args := strings.Split(psArgs, " ") - pids := psPidsArg(procs) + pids := psPidsArg(psList) output, err := exec.Command("ps", append(args, pids)...).Output() if err != nil { // some ps options (such as f) can't be used together with q, @@ -121,14 +245,13 @@ func (daemon *Daemon) ContainerTop(clicontext *cli.Context, name string, psArgs err = errors.New(string(line[0])) } } - return nil, errdefs.System(errors.Wrap(err, "ps")) + return nil } } - procList, err := parsePSOutput(output, procs) + procList, err := parsePSOutput(output, psList) if err != nil { - return nil, err + return err } - //daemon.LogContainerEvent(ctr, "top") - return procList, nil + fmt.Println(procList) + return nil } -