//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 [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/. 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//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//stat. cpuTime, rssPages, err := readStat(pid) if err != nil { return process, err } process.CPUTime = cpuTime process.RSSPages = rssPages // Swap from /proc//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//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) } }