//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) }