// 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 proc

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/pkg/errors"
)

// Status is a direct translation of a `/proc/[pid]/status`, which provides much
// of the information in /proc/[pid]/stat and /proc/[pid]/statm in a format
// that's easier for humans to parse.
type Status struct {
	// Name: Command run by this process.
	Name string
	// Umask: Process umask, expressed in octal with a leading  zero;  see
	// umask(2). (Since Linux 4.7.)
	Umask string
	// State:  Current  state  of the process.  One of "R (running)", "S
	// (sleeping)", "D (disk sleep)", "T (stopped)", "T (tracing stop)", "Z
	// (zombie)", or "X (dead)".
	State string
	// Tgid: Thread group ID (i.e., Process ID).
	Tgid string
	// Ngid: NUMA group ID (0 if none; since Linux 3.13).
	Ngid string
	// Pid: Thread ID (see gettid(2)).
	Pid string
	// PPid: PID of parent process.
	PPid string
	// TracerPid: PID of process tracing this process (0 if not being traced).
	TracerPid string
	// Uids: Real, effective, saved set, and filesystem.
	Uids []string
	// Gids: Real, effective, saved set, and filesystem.
	Gids []string
	// FDSize: Number of file descriptor slots currently allocated.
	FdSize string
	// Groups: Supplementary group list.
	Groups []string
	// NStgid : Thread group ID (i.e., PID) in each of the PID namespaces
	// of which [pid] is  a member.   The  leftmost  entry shows the value
	// with respect to the PID namespace of the reading process, followed
	// by the value in successively nested inner namespaces.  (Since Linux
	// 4.1.)
	NStgid string
	// NSpid:  Thread ID in each of the PID namespaces of which [pid] is a
	// member.  The fields are ordered as for NStgid.  (Since Linux 4.1.)
	NSpid []string
	// NSpgid: Process group ID in each of the PID namespaces of which
	// [pid] is a member.  The fields are ordered as for NStgid.  (Since
	// Linux 4.1.)
	NSpgid string
	// NSsid:  descendant  namespace session ID hierarchy Session ID in
	// each of the PID names- paces of which [pid] is a member.  The fields
	// are ordered as for NStgid.  (Since  Linux 4.1.)
	NSsid string
	// VMPeak: Peak virtual memory size.
	VMPeak string
	// VMSize: Virtual memory size.
	VMSize string
	// VMLck: Locked memory size (see mlock(3)).
	VMLCK string
	// VMPin:  Pinned  memory  size  (since  Linux  3.2).  These are pages
	// that can't be moved because something needs to directly access
	// physical memory.
	VMPin string
	// VMHWM: Peak resident set size ("high water mark").
	VMHWM string
	// VMRSS: Resident set size.  Note that the value here is the sum of
	// RssAnon, RssFile, and RssShmem.
	VMRSS string
	// RssAnon: Size of resident anonymous memory.  (since Linux 4.5).
	RssAnon string
	// RssFile: Size of resident file mappings.  (since Linux 4.5).
	RssFile string
	// RssShmem:  Size  of  resident  shared memory (includes System V
	// shared memory, mappings from tmpfs(5), and shared anonymous
	// mappings).  (since Linux 4.5).
	RssShmem string
	// VMData: Size of data segment.
	VMData string
	// VMStk: Size of stack segment.
	VMStk string
	// VMExe: Size of text segment.
	VMExe string
	// VMLib: Shared library code size.
	VMLib string
	// VMPTE: Page table entries size (since Linux 2.6.10).
	VMPTE string
	// VMPMD: Size of second-level page tables (since Linux 4.0).
	VMPMD string
	// VMSwap: Swapped-out virtual memory size by anonymous private pages;
	// shmem swap usage is not included (since Linux 2.6.34).
	VMSwap string
	// HugetlbPages: Size of hugetlb memory portions.  (since Linux 4.4).
	HugetlbPages string
	// Threads: Number of threads in process containing this thread.
	Threads string
	// SigQ: This field contains two slash-separated numbers that relate to
	// queued signals for the real user ID of this process.  The first of
	// these is the number of currently queued signals  for  this  real
	// user ID, and the second is the resource limit on the number of
	// queued signals for this process (see the  description  of
	// RLIMIT_SIGPENDING  in  getr- limit(2)).
	SigQ string
	// SigPnd:  Number  of signals pending for thread and for (see pthreads(7)).
	SigPnd string
	// ShdPnd:  Number  of signals pending for process as a whole (see
	// signal(7)).
	ShdPnd string
	//  SigBlk: Mask indicating signals being  blocked (see signal(7)).
	SigBlk string
	//  SigIgn: Mask indicating signals being ignored (see signal(7)).
	SigIgn string
	//  SigCgt: Mask indicating signals being  blocked caught (see signal(7)).
	SigCgt string
	// CapInh:  Mask of capabilities enabled in inheritable sets (see
	// capabilities(7)).
	CapInh string
	// CapPrm:  Mask of capabilities enabled in permitted sets (see
	// capabilities(7)).
	CapPrm string
	// CapEff:  Mask of capabilities enabled in effective sets (see
	// capabilities(7)).
	CapEff string
	// CapBnd: Capability Bounding set (since Linux 2.6.26, see
	// capabilities(7)).
	CapBnd string
	// CapAmb: Ambient capability set (since Linux 4.3, see capabilities(7)).
	CapAmb string
	// NoNewPrivs: Value of the no_new_privs bit (since Linux 4.10, see
	// prctl(2)).
	NoNewPrivs string
	// Seccomp: Seccomp mode of the process (since Linux 3.8, see
	// seccomp(2)).  0  means  SEC- COMP_MODE_DISABLED;  1  means
	// SECCOMP_MODE_STRICT;  2 means SECCOMP_MODE_FILTER.  This field is
	// provided only if the kernel was built with the CONFIG_SECCOMP kernel
	// configu- ration option enabled.
	Seccomp string
	// Cpus_allowed:  Mask  of  CPUs  on  which  this process may run
	// (since Linux 2.6.24, see cpuset(7)).
	CpusAllowed string
	// Cpus_allowed_list: Same as previous, but in "list  format"  (since
	// Linux  2.6.26,  see cpuset(7)).
	CpusAllowedList string
	// Mems_allowed:  Mask  of  memory  nodes allowed to this process
	// (since Linux 2.6.24, see cpuset(7)).
	MemsAllowed string
	// Mems_allowed_list: Same as previous, but in "list  format"  (since
	// Linux  2.6.26,  see cpuset(7)).
	MemsAllowedList string
	// voluntaryCtxtSwitches:  Number of voluntary context switches
	// (since Linux 2.6.23).
	VoluntaryCtxtSwitches string
	// nonvoluntaryCtxtSwitches:  Number of involuntary context switches
	// (since Linux 2.6.23).
	NonvoluntaryCtxtSwitches string
}

