package main import ( "encoding/json" "fmt" "net/http" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) 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) < 4 { http.Error(w, "invalid /proc/loadavg", http.StatusInternalServerError) return } boot, uptime := noValue, noValue if b, err := os.ReadFile("/proc/uptime"); err == nil { if fields := strings.Fields(string(b)); len(fields) > 0 { if sec, err := strconv.ParseFloat(fields[0], 64); err == nil { d := time.Duration(sec) * time.Second boot = time.Now().Add(-d).Format("2006-01-02 15:04:05") day := int(d.Hours() / 24) if day > 0 { uptime = fmt.Sprintf("%d天 %02d:%02d:%02d", int(d.Hours()/24), int(d.Hours())%24, int(d.Minutes())%60, int(d.Seconds())%60) } else { uptime = fmt.Sprintf("%02d:%02d:%02d", int(d.Hours())%24, int(d.Minutes())%60, int(d.Seconds())%60) } } } } resp := struct { Load1m string `json:"load1m"` Load5m string `json:"load5m"` Load15m string `json:"load15m"` RunQueue string `json:"runQueue"` BootTime string `json:"bootTime"` UpTime string `json:"uptime"` }{ Load1m: formatLoad(fields[0]), Load5m: formatLoad(fields[1]), Load15m: formatLoad(fields[2]), RunQueue: fields[3], BootTime: boot, UpTime: uptime, } 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) } var services = []struct { name string dir string bin string }{ {"yfkj-networkd.service", "/opt/yfkj/networkd.service", "networkd"}, {"yfkj-timesyncd.service", "/opt/yfkj/timesyncd.service", "timesyncd"}, {"yfkj-gnss.service", "/opt/yfkj/gnss.service", "gnss"}, {"yfkj-sshd-mqtt-bridge.service", "/opt/yfkj/sshd-mqtt-bridge.service", "sshd-mqtt-bridge"}, {"yfkj-camera-capture.service", "/opt/yfkj/camera-capture.service", "camera-capture"}, {"yfkj-app-install.service", "/opt/yfkj/app-install.service", "app-install"}, {"yfkj-web-ui.service", "/opt/yfkj/web-ui.service", "web-ui"}, {"yfkj-upgrade.service", "/opt/yfkj/upgrade.service", "upgrade"}, {"yfkj-local-startup.service", "/opt/yfkj/local-startup.service", "startup.sh"}, } func serviceStatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } data := make(map[string]string, len(services)*2) for i, s := range services { n := i + 1 status := noValue if out, err := exec.Command("systemctl", "show", s.name, "-p", "ActiveState", "-p", "SubState").Output(); err == nil { v := string(out) switch { case strings.Contains(v, "ActiveState=active") && strings.Contains(v, "SubState=running"): status = "🟢运行中" case strings.Contains(v, "ActiveState=active") && strings.Contains(v, "SubState=exited"): 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) } type VersionSwitchReq struct { Service string `json:"service"` Action string `json:"action"` Password string `json:"password"` } type VersionResp struct { Status string `json:"status"` Message string `json:"message,omitempty"` } var versionSwitchLock sync.Mutex func serviceVersionSwitchHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } versionSwitchLock.Lock() defer versionSwitchLock.Unlock() var req VersionSwitchReq if json.NewDecoder(r.Body).Decode(&req) != nil { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "invalid request"}) return } if req.Password != "yfkj123456" { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "输入的密码不正确"}) return } var cfg struct{ name, dir, bin string } for _, v := range services { if v.name == req.Service { cfg = v break } } if cfg.name == "" { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "unknown service"}) return } link := filepath.Join(cfg.dir, cfg.bin) oldLink, err := os.Readlink(link) if err != nil { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "读取当前版本失败"}) return } current := filepath.Base(filepath.Dir(oldLink)) if current != "a" && current != "b" && current != "c" { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "当前版本状态异常"}) return } target := "" switch req.Action { case "factory": if current == "a" { json.NewEncoder(w).Encode(VersionResp{Status: "ok", Message: "当前已是出厂版本"}) return } target = "a" case "previous": switch current { case "b": if info, err := os.Stat(filepath.Join(cfg.dir, "c", cfg.bin)); err == nil && !info.IsDir() && info.Size() > 0 && info.Mode().Perm()&0111 != 0 { target = "c" } case "c": if info, err := os.Stat(filepath.Join(cfg.dir, "b", cfg.bin)); err == nil && !info.IsDir() && info.Size() > 0 && info.Mode().Perm()&0111 != 0 { target = "b" } } if target == "" { json.NewEncoder(w).Encode(VersionResp{ Status: "error", Message: "当前没有可回退的上一版本", }) return } default: json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: "invalid action"}) return } targetBin := filepath.Join(cfg.dir, target, cfg.bin) info, err := os.Stat(targetBin) if err != nil || info.IsDir() || info.Size() == 0 || info.Mode().Perm()&0111 == 0 { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: fmt.Sprintf("目标版本文件异常: %s", target)}) return } if out, err := exec.Command("systemctl", "stop", req.Service).CombinedOutput(); err != nil { json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: fmt.Sprintf("停止当前服务失败: %s", out)}) return } tmp := filepath.Join(cfg.dir, cfg.bin+".tmp") os.Remove(tmp) if err := os.Symlink(filepath.Join(target, cfg.bin), tmp); err != nil { exec.Command("systemctl", "start", req.Service).Run() json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: err.Error()}) return } if err := os.Rename(tmp, link); err != nil { os.Remove(tmp) exec.Command("systemctl", "start", req.Service).Run() json.NewEncoder(w).Encode(VersionResp{Status: "error", Message: err.Error()}) return } if out, err := exec.Command("systemctl", "start", req.Service).CombinedOutput(); err != nil { tmp := filepath.Join(cfg.dir, cfg.bin+".tmp") os.Remove(tmp) if e := os.Symlink(oldLink, tmp); e == nil { if e = os.Rename(tmp, link); e != nil { os.Remove(tmp) } } exec.Command("systemctl", "start", req.Service).Run() json.NewEncoder(w).Encode(VersionResp{ Status: "error", Message: fmt.Sprintf("目标版本启动失败,已恢复旧版本: %s", out), }) return } json.NewEncoder(w).Encode(VersionResp{Status: "ok"}) }