525 lines
9.6 KiB
Go
525 lines
9.6 KiB
Go
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
)
|
|
|
|
var w *tabwriter.Writer
|
|
|
|
type Process struct {
|
|
PID int
|
|
Command string
|
|
CPUTime uint64 // clock ticks
|
|
RSSPages uint64
|
|
SwapBytes uint64
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Processes map[int]Process
|
|
TotalCPU uint64
|
|
}
|
|
|
|
const INPUT_FILENAME = "/home/hpmi/Videos/imp/tests/others/go/process_info/input/process.txt"
|
|
|
|
func getProcessList() (processes []string, err error) {
|
|
file, err := os.Open(INPUT_FILENAME)
|
|
|
|
if err != nil {
|
|
fmt.Println("err: ", err)
|
|
return nil, err
|
|
}
|
|
|
|
sc := bufio.NewScanner(file)
|
|
|
|
processes = []string{}
|
|
|
|
for sc.Scan() {
|
|
processes = append(processes, sc.Text())
|
|
}
|
|
|
|
if err := sc.Err(); err != nil {
|
|
log.Fatalf("Error reading file: %s", err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func printResultOfKeyword(keyword string) {
|
|
keyword = strings.ToLower(keyword)
|
|
|
|
interval := time.Second / 10
|
|
|
|
numCPU := uint64(os.Getuid()) // replaced below
|
|
_ = numCPU
|
|
|
|
cpuCount := uint64(getCPUCount())
|
|
|
|
if cpuCount == 0 {
|
|
cpuCount = 1
|
|
}
|
|
|
|
// First snapshot.
|
|
first, err := takeSnapshot(keyword)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "snapshot error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// _ = interval
|
|
time.Sleep(interval)
|
|
|
|
// Second snapshot.
|
|
second, err := takeSnapshot(keyword)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "snapshot error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
result := calculate(first, second, cpuCount)
|
|
|
|
printResult(keyword, result)
|
|
}
|
|
|
|
func main() {
|
|
w = tabwriter.NewWriter(os.Stdout, 1, 1, 3, ' ', 0)
|
|
|
|
defer w.Flush()
|
|
|
|
if len(os.Args) < 2 {
|
|
// fmt.Printf("Usage: %s <keyword> [interval]\n", os.Args[0])
|
|
// fmt.Printf("\nExamples:\n")
|
|
// fmt.Printf(" %s firefox\n", os.Args[0])
|
|
// fmt.Printf(" %s firefox 1\n", os.Args[0])
|
|
// os.Exit(1)
|
|
|
|
processes, err := getProcessList()
|
|
|
|
if err != nil {
|
|
fmt.Println("err: ", err)
|
|
return
|
|
}
|
|
|
|
// fmt.Println("processes: ", processes)
|
|
|
|
printHeaders()
|
|
for _, process := range processes {
|
|
// fmt.Println(i + 1)
|
|
|
|
printResultOfKeyword(process)
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
printHeaders()
|
|
printResultOfKeyword(os.Args[1])
|
|
|
|
if len(os.Args) >= 3 {
|
|
seconds, err := strconv.ParseFloat(os.Args[2], 64)
|
|
if err != nil || seconds <= 0 {
|
|
fmt.Fprintf(os.Stderr, "invalid interval: %s\n", os.Args[2])
|
|
os.Exit(1)
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
type Result struct {
|
|
ProcessCount int
|
|
CPUPercent float64
|
|
RAMBytes uint64
|
|
SwapBytes uint64
|
|
}
|
|
|
|
func calculate(first, second Snapshot, cpuCount uint64) Result {
|
|
var result Result
|
|
|
|
// RAM and swap are taken from the second snapshot.
|
|
for _, p := range second.Processes {
|
|
result.ProcessCount++
|
|
|
|
result.RAMBytes += pagesToBytes(p.RSSPages)
|
|
result.SwapBytes += p.SwapBytes
|
|
}
|
|
|
|
/*
|
|
CPU calculation:
|
|
|
|
Process CPU time is measured in clock ticks.
|
|
|
|
We calculate:
|
|
|
|
process_delta / total_cpu_delta * number_of_cpus * 100
|
|
|
|
This allows CPU usage to exceed 100% when a process/group
|
|
is using multiple cores.
|
|
*/
|
|
|
|
var processDelta uint64
|
|
|
|
for pid, secondProcess := range second.Processes {
|
|
firstProcess, exists := first.Processes[pid]
|
|
if !exists {
|
|
continue
|
|
}
|
|
|
|
if secondProcess.CPUTime >= firstProcess.CPUTime {
|
|
processDelta += secondProcess.CPUTime - firstProcess.CPUTime
|
|
}
|
|
}
|
|
|
|
totalCPUDelta := second.TotalCPU - first.TotalCPU
|
|
|
|
if totalCPUDelta > 0 {
|
|
// result.CPUPercent =
|
|
// (float64(processDelta) / float64(totalCPUDelta)) *
|
|
// float64(cpuCount) *
|
|
// 100
|
|
|
|
// so instead of cpu percentage being 200% or something it will only be relative to 100%
|
|
result.CPUPercent =
|
|
(float64(processDelta) / float64(totalCPUDelta)) * 100
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func takeSnapshot(keyword string) (Snapshot, error) {
|
|
snapshot := Snapshot{
|
|
Processes: make(map[int]Process),
|
|
}
|
|
|
|
entries, err := os.ReadDir("/proc")
|
|
if err != nil {
|
|
return snapshot, err
|
|
}
|
|
|
|
selfPID := os.Getpid()
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
pid, err := strconv.Atoi(entry.Name())
|
|
if err != nil {
|
|
// Not a PID directory.
|
|
continue
|
|
}
|
|
|
|
// Don't count procusage itself.
|
|
if pid == selfPID {
|
|
continue
|
|
}
|
|
|
|
process, err := readProcess(pid)
|
|
if err != nil {
|
|
// Process may have exited between ReadDir and reading
|
|
// /proc/<pid>. That's normal.
|
|
continue
|
|
}
|
|
|
|
commandLower := strings.ToLower(process.Command)
|
|
|
|
if strings.Contains(commandLower, keyword) {
|
|
snapshot.Processes[pid] = process
|
|
}
|
|
}
|
|
|
|
snapshot.TotalCPU, _ = readTotalCPU()
|
|
|
|
return snapshot, nil
|
|
}
|
|
|
|
func readProcess(pid int) (Process, error) {
|
|
var process Process
|
|
|
|
process.PID = pid
|
|
|
|
// Read the complete command line.
|
|
cmdline, err := os.ReadFile(
|
|
filepath.Join("/proc", strconv.Itoa(pid), "cmdline"),
|
|
)
|
|
if err != nil {
|
|
return process, err
|
|
}
|
|
|
|
/*
|
|
/proc/<pid>/cmdline is separated by NUL bytes:
|
|
|
|
/usr/bin/firefox\0--some-option\0foo\0
|
|
|
|
Convert those NULs into spaces so matching and displaying
|
|
the command line is convenient.
|
|
*/
|
|
command := strings.ReplaceAll(string(cmdline), "\x00", " ")
|
|
command = strings.TrimSpace(command)
|
|
|
|
// Some kernel/system processes have an empty cmdline.
|
|
// Fall back to /comm if necessary.
|
|
if command == "" {
|
|
comm, err := os.ReadFile(
|
|
filepath.Join("/proc", strconv.Itoa(pid), "comm"),
|
|
)
|
|
if err != nil {
|
|
return process, err
|
|
}
|
|
|
|
command = strings.TrimSpace(string(comm))
|
|
}
|
|
|
|
process.Command = command
|
|
|
|
// CPU time from /proc/<pid>/stat.
|
|
cpuTime, rssPages, err := readStat(pid)
|
|
if err != nil {
|
|
return process, err
|
|
}
|
|
|
|
process.CPUTime = cpuTime
|
|
process.RSSPages = rssPages
|
|
|
|
// Swap from /proc/<pid>/status.
|
|
process.SwapBytes = readSwap(pid)
|
|
|
|
return process, nil
|
|
}
|
|
|
|
func readStat(pid int) (cpuTime uint64, rssPages uint64, err error) {
|
|
path := filepath.Join("/proc", strconv.Itoa(pid), "stat")
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
line := string(data)
|
|
|
|
/*
|
|
/proc/<pid>/stat contains:
|
|
|
|
pid (comm) state ppid ...
|
|
|
|
The comm field itself can contain spaces and parentheses,
|
|
so we cannot simply strings.Fields() the whole line.
|
|
|
|
Find the LAST ')' which terminates comm.
|
|
*/
|
|
closeParen := strings.LastIndex(line, ")")
|
|
if closeParen == -1 {
|
|
return 0, 0, errors.New("invalid /proc/stat format")
|
|
}
|
|
|
|
rest := strings.TrimSpace(line[closeParen+1:])
|
|
fields := strings.Fields(rest)
|
|
|
|
/*
|
|
After comm, fields start at:
|
|
|
|
field 3 = state
|
|
field 4 = ppid
|
|
...
|
|
field 14 = utime
|
|
field 15 = stime
|
|
...
|
|
field 24 = rss
|
|
|
|
Because fields[] starts at field 3:
|
|
|
|
utime -> index 11
|
|
stime -> index 12
|
|
rss -> index 21
|
|
*/
|
|
|
|
if len(fields) <= 21 {
|
|
return 0, 0, errors.New("short /proc/stat entry")
|
|
}
|
|
|
|
utime, err := strconv.ParseUint(fields[11], 10, 64)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
stime, err := strconv.ParseUint(fields[12], 10, 64)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
rss, err := strconv.ParseInt(fields[21], 10, 64)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
if rss < 0 {
|
|
rss = 0
|
|
}
|
|
|
|
return utime + stime, uint64(rss), nil
|
|
}
|
|
|
|
func readSwap(pid int) uint64 {
|
|
path := filepath.Join("/proc", strconv.Itoa(pid), "status")
|
|
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if !strings.HasPrefix(line, "VmSwap:") {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
if len(fields) < 2 {
|
|
return 0
|
|
}
|
|
|
|
/*
|
|
VmSwap is normally reported in kB.
|
|
*/
|
|
kb, err := strconv.ParseUint(fields[1], 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
return kb * 1024
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func readTotalCPU() (uint64, error) {
|
|
data, err := os.ReadFile("/proc/stat")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(string(data)))
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if !strings.HasPrefix(line, "cpu ") {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
|
|
if len(fields) < 2 {
|
|
return 0, errors.New("invalid /proc/stat cpu line")
|
|
}
|
|
|
|
var total uint64
|
|
|
|
for _, field := range fields[1:] {
|
|
value, err := strconv.ParseUint(field, 10, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
total += value
|
|
}
|
|
|
|
return total, nil
|
|
}
|
|
|
|
return 0, errors.New("cpu line not found")
|
|
}
|
|
|
|
func pagesToBytes(pages uint64) uint64 {
|
|
pageSize := uint64(os.Getpagesize())
|
|
return pages * pageSize
|
|
}
|
|
|
|
func getCPUCount() int {
|
|
count := os.Getenv("NUMBER_OF_PROCESSORS")
|
|
|
|
if count != "" {
|
|
if n, err := strconv.Atoi(count); err == nil && n > 0 {
|
|
return n
|
|
}
|
|
}
|
|
|
|
// Linux: use /proc/cpuinfo.
|
|
file, err := os.Open("/proc/cpuinfo")
|
|
if err != nil {
|
|
return 1
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
|
|
cpus := 0
|
|
|
|
for scanner.Scan() {
|
|
if strings.HasPrefix(scanner.Text(), "processor") {
|
|
cpus++
|
|
}
|
|
}
|
|
|
|
if cpus == 0 {
|
|
return 1
|
|
}
|
|
|
|
return cpus
|
|
}
|
|
|
|
func printHeaders() {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", "Name", "Processes", "CPU", "RAM", "Swap")
|
|
}
|
|
|
|
func printResult(keyword string, result Result) {
|
|
|
|
// fmt.Printf("%s\n", keyword)
|
|
// fmt.Printf(" Processes : %d\n", result.ProcessCount)
|
|
// fmt.Printf(" CPU : %.1f%%\n", result.CPUPercent)
|
|
// fmt.Printf(" RAM : %s\n", formatBytes(result.RAMBytes))
|
|
// fmt.Printf(" Swap : %s\n", formatBytes(result.SwapBytes))
|
|
// fmt.Printf("\n")
|
|
// fmt.Printf("\n")
|
|
|
|
fmt.Fprintf(w, "%s\t%d\t%.1f\t%s\t%s\n", keyword, result.ProcessCount, result.CPUPercent, formatBytes(result.RAMBytes), formatBytes(result.SwapBytes))
|
|
|
|
}
|
|
|
|
func formatBytes(bytes uint64) string {
|
|
const (
|
|
KB = 1024
|
|
MB = 1024 * KB
|
|
GB = 1024 * MB
|
|
)
|
|
|
|
switch {
|
|
case bytes >= GB:
|
|
return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB))
|
|
|
|
case bytes >= MB:
|
|
return fmt.Sprintf("%.0f MB", float64(bytes)/float64(MB))
|
|
|
|
case bytes >= KB:
|
|
return fmt.Sprintf("%.0f KB", float64(bytes)/float64(KB))
|
|
|
|
default:
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
}
|