generated from sendnrw/template_golang
Anpassung auf Windows-Dienst
This commit is contained in:
@@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
## Features
|
## 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
284
main.go
284
main.go
@@ -1,6 +1,9 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
@@ -9,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +21,9 @@ import (
|
|||||||
"github.com/shirou/gopsutil/v3/disk"
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NetInterface struct {
|
type NetInterface struct {
|
||||||
@@ -64,17 +71,126 @@ type Summary struct {
|
|||||||
Disks []DiskInfo `json:"disks"`
|
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 (
|
var (
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
lastCPUUsage float64
|
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
|
// background sampler for CPU percentage to avoid blocking API calls
|
||||||
func startCPUSampler() {
|
func startCPUSampler(ctx context.Context) {
|
||||||
go func() {
|
go func() {
|
||||||
// Prime once
|
// Prime once
|
||||||
_, _ = cpu.Percent(0, false)
|
_, _ = cpu.Percent(0, false)
|
||||||
for {
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
pcts, err := cpu.Percent(time.Second, false)
|
pcts, err := cpu.Percent(time.Second, false)
|
||||||
if err == nil && len(pcts) > 0 {
|
if err == nil && len(pcts) > 0 {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
@@ -92,7 +208,6 @@ func getInterfaces() []NetInterface {
|
|||||||
}
|
}
|
||||||
var out []NetInterface
|
var out []NetInterface
|
||||||
for _, ifc := range ifaces {
|
for _, ifc := range ifaces {
|
||||||
// Skip interfaces that are down
|
|
||||||
if (ifc.Flags & net.FlagUp) == 0 {
|
if (ifc.Flags & net.FlagUp) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -115,7 +230,6 @@ func getInterfaces() []NetInterface {
|
|||||||
}
|
}
|
||||||
ips = append(ips, ip.String())
|
ips = append(ips, ip.String())
|
||||||
}
|
}
|
||||||
// keep even if no IPs, to show MAC etc., but skip pure loopback
|
|
||||||
if isLoop {
|
if isLoop {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -141,7 +255,6 @@ func getDisks() []DiskInfo {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// read-only erkennen (gopsutil: Opts kann string ODER []string sein)
|
|
||||||
ro := false
|
ro := false
|
||||||
switch opts := any(p.Opts).(type) {
|
switch opts := any(p.Opts).(type) {
|
||||||
case string:
|
case string:
|
||||||
@@ -200,7 +313,7 @@ func collectSummary() Summary {
|
|||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Hostname: safe(hi, func(h *host.InfoStat) string { return h.Hostname }),
|
Hostname: safe(hi, func(h *host.InfoStat) string { return h.Hostname }),
|
||||||
Username: username,
|
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 }),
|
Platform: safe(hi, func(h *host.InfoStat) string { return h.Platform }),
|
||||||
PlatformVersion: safe(hi, func(h *host.InfoStat) string { return h.PlatformVersion }),
|
PlatformVersion: safe(hi, func(h *host.InfoStat) string { return h.PlatformVersion }),
|
||||||
KernelVersion: safe(hi, func(h *host.InfoStat) string { return h.KernelVersion }),
|
KernelVersion: safe(hi, func(h *host.InfoStat) string { return h.KernelVersion }),
|
||||||
@@ -239,7 +352,7 @@ var page = template.Must(template.New("index").Parse(`<!doctype html>
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Windows Systeminfo</title>
|
<title>Windows Systeminfo</title>
|
||||||
<style>
|
<style>
|
||||||
:root { --bg:#0b1220; --fg:#e7eefc; --muted:#a3b1cc; --card:#111a2e; --accent:#5aa9ff; }
|
:root { --bg:#000000; --fg:#e7eefc; --muted:#a3b1cc; --card:#111a2e; --accent:#5aa9ff; }
|
||||||
*{box-sizing:border-box}
|
*{box-sizing:border-box}
|
||||||
body{max-width:980px;margin:0 auto;padding:24px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--fg)}
|
body{max-width:980px;margin:0 auto;padding:24px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--fg)}
|
||||||
h1{font-size:22px;margin:0 0 16px 0}
|
h1{font-size:22px;margin:0 0 16px 0}
|
||||||
@@ -306,6 +419,28 @@ var page = template.Must(template.New("index").Parse(`<!doctype html>
|
|||||||
<tbody id="disks"></tbody>
|
<tbody id="disks"></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="k">Installierte Programme</div>
|
||||||
|
<div class="row" style="gap:8px;align-items:center;">
|
||||||
|
<button id="btnLoadApps" class="pill" style="cursor:pointer;">Laden</button>
|
||||||
|
<input id="appsFilter" type="text" placeholder="Suchen …"
|
||||||
|
style="flex:1;padding:6px 10px;border-radius:8px;border:1px solid #1b2a4a;background:#0b1220;color:var(--fg)">
|
||||||
|
<span class="k" id="appsCount"></span>
|
||||||
|
</div>
|
||||||
|
<div class="apps">
|
||||||
|
<table style="width:100%;border-collapse:collapse">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #1b2a4a">Name</th>
|
||||||
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #1b2a4a">Version</th>
|
||||||
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #1b2a4a">Publisher</th>
|
||||||
|
<th style="text-align:left;padding:8px;border-bottom:1px solid #1b2a4a">Quelle</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="apps"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>Aktualisiert alle 2s</footer>
|
<footer>Aktualisiert alle 2s</footer>
|
||||||
@@ -362,6 +497,55 @@ async function load(){
|
|||||||
|
|
||||||
load();
|
load();
|
||||||
setInterval(load, 2000);
|
setInterval(load, 2000);
|
||||||
|
|
||||||
|
let appsCache = null;
|
||||||
|
|
||||||
|
function renderApps(list){
|
||||||
|
const tbody = document.getElementById('apps');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
(list||[]).forEach(a=>{
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td class="mono" style="padding:6px 8px;border-bottom:1px solid #1b2a4a">'+(a.name||'-')+'</td>'+
|
||||||
|
'<td style="padding:6px 8px;border-bottom:1px solid #1b2a4a">'+(a.version||'')+'</td>'+
|
||||||
|
'<td style="padding:6px 8px;border-bottom:1px solid #1b2a4a">'+(a.publisher||'')+'</td>'+
|
||||||
|
'<td style="padding:6px 8px;border-bottom:1px solid #1b2a4a">'+(a.source||'')+'</td>';
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
const c = document.getElementById('appsCount');
|
||||||
|
if(c) c.textContent = (list||[]).length+' Einträge';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAppsOnce(){
|
||||||
|
if(appsCache) return; // schon geladen
|
||||||
|
try{
|
||||||
|
const r = await fetch('/api/apps');
|
||||||
|
appsCache = await r.json();
|
||||||
|
renderApps(appsCache);
|
||||||
|
}catch(e){
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', ()=>{
|
||||||
|
const btn = document.getElementById('btnLoadApps');
|
||||||
|
const inp = document.getElementById('appsFilter');
|
||||||
|
if(btn){ btn.addEventListener('click', loadAppsOnce); }
|
||||||
|
if(inp){
|
||||||
|
inp.addEventListener('input', ()=>{
|
||||||
|
if(!appsCache) return;
|
||||||
|
const q = inp.value.toLowerCase();
|
||||||
|
const filtered = appsCache.filter(a =>
|
||||||
|
(a.name||'').toLowerCase().includes(q) ||
|
||||||
|
(a.version||'').toLowerCase().includes(q) ||
|
||||||
|
(a.publisher||'').toLowerCase().includes(q) ||
|
||||||
|
(a.source||'').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
renderApps(filtered);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`))
|
</html>`))
|
||||||
@@ -383,16 +567,96 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// --- ab hier: Dienst-Integration ---
|
||||||
startCPUSampler()
|
|
||||||
|
|
||||||
|
const serviceName = "GoSysInfoService"
|
||||||
|
|
||||||
|
// runHTTP startet deinen bisherigen HTTP-Server und reagiert auf ctx.Done()
|
||||||
|
func runHTTP(ctx context.Context) error {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", indexHandler)
|
mux.HandleFunc("/", indexHandler)
|
||||||
|
mux.HandleFunc("/api/apps", appsHandler)
|
||||||
mux.HandleFunc("/api/summary", apiHandler)
|
mux.HandleFunc("/api/summary", apiHandler)
|
||||||
|
|
||||||
addr := "127.0.0.1:8080" // bind strictly to loopback (IPv4)
|
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)
|
log.Printf("Starte lokales Dashboard auf http://%s … (nur von diesem Rechner erreichbar)", addr)
|
||||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
||||||
|
// 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)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
pcinfo.exe
Normal file
BIN
pcinfo.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user