cynfo3000 8116b71b48 feat: Windows Agent v1.0.0
- Windows-Dienst (golang.org/x/sys/windows/svc)
- Collector: CPU (GetSystemTimes), RAM (GlobalMemoryStatusEx), Disks (GetDiskFreeSpaceEx)
- WS-Client: gleiche Verbindungslogik wie Linux/BSD Agent
- Commands: exec, winget list/install/upgrade/upgrade-all, Windows Updates check/install, reboot
- install.ps1: automatische Service-Installation mit Parametern
- make agent-windows VERSION=x.x.x
2026-03-10 01:22:19 +01:00

283 lines
7.0 KiB
Go

//go:build windows
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"time"
"golang.org/x/sys/windows/svc"
"golang.org/x/sys/windows/svc/eventlog"
"golang.org/x/sys/windows/svc/mgr"
"github.com/cynfo/rmm-agent-windows/client"
"github.com/cynfo/rmm-agent-windows/collector"
"github.com/cynfo/rmm-agent-windows/config"
"github.com/cynfo/rmm-agent-windows/ws"
)
var Version = "1.0.0"
const (
ServiceName = "RMMAgent"
ServiceDisplayName = "RMM Agent"
ServiceDescription = "RMM Agent - Remote Monitoring & Management"
AgentIDFile = "agent_id.txt"
)
// agentService implementiert das Windows Service Interface
type agentService struct {
cfg *config.Config
cfgPath string
}
func (s *agentService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
stop := make(chan struct{})
go runAgent(s.cfg, s.cfgPath, stop)
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
for c := range r {
switch c.Cmd {
case svc.Stop, svc.Shutdown:
changes <- svc.Status{State: svc.StopPending}
close(stop)
return false, 0
}
}
return false, 0
}
func main() {
var (
cfgPath = flag.String("config", defaultConfigPath(), "Pfad zur Konfigurationsdatei")
flagDebug = flag.Bool("debug", false, "Im Vordergrund ausfuehren (kein Dienst)")
flagInst = flag.Bool("install", false, "Als Windows-Dienst installieren")
flagUninst = flag.Bool("uninstall", false, "Windows-Dienst deinstallieren")
)
flag.Parse()
// Logging in Datei (neben Binary)
logPath := filepath.Join(filepath.Dir(*cfgPath), "rmm-agent.log")
if lf, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil {
log.SetOutput(lf)
}
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("RMM Windows Agent v%s startet", Version)
cfg, err := config.Load(*cfgPath)
if err != nil {
log.Fatalf("Konfiguration laden fehlgeschlagen (%s): %v", *cfgPath, err)
}
if *flagInst {
installService(cfg, *cfgPath)
return
}
if *flagUninst {
uninstallService()
return
}
// Interaktiv (debug) oder als Dienst?
isInteractive, err := svc.IsAnInteractiveSession()
if err != nil {
log.Printf("IsAnInteractiveSession: %v", err)
isInteractive = true
}
if *flagDebug || isInteractive {
log.Println("Starte im Debug-Modus (Vordergrund)")
stop := make(chan struct{})
runAgent(cfg, *cfgPath, stop)
} else {
if err := svc.Run(ServiceName, &agentService{cfg: cfg, cfgPath: *cfgPath}); err != nil {
log.Fatalf("Dienst-Fehler: %v", err)
}
}
}
// runAgent ist die Hauptschleife des Agents
func runAgent(cfg *config.Config, cfgPath string, stop <-chan struct{}) {
c := client.New(cfg.BackendURL, cfg.APIKey, cfg.Insecure)
// Agent-ID laden oder registrieren
agentID, err := loadOrRegister(c, cfg, cfgPath)
if err != nil {
log.Fatalf("Registrierung fehlgeschlagen: %v", err)
}
log.Printf("Agent-ID: %s", agentID)
// WS-Handler und -Client starten
handler := ws.NewHandler()
wsClient := ws.NewClient(cfg.BackendURL, agentID, cfg.APIKey, cfg.Insecure, handler)
go wsClient.Run(stop)
// Heartbeat-Schleife
interval := time.Duration(cfg.IntervalSeconds) * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
// Ersten Heartbeat sofort
sendHeartbeat(c, agentID)
for {
select {
case <-stop:
log.Println("Agent wird gestoppt")
return
case <-ticker.C:
sendHeartbeat(c, agentID)
}
}
}
func sendHeartbeat(c *client.Client, agentID string) {
sysData, err := collector.Collect()
if err != nil {
log.Printf("Collector-Fehler: %v", err)
}
var rawData json.RawMessage
if sysData != nil {
rawData, _ = sysData.ToJSON()
}
resp, err := c.Heartbeat(client.HeartbeatRequest{
AgentID: agentID,
AgentVersion: Version,
Platform: "windows",
SystemData: rawData,
})
if err != nil {
log.Printf("Heartbeat fehlgeschlagen: %v", err)
return
}
log.Printf("Heartbeat OK")
if resp.UpdateAvailable {
log.Printf("Update verfuegbar: v%s — OTA-Update noch nicht implementiert", resp.UpdateVersion)
// TODO: OTA-Updater implementieren (aehnlich rmm-updater auf Linux)
}
}
// loadOrRegister liest die Agent-ID aus Datei oder registriert sich neu
func loadOrRegister(c *client.Client, cfg *config.Config, cfgPath string) (string, error) {
idFile := filepath.Join(filepath.Dir(cfgPath), AgentIDFile)
if data, err := os.ReadFile(idFile); err == nil {
id := string(data)
if len(id) > 0 {
// Re-registrieren um sicherzustellen dass der Agent im Backend bekannt ist
hostname, _ := os.Hostname()
c.Register(client.RegisterRequest{
AgentID: id,
Name: cfg.AgentName,
Hostname: hostname,
IP: "",
AgentVersion: Version,
Platform: "windows",
})
return id, nil
}
}
// Neu registrieren
hostname, _ := os.Hostname()
name := cfg.AgentName
if name == "" {
name = hostname
}
id, err := c.Register(client.RegisterRequest{
Name: name,
Hostname: hostname,
AgentVersion: Version,
Platform: "windows",
})
if err != nil {
return "", fmt.Errorf("Registrierung: %w", err)
}
if err := os.WriteFile(idFile, []byte(id), 0644); err != nil {
log.Printf("Agent-ID speichern fehlgeschlagen: %v", err)
}
log.Printf("Neu registriert als: %s", id)
return id, nil
}
// defaultConfigPath gibt den Standardpfad zur Konfigurationsdatei zurück
func defaultConfigPath() string {
exe, err := os.Executable()
if err != nil {
return `C:\Program Files\RMMAgent\config.yaml`
}
return filepath.Join(filepath.Dir(exe), "config.yaml")
}
// installService installiert den Agent als Windows-Dienst
func installService(cfg *config.Config, cfgPath string) {
m, err := mgr.Connect()
if err != nil {
log.Fatalf("Service Manager: %v", err)
}
defer m.Disconnect()
s, err := m.OpenService(ServiceName)
if err == nil {
s.Close()
log.Fatalf("Dienst '%s' existiert bereits — zuerst deinstallieren", ServiceName)
}
exe, err := os.Executable()
if err != nil {
log.Fatalf("Executable-Pfad: %v", err)
}
s, err = m.CreateService(ServiceName, exe, mgr.Config{
DisplayName: ServiceDisplayName,
Description: ServiceDescription,
StartType: mgr.StartAutomatic,
ErrorControl: mgr.ErrorNormal,
}, "-config", cfgPath)
if err != nil {
log.Fatalf("Dienst erstellen: %v", err)
}
defer s.Close()
// Event Log registrieren
eventlog.InstallAsEventCreate(ServiceName, eventlog.Error|eventlog.Warning|eventlog.Info)
log.Printf("Dienst '%s' installiert", ServiceName)
log.Println("Starten mit: net start RMMAgent")
}
// uninstallService entfernt den Windows-Dienst
func uninstallService() {
m, err := mgr.Connect()
if err != nil {
log.Fatalf("Service Manager: %v", err)
}
defer m.Disconnect()
s, err := m.OpenService(ServiceName)
if err != nil {
log.Fatalf("Dienst nicht gefunden: %v", err)
}
defer s.Close()
s.Control(svc.Stop)
time.Sleep(2 * time.Second)
if err := s.Delete(); err != nil {
log.Fatalf("Dienst loeschen: %v", err)
}
eventlog.Remove(ServiceName)
log.Printf("Dienst '%s' deinstalliert", ServiceName)
}