From f68d7c811a5da2acc8e7c9d836c18e63e52025fb Mon Sep 17 00:00:00 2001 From: jbergner Date: Wed, 29 Oct 2025 11:28:48 +0100 Subject: [PATCH] init --- .../workflows/container.yml => container.yml | 0 go.mod | 16 + go.sum | 36 ++ main.go | 398 ++++++++++++++++++ 4 files changed, 450 insertions(+) rename .gitea/workflows/container.yml => container.yml (100%) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitea/workflows/container.yml b/container.yml similarity index 100% rename from .gitea/workflows/container.yml rename to container.yml diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0943649 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module git.send.nrw/sendnrw/pcinfo + +go 1.24.1 + +require github.com/shirou/gopsutil/v3 v3.24.5 + +require ( + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/sys v0.20.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..61e6f38 --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..c456324 --- /dev/null +++ b/main.go @@ -0,0 +1,398 @@ +package main + +import ( + "encoding/json" + "html/template" + "log" + "net" + "net/http" + "os" + "os/user" + "runtime" + "strings" + "sync" + "time" + + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/host" + "github.com/shirou/gopsutil/v3/mem" +) + +type NetInterface struct { + Name string `json:"name"` + MAC string `json:"mac"` + Addresses []string `json:"addresses"` + IsLoopback bool `json:"is_loopback"` +} + +type DiskInfo struct { + Device string `json:"device"` + Mountpoint string `json:"mountpoint"` + Fstype string `json:"fstype"` + Total uint64 `json:"total"` + Used uint64 `json:"used"` + Free uint64 `json:"free"` + UsedPercent float64 `json:"used_percent"` + ReadOnly bool `json:"read_only"` +} + +type Summary struct { + Timestamp time.Time `json:"timestamp"` + Hostname string `json:"hostname"` + Username string `json:"username"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + KernelVersion string `json:"kernel_version"` + Arch string `json:"arch"` + UptimeSeconds uint64 `json:"uptime_seconds"` + BootTime time.Time `json:"boot_time"` + Interfaces []NetInterface `json:"interfaces"` + CPUModel string `json:"cpu_model"` + PhysicalCores int `json:"physical_cores"` + LogicalCores int `json:"logical_cores"` + CPUPercent float64 `json:"cpu_percent"` + MemoryTotal uint64 `json:"memory_total"` + MemoryUsed uint64 `json:"memory_used"` + MemoryFree uint64 `json:"memory_free"` + MemoryUsedPct float64 `json:"memory_used_percent"` + SwapTotal uint64 `json:"swap_total"` + SwapUsed uint64 `json:"swap_used"` + SwapFree uint64 `json:"swap_free"` + SwapUsedPct float64 `json:"swap_used_percent"` + Disks []DiskInfo `json:"disks"` +} + +var ( + mu sync.RWMutex + lastCPUUsage float64 +) + +// background sampler for CPU percentage to avoid blocking API calls +func startCPUSampler() { + go func() { + // Prime once + _, _ = cpu.Percent(0, false) + for { + pcts, err := cpu.Percent(time.Second, false) + if err == nil && len(pcts) > 0 { + mu.Lock() + lastCPUUsage = pcts[0] + mu.Unlock() + } + } + }() +} + +func getInterfaces() []NetInterface { + ifaces, err := net.Interfaces() + if err != nil { + return nil + } + var out []NetInterface + for _, ifc := range ifaces { + // Skip interfaces that are down + if (ifc.Flags & net.FlagUp) == 0 { + continue + } + addrs, _ := ifc.Addrs() + var ips []string + isLoop := (ifc.Flags & net.FlagLoopback) != 0 + for _, a := range addrs { + var ip net.IP + switch v := a.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil { + continue + } + if ip.IsLoopback() { + continue + } + ips = append(ips, ip.String()) + } + // keep even if no IPs, to show MAC etc., but skip pure loopback + if isLoop { + continue + } + out = append(out, NetInterface{ + Name: ifc.Name, + MAC: ifc.HardwareAddr.String(), + Addresses: ips, + IsLoopback: isLoop, + }) + } + return out +} + +func getDisks() []DiskInfo { + parts, err := disk.Partitions(true) + if err != nil { + return nil + } + var out []DiskInfo + for _, p := range parts { + du, err := disk.Usage(p.Mountpoint) + if err != nil { + continue + } + + // read-only erkennen (gopsutil: Opts kann string ODER []string sein) + ro := false + switch opts := any(p.Opts).(type) { + case string: + ro = strings.Contains(strings.ToLower(opts), "ro") + case []string: + for _, o := range opts { + if strings.EqualFold(o, "ro") { + ro = true + break + } + } + } + + out = append(out, DiskInfo{ + Device: p.Device, + Mountpoint: p.Mountpoint, + Fstype: p.Fstype, + Total: du.Total, + Used: du.Used, + Free: du.Free, + UsedPercent: du.UsedPercent, + ReadOnly: ro, + }) + } + return out +} + +func collectSummary() Summary { + hi, _ := host.Info() + vm, _ := mem.VirtualMemory() + sw, _ := mem.SwapMemory() + coresPhys, _ := cpu.Counts(false) + coresLog, _ := cpu.Counts(true) + cpuInfos, _ := cpu.Info() + username := "" + if u, err := user.Current(); err == nil { + username = u.Username + } else { + username = os.Getenv("USERNAME") + } + model := "" + if len(cpuInfos) > 0 { + model = cpuInfos[0].ModelName + } + + mu.RLock() + cpuPct := lastCPUUsage + mu.RUnlock() + + var boot time.Time + if hi != nil && hi.BootTime > 0 { + boot = time.Unix(int64(hi.BootTime), 0) + } + + return Summary{ + Timestamp: time.Now(), + Hostname: safe(hi, func(h *host.InfoStat) string { return h.Hostname }), + Username: username, + OS: safe(hi, func(h *host.InfoStat) string { return h.OS }), // "windows" + Platform: safe(hi, func(h *host.InfoStat) string { return h.Platform }), + PlatformVersion: safe(hi, func(h *host.InfoStat) string { return h.PlatformVersion }), + KernelVersion: safe(hi, func(h *host.InfoStat) string { return h.KernelVersion }), + Arch: runtime.GOARCH, + UptimeSeconds: safe(hi, func(h *host.InfoStat) uint64 { return h.Uptime }), + BootTime: boot, + Interfaces: getInterfaces(), + CPUModel: model, + PhysicalCores: coresPhys, + LogicalCores: coresLog, + CPUPercent: cpuPct, + MemoryTotal: vm.Total, + MemoryUsed: vm.Used, + MemoryFree: vm.Free, + MemoryUsedPct: vm.UsedPercent, + SwapTotal: sw.Total, + SwapUsed: sw.Used, + SwapFree: sw.Free, + SwapUsedPct: sw.UsedPercent, + Disks: getDisks(), + } +} + +func safe[T any, R any](v T, f func(T) R) R { + if any(v) == nil { + var zero R + return zero + } + return f(v) +} + +var page = template.Must(template.New("index").Parse(` + + + + +Windows Systeminfo + + + +

Windows Systeminfo

+

+
+
+
Host
+
Hostname
+
User
+
Uptime
+
Boot
+
Arch
+
+ +
+
OS
+
Betriebssystem
+
Platform
+
Kernel
+
Stand
+
+ +
+
CPU
+
Modell
+
Kerne (phys/log)
+
Auslastung
+
+
+ +
+
Speicher
+
RAM
+
+
Swap
+
+
+ +
+
Netzwerk
+
+
+ +
+
Laufwerke
+ + + +
MountTypBelegtGesamt%
+
+
+ + + + + +`)) + +func indexHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := page.Execute(w, nil); err != nil { + http.Error(w, err.Error(), 500) + } +} + +func apiHandler(w http.ResponseWriter, r *http.Request) { + sum := collectSummary() + w.Header().Set("Content-Type", "application/json") + enc := json.NewEncoder(w) + enc.SetIndent("", " ") + if err := enc.Encode(sum); err != nil { + http.Error(w, err.Error(), 500) + } +} + +func main() { + startCPUSampler() + + mux := http.NewServeMux() + mux.HandleFunc("/", indexHandler) + mux.HandleFunc("/api/summary", apiHandler) + + addr := "127.0.0.1:8080" // bind strictly to loopback (IPv4) + log.Printf("Starte lokales Dashboard auf http://%s … (nur von diesem Rechner erreichbar)", addr) + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatal(err) + } +}