package main import ( "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" ) var lastCPU struct { idle, total uint64 mu sync.Mutex } func initCPUUsage() { getCPUUsage() } func systemInfoHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") memPercent, memUsed, memTotal := getMemInfo() diskPercent, diskUsed, diskTotal := getDiskInfo() json.NewEncoder(w).Encode(map[string]any{ "cpu_usage": getCPUUsage(), "cpu_cores": getCPUCore(), "cpu_freq": getCPUFreq(), "cpu_temp": getCPUTemperature(), "mem_percent": memPercent, "mem_used": memUsed, "mem_total": memTotal, "disk_percent": diskPercent, "disk_used": diskUsed, "disk_total": diskTotal, }) } func getCPUUsage() int { lastCPU.mu.Lock() defer lastCPU.mu.Unlock() data, err := os.ReadFile("/proc/stat") if err != nil { return 0 } f := strings.Fields(strings.Split(string(data), "\n")[0]) if len(f) < 5 { return 0 } var idle, total uint64 for i := 1; i < len(f); i++ { v, err := strconv.ParseUint(f[i], 10, 64) if err != nil { continue } total += v // idle + iowait if i == 4 || i == 5 { idle += v } } if lastCPU.total == 0 { lastCPU.idle = idle lastCPU.total = total return 0 } dt := total - lastCPU.total di := idle - lastCPU.idle lastCPU.idle = idle lastCPU.total = total if dt == 0 { return 0 } return int((dt - di) * 100 / dt) } func getCPUCore() int { return runtime.NumCPU() } func getCPUFreq() string { var total, count int for i := 0; ; i++ { data, err := os.ReadFile(fmt.Sprintf("/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", i)) if err != nil { break } freq, err := strconv.Atoi(strings.TrimSpace(string(data))) if err != nil { continue } total += freq / 1000 count++ } if count > 0 { return strconv.Itoa(total / count) } data, err := os.ReadFile("/proc/cpuinfo") if err == nil { for _, line := range strings.Split(string(data), "\n") { if strings.HasPrefix(line, "cpu MHz") { f := strings.Split(line, ":") if len(f) == 2 { v, err := strconv.ParseFloat(strings.TrimSpace(f[1]), 64) if err == nil { return strconv.Itoa(int(v)) } } } } } return noValue } func getCPUTemperature() string { files, _ := filepath.Glob("/sys/class/thermal/thermal_zone*/temp") max := 0 for _, f := range files { data, err := os.ReadFile(f) if err != nil { continue } temp, err := strconv.Atoi(strings.TrimSpace(string(data))) if err == nil && temp > max { max = temp } } if max == 0 { return noValue } return fmt.Sprintf("%d℃", max/1000) } func getMemInfo() (int, string, string) { data, err := os.ReadFile("/proc/meminfo") if err != nil { return 0, noValue, noValue } var total, avail uint64 for _, line := range strings.Split(string(data), "\n") { f := strings.Fields(line) if len(f) < 2 { continue } switch f[0] { case "MemTotal:": total, _ = strconv.ParseUint(f[1], 10, 64) case "MemAvailable:": avail, _ = strconv.ParseUint(f[1], 10, 64) } } if total == 0 { return 0, noValue, noValue } used := total - avail return int(used * 100 / total), formatSize(used * 1024), formatSize(total * 1024) } func getDiskInfo() (int, string, string) { out, err := exec.Command("df", "-h", "/").Output() if err != nil { return 0, noValue, noValue } lines := strings.Split(string(out), "\n") if len(lines) < 2 { return 0, noValue, noValue } f := strings.Fields(lines[1]) if len(f) < 5 { return 0, noValue, noValue } percent, err := strconv.Atoi(strings.TrimSuffix(f[4], "%")) if err != nil { percent = 0 } return percent, f[2], f[1] } func formatSize(size uint64) string { const ( KB = 1024 MB = KB * 1024 GB = MB * 1024 ) switch { case size >= GB: return fmt.Sprintf("%.1fGB", float64(size)/GB) case size >= MB: return fmt.Sprintf("%.0fMB", float64(size)/MB) case size >= KB: return fmt.Sprintf("%.0fKB", float64(size)/KB) default: return fmt.Sprintf("%dB", size) } } func systemLoadAverageHandler(w http.ResponseWriter, r *http.Request) { data, err := os.ReadFile("/proc/loadavg") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fields := strings.Fields(string(data)) if len(fields) < 3 { http.Error(w, "invalid /proc/loadavg", http.StatusInternalServerError) return } resp := struct { Load1m string `json:"load1m"` Load5m string `json:"load5m"` Load15m string `json:"load15m"` }{ Load1m: formatLoad(fields[0]), Load5m: formatLoad(fields[1]), Load15m: formatLoad(fields[2]), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func formatLoad(v string) string { f, err := strconv.ParseFloat(v, 64) if err != nil { return noValue } return strconv.FormatFloat(f, 'f', 2, 64) } func serviceStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } services := []struct { name string dir string }{ {"yfkj-networkd.service", "/opt/yfkj/networkd.service"}, {"yfkj-timesyncd.service", "/opt/yfkj/timesyncd.service"}, {"yfkj-gnss.service", "/opt/yfkj/gnss.service"}, {"yfkj-sshd-mqtt-bridge.service", "/opt/yfkj/sshd-mqtt-bridge.service"}, {"yfkj-camera-capture.service", "/opt/yfkj/camera-capture.service"}, {"yfkj-app-install.service", "/opt/yfkj/app-install.service"}, {"yfkj-web-ui.service", "/opt/yfkj/web-ui.service"}, {"yfkj-upgrade.service", "/opt/yfkj/upgrade.service"}, } data := make(map[string]string, len(services)*2) for i, s := range services { n := i + 1 status := noValue if out, err := exec.Command("systemctl", "is-active", s.name).Output(); err == nil { if strings.TrimSpace(string(out)) == "active" { status = "🟢运行中..." } } data[fmt.Sprintf("var%d", n)] = status if v, err := os.ReadFile(filepath.Join(s.dir, "version.txt")); err == nil { data[fmt.Sprintf("var%d%d", n, n)] = "v" + strings.TrimSpace(string(v)) } else { data[fmt.Sprintf("var%d%d", n, n)] = noValue } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) }