szaydel
11/7/2019 - 3:12 PM

Selected information from /proc about Bro/Zeek processes

package main

/*
#include <time.h>
#include <unistd.h>
extern int clock_gettime(clockid_t clock_id, struct timespec *tp);
extern long sysconf(int name);
*/
import "C"

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

func handleErr(e error, doPanic bool) {
	if e != nil && e != io.EOF {
		if doPanic {
			panic(e)
		}
		fmt.Fprintf(os.Stderr, "Error: %v\n", e)
	}
}

func ticksToNsecs(ticks int64) int64 {
	var hz_per_sec C.long
	var secs int64
	hz_per_sec = C.sysconf(C._SC_CLK_TCK)
	secs = (ticks / int64(hz_per_sec))
	return secs * 1e9
}

func monotonicClockTicks() int64 {
	var ts C.struct_timespec
	var hz_per_sec C.long
	var ns int64
	C.clock_gettime(C.CLOCK_MONOTONIC, &ts)
	hz_per_sec = C.sysconf(C._SC_CLK_TCK)

	ns = int64(ts.tv_sec) * 1e9
	ns += int64(ts.tv_nsec)
	return (ns / 1e9) * int64(hz_per_sec)
}

func monotonicSinceBoot() time.Duration {
	var ts C.struct_timespec
	C.clock_gettime(C.CLOCK_MONOTONIC, &ts)
	return time.Duration(int64(ts.tv_sec*1e9) + int64(ts.tv_nsec))
}

func nullByteToSpace(b []byte) []byte {
	for i, v := range b {
		if v == 0x0 {
			b[i] = 0x20 // ASCII space character
		}
	}
	return b
}

func cmdLineArgs(pid int) []string {
	var buf = make([]byte, 256)
	var f *os.File
	var err error
	var n int

	var cmdLinePath = fmt.Sprintf("/proc/%d/cmdline", pid)
	if f, err = os.Open(cmdLinePath); err != nil {
		handleErr(err, false)
	}
	defer f.Close()
	if n, err = f.Read(buf); err != nil {
		handleErr(err, false)
	}
	return strings.Split(string(nullByteToSpace(buf[:n])), " ")
}

func ReadFileNoStat(filename string) ([]byte, error) {
	const maxBufferSize = 1024 * 512

	f, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	reader := io.LimitReader(f, maxBufferSize)
	return ioutil.ReadAll(reader)
}

// ProcStat is essentially parsed contents of /proc/<pid>/stat.
type ProcStat struct {
	// The process ID.
	PID int `json:"pid"`
	// The filename of the executable.
	Comm string `json:"comm"`
	// The process state.
	State string `json:"process_state"`
	// The PID of the parent of this process.
	PPID int `json:"parent_pid"`
	// The process group ID of the process.
	PGRP int `json:"process_group"`
	// The session ID of the process.
	Session int `json:"session"`
	// The controlling terminal of the process.
	TTY int `json:"tty"`
	// The ID of the foreground process group of the controlling terminal of
	// the process.
	TPGID int `json:"term_process_group_id"`
	// The kernel flags word of the process.
	Flags uint `json:"flags"`
	// The number of minor faults the process has made which have not required
	// loading a memory page from disk.
	MinFlt uint `json:"minor_faults"`
	// The number of minor faults that the process's waited-for children have
	// made.
	CMinFlt uint `json:"child_minor_faults"`
	// The number of major faults the process has made which have required
	// loading a memory page from disk.
	MajFlt uint `json:"major_faults"`
	// The number of major faults that the process's waited-for children have
	// made.
	CMajFlt uint `json:"child_major_faults"`
	// Amount of time that this process has been scheduled in user mode,
	// measured in clock ticks.
	UTime uint `json:"user_time_ticks"`
	// Amount of time that this process has been scheduled in kernel mode,
	// measured in clock ticks.
	STime uint `json:"system_time_ticks"`
	// Amount of time that this process's waited-for children have been
	// scheduled in user mode, measured in clock ticks.
	CUTime uint `json:"child_user_time_ticks"`
	// Amount of time that this process's waited-for children have been
	// scheduled in kernel mode, measured in clock ticks.
	CSTime uint `json:"child_system_time_ticks"`
	// For processes running a real-time scheduling policy, this is the negated
	// scheduling priority, minus one.
	Priority int `json:"priority"`
	// The nice value, a value in the range 19 (low priority) to -20 (high
	// priority).
	Nice int `json:"nice"`
	// Number of threads in this process.
	NumThreads int `json:"num_threads"`
	// The time the process started after system boot, the value is expressed
	// in clock ticks.
	Starttime uint64 `json:"starttime"`
	// Virtual memory size in bytes.
	VSize uint `json:"virt_memory_bytes"`
	// Resident set size in pages.
	RSS int `json:"rss_pages"`
}

