//go:build windows
package main
import (
"context"
"encoding/json"
"html/template"
"log"
"net"
"net/http"
"os"
"os/exec"
"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"`
Profile string `json:"profile"`
}
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
}
// Profile holen (best effort)
profilesByIndex, _ := getWinNetProfiles()
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 || ip.IsLoopback() {
continue
}
ips = append(ips, ip.String())
}
if isLoop {
continue
}
profile := profilesByIndex[ifc.Index] // 👈 jetzt sicher über Index
out = append(out, NetInterface{
Name: ifc.Name,
MAC: ifc.HardwareAddr.String(),
Addresses: ips,
IsLoopback: isLoop,
Profile: profile,
})
}
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
`))
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)
}
}
type psConnProfile struct {
Name string `json:"Name"`
InterfaceAlias string `json:"InterfaceAlias"`
InterfaceIndex int `json:"InterfaceIndex"`
NetworkCategory string `json:"NetworkCategory"` // "Public", "Private", "DomainAuthenticated"
}
func getWinNetProfiles() (map[int]string, error) {
cmd := exec.Command(
"powershell",
"-NoProfile",
"-NonInteractive",
"-Command",
`Get-NetConnectionProfile | Select-Object InterfaceAlias,InterfaceIndex,NetworkCategory | ConvertTo-Json -Depth 3`,
)
out, err := cmd.Output()
if err != nil {
log.Printf("getWinNetProfiles: powershell error: %v", err)
return nil, err
}
if len(out) == 0 {
return map[int]string{}, nil
}
// Wir wissen nicht, ob PS ein Objekt oder ein Array zurückgibt → erst Array versuchen
type psConnProfile struct {
InterfaceAlias string `json:"InterfaceAlias"`
InterfaceIndex int `json:"InterfaceIndex"`
NetworkCategory interface{} `json:"NetworkCategory"`
}
normalizeCat := func(v interface{}) string {
switch vv := v.(type) {
case string:
// "Public", "Private", "DomainAuthenticated"
return vv
case float64:
switch int(vv) {
case 0:
return "Public"
case 1:
return "Private"
case 2:
return "DomainAuthenticated"
default:
return "Unknown"
}
default:
return ""
}
}
// 1) Array probieren
var arr []psConnProfile
if err := json.Unmarshal(out, &arr); err == nil {
res := make(map[int]string, len(arr))
for _, p := range arr {
res[p.InterfaceIndex] = normalizeCat(p.NetworkCategory)
}
return res, nil
}
// 2) Einzelnes Objekt probieren
var single psConnProfile
if err := json.Unmarshal(out, &single); err != nil {
log.Printf("getWinNetProfiles: cannot unmarshal: %v -- out: %s", err, string(out))
return nil, err
}
res := make(map[int]string, 1)
res[single.InterfaceIndex] = normalizeCat(single.NetworkCategory)
return res, nil
}