Skip to content

Commit 7a9ceab

Browse files
authored
Merge pull request #2751 from iwankgb/space_is_allowed_in_command_name
Refactor process parsing to accommodate commands with spaces
2 parents ef294e7 + 0f6fe07 commit 7a9ceab

File tree

2 files changed

+334
-93
lines changed

2 files changed

+334
-93
lines changed

manager/container.go

Lines changed: 126 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838
"github.com/google/cadvisor/summary"
3939
"github.com/google/cadvisor/utils/cpuload"
4040

41-
units "github.com/docker/go-units"
41+
"github.com/docker/go-units"
4242
"k8s.io/klog/v2"
4343
"k8s.io/utils/clock"
4444
)
@@ -47,9 +47,14 @@ import (
4747
var enableLoadReader = flag.Bool("enable_load_reader", false, "Whether to enable cpu load reader")
4848
var HousekeepingInterval = flag.Duration("housekeeping_interval", 1*time.Second, "Interval between container housekeepings")
4949

50+
// TODO: replace regular expressions with something simpler, such as strings.Split().
5051
// cgroup type chosen to fetch the cgroup path of a process.
51-
// Memory has been chosen, as it is one of the default cgroups that is enabled for most containers.
52-
var cgroupPathRegExp = regexp.MustCompile(`memory[^:]*:(.*?)[,;$]`)
52+
// Memory has been chosen, as it is one of the default cgroups that is enabled for most containers...
53+
var cgroupMemoryPathRegExp = regexp.MustCompile(`memory[^:]*:(.*?)[,;$]`)
54+
55+
// ... but there are systems (e.g. Raspberry Pi 4) where memory cgroup controller is disabled by default.
56+
// We should check cpu cgroup then.
57+
var cgroupCPUPathRegExp = regexp.MustCompile(`cpu[^:]*:(.*?)[,;$]`)
5358

5459
type containerInfo struct {
5560
info.ContainerReference
@@ -198,20 +203,28 @@ func (cd *containerData) DerivedStats() (v2.DerivedStats, error) {
198203
return cd.summaryReader.DerivedStats()
199204
}
200205

201-
func (cd *containerData) getCgroupPath(cgroups string) (string, error) {
206+
func (cd *containerData) getCgroupPath(cgroups string) string {
202207
if cgroups == "-" {
203-
return "/", nil
208+
return "/"
204209
}
205210
if strings.HasPrefix(cgroups, "0::") {
206-
return cgroups[3:], nil
211+
return cgroups[3:]
207212
}
208-
matches := cgroupPathRegExp.FindSubmatch([]byte(cgroups))
213+
matches := cgroupMemoryPathRegExp.FindSubmatch([]byte(cgroups))
209214
if len(matches) != 2 {
210-
klog.V(3).Infof("failed to get memory cgroup path from %q", cgroups)
211-
// return root in case of failures - memory hierarchy might not be enabled.
212-
return "/", nil
213-
}
214-
return string(matches[1]), nil
215+
klog.V(3).Infof(
216+
"failed to get memory cgroup path from %q, will try to get cpu cgroup path",
217+
cgroups,
218+
)
219+
// On some systems (e.g. Raspberry PI 4) cgroup memory controlled is disabled by default.
220+
matches = cgroupCPUPathRegExp.FindSubmatch([]byte(cgroups))
221+
if len(matches) != 2 {
222+
klog.V(3).Infof("failed to get cpu cgroup path from %q; assuming root cgroup", cgroups)
223+
// return root in case of failures - memory hierarchy might not be enabled.
224+
return "/"
225+
}
226+
}
227+
return string(matches[1])
215228
}
216229

217230
// Returns contents of a file inside the container root.
@@ -274,10 +287,7 @@ func (cd *containerData) getContainerPids(inHostNamespace bool) ([]string, error
274287
return nil, fmt.Errorf("expected at least %d fields, found %d: output: %q", expectedFields, len(fields), line)
275288
}
276289
pid := fields[0]
277-
cgroup, err := cd.getCgroupPath(fields[1])
278-
if err != nil {
279-
return nil, fmt.Errorf("could not parse cgroup path from %q: %v", fields[1], err)
280-
}
290+
cgroup := cd.getCgroupPath(fields[1])
281291
if cd.info.Name == cgroup {
282292
pids = append(pids, pid)
283293
}
@@ -286,106 +296,130 @@ func (cd *containerData) getContainerPids(inHostNamespace bool) ([]string, error
286296
}
287297

288298
func (cd *containerData) GetProcessList(cadvisorContainer string, inHostNamespace bool) ([]v2.ProcessInfo, error) {
289-
// report all processes for root.
290-
isRoot := cd.info.Name == "/"
291-
rootfs := "/"
292-
if !inHostNamespace {
293-
rootfs = "/rootfs"
294-
}
295299
format := "user,pid,ppid,stime,pcpu,pmem,rss,vsz,stat,time,comm,psr,cgroup"
296300
out, err := cd.getPsOutput(inHostNamespace, format)
297301
if err != nil {
298302
return nil, err
299303
}
300-
expectedFields := 13
304+
return cd.parseProcessList(cadvisorContainer, inHostNamespace, out)
305+
}
306+
307+
func (cd *containerData) parseProcessList(cadvisorContainer string, inHostNamespace bool, out []byte) ([]v2.ProcessInfo, error) {
308+
rootfs := "/"
309+
if !inHostNamespace {
310+
rootfs = "/rootfs"
311+
}
301312
processes := []v2.ProcessInfo{}
302313
lines := strings.Split(string(out), "\n")
303314
for _, line := range lines[1:] {
304-
if len(line) == 0 {
305-
continue
306-
}
307-
fields := strings.Fields(line)
308-
if len(fields) < expectedFields {
309-
return nil, fmt.Errorf("expected at least %d fields, found %d: output: %q", expectedFields, len(fields), line)
310-
}
311-
pid, err := strconv.Atoi(fields[1])
312-
if err != nil {
313-
return nil, fmt.Errorf("invalid pid %q: %v", fields[1], err)
314-
}
315-
ppid, err := strconv.Atoi(fields[2])
316-
if err != nil {
317-
return nil, fmt.Errorf("invalid ppid %q: %v", fields[2], err)
318-
}
319-
percentCPU, err := strconv.ParseFloat(fields[4], 32)
320-
if err != nil {
321-
return nil, fmt.Errorf("invalid cpu percent %q: %v", fields[4], err)
322-
}
323-
percentMem, err := strconv.ParseFloat(fields[5], 32)
324-
if err != nil {
325-
return nil, fmt.Errorf("invalid memory percent %q: %v", fields[5], err)
326-
}
327-
rss, err := strconv.ParseUint(fields[6], 0, 64)
328-
if err != nil {
329-
return nil, fmt.Errorf("invalid rss %q: %v", fields[6], err)
330-
}
331-
// convert to bytes
332-
rss *= 1024
333-
vs, err := strconv.ParseUint(fields[7], 0, 64)
334-
if err != nil {
335-
return nil, fmt.Errorf("invalid virtual size %q: %v", fields[7], err)
336-
}
337-
// convert to bytes
338-
vs *= 1024
339-
psr, err := strconv.Atoi(fields[11])
340-
if err != nil {
341-
return nil, fmt.Errorf("invalid pid %q: %v", fields[1], err)
342-
}
343-
344-
cgroup, err := cd.getCgroupPath(fields[12])
315+
processInfo, err := cd.parsePsLine(line, cadvisorContainer, inHostNamespace)
345316
if err != nil {
346-
return nil, fmt.Errorf("could not parse cgroup path from %q: %v", fields[11], err)
317+
return nil, fmt.Errorf("could not parse line %s: %v", line, err)
347318
}
348-
// Remove the ps command we just ran from cadvisor container.
349-
// Not necessary, but makes the cadvisor page look cleaner.
350-
if !inHostNamespace && cadvisorContainer == cgroup && fields[10] == "ps" {
319+
if processInfo == nil {
351320
continue
352321
}
353-
var cgroupPath string
354-
if isRoot {
355-
cgroupPath = cgroup
356-
}
357322

358323
var fdCount int
359-
dirPath := path.Join(rootfs, "/proc", strconv.Itoa(pid), "fd")
324+
dirPath := path.Join(rootfs, "/proc", strconv.Itoa(processInfo.Pid), "fd")
360325
fds, err := ioutil.ReadDir(dirPath)
361326
if err != nil {
362327
klog.V(4).Infof("error while listing directory %q to measure fd count: %v", dirPath, err)
363328
continue
364329
}
365330
fdCount = len(fds)
331+
processInfo.FdCount = fdCount
366332

367-
if isRoot || cd.info.Name == cgroup {
368-
processes = append(processes, v2.ProcessInfo{
369-
User: fields[0],
370-
Pid: pid,
371-
Ppid: ppid,
372-
StartTime: fields[3],
373-
PercentCpu: float32(percentCPU),
374-
PercentMemory: float32(percentMem),
375-
RSS: rss,
376-
VirtualSize: vs,
377-
Status: fields[8],
378-
RunningTime: fields[9],
379-
Cmd: fields[10],
380-
CgroupPath: cgroupPath,
381-
FdCount: fdCount,
382-
Psr: psr,
383-
})
384-
}
333+
processes = append(processes, *processInfo)
385334
}
386335
return processes, nil
387336
}
388337

338+
func (cd *containerData) isRoot() bool {
339+
return cd.info.Name == "/"
340+
}
341+
342+
func (cd *containerData) parsePsLine(line, cadvisorContainer string, inHostNamespace bool) (*v2.ProcessInfo, error) {
343+
const expectedFields = 13
344+
if len(line) == 0 {
345+
return nil, nil
346+
}
347+
348+
info := v2.ProcessInfo{}
349+
var err error
350+
351+
fields := strings.Fields(line)
352+
if len(fields) < expectedFields {
353+
return nil, fmt.Errorf("expected at least %d fields, found %d: output: %q", expectedFields, len(fields), line)
354+
}
355+
info.User = fields[0]
356+
info.StartTime = fields[3]
357+
info.Status = fields[8]
358+
info.RunningTime = fields[9]
359+
360+
info.Pid, err = strconv.Atoi(fields[1])
361+
if err != nil {
362+
return nil, fmt.Errorf("invalid pid %q: %v", fields[1], err)
363+
}
364+
info.Ppid, err = strconv.Atoi(fields[2])
365+
if err != nil {
366+
return nil, fmt.Errorf("invalid ppid %q: %v", fields[2], err)
367+
}
368+
369+
percentCPU, err := strconv.ParseFloat(fields[4], 32)
370+
if err != nil {
371+
return nil, fmt.Errorf("invalid cpu percent %q: %v", fields[4], err)
372+
}
373+
info.PercentCpu = float32(percentCPU)
374+
percentMem, err := strconv.ParseFloat(fields[5], 32)
375+
if err != nil {
376+
return nil, fmt.Errorf("invalid memory percent %q: %v", fields[5], err)
377+
}
378+
info.PercentMemory = float32(percentMem)
379+
380+
info.RSS, err = strconv.ParseUint(fields[6], 0, 64)
381+
if err != nil {
382+
return nil, fmt.Errorf("invalid rss %q: %v", fields[6], err)
383+
}
384+
info.VirtualSize, err = strconv.ParseUint(fields[7], 0, 64)
385+
if err != nil {
386+
return nil, fmt.Errorf("invalid virtual size %q: %v", fields[7], err)
387+
}
388+
// convert to bytes
389+
info.RSS *= 1024
390+
info.VirtualSize *= 1024
391+
392+
// According to `man ps`: The following user-defined format specifiers may contain spaces: args, cmd, comm, command,
393+
// fname, ucmd, ucomm, lstart, bsdstart, start.
394+
// Therefore we need to be able to parse comm that consists of multiple space-separated parts.
395+
info.Cmd = strings.Join(fields[10:len(fields)-2], " ")
396+
397+
// These are last two parts of the line. We create a subslice of `fields` to handle comm that includes spaces.
398+
lastTwoFields := fields[len(fields)-2:]
399+
info.Psr, err = strconv.Atoi(lastTwoFields[0])
400+
if err != nil {
401+
return nil, fmt.Errorf("invalid psr %q: %v", lastTwoFields[0], err)
402+
}
403+
info.CgroupPath = cd.getCgroupPath(lastTwoFields[1])
404+
405+
// Remove the ps command we just ran from cadvisor container.
406+
// Not necessary, but makes the cadvisor page look cleaner.
407+
if !inHostNamespace && cadvisorContainer == info.CgroupPath && info.Cmd == "ps" {
408+
return nil, nil
409+
}
410+
411+
// Do not report processes from other containers when non-root container requested.
412+
if !cd.isRoot() && info.CgroupPath != cd.info.Name {
413+
return nil, nil
414+
}
415+
416+
// Remove cgroup information when non-root container requested.
417+
if !cd.isRoot() {
418+
info.CgroupPath = ""
419+
}
420+
return &info, nil
421+
}
422+
389423
func newContainerData(containerName string, memoryCache *memory.InMemoryCache, handler container.ContainerHandler, logUsage bool, collectorManager collector.CollectorManager, maxHousekeepingInterval time.Duration, allowDynamicHousekeeping bool, clock clock.Clock) (*containerData, error) {
390424
if memoryCache == nil {
391425
return nil, fmt.Errorf("nil memory storage")
@@ -519,7 +553,7 @@ func (cd *containerData) housekeeping() {
519553
usageCPUNs := uint64(0)
520554
for i := range stats {
521555
if i > 0 {
522-
usageCPUNs += (stats[i].Cpu.Usage.Total - stats[i-1].Cpu.Usage.Total)
556+
usageCPUNs += stats[i].Cpu.Usage.Total - stats[i-1].Cpu.Usage.Total
523557
}
524558
}
525559
usageMemory := stats[numSamples-1].Memory.Usage

0 commit comments

Comments
 (0)