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:
169
Agent/internal/winsvc/service_windows.go
Normal file
169
Agent/internal/winsvc/service_windows.go
Normal file
@@ -0,0 +1,169 @@
|
||||
//go:build windows
|
||||
|
||||
package winsvc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceName = "NexusRMMAgent"
|
||||
ServiceDisplayName = "NexusRMM Agent"
|
||||
ServiceDescription = "NexusRMM Remote Monitoring and Management Agent"
|
||||
)
|
||||
|
||||
// IsWindowsService gibt true zurück wenn der Prozess als Windows Service läuft.
|
||||
func IsWindowsService() (bool, error) {
|
||||
return svc.IsWindowsService()
|
||||
}
|
||||
|
||||
type agentSvc struct {
|
||||
runFn func(stop <-chan struct{})
|
||||
}
|
||||
|
||||
func (s *agentSvc) Execute(_ []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown
|
||||
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
s.runFn(stop)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case c := <-r:
|
||||
switch c.Cmd {
|
||||
case svc.Stop, svc.Shutdown:
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
close(stop)
|
||||
break loop
|
||||
}
|
||||
case <-done:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Auf Agent-Shutdown warten (max 10s)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Run führt den Agent als Windows Service aus.
|
||||
// runFn wird in einer Goroutine gestartet; stop wird geschlossen wenn der Service gestoppt wird.
|
||||
func Run(runFn func(stop <-chan struct{})) error {
|
||||
return svc.Run(ServiceName, &agentSvc{runFn: runFn})
|
||||
}
|
||||
|
||||
// Install registriert den Service im Windows Service Manager.
|
||||
// exePath muss der absolute Pfad zur nexus-agent.exe sein.
|
||||
func Install(exePath string) error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service Manager Verbindung fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
// Prüfen ob Service bereits existiert
|
||||
existing, err := m.OpenService(ServiceName)
|
||||
if err == nil {
|
||||
existing.Close()
|
||||
return fmt.Errorf("Service '%s' ist bereits installiert", ServiceName)
|
||||
}
|
||||
|
||||
s, err := m.CreateService(ServiceName, exePath, mgr.Config{
|
||||
DisplayName: ServiceDisplayName,
|
||||
Description: ServiceDescription,
|
||||
StartType: mgr.StartAutomatic,
|
||||
ErrorControl: mgr.ServiceRestart,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service erstellen fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Uninstall entfernt den Service (stoppt ihn vorher falls nötig).
|
||||
func Uninstall() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service Manager Verbindung fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ServiceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service '%s' nicht gefunden: %w", ServiceName, err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
// Service stoppen falls er läuft
|
||||
status, err := s.Query()
|
||||
if err == nil && status.State != svc.Stopped {
|
||||
if _, err := s.Control(svc.Stop); err == nil {
|
||||
// Kurz warten bis er gestoppt ist
|
||||
for i := 0; i < 10; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
st, e := s.Query()
|
||||
if e != nil || st.State == svc.Stopped {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Delete(); err != nil {
|
||||
return fmt.Errorf("Service löschen fehlgeschlagen: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start startet den installierten Service.
|
||||
func Start() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service Manager Verbindung fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ServiceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service '%s' nicht gefunden: %w", ServiceName, err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
return s.Start()
|
||||
}
|
||||
|
||||
// Stop stoppt den laufenden Service.
|
||||
func Stop() error {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service Manager Verbindung fehlgeschlagen: %w", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
s, err := m.OpenService(ServiceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Service '%s' nicht gefunden: %w", ServiceName, err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
_, err = s.Control(svc.Stop)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user