//go:build windows package main import ( "context" "encoding/json" "html/template" "log" "net" "net/http" "os" "os/user" "runtime" "sort" "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" "golang.org/x/sys/windows/registry" "golang.org/x/sys/windows/svc" ) 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"` } type InstalledApp struct { Name string `json:"name"` Version string `json:"version"` Publisher string `json:"publisher"` InstallDate string `json:"install_date"` Source string `json:"source"` // HKLM-64, HKLM-32, HKCU } var ( mu sync.RWMutex lastCPUUsage float64 ) func readUninstallKey(base registry.Key, path, src string) []InstalledApp { k, err := registry.OpenKey(base, path, registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE) if err != nil { return nil } defer k.Close() names, err := k.ReadSubKeyNames(-1) if err != nil { return nil } apps := make([]InstalledApp, 0, len(names)) for _, name := range names { sk, err := registry.OpenKey(k, name, registry.QUERY_VALUE) if err != nil { continue } // versteckte System-Komponenten ausblenden if v, _, err := sk.GetIntegerValue("SystemComponent"); err == nil && v == 1 { sk.Close() continue } displayName, _, _ := sk.GetStringValue("DisplayName") if strings.TrimSpace(displayName) == "" { sk.Close() continue } displayVersion, _, _ := sk.GetStringValue("DisplayVersion") publisher, _, _ := sk.GetStringValue("Publisher") installDate, _, _ := sk.GetStringValue("InstallDate") apps = append(apps, InstalledApp{ Name: displayName, Version: displayVersion, Publisher: publisher, InstallDate: installDate, Source: src, }) sk.Close() } return apps } func getInstalledApps() []InstalledApp { var all []InstalledApp // Maschinenweit 64-bit all = append(all, readUninstallKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, "HKLM-64")..., ) // Maschinenweit 32-bit auf 64-bit all = append(all, readUninstallKey(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, "HKLM-32")..., ) // Aktueller Benutzer all = append(all, readUninstallKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, "HKCU")..., ) // Deduplizieren (Name+Version+Publisher) uniq := make(map[string]InstalledApp, len(all)) for _, a := range all { key := strings.ToLower(strings.TrimSpace(a.Name) + "|" + strings.TrimSpace(a.Version) + "|" + strings.TrimSpace(a.Publisher)) if _, ok := uniq[key]; !ok { uniq[key] = a } } out := make([]InstalledApp, 0, len(uniq)) for _, v := range uniq { out = append(out, v) } sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name) }) return out } func appsHandler(w http.ResponseWriter, r *http.Request) { apps := getInstalledApps() w.Header().Set("Content-Type", "application/json") enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(apps) } // background sampler for CPU percentage to avoid blocking API calls func startCPUSampler(ctx context.Context) { go func() { // Prime once _, _ = cpu.Percent(0, false) for { select { case <-ctx.Done(): return default: } 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 { 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()) } 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 } 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 }), 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%
Installierte Programme
Name Version Publisher Quelle
`)) 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) } } // --- ab hier: Dienst-Integration --- const serviceName = "GoSysInfoService" // runHTTP startet deinen bisherigen HTTP-Server und reagiert auf ctx.Done() func runHTTP(ctx context.Context) error { mux := http.NewServeMux() mux.HandleFunc("/", indexHandler) mux.HandleFunc("/api/apps", appsHandler) mux.HandleFunc("/api/summary", apiHandler) addr := "127.0.0.1:8080" srv := &http.Server{ Addr: addr, Handler: mux, } // CPU-Sampler an das gleiche ctx hängen startCPUSampler(ctx) log.Printf("Starte lokales Dashboard auf http://%s … (nur von diesem Rechner erreichbar)", addr) // Shutdown-Goroutine go func() { <-ctx.Done() shCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) _ = srv.Shutdown(shCtx) cancel() // explizit aufrufen, kein defer in der Goroutine nötig }() // Blockiert hier bis Stop oder Fehler err := srv.ListenAndServe() // ListenAndServe gibt bei Shutdown typischerweise http.ErrServerClosed zurück if err == http.ErrServerClosed { return nil } return err } // windows-svc Wrapper type winService struct{} func (s *winService) Execute(args []string, r <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { const accepted = svc.AcceptStop | svc.AcceptShutdown status <- svc.Status{State: svc.StartPending} ctx, cancel := context.WithCancel(context.Background()) defer cancel() // <-- neu: für alle Rückgabepfade go func() { if err := runHTTP(ctx); err != nil { log.Printf("HTTP server stopped: %v", err) } }() status <- svc.Status{State: svc.Running, Accepts: accepted} for c := range r { switch c.Cmd { case svc.Interrogate: status <- c.CurrentStatus case svc.Stop, svc.Shutdown: status <- svc.Status{State: svc.StopPending} return false, 0 // cancel() läuft trotzdem dank defer } } // falls der Channel mal zu ist return false, 0 } func main() { // Prüfen, ob wir als Windows-Dienst laufen isService, err := svc.IsWindowsService() if err != nil { log.Fatalf("svc.IsWindowsService: %v", err) } if isService { // Hier spricht das Programm mit dem Service Control Manager if err := svc.Run(serviceName, &winService{}); err != nil { log.Fatalf("svc.Run failed: %v", err) } return } // normaler Konsolenmodus ctx := context.Background() if err := runHTTP(ctx); err != nil { log.Fatal(err) } }