package stats import ( "bytes" "os" "path/filepath" "runtime" "strconv" "strings" "time" ) // Server statistics from /proc: named's process and the host. They need no // privileges: /proc/PID/stat and /proc/PID/status are readable by every // user (unless /proc is mounted with hidepid). // clockTicks is USER_HZ, the unit of the CPU times in /proc/PID/stat. It // is 100 on every Linux architecture Jarl runs on. const clockTicks = 100 // procRoot is where /proc is (tests point it elsewhere). var procRoot = "/proc" // namedProc is a sample of named's process. type namedProc struct { PID int RSS int64 // bytes Threads int64 CPU int64 // user + system time, in clock ticks At time.Time } // findNamed returns the PID of named: the process named "named" whose // command line mentions mainConfig, or else the oldest one (the lowest // PID). 0 when there is none. func findNamed(mainConfig string) int { entries, err := os.ReadDir(procRoot) if err != nil { return 0 } best := 0 for _, e := range entries { pid, err := strconv.Atoi(e.Name()) if err != nil { continue } comm, err := os.ReadFile(filepath.Join(procRoot, e.Name(), "comm")) if err != nil || strings.TrimSpace(string(comm)) != "named" { continue } if mainConfig != "" { if cmd, err := os.ReadFile(filepath.Join(procRoot, e.Name(), "cmdline")); err == nil && bytes.Contains(cmd, []byte(mainConfig)) { return pid } } if best == 0 || pid < best { best = pid } } return best } // readNamedProc samples a process. func readNamedProc(pid int, now time.Time) (*namedProc, bool) { dir := filepath.Join(procRoot, strconv.Itoa(pid)) stat, err := os.ReadFile(filepath.Join(dir, "stat")) if err != nil { return nil, false } // The command name is in parentheses and may contain spaces: the // fields that follow start after the last ')'. i := bytes.LastIndexByte(stat, ')') if i < 0 { return nil, false } f := strings.Fields(string(stat[i+1:])) // f[0] is field 3 (state); utime and stime are fields 14 and 15. if len(f) < 13 { return nil, false } utime, _ := strconv.ParseInt(f[11], 10, 64) stime, _ := strconv.ParseInt(f[12], 10, 64) p := &namedProc{PID: pid, CPU: utime + stime, At: now} status, err := os.ReadFile(filepath.Join(dir, "status")) if err != nil { return nil, false } for _, line := range strings.Split(string(status), "\n") { k, v, ok := strings.Cut(line, ":") if !ok { continue } switch k { case "VmRSS": p.RSS = kib(v) case "Threads": p.Threads, _ = strconv.ParseInt(strings.TrimSpace(v), 10, 64) } } return p, true } // kib parses a "1234 kB" value from /proc into bytes. func kib(v string) int64 { n, _ := strconv.ParseInt(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(v), "kB")), 10, 64) return n * 1024 } // Host is the machine's memory, load and uptime. type Host struct { MemTotal int64 `json:"mem_total"` MemAvailable int64 `json:"mem_available"` Load []float64 `json:"load"` // 1, 5 and 15 minutes CPUs int `json:"cpus"` Uptime int64 `json:"uptime"` // seconds } func readHost() *Host { h := &Host{CPUs: runtime.NumCPU()} if b, err := os.ReadFile(filepath.Join(procRoot, "meminfo")); err == nil { for _, line := range strings.Split(string(b), "\n") { k, v, _ := strings.Cut(line, ":") switch k { case "MemTotal": h.MemTotal = kib(v) case "MemAvailable": h.MemAvailable = kib(v) } } } if b, err := os.ReadFile(filepath.Join(procRoot, "loadavg")); err == nil { for _, f := range strings.Fields(string(b))[:min(3, len(strings.Fields(string(b))))] { x, _ := strconv.ParseFloat(f, 64) h.Load = append(h.Load, x) } } if b, err := os.ReadFile(filepath.Join(procRoot, "uptime")); err == nil { if f := strings.Fields(string(b)); len(f) > 0 { x, _ := strconv.ParseFloat(f[0], 64) h.Uptime = int64(x) } } return h }