gopkg.in/docker/docker.v20@v20.10.27/daemon/top_unix.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package daemon // import "github.com/docker/docker/daemon"
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"fmt"
    10  	"os/exec"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/docker/docker/api/types/container"
    16  	"github.com/docker/docker/errdefs"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  func validatePSArgs(psArgs string) error {
    21  	// NOTE: \\s does not detect unicode whitespaces.
    22  	// So we use fieldsASCII instead of strings.Fields in parsePSOutput.
    23  	// See https://github.com/docker/docker/pull/24358
    24  	//nolint: gosimple
    25  	re := regexp.MustCompile("\\s+([^\\s]*)=\\s*(PID[^\\s]*)")
    26  	for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
    27  		if len(group) >= 3 {
    28  			k := group[1]
    29  			v := group[2]
    30  			if k != "pid" {
    31  				return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
    32  			}
    33  		}
    34  	}
    35  	return nil
    36  }
    37  
    38  // fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
    39  func fieldsASCII(s string) []string {
    40  	fn := func(r rune) bool {
    41  		switch r {
    42  		case '\t', '\n', '\f', '\r', ' ':
    43  			return true
    44  		}
    45  		return false
    46  	}
    47  	return strings.FieldsFunc(s, fn)
    48  }
    49  
    50  func appendProcess2ProcList(procList *container.ContainerTopOKBody, fields []string) {
    51  	// Make sure number of fields equals number of header titles
    52  	// merging "overhanging" fields
    53  	process := fields[:len(procList.Titles)-1]
    54  	process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
    55  	procList.Processes = append(procList.Processes, process)
    56  }
    57  
    58  func hasPid(procs []uint32, pid int) bool {
    59  	for _, p := range procs {
    60  		if int(p) == pid {
    61  			return true
    62  		}
    63  	}
    64  	return false
    65  }
    66  
    67  func parsePSOutput(output []byte, procs []uint32) (*container.ContainerTopOKBody, error) {
    68  	procList := &container.ContainerTopOKBody{}
    69  
    70  	lines := strings.Split(string(output), "\n")
    71  	procList.Titles = fieldsASCII(lines[0])
    72  
    73  	pidIndex := -1
    74  	for i, name := range procList.Titles {
    75  		if name == "PID" {
    76  			pidIndex = i
    77  			break
    78  		}
    79  	}
    80  	if pidIndex == -1 {
    81  		return nil, fmt.Errorf("Couldn't find PID field in ps output")
    82  	}
    83  
    84  	// loop through the output and extract the PID from each line
    85  	// fixing #30580, be able to display thread line also when "m" option used
    86  	// in "docker top" client command
    87  	preContainedPidFlag := false
    88  	for _, line := range lines[1:] {
    89  		if len(line) == 0 {
    90  			continue
    91  		}
    92  		fields := fieldsASCII(line)
    93  
    94  		var (
    95  			p   int
    96  			err error
    97  		)
    98  
    99  		if fields[pidIndex] == "-" {
   100  			if preContainedPidFlag {
   101  				appendProcess2ProcList(procList, fields)
   102  			}
   103  			continue
   104  		}
   105  		p, err = strconv.Atoi(fields[pidIndex])
   106  		if err != nil {
   107  			return nil, fmt.Errorf("Unexpected pid '%s': %s", fields[pidIndex], err)
   108  		}
   109  
   110  		if hasPid(procs, p) {
   111  			preContainedPidFlag = true
   112  			appendProcess2ProcList(procList, fields)
   113  			continue
   114  		}
   115  		preContainedPidFlag = false
   116  	}
   117  	return procList, nil
   118  }
   119  
   120  // psPidsArg converts a slice of PIDs to a string consisting
   121  // of comma-separated list of PIDs prepended by "-q".
   122  // For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
   123  func psPidsArg(pids []uint32) string {
   124  	b := []byte{'-', 'q'}
   125  	for i, p := range pids {
   126  		b = strconv.AppendUint(b, uint64(p), 10)
   127  		if i < len(pids)-1 {
   128  			b = append(b, ',')
   129  		}
   130  	}
   131  	return string(b)
   132  }
   133  
   134  // ContainerTop lists the processes running inside of the given
   135  // container by calling ps with the given args, or with the flags
   136  // "-ef" if no args are given.  An error is returned if the container
   137  // is not found, or is not running, or if there are any problems
   138  // running ps, or parsing the output.
   139  func (daemon *Daemon) ContainerTop(name string, psArgs string) (*container.ContainerTopOKBody, error) {
   140  	if psArgs == "" {
   141  		psArgs = "-ef"
   142  	}
   143  
   144  	if err := validatePSArgs(psArgs); err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	ctr, err := daemon.GetContainer(name)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	if !ctr.IsRunning() {
   154  		return nil, errNotRunning(ctr.ID)
   155  	}
   156  
   157  	if ctr.IsRestarting() {
   158  		return nil, errContainerIsRestarting(ctr.ID)
   159  	}
   160  
   161  	procs, err := daemon.containerd.ListPids(context.Background(), ctr.ID)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	args := strings.Split(psArgs, " ")
   167  	pids := psPidsArg(procs)
   168  	output, err := exec.Command("ps", append(args, pids)...).Output()
   169  	if err != nil {
   170  		// some ps options (such as f) can't be used together with q,
   171  		// so retry without it
   172  		output, err = exec.Command("ps", args...).Output()
   173  		if err != nil {
   174  			if ee, ok := err.(*exec.ExitError); ok {
   175  				// first line of stderr shows why ps failed
   176  				line := bytes.SplitN(ee.Stderr, []byte{'\n'}, 2)
   177  				if len(line) > 0 && len(line[0]) > 0 {
   178  					err = errors.New(string(line[0]))
   179  				}
   180  			}
   181  			return nil, errdefs.System(errors.Wrap(err, "ps"))
   182  		}
   183  	}
   184  	procList, err := parsePSOutput(output, procs)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	daemon.LogContainerEvent(ctr, "top")
   189  	return procList, nil
   190  }