// Copyright 2018 psgo authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package psgo is a ps (1) AIX-format compatible golang library extended with // various descriptors useful for displaying container-related data. // // The idea behind the library is to provide an easy to use way of extracting // process-related data, just as ps (1) does. The problem when using ps (1) is // that the ps format strings split columns with whitespaces, making the output // nearly impossible to parse. It also adds some jitter as we have to fork and // execute ps either in the container or filter the output afterwards, further // limiting applicability. // // Please visit https://github.com/containers/psgo for further details about // supported format descriptors and to see some usage examples. package psgo import ( "errors" "fmt" "io/ioutil" "os" "runtime" "sort" "strconv" "strings" "sync" "github.com/containers/psgo/internal/capabilities" "github.com/containers/psgo/internal/dev" "github.com/containers/psgo/internal/proc" "github.com/containers/psgo/internal/process" "github.com/containers/storage/pkg/idtools" "golang.org/x/sys/unix" ) // JoinNamespaceOpts specifies different options for joining the specified namespaces. type JoinNamespaceOpts struct { // UIDMap specifies a mapping for UIDs in the container. If specified // huser will perform the reverse mapping. UIDMap []idtools.IDMap // GIDMap specifies a mapping for GIDs in the container. If specified // hgroup will perform the reverse mapping. GIDMap []idtools.IDMap // FillMappings specified whether UIDMap and GIDMap must be initialized // with the current user namespace. FillMappings bool } type psContext struct { // Processes in the container. containersProcesses []*process.Process // Processes on the host. Used to map those to the ones running in the container. hostProcesses []*process.Process // tty and pty devices. ttys *[]dev.TTY // Various options opts *JoinNamespaceOpts } // processFunc is used to map a given aixFormatDescriptor to a corresponding // function extracting the desired data from a process. type processFunc func(*process.Process, *psContext) (string, error) // aixFormatDescriptor as mentioned in the ps(1) manpage. A given descriptor // can either be specified via its code (e.g., "%C") or its normal representation // (e.g., "pcpu") and will be printed under its corresponding header (e.g, "%CPU"). type aixFormatDescriptor struct { // code descriptor in the short form (e.g., "%C"). code string // normal descriptor in the long form (e.g., "pcpu"). normal string // header of the descriptor (e.g., "%CPU"). header string // onHost controls if data of the corresponding host processes will be // extracted as well. onHost bool // procFN points to the corresponding method to extract the desired data. procFn processFunc } // findID converts the specified id to the host mapping func findID(idStr string, mapping []idtools.IDMap, lookupFunc func(uid string) (string, error), overflowFile string) (string, error) { if len(mapping) == 0 { return idStr, nil } id, err := strconv.ParseInt(idStr, 10, 0) if err != nil { return "", fmt.Errorf("cannot parse ID: %w", err) } for _, m := range mapping { if int(id) >= m.ContainerID && int(id) < m.ContainerID+m.Size { user := fmt.Sprintf("%d", m.HostID+(int(id)-m.ContainerID)) return lookupFunc(user) } } // User not found, read the overflow overflow, err := ioutil.ReadFile(overflowFile) if err != nil { return "", err } return string(overflow), nil } // translateDescriptors parses the descriptors and returns a correspodning slice of // aixFormatDescriptors. Descriptors can be specified in the normal and in the // code form (if supported). If the descriptors slice is empty, the // `DefaultDescriptors` is used. func translateDescriptors(descriptors []string) ([]aixFormatDescriptor, error) { if len(descriptors) == 0 { descriptors = DefaultDescriptors } formatDescriptors := []aixFormatDescriptor{} for _, d := range descriptors { d = strings.TrimSpace(d) found := false for _, aix := range aixFormatDescriptors { if d == aix.code || d == aix.normal { formatDescriptors = append(formatDescriptors, aix) found = true } } if !found { return nil, fmt.Errorf("'%s': %w", d, ErrUnknownDescriptor) } } return formatDescriptors, nil } var ( // DefaultDescriptors is the `ps -ef` compatible default format. DefaultDescriptors = []string{"user", "pid", "ppid", "pcpu", "etime", "tty", "time", "args"} // ErrUnknownDescriptor is returned when an unknown descriptor is parsed. ErrUnknownDescriptor = errors.New("unknown descriptor") aixFormatDescriptors = []aixFormatDescriptor{ { code: "%C", normal: "pcpu", header: "%CPU", procFn: processPCPU, }, { code: "%G", normal: "group", header: "GROUP", procFn: processGROUP, }, { normal: "groups", header: "GROUPS", procFn: processGROUPS, }, { code: "%P", normal: "ppid", header: "PPID", procFn: processPPID, }, { code: "%U", normal: "user", header: "USER", procFn: processUSER, }, { code: "%a", normal: "args", header: "COMMAND", procFn: processARGS, }, { code: "%c", normal: "comm", header: "COMMAND", procFn: processCOMM, }, { code: "%g", normal: "rgroup", header: "RGROUP", procFn: processRGROUP, }, { code: "%n", normal: "nice", header: "NI", procFn: processNICE, }, { code: "%p", normal: "pid", header: "PID", procFn: processPID, }, { code: "%r", normal: "pgid", header: "PGID", procFn: processPGID, }, { code: "%t", normal: "etime", header: "ELAPSED", procFn: processETIME, }, { code: "%u", normal: "ruser", header: "RUSER", procFn: processRUSER, }, { code: "%x", normal: "time", header: "TIME", procFn: processTIME, }, { code: "%y", normal: "tty", header: "TTY", procFn: processTTY, }, { code: "%z", normal: "vsz", header: "VSZ", procFn: processVSZ, }, { normal: "capamb", header: "AMBIENT CAPS", procFn: processCAPAMB, }, { normal: "capinh", header: "INHERITED CAPS", procFn: processCAPINH, }, { normal: "capprm", header: "PERMITTED CAPS", procFn: processCAPPRM, }, { normal: "capeff", header: "EFFECTIVE CAPS", procFn: processCAPEFF, }, { normal: "capbnd", header: "BOUNDING CAPS", procFn: processCAPBND, }, { normal: "seccomp", header: "SECCOMP", procFn: processSECCOMP, }, { normal: "label", header: "LABEL", procFn: processLABEL, }, { normal: "hpid", header: "HPID", onHost: true, procFn: processHPID, }, { normal: "huser", header: "HUSER", onHost: true, procFn: processHUSER, }, { normal: "hgroup", header: "HGROUP", onHost: true, procFn: processHGROUP, }, { normal: "hgroups", header: "HGROUPS", onHost: true, procFn: processHGROUPS, }, { normal: "rss", header: "RSS", procFn: processRSS, }, { normal: "state", header: "STATE", procFn: processState, }, { normal: "stime", header: "STIME", procFn: processStartTime, }, } ) // ListDescriptors returns a string slice of all supported AIX format // descriptors in the normal form. func ListDescriptors() (list []string) { for _, d := range aixFormatDescriptors { list = append(list, d.normal) } sort.Strings(list) return } // JoinNamespaceAndProcessInfo has the same semantics as ProcessInfo but joins // the mount namespace of the specified pid before extracting data from `/proc`. func JoinNamespaceAndProcessInfo(pid string, descriptors []string) ([][]string, error) { return JoinNamespaceAndProcessInfoWithOptions(pid, descriptors, &JoinNamespaceOpts{}) } func contextFromOptions(options *JoinNamespaceOpts) (*psContext, error) { ctx := new(psContext) ctx.opts = options if ctx.opts != nil && ctx.opts.FillMappings { uidMappings, err := proc.ReadMappings("/proc/self/uid_map") if err != nil { return nil, err } gidMappings, err := proc.ReadMappings("/proc/self/gid_map") if err != nil { return nil, err } ctx.opts.UIDMap = uidMappings ctx.opts.GIDMap = gidMappings ctx.opts.FillMappings = false } return ctx, nil } // JoinNamespaceAndProcessInfoWithOptions has the same semantics as ProcessInfo but joins // the mount namespace of the specified pid before extracting data from `/proc`. func JoinNamespaceAndProcessInfoWithOptions(pid string, descriptors []string, options *JoinNamespaceOpts) ([][]string, error) { var ( data [][]string dataErr error wg sync.WaitGroup ) aixDescriptors, err := translateDescriptors(descriptors) if err != nil { return nil, err } ctx, err := contextFromOptions(options) if err != nil { return nil, err } // extract data from host processes only on-demand / when at least one // of the specified descriptors requires host data for _, d := range aixDescriptors { if d.onHost { ctx.hostProcesses, err = hostProcesses(pid) if err != nil { return nil, err } break } } wg.Add(1) go func() { defer wg.Done() runtime.LockOSThread() // extract user namespaces prior to joining the mount namespace currentUserNs, err := proc.ParseUserNamespace("self") if err != nil { dataErr = fmt.Errorf("error determining user namespace: %w", err) return } pidUserNs, err := proc.ParseUserNamespace(pid) if err != nil { dataErr = fmt.Errorf("error determining user namespace of PID %s: %w", pid, err) } // join the mount namespace of pid fd, err := os.Open(fmt.Sprintf("/proc/%s/ns/mnt", pid)) if err != nil { dataErr = err return } defer fd.Close() // create a new mountns on the current thread if err = unix.Unshare(unix.CLONE_NEWNS); err != nil { dataErr = err return } if err := unix.Setns(int(fd.Fd()), unix.CLONE_NEWNS); err != nil { dataErr = err return } // extract all pids mentioned in pid's mount namespace pids, err := proc.GetPIDs() if err != nil { dataErr = err return } // join the user NS if the pid's user NS is different // to the caller's user NS. joinUserNS := currentUserNs != pidUserNs ctx.containersProcesses, err = process.FromPIDs(pids, joinUserNS) if err != nil { dataErr = err return } data, dataErr = processDescriptors(aixDescriptors, ctx) }() wg.Wait() return data, dataErr } // JoinNamespaceAndProcessInfoByPidsWithOptions has similar semantics to // JoinNamespaceAndProcessInfo and avoids duplicate entries by joining a giving // PID namespace only once. func JoinNamespaceAndProcessInfoByPidsWithOptions(pids []string, descriptors []string, options *JoinNamespaceOpts) ([][]string, error) { // Extracting data from processes that share the same PID namespace // would yield duplicate results. Avoid that by extracting data only // from the first process in `pids` from a given PID namespace. // `nsMap` is used for quick lookups if a given PID namespace is // already covered, `pidList` is used to preserve the order which is // not guaranteed by nondeterministic maps in golang. nsMap := make(map[string]bool) pidList := []string{} for _, pid := range pids { ns, err := proc.ParsePIDNamespace(pid) if err != nil { if errors.Is(err, os.ErrNotExist) { // catch race conditions continue } return nil, fmt.Errorf("error extracting PID namespace: %w", err) } if _, exists := nsMap[ns]; !exists { nsMap[ns] = true pidList = append(pidList, pid) } } data := [][]string{} for i, pid := range pidList { pidData, err := JoinNamespaceAndProcessInfoWithOptions(pid, descriptors, options) if errors.Is(err, os.ErrNotExist) { // catch race conditions continue } if err != nil { return nil, err } if i == 0 { data = append(data, pidData[0]) } data = append(data, pidData[1:]...) } return data, nil } // JoinNamespaceAndProcessInfoByPids has similar semantics to // JoinNamespaceAndProcessInfo and avoids duplicate entries by joining a giving // PID namespace only once. func JoinNamespaceAndProcessInfoByPids(pids []string, descriptors []string) ([][]string, error) { return JoinNamespaceAndProcessInfoByPidsWithOptions(pids, descriptors, &JoinNamespaceOpts{}) } // ProcessInfo returns the process information of all processes in the current // mount namespace. The input format must be a comma-separated list of // supported AIX format descriptors. If the input string is empty, the // `DefaultDescriptors` is used. // The return value is an array of tab-separated strings, to easily use the // output for column-based formatting (e.g., with the `text/tabwriter` package). func ProcessInfo(descriptors []string) ([][]string, error) { pids, err := proc.GetPIDs() if err != nil { return nil, err } return ProcessInfoByPids(pids, descriptors) } // ProcessInfoByPids is like ProcessInfo, but the process information returned // is limited to a list of user specified PIDs. func ProcessInfoByPids(pids []string, descriptors []string) ([][]string, error) { aixDescriptors, err := translateDescriptors(descriptors) if err != nil { return nil, err } ctx, err := contextFromOptions(nil) if err != nil { return nil, err } ctx.containersProcesses, err = process.FromPIDs(pids, false) if err != nil { return nil, err } return processDescriptors(aixDescriptors, ctx) } // hostProcesses returns all processes running in the current namespace. func hostProcesses(pid string) ([]*process.Process, error) { // get processes pids, err := proc.GetPIDsFromCgroup(pid) if err != nil { return nil, err } processes, err := process.FromPIDs(pids, false) if err != nil { return nil, err } // set the additional host data for _, p := range processes { if err := p.SetHostData(); err != nil { return nil, err } } return processes, nil } // processDescriptors calls each `procFn` of all formatDescriptors on each // process and returns an array of tab-separated strings. func processDescriptors(formatDescriptors []aixFormatDescriptor, ctx *psContext) ([][]string, error) { data := [][]string{} // create header header := []string{} for _, desc := range formatDescriptors { header = append(header, desc.header) } data = append(data, header) // dispatch all descriptor functions on each process for _, proc := range ctx.containersProcesses { pData := []string{} for _, desc := range formatDescriptors { dataStr, err := desc.procFn(proc, ctx) if err != nil { return nil, err } pData = append(pData, dataStr) } data = append(data, pData) } return data, nil } // findHostProcess returns the corresponding process from `hostProcesses` or // nil if non is found. func findHostProcess(p *process.Process, ctx *psContext) *process.Process { for _, hp := range ctx.hostProcesses { // We expect the host process to be in another namespace, so // /proc/$pid/status.NSpid must have at least two entries. if len(hp.Status.NSpid) < 2 { continue } // The process' PID must match the one in the NS of the host // process and both must share the same pid NS. if p.Pid == hp.Status.NSpid[1] && p.PidNS == hp.PidNS { return hp } } return nil } // processGROUP returns the effective group ID of the process. This will be // the textual group ID, if it can be obtained, or a decimal representation // otherwise. func processGROUP(p *process.Process, ctx *psContext) (string, error) { return process.LookupGID(p.Status.Gids[1]) } // processGROUPS returns the supplementary groups of the process separated by // comma. This will be the textual group ID, if it can be obtained, or a // decimal representation otherwise. func processGROUPS(p *process.Process, ctx *psContext) (string, error) { var err error groups := make([]string, len(p.Status.Groups)) for i, g := range p.Status.Groups { groups[i], err = process.LookupGID(g) if err != nil { return "", err } } return strings.Join(groups, ","), nil } // processRGROUP returns the real group ID of the process. This will be // the textual group ID, if it can be obtained, or a decimal representation // otherwise. func processRGROUP(p *process.Process, ctx *psContext) (string, error) { return process.LookupGID(p.Status.Gids[0]) } // processPPID returns the parent process ID of process p. func processPPID(p *process.Process, ctx *psContext) (string, error) { return p.Status.PPid, nil } // processUSER returns the effective user name of the process. This will be // the textual user ID, if it can be obtained, or a decimal representation // otherwise. func processUSER(p *process.Process, ctx *psContext) (string, error) { return process.LookupUID(p.Status.Uids[1]) } // processRUSER returns the effective user name of the process. This will be // the textual user ID, if it can be obtained, or a decimal representation // otherwise. func processRUSER(p *process.Process, ctx *psContext) (string, error) { return process.LookupUID(p.Status.Uids[0]) } // processName returns the name of process p in the format "[$name]". func processName(p *process.Process, ctx *psContext) (string, error) { return fmt.Sprintf("[%s]", p.Status.Name), nil } // processARGS returns the command of p with all its arguments. func processARGS(p *process.Process, ctx *psContext) (string, error) { // ps (1) returns "[$name]" if command/args are empty if p.CmdLine[0] == "" { return processName(p, ctx) } return strings.Join(p.CmdLine, " "), nil } // processCOMM returns the command name (i.e., executable name) of process p. func processCOMM(p *process.Process, ctx *psContext) (string, error) { return p.Stat.Comm, nil } // processNICE returns the nice value of process p. func processNICE(p *process.Process, ctx *psContext) (string, error) { return p.Stat.Nice, nil } // processPID returns the process ID of process p. func processPID(p *process.Process, ctx *psContext) (string, error) { return p.Pid, nil } // processPGID returns the process group ID of process p. func processPGID(p *process.Process, ctx *psContext) (string, error) { return p.Stat.Pgrp, nil } // processPCPU returns how many percent of the CPU time process p uses as // a three digit float as string. func processPCPU(p *process.Process, ctx *psContext) (string, error) { elapsed, err := p.ElapsedTime() if err != nil { return "", err } cpu, err := p.CPUTime() if err != nil { return "", err } pcpu := 100 * cpu.Seconds() / elapsed.Seconds() return strconv.FormatFloat(pcpu, 'f', 3, 64), nil } // processETIME returns the elapsed time since the process was started. func processETIME(p *process.Process, ctx *psContext) (string, error) { elapsed, err := p.ElapsedTime() if err != nil { return "", nil } return fmt.Sprintf("%v", elapsed), nil } // processTIME returns the cumulative CPU time of process p. func processTIME(p *process.Process, ctx *psContext) (string, error) { cpu, err := p.CPUTime() if err != nil { return "", err } return fmt.Sprintf("%v", cpu), nil } // processStartTime returns the start time of process p. func processStartTime(p *process.Process, ctx *psContext) (string, error) { sTime, err := p.StartTime() if err != nil { return "", err } return fmt.Sprintf("%v", sTime), nil } // processTTY returns the controlling tty (terminal) of process p. func processTTY(p *process.Process, ctx *psContext) (string, error) { ttyNr, err := strconv.ParseUint(p.Stat.TtyNr, 10, 64) if err != nil { return "", nil } tty, err := dev.FindTTY(ttyNr, ctx.ttys) if err != nil { return "", nil } ttyS := "?" if tty != nil { ttyS = strings.TrimPrefix(tty.Path, "/dev/") } return ttyS, nil } // processVSZ returns the virtual memory size of process p in KiB (1024-byte // units). func processVSZ(p *process.Process, ctx *psContext) (string, error) { vmsize, err := strconv.Atoi(p.Stat.Vsize) if err != nil { return "", err } return fmt.Sprintf("%d", vmsize/1024), nil } // parseCAP parses cap (a string bit mask) and returns the associated set of // capabilities. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func parseCAP(cap string) (string, error) { mask, err := strconv.ParseUint(cap, 16, 64) if err != nil { return "", err } if mask == capabilities.FullCAPs { return "full", nil } caps := capabilities.TranslateMask(mask) if len(caps) == 0 { return "none", nil } sort.Strings(caps) return strings.Join(caps, ","), nil } // processCAPAMB returns the set of ambient capabilities associated with // process p. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func processCAPAMB(p *process.Process, ctx *psContext) (string, error) { return parseCAP(p.Status.CapAmb) } // processCAPINH returns the set of inheritable capabilities associated with // process p. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func processCAPINH(p *process.Process, ctx *psContext) (string, error) { return parseCAP(p.Status.CapInh) } // processCAPPRM returns the set of permitted capabilities associated with // process p. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func processCAPPRM(p *process.Process, ctx *psContext) (string, error) { return parseCAP(p.Status.CapPrm) } // processCAPEFF returns the set of effective capabilities associated with // process p. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func processCAPEFF(p *process.Process, ctx *psContext) (string, error) { return parseCAP(p.Status.CapEff) } // processCAPBND returns the set of bounding capabilities associated with // process p. If all capabilities are set, "full" is returned. If no // capability is enabled, "none" is returned. func processCAPBND(p *process.Process, ctx *psContext) (string, error) { return parseCAP(p.Status.CapBnd) } // processSECCOMP returns the seccomp mode of the process (i.e., disabled, // strict or filter) or "?" if /proc/$pid/status.seccomp has a unknown value. func processSECCOMP(p *process.Process, ctx *psContext) (string, error) { switch p.Status.Seccomp { case "0": return "disabled", nil case "1": return "strict", nil case "2": return "filter", nil default: return "?", nil } } // processLABEL returns the process label of process p or "?" if the system // doesn't support labeling. func processLABEL(p *process.Process, ctx *psContext) (string, error) { return p.Label, nil } // processHPID returns the PID of the corresponding host process of the // (container) or "?" if no corresponding process could be found. func processHPID(p *process.Process, ctx *psContext) (string, error) { if hp := findHostProcess(p, ctx); hp != nil { return hp.Pid, nil } return "?", nil } // processHUSER returns the effective user ID of the corresponding host process // of the (container) or "?" if no corresponding process could be found. func processHUSER(p *process.Process, ctx *psContext) (string, error) { if hp := findHostProcess(p, ctx); hp != nil { if ctx.opts != nil && len(ctx.opts.UIDMap) > 0 { return findID(hp.Status.Uids[1], ctx.opts.UIDMap, process.LookupUID, "/proc/sys/fs/overflowuid") } return hp.Huser, nil } return "?", nil } // processHGROUP returns the effective group ID of the corresponding host // process of the (container) or "?" if no corresponding process could be // found. func processHGROUP(p *process.Process, ctx *psContext) (string, error) { if hp := findHostProcess(p, ctx); hp != nil { if ctx.opts != nil && len(ctx.opts.GIDMap) > 0 { return findID(hp.Status.Gids[1], ctx.opts.GIDMap, process.LookupGID, "/proc/sys/fs/overflowgid") } return hp.Hgroup, nil } return "?", nil } // processHGROUPS returns the supplementary groups of the corresponding host // process of the (container) or "?" if no corresponding process could be // found. func processHGROUPS(p *process.Process, ctx *psContext) (string, error) { if hp := findHostProcess(p, ctx); hp != nil { groups := hp.Status.Groups if ctx.opts != nil && len(ctx.opts.GIDMap) > 0 { var err error for i, g := range groups { groups[i], err = findID(g, ctx.opts.GIDMap, process.LookupGID, "/proc/sys/fs/overflowgid") if err != nil { return "", err } } } return strings.Join(groups, ","), nil } return "?", nil } // processRSS returns the resident set size of process p in KiB (1024-byte // units). func processRSS(p *process.Process, ctx *psContext) (string, error) { if p.Status.VMRSS == "" { // probably a kernel thread return "0", nil } return p.Status.VMRSS, nil } // processState returns the process state of process p. func processState(p *process.Process, ctx *psContext) (string, error) { return p.Status.State, nil }