package main import ( "context" "encoding/json" "fmt" "log" "os" "os/signal" "path/filepath" "runtime" "strings" "syscall" "time" "nexusrmm.local/agent/internal/collector" "nexusrmm.local/agent/internal/config" "nexusrmm.local/agent/internal/connection" "nexusrmm.local/agent/internal/deployer" "nexusrmm.local/agent/internal/executor" "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) 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) }() makeRunFn(configPath)(stop) } // 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) 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 } } } } func doHeartbeat(ctx context.Context, client *connection.GrpcClient, cfg *config.Config) { metrics, err := collector.Collect() if err != nil { log.Printf("Metric collection error: %v", err) return } req := &pb.HeartbeatRequest{ AgentId: cfg.AgentID, Metrics: &pb.SystemMetrics{ CpuUsagePercent: metrics.CPUPercent, MemoryUsagePercent: metrics.MemoryPercent, MemoryTotalBytes: int64(metrics.MemoryTotal), MemoryAvailableBytes: int64(metrics.MemoryAvailable), UptimeSeconds: metrics.UptimeSeconds, }, } for _, d := range metrics.Disks { req.Metrics.Disks = append(req.Metrics.Disks, &pb.DiskInfo{ MountPoint: d.MountPoint, TotalBytes: int64(d.Total), FreeBytes: int64(d.Free), Filesystem: d.Filesystem, }) } resp, err := client.Client.Heartbeat(ctx, req) if err != nil { log.Printf("Heartbeat error: %v", err) return } for _, cmd := range resp.PendingCommands { log.Printf("Executing command %s (type: %v)", cmd.CommandId, cmd.Type) go executeCommand(ctx, client, cfg, cmd) } } func executeCommand(ctx context.Context, client *connection.GrpcClient, cfg *config.Config, cmd *pb.AgentCommand) { agentID := cfg.AgentID var result *executor.Result switch cmd.Type { case pb.CommandType_COMMAND_TYPE_SHELL: result = executor.Execute(ctx, cmd.Payload, 300) case pb.CommandType_COMMAND_TYPE_INSTALL_SOFTWARE: result = deployer.Install(ctx, cmd.Payload) case pb.CommandType_COMMAND_TYPE_UNINSTALL_SOFTWARE: result = deployer.Uninstall(ctx, cmd.Payload) case pb.CommandType_COMMAND_TYPE_NETWORK_SCAN: var params struct { Subnet string `json:"subnet"` } _ = json.Unmarshal([]byte(cmd.Payload), ¶ms) devices, err := scanner.Scan(ctx, params.Subnet) if err != nil { result = &executor.Result{ExitCode: 1, Stderr: err.Error()} } else { result = &executor.Result{ ExitCode: 0, Stdout: scanner.ToJSON(devices), Success: true, } } case pb.CommandType_COMMAND_TYPE_UPDATE_AGENT: var params struct { ServerAddress string `json:"serverAddress"` } _ = json.Unmarshal([]byte(cmd.Payload), ¶ms) if params.ServerAddress == "" { params.ServerAddress = strings.Replace(cfg.ServerAddress, "5001", "5000", 1) } info, err := updater.CheckForUpdate(ctx, params.ServerAddress, version) if err != nil { result = &executor.Result{ExitCode: 1, Stderr: err.Error()} } else if info == nil { result = &executor.Result{ExitCode: 0, Stdout: "Bereits aktuell: " + version, Success: true} } else { if updateErr := updater.Update(ctx, info); updateErr != nil { result = &executor.Result{ExitCode: 1, Stderr: updateErr.Error()} } else { result = &executor.Result{ExitCode: 0, Stdout: "Update auf " + info.Version + " gestartet", Success: true} } } default: result = &executor.Result{ExitCode: -1, Stderr: fmt.Sprintf("unknown command type: %v", cmd.Type)} } if err := func() error { _, err := client.Client.ReportCommandResult(ctx, &pb.CommandResult{ AgentId: agentID, CommandId: cmd.CommandId, ExitCode: int32(result.ExitCode), Stdout: result.Stdout, Stderr: result.Stderr, Success: result.Success, }) return err }(); err != nil { log.Printf("Failed to report result for %s: %v", cmd.CommandId, err) } }