psgo.go
File `psgo.go` has 685 lines of code (exceeds 500 allowed). Consider refactoring.
Your code does not pass gofmt in 1 place. Go fmt your code!// Copyright 2021 Jeffery H. Johnson <trnsz@pobox.com>// Copyright 2021 Gridfinity, LLC.// Copyright 2020 The 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 gfpsgo 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.//package gfpsgo // import "github.com/johnsonjh/gfpsgo" import ( "fmt" "io/ioutil" "os" "runtime" "sort" "strconv" "strings" "sync" "github.com/johnsonjh/gfpsgo/internal/capabilities" "github.com/johnsonjh/gfpsgo/internal/dev" "github.com/johnsonjh/gfpsgo/internal/proc" "github.com/johnsonjh/gfpsgo/internal/process" "github.com/pkg/errors" "golang.org/x/sys/unix") // IDMap specifies a mapping range from the host to the container IDs.type IDMap struct { // ContainerID is the first ID in the container. ContainerID int // HostID is the first ID in the host. HostID int // Size specifies how long is the range. e.g. 1 means a single user // is mapped. Size int} // 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 []IDMap // GIDMap specifies a mapping for GIDs in the container. If specified // hgroup will perform the reverse mapping. GIDMap []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 mappingFunction `findID` has 5 return statements (exceeds 4 allowed).func findID( idStr string, mapping []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 "", errors.Wrapf(err, "cannot parse %s", idStr) } 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 "", errors.Wrapf(err, "cannot read %s", overflowFile) } 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, errors.Wrapf(ErrUnknownDescriptor, "'%s'", d) } } 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, }, { 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: "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{}, )} // ReadMappings ...func ReadMappings(path string) ([]IDMap, error) { mappings, err := proc.ReadMappings(path) if err != nil { return nil, err } var res []IDMap for _, i := range mappings { m := IDMap{ ContainerID: i.ContainerID, HostID: i.HostID, Size: i.Size, } res = append(res, m) } return res, nil} func contextFromOptions(options *JoinNamespaceOpts) (*psContext, error) { ctx := new(psContext) ctx.opts = options if ctx.opts != nil && ctx.opts.FillMappings { uidMappings, err := ReadMappings("/proc/self/uid_map") if err != nil { return nil, err } gidMappings, err := 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`.Function `JoinNamespaceAndProcessInfoWithOptions` has 68 lines of code (exceeds 50 allowed). Consider refactoring.
Function `JoinNamespaceAndProcessInfoWithOptions` has 10 return statements (exceeds 4 allowed).
Function `JoinNamespaceAndProcessInfoWithOptions` has a Cognitive Complexity of 22 (exceeds 20 allowed). Consider refactoring.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 = errors.Wrapf(err, "error determining user namespace") return } pidUserNs, err := proc.ParseUserNamespace(pid) if err != nil { dataErr = errors.Wrapf( err, "error determining user namespace of PID %s", pid, ) } // 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, 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 os.IsNotExist(errors.Cause(err)) { // catch race conditions continue } return nil, errors.Wrapf(err, "error extracting PID namespace") } 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 os.IsNotExist(errors.Cause(err)) { // 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, 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, 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 optained, or a decimal representation// otherwise.func processGROUP(p *process.Process, _ *psContext) (string, error) { return process.LookupGID(p.Status.Gids[1])} // processRGROUP returns the real group ID of the process. This will be// the textual group ID, if it can be optained, or a decimal representation// otherwise.func processRGROUP(p *process.Process, _ *psContext) (string, error) { return process.LookupGID(p.Status.Gids[0])} // processPPID returns the parent process ID of process p.func processPPID(p *process.Process, _ *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 optained, or a decimal representation// otherwise.func processUSER(p *process.Process, _ *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 optained, or a decimal representation// otherwise.func processRUSER(p *process.Process, _ *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, _ *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, _ *psContext) (string, error) { return p.Stat.Comm, nil} // processNICE returns the nice value of process p.func processNICE(p *process.Process, _ *psContext) (string, error) { return p.Stat.Nice, nil} // processPID returns the process ID of process p.func processPID(p *process.Process, _ *psContext) (string, error) { return p.Pid, nil} // processPGID returns the process group ID of process p.func processPGID(p *process.Process, _ *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, _ *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, _ *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, _ *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, _ *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, _ *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(capz string) (string, error) { mask, err := strconv.ParseUint(capz, 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, _ *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, _ *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, _ *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, _ *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, _ *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, _ *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, _ *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.Similar blocks of code found in 2 locations. Consider refactoring.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.Similar blocks of code found in 2 locations. Consider refactoring.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} // processRSS returns the resident set size of process p in KiB (1024-byte// units).func processRSS(p *process.Process, _ *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, _ *psContext) (string, error) { return p.Status.State, nil}