dotcloud/docker

View on GitHub
daemon/top_unix.go

Summary

Maintainability
A
2 hrs
Test Coverage
//go:build !windows

package daemon // import "github.com/docker/docker/daemon"

import (
    "bytes"
    "context"
    "fmt"
    "os/exec"
    "regexp"
    "strconv"
    "strings"

    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/events"
    "github.com/docker/docker/errdefs"
    libcontainerdtypes "github.com/docker/docker/libcontainerd/types"
    "github.com/pkg/errors"
)

func validatePSArgs(psArgs string) error {
    // NOTE: \\s does not detect unicode whitespaces.
    // So we use fieldsASCII instead of strings.Fields in parsePSOutput.
    // See https://github.com/docker/docker/pull/24358
    //nolint: gosimple
    re := regexp.MustCompile("\\s+([^\\s]*)=\\s*(PID[^\\s]*)")
    for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
        if len(group) >= 3 {
            k := group[1]
            v := group[2]
            if k != "pid" {
                return fmt.Errorf(`specifying "%s=%s" is not allowed`, k, v)
            }
        }
    }
    return nil
}

// 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)
}

func appendProcess2ProcList(procList *container.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)
}

func hasPid(procs []uint32, pid int) bool {
    for _, p := range procs {
        if int(p) == pid {
            return true
        }
    }
    return false
}

func parsePSOutput(output []byte, procs []uint32) (*container.ContainerTopOKBody, error) {
    procList := &container.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
}

// 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)
}

// 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(name string, psArgs string) (*container.ContainerTopOKBody, error) {
    if psArgs == "" {
        psArgs = "-ef"
    }

    if err := validatePSArgs(psArgs); err != nil {
        return nil, err
    }

    ctr, err := daemon.GetContainer(name)
    if err != nil {
        return nil, err
    }

    tsk, err := func() (libcontainerdtypes.Task, error) {
        ctr.Lock()
        defer ctr.Unlock()

        tsk, err := ctr.GetRunningTask()
        if err != nil {
            return nil, err
        }
        if ctr.Restarting {
            return nil, errContainerIsRestarting(ctr.ID)
        }
        return tsk, nil
    }()
    if err != nil {
        return nil, err
    }

    infos, err := tsk.Pids(context.Background())
    if err != nil {
        return nil, err
    }
    procs := make([]uint32, len(infos))
    for i, p := range infos {
        procs[i] = p.Pid
    }

    args := strings.Split(psArgs, " ")
    pids := psPidsArg(procs)
    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,
        // so retry without it
        output, err = exec.Command("ps", args...).Output()
        if err != nil {
            if ee, ok := err.(*exec.ExitError); ok {
                // first line of stderr shows why ps failed
                line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
                if len(line) > 0 && len(line[0]) > 0 {
                    err = errors.New(string(line[0]))
                }
            }
            return nil, errdefs.System(errors.Wrap(err, "ps"))
        }
    }
    procList, err := parsePSOutput(output, procs)
    if err != nil {
        return nil, err
    }
    daemon.LogContainerEvent(ctr, events.ActionTop)
    return procList, nil
}