github.com/observiq/bindplane-agent@v1.51.0/internal/service/service_windows.go (about) 1 // Copyright observIQ, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 //go:build windows 16 17 package service 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 "os/signal" 25 "path/filepath" 26 "syscall" 27 "time" 28 29 "go.uber.org/zap" 30 "golang.org/x/sys/windows" 31 "golang.org/x/sys/windows/svc" 32 ) 33 34 // windowsServiceShutdownTimeout is the amount of time to wait for the underlying service to stop before 35 // forcefully stopping the process. 36 var windowsServiceShutdownTimeout = 20 * time.Second 37 38 // The following constants specify error codes for the service. 39 // See https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299- 40 const ( 41 statusCodeInvalidServiceCommand = uint32(1052) 42 statusCodeServiceException = uint32(1064) 43 statusCodeInvalidServiceName = uint32(1213) 44 ) 45 46 func RunService(logger *zap.Logger, rSvc RunnableService) error { 47 isService, err := checkIsService() 48 if err != nil { 49 return fmt.Errorf("failed checking if running as service: %w", err) 50 } 51 52 if isService { 53 // Change working directory to executable directory 54 ex, err := os.Executable() 55 if err != nil { 56 logger.Warn("Failed to retrieve executable directory", zap.Error(err)) 57 } else { 58 execDirPath := filepath.Dir(ex) 59 if err := os.Chdir(execDirPath); err != nil { 60 logger.Warn("Failed to modify current working directory", zap.Error(err)) 61 } 62 } 63 64 // Redirect stderr to file, so we can see panic information 65 if err := redirectStderr(); err != nil { 66 logger.Error("Failed to redirect stderr", zap.Error(err)) 67 } 68 69 // Service name doesn't need to be specified when directly run by the service manager. 70 return svc.Run("", newWindowsServiceHandler(logger, rSvc)) 71 } else { 72 ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 73 defer cancel() 74 75 return runServiceInteractive(ctx, logger, rSvc) 76 } 77 } 78 79 // windowsServiceHandler implements svc.Handler 80 type windowsServiceHandler struct { 81 svc RunnableService 82 logger *zap.Logger 83 } 84 85 // newWindowsServiceHandler creates a new windowsServiceHandler, which implements svc.Handler 86 func newWindowsServiceHandler(logger *zap.Logger, svc RunnableService) *windowsServiceHandler { 87 return &windowsServiceHandler{ 88 svc: svc, 89 logger: logger, 90 } 91 } 92 93 // Execute handles the Windows service event loop. 94 func (sh *windowsServiceHandler) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { 95 if len(args) == 0 { 96 // Service name is the first argument, and must be provided to open the event log for service logs. 97 return false, statusCodeInvalidServiceName 98 } 99 100 s <- svc.Status{State: svc.StartPending} 101 102 err := sh.svc.Start(context.Background()) 103 if err != nil { 104 sh.logger.Error("Failed to start service", zap.Error(err)) 105 return false, statusCodeServiceException 106 } 107 108 s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} 109 for { 110 select { 111 case req := <-r: 112 switch req.Cmd { 113 case svc.Interrogate: 114 s <- req.CurrentStatus 115 case svc.Stop, svc.Shutdown: 116 err := sh.shutdown(s) 117 if err != nil { 118 sh.logger.Error("Failed during service shutdown", zap.Error(err)) 119 return false, statusCodeServiceException 120 } 121 122 return false, 0 123 default: 124 sh.logger.Error("Got unexpected service command", zap.Uint32("command", uint32(req.Cmd))) 125 err := sh.shutdown(s) 126 if err != nil { 127 sh.logger.Error("Failed during service shutdown", zap.Error(err)) 128 return false, statusCodeServiceException 129 } 130 131 return false, statusCodeInvalidServiceCommand 132 } 133 case err := <-sh.svc.Error(): 134 sh.logger.Error("Got unexpected service error", zap.Error(err)) 135 136 sh.shutdown(s) 137 138 if err != nil { 139 sh.logger.Error("Failed during service shutdown", zap.Error(err)) 140 } 141 142 return false, statusCodeServiceException 143 } 144 } 145 } 146 147 func (sh windowsServiceHandler) shutdown(s chan<- svc.Status) error { 148 s <- svc.Status{State: svc.StopPending} 149 150 stopTimeoutCtx, stopCancel := context.WithTimeout(context.Background(), stopTimeout) 151 defer stopCancel() 152 153 stopErrChan := make(chan error, 1) 154 go func() { 155 stopErrChan <- sh.svc.Stop(stopTimeoutCtx) 156 }() 157 158 var err error 159 select { 160 case <-time.After(windowsServiceShutdownTimeout): 161 err = fmt.Errorf("the service failed to shut down in a timely manner (timeout: %s)", windowsServiceShutdownTimeout) 162 case stopErr := <-stopErrChan: 163 err = stopErr 164 } 165 166 s <- svc.Status{State: svc.Stopped} 167 168 return err 169 } 170 171 // checkIsService returns whether the current process is running as a Windows service. 172 func checkIsService() (bool, error) { 173 // NO_WINDOWS_SERVICE may be set non-zero to override the service detection logic. 174 if value, present := os.LookupEnv("NO_WINDOWS_SERVICE"); present && value != "0" { 175 return true, nil 176 } 177 178 isWindowsService, err := svc.IsWindowsService() 179 if err != nil { 180 return false, fmt.Errorf("failed to determine if we are running in an windows service: %w", err) 181 } 182 183 return isWindowsService, nil 184 } 185 186 // redirectStderr redirects stderr so that panic information is output to $INSTALL_DIR/log/observiq_collector.err, 187 // instead of it being dropped by Windows services. 188 // Most output should go through the zap logger instead of to stderr. 189 func redirectStderr() error { 190 homeDir, ok := os.LookupEnv("OIQ_OTEL_COLLECTOR_HOME") 191 if !ok { 192 return errors.New("OIQ_OTEL_COLLECTOR_HOME environment variable not set") 193 } 194 195 path := filepath.Join(homeDir, "log", "observiq_collector.err") 196 f, err := os.OpenFile(filepath.Clean(path), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0660) 197 if err != nil { 198 return fmt.Errorf("failed to open file: %w", err) 199 } 200 201 if err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(f.Fd())); err != nil { 202 return fmt.Errorf("failed to set stderr handle: %w (close err: %s)", err, f.Close()) 203 } else { 204 os.Stderr = f 205 } 206 207 return nil 208 }