/* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; ** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ** See the GNU Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ package main import ( "errors" "fmt" "os" "path/filepath" "sync" "time" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" "golang.zabbix.com/agent2/internal/agent" "golang.zabbix.com/agent2/internal/agent/keyaccess" "golang.zabbix.com/agent2/internal/agent/scheduler" "golang.zabbix.com/agent2/internal/monitor" "golang.zabbix.com/sdk/errs" "golang.zabbix.com/sdk/log" "golang.zabbix.com/sdk/zbxflag" ) const usageMessageExampleConfPath = `C:\zabbix\zabbix_agent2.conf` const ( startTypeAutomatic = "automatic" startTypeDelayed = "delayed" startTypeManual = "manual" startTypeDisabled = "disabled" ) var ( serviceName = "Zabbix Agent 2" svcInstallFlag bool svcUninstallFlag bool svcStartFlag bool svcStopFlag bool svcMultipleAgentFlag bool svcStartType string winServiceRun bool eLog *eventlog.Log winServiceWg sync.WaitGroup fatalStopWg sync.WaitGroup fatalStopChan chan bool startChan chan bool stopChan = make(chan bool) ) func osDependentFlags() zbxflag.Flags { return zbxflag.Flags{ &zbxflag.BoolFlag{ Flag: zbxflag.Flag{ Name: "multiple-agents", Shorthand: "m", Description: "For -i -d -s -x functions service name will " + "include Hostname parameter specified in configuration file", }, Default: false, Dest: &svcMultipleAgentFlag, }, &zbxflag.BoolFlag{ Flag: zbxflag.Flag{ Name: "install", Shorthand: "i", Description: "Install Zabbix agent 2 as service", }, Default: false, Dest: &svcInstallFlag, }, &zbxflag.BoolFlag{ Flag: zbxflag.Flag{ Name: "uninstall", Shorthand: "d", Description: "Uninstall Zabbix agent 2 from service", }, Default: false, Dest: &svcUninstallFlag, }, &zbxflag.BoolFlag{ Flag: zbxflag.Flag{ Name: "start", Shorthand: "s", Description: "Start Zabbix agent 2 service", }, Default: false, Dest: &svcStartFlag, }, &zbxflag.BoolFlag{ Flag: zbxflag.Flag{ Name: "stop", Shorthand: "x", Description: "Stop Zabbix agent 2 service", }, Default: false, Dest: &svcStopFlag, }, &zbxflag.StringFlag{ Flag: zbxflag.Flag{ Name: "startup-type", Shorthand: "S", Description: fmt.Sprintf( "Set startup type of the Zabbix Windows agent service to be installed."+ " Allowed values: %s (default), %s, %s, %s", startTypeAutomatic, startTypeDelayed, startTypeManual, startTypeDisabled, ), }, Default: "", Dest: &svcStartType, }, } } func isWinLauncher() bool { if svcInstallFlag || svcUninstallFlag || svcStartFlag || svcStopFlag || svcMultipleAgentFlag || svcStartType != "" { return true } return false } func setServiceRun(foreground bool) { winServiceRun = !foreground } func fatalCloseOSItems() { if winServiceRun { sendFatalStopSig() } closeEventLog() } func openEventLog() (err error) { if isWinLauncher() { eLog, err = eventlog.Open(serviceName) } return } func sendFatalStopSig() { fatalStopWg.Add(1) select { case fatalStopChan <- true: fatalStopWg.Wait() default: fatalStopWg.Done() } } func closeEventLog() { if eLog != nil { eLog.Close() } return } func eventLogInfo(msg string) (err error) { if isWinLauncher() { return eLog.Info(1, msg) } return nil } // eventLogErr reports err to Windows event log if agent is launched on // windows. // On success returns parameter err. // On failure returns error that occurred during reporting to event log. func eventLogErr(err error) error { if isWinLauncher() { elErr := eLog.Error(3, err.Error()) if elErr != nil { return errs.Wrapf( elErr, "failed to report error (%s) to event log", err.Error(), ) } } return err } func validateMultipleAgentFlag() bool { if svcMultipleAgentFlag && !(svcInstallFlag || svcUninstallFlag || svcStartFlag || svcStopFlag || svcStartType != "") && !winServiceRun { return false } return true } func validateExclusiveFlags(args *Arguments) error { var ( exclusiveFlagsSet = []bool{ svcInstallFlag, svcUninstallFlag, svcStartFlag, svcStopFlag, args.print, args.test != "", args.runtimeCommand != "", args.testConfig, } count int ) if args.verbose && !(args.test != "" || args.print) { return errors.New("option -v, --verbose can only be specified with -t or -p") } for _, exclusiveFlagSet := range exclusiveFlagsSet { if exclusiveFlagSet { count++ } if count >= 2 { //nolint:gomnd return errors.New("mutually exclusive options used, see -h, --help for more information") } } if !validateMultipleAgentFlag() { return errors.New( "option -m, --multiple-agents can only be used with one of the service options, see -h, --help for more information", ) } return nil } func isInteractiveSession() (bool, error) { return svc.IsAnInteractiveSession() } func setHostname() error { if err := log.Open(log.Console, log.None, "", 0); err != nil { return fmt.Errorf("cannot initialize logger: %s", err) } if err := keyaccess.LoadRules(agent.Options.AllowKey, agent.Options.DenyKey); err != nil { return fmt.Errorf("failed to load key access rules: %s", err) } var m *scheduler.Manager var err error if m, err = scheduler.NewManager(&agent.Options, make(agent.PluginSystemOptions)); err != nil { return fmt.Errorf("cannot create scheduling manager: %s", err) } m.Start() if err = configUpdateItemParameters(m, &agent.Options); err != nil { return fmt.Errorf("cannot process configuration: %s", err) } m.Stop() monitor.Wait(monitor.Scheduler) return nil } func handleWindowsService(confPath string) error { if svcMultipleAgentFlag { if len(agent.Options.Hostname) == 0 { if err := setHostname(); err != nil { return err } } hostnames, err := agent.ValidateHostnames(agent.Options.Hostname) if err != nil { return fmt.Errorf( "cannot parse the \"Hostname\" parameter: %s", err, ) } agent.FirstHostname = hostnames[0] serviceName = fmt.Sprintf("%s [%s]", serviceName, agent.FirstHostname) } if svcInstallFlag || svcUninstallFlag || svcStartFlag || svcStopFlag || svcStartType != "" { absPath, err := filepath.Abs(confPath) if err != nil { return err } if err := resolveWindowsService(absPath); err != nil { return err } closeEventLog() os.Exit(0) } if winServiceRun { fatalStopChan = make(chan bool, 1) startChan = make(chan bool) go runService() } return nil } func resolveWindowsService(confPath string) error { var msg string switch true { case svcInstallFlag: if err := svcInstall(confPath); err != nil { return fmt.Errorf( "failed to install %s as service: %s", serviceName, err, ) } msg = fmt.Sprintf("'%s' installed successfully", serviceName) case svcUninstallFlag: if err := svcUninstall(); err != nil { return fmt.Errorf( "failed to uninstall %s as service: %s", serviceName, err, ) } msg = fmt.Sprintf("'%s' uninstalled successfully", serviceName) case svcStartFlag: if err := svcStart(confPath); err != nil { return fmt.Errorf( "failed to start %s service: %s", serviceName, err, ) } msg = fmt.Sprintf("'%s' started successfully", serviceName) case svcStopFlag: if err := svcStop(); err != nil { return fmt.Errorf("failed to stop %s service: %s", serviceName, err) } msg = fmt.Sprintf("'%s' stopped successfully", serviceName) case svcStartType != "": if err := svcStartupTypeSet(); err != nil { return errs.Wrapf(err, "failed to set service '%s' startup type", serviceName) } msg = fmt.Sprintf("service '%s' startup type configured successfully", serviceName) } msg = fmt.Sprintf("zabbix_agent2 [%d]: %s\n", os.Getpid(), msg) fmt.Fprintf(os.Stdout, msg) if err := eventLogInfo(msg); err != nil { return fmt.Errorf("failed to log to event log: %s", err) } return nil } func getAgentPath() (p string, err error) { if p, err = filepath.Abs(os.Args[0]); err != nil { return } var i os.FileInfo if i, err = os.Stat(p); err != nil { if filepath.Ext(p) == "" { p += ".exe" i, err = os.Stat(p) if err != nil { return } } } if i.Mode().IsDir() { return p, fmt.Errorf("incorrect path to executable '%s'", p) } return } func svcStartTypeFlagParse() (uint32, bool, error) { var startType uint32 var delayedAutoStart bool var err error switch svcStartType { case "": startType = mgr.StartAutomatic case startTypeAutomatic: startType = mgr.StartAutomatic case startTypeDelayed: delayedAutoStart = true startType = mgr.StartAutomatic case startTypeManual: startType = mgr.StartManual case startTypeDisabled: startType = mgr.StartDisabled default: err = fmt.Errorf("unknown service start type: '%s'", svcStartType) } return startType, delayedAutoStart, err } func svcInstall(conf string) error { exepath, err := getAgentPath() if err != nil { return fmt.Errorf("failed to get Zabbix Agent 2 executable path: %s", err.Error()) } m, err := mgr.Connect() if err != nil { return fmt.Errorf( "failed to connect to service manager: %s", err.Error(), ) } defer m.Disconnect() s, err := m.OpenService(serviceName) if err == nil { s.Close() return errors.New("service already exists") } startType, delayedAutoStart, err := svcStartTypeFlagParse() if err != nil { return errs.Wrap(err, "failed to get new startup type") } s, err = m.CreateService( serviceName, exepath, mgr.Config{ StartType: startType, DisplayName: serviceName, Description: "Provides system monitoring", BinaryPathName: fmt.Sprintf("%s -c %s -f=false", exepath, conf), DelayedAutoStart: delayedAutoStart, }, "-c", conf, "-f=false", ) if err != nil { return fmt.Errorf("failed to create service: %s", err.Error()) } defer s.Close() if err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil { err = fmt.Errorf( "failed to report service into the event log: %s", err.Error(), ) derr := s.Delete() if derr != nil { return fmt.Errorf("%s and %s", err.Error(), derr.Error()) } return err } return nil } func svcUninstall() error { m, err := mgr.Connect() if err != nil { return fmt.Errorf( "failed to connect to service manager: %s", err.Error(), ) } defer m.Disconnect() s, err := m.OpenService(serviceName) if err != nil { return fmt.Errorf("failed to open service: %s", err.Error()) } defer s.Close() if err = s.Delete(); err != nil { return fmt.Errorf("failed to delete service: %s", err.Error()) } if err = eventlog.Remove(serviceName); err != nil { return fmt.Errorf( "failed to remove service from the event log: %s", err.Error(), ) } return nil } func svcStart(conf string) error { m, err := mgr.Connect() if err != nil { return fmt.Errorf( "failed to connect to service manager: %s", err.Error(), ) } defer m.Disconnect() s, err := m.OpenService(serviceName) if err != nil { return fmt.Errorf("failed to open service: %s", err.Error()) } defer s.Close() if err = s.Start("-c", conf, "-f=false"); err != nil { return fmt.Errorf("failed to start service: %s", err.Error()) } return nil } func svcStop() error { m, err := mgr.Connect() if err != nil { return fmt.Errorf( "failed to connect to service manager: %s", err.Error(), ) } defer m.Disconnect() s, err := m.OpenService(serviceName) if err != nil { return fmt.Errorf("failed to open service: %s", err.Error()) } defer s.Close() status, err := s.Control(svc.Stop) if err != nil { return fmt.Errorf( "failed to send stop request to service: %s", err.Error(), ) } timeout := time.Now().Add(10 * time.Second) for status.State != svc.Stopped { if timeout.Before(time.Now()) { return fmt.Errorf("failed to stop '%s' service", serviceName) } time.Sleep(300 * time.Millisecond) if status, err = s.Query(); err != nil { return fmt.Errorf("failed to get service status: %s", err.Error()) } } return nil } func svcStartupTypeSet() error { m, err := mgr.Connect() if err != nil { return errs.Wrap(err, "failed to connect to service manager") } defer m.Disconnect() s, err := m.OpenService(serviceName) if err != nil { return errs.Wrap(err, "failed to open service") } defer s.Close() c, err := s.Config() if err != nil { return errs.Wrap(err, "failed to retrieve service config") } c.StartType, c.DelayedAutoStart, err = svcStartTypeFlagParse() if err != nil { return errs.Wrap(err, "failed to get new startup type") } err = s.UpdateConfig(c) if err != nil { return errs.Wrap(err, "failed to update service config") } return nil } func confirmService() { if winServiceRun { startChan <- true } } func waitServiceClose() { if winServiceRun { winServiceWg.Done() <-closeChan } } func sendServiceStop() { if winServiceRun { winServiceWg.Add(1) stopChan <- true } } func runService() { if err := svc.Run(serviceName, &winService{}); err != nil { panic(errs.Wrap(err, "use foreground option to run Zabbix agent as console application")) } } type winService struct{} func (ws *winService) Execute( args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status, ) (ssec bool, errno uint32) { changes <- svc.Status{State: svc.StartPending} select { case <-startChan: changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} case <-fatalStopChan: changes <- svc.Status{State: svc.Stopped} // This is needed to make sure that windows will receive the status stopped before zabbix agent 2 process ends <-time.After(time.Millisecond * 500) fatalStopWg.Done() return } loop: for { select { case c := <-r: switch c.Cmd { case svc.Stop, svc.Shutdown: changes <- svc.Status{State: svc.StopPending} winServiceWg.Add(1) closeChan <- true winServiceWg.Wait() changes <- svc.Status{State: svc.Stopped} // This is needed to make sure that windows will receive the status stopped before zabbix agent 2 process ends <-time.After(time.Millisecond * 500) closeChan <- true break loop default: log.Debugf("unsupported windows service command '%s' received", getCmdName(c.Cmd)) } case <-stopChan: changes <- svc.Status{State: svc.StopPending} winServiceWg.Wait() changes <- svc.Status{State: svc.Stopped} // This is needed to make sure that windows will receive the status stopped before zabbix agent 2 process ends <-time.After(time.Millisecond * 500) closeChan <- true break loop } } return } func getCmdName(cmd svc.Cmd) string { switch cmd { case svc.Stop: return "Stop" case svc.Pause: return "Pause" case svc.Continue: return "Continue" case svc.Interrogate: return "Interrogate" case svc.Shutdown: return "Shutdown" case svc.ParamChange: return "ParamChange" case svc.NetBindAdd: return "NetBindAdd" case svc.NetBindRemove: return "NetBindRemove" case svc.NetBindEnable: return "NetBindEnable" case svc.NetBindDisable: return "NetBindDisable" case svc.DeviceEvent: return "DeviceEvent" case svc.HardwareProfileChange: return "HardwareProfileChange" case svc.PowerEvent: return "PowerEvent" case svc.SessionChange: return "SessionChange" default: return "unknown" } }