github.com/toplink-cn/moby@v0.0.0-20240305205811-460b4aebdf81/daemon/top_unix.go (about)

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