diff --git a/README.md b/README.md index ced480c..2e0ecd2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ ## Features -- Funktion 1 +- sc.exe create GoSysInfoService binPath="E:\GoProjects\pcinfo\pcinfo.exe" start=auto +- sc.exe description GoSysInfoService "Hilden PC-Info" +- sc.exe start GoSysInfoService --- diff --git a/main.go b/main.go index c456324..1fcb83c 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,9 @@ +//go:build windows + package main import ( + "context" "encoding/json" "html/template" "log" @@ -9,6 +12,7 @@ import ( "os" "os/user" "runtime" + "sort" "strings" "sync" "time" @@ -17,6 +21,9 @@ import ( "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 { @@ -64,17 +71,126 @@ type Summary struct { 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() { +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() @@ -92,7 +208,6 @@ func getInterfaces() []NetInterface { } var out []NetInterface for _, ifc := range ifaces { - // Skip interfaces that are down if (ifc.Flags & net.FlagUp) == 0 { continue } @@ -115,7 +230,6 @@ func getInterfaces() []NetInterface { } ips = append(ips, ip.String()) } - // keep even if no IPs, to show MAC etc., but skip pure loopback if isLoop { continue } @@ -141,7 +255,6 @@ func getDisks() []DiskInfo { continue } - // read-only erkennen (gopsutil: Opts kann string ODER []string sein) ro := false switch opts := any(p.Opts).(type) { case string: @@ -200,7 +313,7 @@ func collectSummary() 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" + 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 }), @@ -239,7 +352,7 @@ var page = template.Must(template.New("index").Parse(`