// readStatusUserNS joins the user namespace of pid and returns the content of
// /proc/pid/status as a string slice.
func readStatusUserNS(pid string) ([]string, error) {
	path := fmt.Sprintf("/proc/%s/status", pid)
	args := []string{"nsenter", "-U", "-t", pid, "cat", path}

	c := exec.Command(args[0], args[1:]...)
	output, err := c.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("error executing %q: %v", strings.Join(args, " "), err)
	}

	return strings.Split(string(output), "\n"), nil
}

// readStatusDefault returns the content of /proc/pid/status as a string slice.
func readStatusDefault(pid string) ([]string, error) {
	path := fmt.Sprintf("/proc/%s/status", pid)
	f, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	lines := []string{}
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		lines = append(lines, scanner.Text())
	}
	return lines, nil
}

// ParseStatus parses the /proc/$pid/status file and returns a *Status.
func ParseStatus(pid string, joinUserNS bool) (*Status, error) {
	var lines []string
	var err error

	if joinUserNS {
		lines, err = readStatusUserNS(pid)
	} else {
		lines, err = readStatusDefault(pid)
	}

	if err != nil {
		return nil, err
	}
	return parseStatus(pid, lines)
}

// parseStatus extracts data from lines and returns a *Status.
func parseStatus(pid string, lines []string) (*Status, error) {
	s := Status{}
	errUnexpectedInput := fmt.Errorf("unexpected input from /proc/%s/status", pid)
	for _, line := range lines {
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}

		switch fields[0] {
		case "Name:":
			s.Name = fields[1]
		case "Umask:":
			s.Umask = fields[1]
		case "State:":
			s.State = fields[1]
		case "Tgid:":
			s.Tgid = fields[1]
		case "Ngid:":
			s.Ngid = fields[1]
		case "Pid:":
			s.Pid = fields[1]
		case "PPid:":
			s.PPid = fields[1]
		case "TracerPid:":
			s.TracerPid = fields[1]
		case "Uid:":
			if len(fields) != 5 {
				return nil, errors.Wrap(errUnexpectedInput, line)
			}
			s.Uids = []string{fields[1], fields[2], fields[3], fields[4]}
		case "Gid:":
			if len(fields) != 5 {
				return nil, errors.Wrap(errUnexpectedInput, line)
			}
			s.Gids = []string{fields[1], fields[2], fields[3], fields[4]}
		case "FDSize:":
			s.FdSize = fields[1]
		case "Groups:":
			s.Groups = fields[1:]
		case "NStgid:":
			s.NStgid = fields[1]
		case "NSpid:":
			s.NSpid = fields[1:]
		case "NSpgid:":
			s.NSpgid = fields[1]
		case "NSsid:":
			s.NSsid = fields[1]
		case "VmPeak:":
			s.VMPeak = fields[1]
		case "VmSize:":
			s.VMSize = fields[1]
		case "VmLck:":
			s.VMLCK = fields[1]
		case "VmPin:":
			s.VMPin = fields[1]
		case "VmHWM:":
			s.VMHWM = fields[1]
		case "VmRSS:":
			s.VMRSS = fields[1]
		case "RssAnon:":
			s.RssAnon = fields[1]
		case "RssFile:":
			s.RssFile = fields[1]
		case "RssShmem:":
			s.RssShmem = fields[1]
		case "VmData:":
			s.VMData = fields[1]
		case "VmStk:":
			s.VMStk = fields[1]
		case "VmExe:":
			s.VMExe = fields[1]
		case "VmLib:":
			s.VMLib = fields[1]
		case "VmPTE:":
			s.VMPTE = fields[1]
		case "VmPMD:":
			s.VMPMD = fields[1]
		case "VmSwap:":
			s.VMSwap = fields[1]
		case "HugetlbPages:":
			s.HugetlbPages = fields[1]
		case "Threads:":
			s.Threads = fields[1]
		case "SigQ:":
			s.SigQ = fields[1]
		case "SigPnd:":
			s.SigPnd = fields[1]
		case "ShdPnd:":
			s.ShdPnd = fields[1]
		case "SigBlk:":
			s.SigBlk = fields[1]
		case "SigIgn:":
			s.SigIgn = fields[1]
		case "SigCgt:":
			s.SigCgt = fields[1]
		case "CapInh:":
			s.CapInh = fields[1]
		case "CapPrm:":
			s.CapPrm = fields[1]
		case "CapEff:":
			s.CapEff = fields[1]
		case "CapBnd:":
			s.CapBnd = fields[1]
		case "CapAmb:":
			s.CapAmb = fields[1]
		case "NoNewPrivs:":
			s.NoNewPrivs = fields[1]
		case "Seccomp:":
			s.Seccomp = fields[1]
		case "Cpus_allowed:":
			s.CpusAllowed = fields[1]
		case "Cpus_allowed_list:":
			s.CpusAllowedList = fields[1]
		case "Mems_allowed:":
			s.MemsAllowed = fields[1]
		case "Mems_allowed_list:":
			s.MemsAllowedList = fields[1]
		case "voluntary_ctxt_switches:":
			s.VoluntaryCtxtSwitches = fields[1]
		case "nonvoluntary_ctxt_switches:":
			s.NonvoluntaryCtxtSwitches = fields[1]
		}
	}

	return &s, nil
}
