feat: Windows Service Wrapper für NexusRMM Agent
- winsvc package mit Build-Tag-basierter plattformspezifischer Implementierung: - service_windows.go: svc.Handler + Install/Uninstall/Start/Stop via windows/svc/mgr - service_stub.go: Stub-Implementierungen für nicht-Windows Builds - main.go refaktoriert: - os.Executable() für absoluten Config-Pfad (Service-Modus: CWD = C:\Windows\System32) - Kommandozeilen-Args: install / uninstall / start / stop - winsvc.IsWindowsService() Erkennung → Service-Modus oder Konsolen-Modus - Agent-Loop als makeRunFn() extrahiert (wiederverwendbar für beide Modi) - install-service.ps1: Convenience-Skript zum Bauen, Installieren und Starten Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -20,91 +21,164 @@ import (
|
||||
"nexusrmm.local/agent/internal/meshagent"
|
||||
"nexusrmm.local/agent/internal/scanner"
|
||||
"nexusrmm.local/agent/internal/updater"
|
||||
"nexusrmm.local/agent/internal/winsvc"
|
||||
pb "nexusrmm.local/agent/pkg/proto"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
// Absoluten Pfad zur EXE ermitteln — kritisch im Service-Modus,
|
||||
// da der Working Directory dann C:\Windows\System32 ist.
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatalf("Konnte EXE-Pfad nicht ermitteln: %v", err)
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
configPath := filepath.Join(exeDir, "nexus-agent.yaml")
|
||||
|
||||
// Kommandozeilen-Argument verarbeiten (install / uninstall / start / stop)
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "install":
|
||||
if err := winsvc.Install(exePath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Service erfolgreich installiert.")
|
||||
return
|
||||
case "uninstall":
|
||||
if err := winsvc.Uninstall(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Service erfolgreich deinstalliert.")
|
||||
return
|
||||
case "start":
|
||||
if err := winsvc.Start(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Service gestartet.")
|
||||
return
|
||||
case "stop":
|
||||
if err := winsvc.Stop(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Service gestoppt.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob wir als Windows Service laufen
|
||||
isSvc, err := winsvc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.Fatalf("Service-Erkennung fehlgeschlagen: %v", err)
|
||||
}
|
||||
|
||||
if isSvc {
|
||||
// Als Windows Service starten — blockiert bis Service gestoppt wird
|
||||
if err := winsvc.Run(makeRunFn(configPath)); err != nil {
|
||||
log.Fatalf("Service-Fehler: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Konsolen-Modus: direkt ausführen mit Ctrl+C-Signal
|
||||
log.Printf("NexusRMM Agent %s starting on %s/%s", version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
cfg, err := config.Load("nexus-agent.yaml")
|
||||
if err != nil {
|
||||
log.Fatalf("Config load error: %v", err)
|
||||
}
|
||||
|
||||
client, err := connection.ConnectWithRetry(cfg.ServerAddress, 10)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if cfg.AgentID == "" {
|
||||
hostname, _ := os.Hostname()
|
||||
metrics, _ := collector.Collect()
|
||||
// Beste Netzwerkschnittstelle für Enrollment wählen (bevorzuge IPv4)
|
||||
mac, ip := "", ""
|
||||
for _, n := range metrics.Networks {
|
||||
if n.MAC == "" || n.IPAddress == "" {
|
||||
continue
|
||||
}
|
||||
if mac == "" {
|
||||
mac, ip = n.MAC, n.IPAddress
|
||||
}
|
||||
// Sobald IPv4 gefunden: nehmen und fertig
|
||||
if !strings.Contains(n.IPAddress, ":") {
|
||||
mac, ip = n.MAC, n.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Client.Enroll(context.Background(), &pb.EnrollRequest{
|
||||
Hostname: hostname,
|
||||
OsType: runtime.GOOS,
|
||||
OsVersion: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
MacAddress: mac,
|
||||
IpAddress: ip,
|
||||
AgentVersion: version,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Enrollment failed: %v", err)
|
||||
}
|
||||
cfg.AgentID = resp.AgentId
|
||||
cfg.HeartbeatInterval = int(resp.HeartbeatInterval)
|
||||
if err := cfg.Save("nexus-agent.yaml"); err != nil {
|
||||
log.Printf("Warning: could not save config: %v", err)
|
||||
}
|
||||
log.Printf("Enrolled with ID: %s", cfg.AgentID)
|
||||
}
|
||||
|
||||
// MeshAgent installieren falls konfiguriert
|
||||
if cfg.MeshEnabled && cfg.MeshCentralUrl != "" {
|
||||
log.Printf("Installiere MeshAgent von %s...", cfg.MeshCentralUrl)
|
||||
if err := meshagent.Install(context.Background(), cfg.MeshCentralUrl); err != nil {
|
||||
log.Printf("MeshAgent-Installation fehlgeschlagen (nicht kritisch): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
stop := make(chan struct{})
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigCh
|
||||
log.Println("Shutting down...")
|
||||
close(stop)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(time.Duration(cfg.HeartbeatInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
makeRunFn(configPath)(stop)
|
||||
}
|
||||
|
||||
log.Printf("Agent running. Heartbeat every %ds", cfg.HeartbeatInterval)
|
||||
doHeartbeat(ctx, client, cfg)
|
||||
// makeRunFn gibt die Haupt-Agent-Loop als Funktion zurück.
|
||||
// stop wird geschlossen wenn der Agent beendet werden soll (Service-Stop oder Ctrl+C).
|
||||
func makeRunFn(configPath string) func(stop <-chan struct{}) {
|
||||
return func(stop <-chan struct{}) {
|
||||
log.Printf("NexusRMM Agent %s starting on %s/%s", version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
doHeartbeat(ctx, client, cfg)
|
||||
case <-sigCh:
|
||||
log.Println("Shutting down...")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Config load error: %v", err)
|
||||
}
|
||||
|
||||
client, err := connection.ConnectWithRetry(cfg.ServerAddress, 10)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if cfg.AgentID == "" {
|
||||
hostname, _ := os.Hostname()
|
||||
metrics, _ := collector.Collect()
|
||||
mac, ip := "", ""
|
||||
for _, n := range metrics.Networks {
|
||||
if n.MAC == "" || n.IPAddress == "" {
|
||||
continue
|
||||
}
|
||||
if mac == "" {
|
||||
mac, ip = n.MAC, n.IPAddress
|
||||
}
|
||||
if !strings.Contains(n.IPAddress, ":") {
|
||||
mac, ip = n.MAC, n.IPAddress
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Client.Enroll(context.Background(), &pb.EnrollRequest{
|
||||
Hostname: hostname,
|
||||
OsType: runtime.GOOS,
|
||||
OsVersion: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
MacAddress: mac,
|
||||
IpAddress: ip,
|
||||
AgentVersion: version,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Enrollment failed: %v", err)
|
||||
}
|
||||
cfg.AgentID = resp.AgentId
|
||||
cfg.HeartbeatInterval = int(resp.HeartbeatInterval)
|
||||
if err := cfg.Save(configPath); err != nil {
|
||||
log.Printf("Warning: could not save config: %v", err)
|
||||
}
|
||||
log.Printf("Enrolled with ID: %s", cfg.AgentID)
|
||||
}
|
||||
|
||||
if cfg.MeshEnabled && cfg.MeshCentralUrl != "" {
|
||||
log.Printf("Installiere MeshAgent von %s...", cfg.MeshCentralUrl)
|
||||
if err := meshagent.Install(context.Background(), cfg.MeshCentralUrl); err != nil {
|
||||
log.Printf("MeshAgent-Installation fehlgeschlagen (nicht kritisch): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
ticker := time.NewTicker(time.Duration(cfg.HeartbeatInterval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Printf("Agent running. Heartbeat every %ds", cfg.HeartbeatInterval)
|
||||
doHeartbeat(ctx, client, cfg)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
doHeartbeat(ctx, client, cfg)
|
||||
case <-stop:
|
||||
log.Println("Agent wird beendet...")
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,13 +247,11 @@ func executeCommand(ctx context.Context, client *connection.GrpcClient, cfg *con
|
||||
}
|
||||
}
|
||||
case pb.CommandType_COMMAND_TYPE_UPDATE_AGENT:
|
||||
// Payload optional: {"serverAddress": "localhost:5000"}
|
||||
var params struct {
|
||||
ServerAddress string `json:"serverAddress"`
|
||||
}
|
||||
_ = json.Unmarshal([]byte(cmd.Payload), ¶ms)
|
||||
if params.ServerAddress == "" {
|
||||
// Aus gRPC-Adresse REST-Adresse ableiten (Port 5000 statt 5001)
|
||||
params.ServerAddress = strings.Replace(cfg.ServerAddress, "5001", "5000", 1)
|
||||
}
|
||||
info, err := updater.CheckForUpdate(ctx, params.ServerAddress, version)
|
||||
|
||||
Reference in New Issue
Block a user