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  }