type ProcInfo struct {
	Name        string        `json:"name"`
	Args        []string      `json:"args"`
	PID         int           `json:"pid"`
	AgeTicks    int64         `json:"age_ticks"`
	AgeDuration time.Duration `json:"age_duration"`
	S           *ProcStat     `json:"process_stats"`
}

// Stat returns the current status information of the process.
func (p ProcInfo) Stat() (ProcStat, error) {
	data, err := ReadFileNoStat(p.path("stat"))
	if err != nil {
		handleErr(err, true)
		return ProcStat{}, err
	}
	var (
		ignore int

		s = ProcStat{PID: p.PID}
		l = bytes.Index(data, []byte("("))
		r = bytes.LastIndex(data, []byte(")"))
	)

	if l < 0 || r < 0 {
		return ProcStat{}, fmt.Errorf(
			"unexpected format, couldn't extract comm: %s",
			data,
		)
	}

	s.Comm = string(data[l+1 : r])
	_, err = fmt.Fscan(
		bytes.NewBuffer(data[r+2:]),
		&s.State,
		&s.PPID,
		&s.PGRP,
		&s.Session,
		&s.TTY,
		&s.TPGID,
		&s.Flags,
		&s.MinFlt,
		&s.CMinFlt,
		&s.MajFlt,
		&s.CMajFlt,
		&s.UTime,
		&s.STime,
		&s.CUTime,
		&s.CSTime,
		&s.Priority,
		&s.Nice,
		&s.NumThreads,
		&ignore,
		&s.Starttime,
		&s.VSize,
		&s.RSS,
	)
	if err != nil {
		return ProcStat{}, err
	}
	return s, nil
}

func (p ProcInfo) path(name string) string {
	return fmt.Sprintf("/proc/%d/%s", p.PID, name)
}

func (p ProcInfo) ProcAgeAsTicks() int64 {
	return monotonicClockTicks() - int64(p.S.Starttime)
}

func (p ProcInfo) ProcAgeAsDuration() time.Duration {
	return time.Duration(
		monotonicSinceBoot().Nanoseconds() -
			ticksToNsecs(int64(p.S.Starttime)))
}

func findProcsByName(name string) []*ProcInfo {
	paths, err := filepath.Glob("/proc/[0-9]*")
	handleErr(err, true)

	var piSlc = make([]*ProcInfo, 0)
	for _, p := range paths {
		pSlc := strings.Split(p, "/")
		// FIXME: Check length of slice
		pid, err := strconv.Atoi(pSlc[len(pSlc)-1])
		handleErr(err, true)
		args := cmdLineArgs(pid)
		if args[0] == name {
			proci := &ProcInfo{}
			proci.Args = cmdLineArgs(pid)
			proci.Name = proci.Args[2]
			proci.PID = pid
			s, _ := proci.Stat()
			proci.S = &s
			proci.AgeTicks = proci.ProcAgeAsTicks()
			proci.AgeDuration = proci.ProcAgeAsDuration()
			piSlc = append(piSlc, proci)
		}
	}
	return piSlc
}

func main() {
	processes := findProcsByName("bro")
	j, _ := json.Marshal(processes)
	fmt.Printf("%v\n", string(j))
}