top_unix.go 4.7 KB

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