- 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
283 lines
7.0 KiB
Go
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)
|
|
}
|