github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/cmd/container/top.go (about) 1 /* 2 Copyright The containerd Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 /* 18 Portions from: 19 - https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go 20 - https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go 21 Copyright (C) The Moby authors. 22 Licensed under the Apache License, Version 2.0 23 NOTICE: https://github.com/moby/moby/blob/v20.10.6/NOTICE 24 */ 25 26 package container 27 28 import ( 29 "context" 30 "fmt" 31 "regexp" 32 "strconv" 33 "strings" 34 35 "github.com/containerd/containerd" 36 "github.com/containerd/nerdctl/v2/pkg/api/types" 37 "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" 38 ) 39 40 // ContainerTopOKBody is from https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go 41 // 42 // ContainerTopOKBody OK response to ContainerTop operation 43 type ContainerTopOKBody struct { //nolint:revive 44 45 // Each process running in the container, where each is process 46 // is an array of values corresponding to the titles. 47 // 48 // Required: true 49 Processes [][]string `json:"Processes"` 50 51 // The ps column titles 52 // Required: true 53 Titles []string `json:"Titles"` 54 } 55 56 // Top performs the equivalent of running `top` inside of container(s) 57 func Top(ctx context.Context, client *containerd.Client, containers []string, opt types.ContainerTopOptions) error { 58 walker := &containerwalker.ContainerWalker{ 59 Client: client, 60 OnFound: func(ctx context.Context, found containerwalker.Found) error { 61 if found.MatchCount > 1 { 62 return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) 63 } 64 return containerTop(ctx, opt.Stdout, client, found.Container.ID(), strings.Join(containers[1:], " ")) 65 }, 66 } 67 68 n, err := walker.Walk(ctx, containers[0]) 69 if err != nil { 70 return err 71 } else if n == 0 { 72 return fmt.Errorf("no such container %s", containers[0]) 73 } 74 return nil 75 } 76 77 // appendProcess2ProcList is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L49-L55 78 func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) { 79 // Make sure number of fields equals number of header titles 80 // merging "overhanging" fields 81 process := fields[:len(procList.Titles)-1] 82 process = append(process, strings.Join(fields[len(procList.Titles)-1:], " ")) 83 procList.Processes = append(procList.Processes, process) 84 } 85 86 // psPidsArg is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L119-L131 87 // 88 // psPidsArg converts a slice of PIDs to a string consisting 89 // of comma-separated list of PIDs prepended by "-q". 90 // For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3". 91 func psPidsArg(pids []uint32) string { 92 b := []byte{'-', 'q'} 93 for i, p := range pids { 94 b = strconv.AppendUint(b, uint64(p), 10) 95 if i < len(pids)-1 { 96 b = append(b, ',') 97 } 98 } 99 return string(b) 100 } 101 102 // validatePSArgs is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L19-L35 103 func validatePSArgs(psArgs string) error { 104 // NOTE: \\s does not detect unicode whitespaces. 105 // So we use fieldsASCII instead of strings.Fields in parsePSOutput. 106 // See https://github.com/docker/docker/pull/24358 107 // nolint: gosimple 108 re := regexp.MustCompile(`\s+(\S*)=\s*(PID\S*)`) 109 for _, group := range re.FindAllStringSubmatch(psArgs, -1) { 110 if len(group) >= 3 { 111 k := group[1] 112 v := group[2] 113 if k != "pid" { 114 return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v) 115 } 116 } 117 } 118 return nil 119 } 120 121 // fieldsASCII is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L37-L47 122 // 123 // fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces 124 func fieldsASCII(s string) []string { 125 fn := func(r rune) bool { 126 switch r { 127 case '\t', '\n', '\f', '\r', ' ': 128 return true 129 } 130 return false 131 } 132 return strings.FieldsFunc(s, fn) 133 } 134 135 // hasPid is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L57-L64 136 func hasPid(procs []uint32, pid int) bool { 137 for _, p := range procs { 138 if int(p) == pid { 139 return true 140 } 141 } 142 return false 143 } 144 145 // parsePSOutput is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L66-L117 146 func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) { 147 procList := &ContainerTopOKBody{} 148 149 lines := strings.Split(string(output), "\n") 150 procList.Titles = fieldsASCII(lines[0]) 151 152 pidIndex := -1 153 for i, name := range procList.Titles { 154 if name == "PID" { 155 pidIndex = i 156 break 157 } 158 } 159 if pidIndex == -1 { 160 return nil, fmt.Errorf("couldn't find PID field in ps output") 161 } 162 163 // loop through the output and extract the PID from each line 164 // fixing #30580, be able to display thread line also when "m" option used 165 // in "docker top" client command 166 preContainedPidFlag := false 167 for _, line := range lines[1:] { 168 if len(line) == 0 { 169 continue 170 } 171 fields := fieldsASCII(line) 172 173 var ( 174 p int 175 err error 176 ) 177 178 if fields[pidIndex] == "-" { 179 if preContainedPidFlag { 180 appendProcess2ProcList(procList, fields) 181 } 182 continue 183 } 184 p, err = strconv.Atoi(fields[pidIndex]) 185 if err != nil { 186 return nil, fmt.Errorf("unexpected pid '%s': %s", fields[pidIndex], err) 187 } 188 189 if hasPid(procs, p) { 190 preContainedPidFlag = true 191 appendProcess2ProcList(procList, fields) 192 continue 193 } 194 preContainedPidFlag = false 195 } 196 return procList, nil 197 }