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