5bfaae9202
HTTPS certificate can be reloaded on demand sending a SIGHUP signal on Unix based systems and a "paramchange" request to the running service on Windows
295 lines
6.6 KiB
Go
295 lines
6.6 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/drakkan/sftpgo/dataprovider"
|
|
"github.com/drakkan/sftpgo/httpd"
|
|
"github.com/drakkan/sftpgo/logger"
|
|
|
|
"golang.org/x/sys/windows/svc"
|
|
"golang.org/x/sys/windows/svc/eventlog"
|
|
"golang.org/x/sys/windows/svc/mgr"
|
|
)
|
|
|
|
const (
|
|
serviceName = "SFTPGo"
|
|
serviceDesc = "Full featured and highly configurable SFTP server"
|
|
)
|
|
|
|
// Status defines service status
|
|
type Status uint8
|
|
|
|
// Supported values for service status
|
|
const (
|
|
StatusUnknown Status = iota
|
|
StatusRunning
|
|
StatusStopped
|
|
StatusPaused
|
|
StatusStartPending
|
|
StatusPausePending
|
|
StatusContinuePending
|
|
StatusStopPending
|
|
)
|
|
|
|
type WindowsService struct {
|
|
Service Service
|
|
isInteractive bool
|
|
}
|
|
|
|
func (s Status) String() string {
|
|
switch s {
|
|
case StatusRunning:
|
|
return "running"
|
|
case StatusStopped:
|
|
return "stopped"
|
|
case StatusStartPending:
|
|
return "start pending"
|
|
case StatusPausePending:
|
|
return "pause pending"
|
|
case StatusPaused:
|
|
return "paused"
|
|
case StatusContinuePending:
|
|
return "continue pending"
|
|
case StatusStopPending:
|
|
return "stop pending"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
|
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange
|
|
changes <- svc.Status{State: svc.StartPending}
|
|
if err := s.Service.Start(); err != nil {
|
|
return true, 1
|
|
}
|
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
|
loop:
|
|
for {
|
|
c := <-r
|
|
switch c.Cmd {
|
|
case svc.Interrogate:
|
|
logger.Debug(logSender, "", "Received service interrogate request, current status: %v", c.CurrentStatus)
|
|
changes <- c.CurrentStatus
|
|
case svc.Stop, svc.Shutdown:
|
|
logger.Debug(logSender, "", "Received service stop request")
|
|
changes <- svc.Status{State: svc.StopPending}
|
|
s.Service.Stop()
|
|
break loop
|
|
case svc.ParamChange:
|
|
logger.Debug(logSender, "", "Received reload request")
|
|
dataprovider.ReloadConfig()
|
|
httpd.ReloadTLSCertificate()
|
|
default:
|
|
continue loop
|
|
}
|
|
}
|
|
|
|
return false, 0
|
|
}
|
|
|
|
func (s *WindowsService) RunService() error {
|
|
exePath, err := s.getExePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isIntSess, err := svc.IsAnInteractiveSession()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.isInteractive = isIntSess
|
|
dir := filepath.Dir(exePath)
|
|
if err = os.Chdir(dir); err != nil {
|
|
return err
|
|
}
|
|
if s.isInteractive {
|
|
return s.Start()
|
|
}
|
|
return svc.Run(serviceName, s)
|
|
}
|
|
|
|
func (s *WindowsService) Start() error {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("could not access service: %v", err)
|
|
}
|
|
defer service.Close()
|
|
err = service.Start()
|
|
if err != nil {
|
|
return fmt.Errorf("could not start service: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *WindowsService) Reload() error {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("could not access service: %v", err)
|
|
}
|
|
defer service.Close()
|
|
_, err = service.Control(svc.ParamChange)
|
|
if err != nil {
|
|
return fmt.Errorf("could not send control=%d: %v", svc.ParamChange, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *WindowsService) Install(args ...string) error {
|
|
exePath, err := s.getExePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err == nil {
|
|
service.Close()
|
|
return fmt.Errorf("service %s already exists", serviceName)
|
|
}
|
|
config := mgr.Config{
|
|
DisplayName: serviceName,
|
|
Description: serviceDesc,
|
|
StartType: mgr.StartAutomatic}
|
|
service, err = m.CreateService(serviceName, exePath, config, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer service.Close()
|
|
err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "exists") {
|
|
service.Delete()
|
|
return fmt.Errorf("SetupEventLogSource() failed: %s", err)
|
|
}
|
|
}
|
|
recoveryActions := []mgr.RecoveryAction{
|
|
{
|
|
Type: mgr.ServiceRestart,
|
|
Delay: 0,
|
|
},
|
|
{
|
|
Type: mgr.ServiceRestart,
|
|
Delay: 60 * time.Second,
|
|
},
|
|
{
|
|
Type: mgr.ServiceRestart,
|
|
Delay: 90 * time.Second,
|
|
},
|
|
}
|
|
err = service.SetRecoveryActions(recoveryActions, uint32(86400))
|
|
if err != nil {
|
|
service.Delete()
|
|
return fmt.Errorf("unable to set recovery actions: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *WindowsService) Uninstall() error {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("service %s is not installed", serviceName)
|
|
}
|
|
defer service.Close()
|
|
err = service.Delete()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = eventlog.Remove(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("RemoveEventLogSource() failed: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *WindowsService) Stop() error {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err != nil {
|
|
return fmt.Errorf("could not access service: %v", err)
|
|
}
|
|
defer service.Close()
|
|
status, err := service.Control(svc.Stop)
|
|
if err != nil {
|
|
return fmt.Errorf("could not send control=%d: %v", svc.Stop, err)
|
|
}
|
|
timeout := time.Now().Add(10 * time.Second)
|
|
for status.State != svc.Stopped {
|
|
if timeout.Before(time.Now()) {
|
|
return fmt.Errorf("timeout waiting for service to go to state=%d", svc.Stopped)
|
|
}
|
|
time.Sleep(300 * time.Millisecond)
|
|
status, err = service.Query()
|
|
if err != nil {
|
|
return fmt.Errorf("could not retrieve service status: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *WindowsService) Status() (Status, error) {
|
|
m, err := mgr.Connect()
|
|
if err != nil {
|
|
return StatusUnknown, err
|
|
}
|
|
defer m.Disconnect()
|
|
service, err := m.OpenService(serviceName)
|
|
if err != nil {
|
|
return StatusUnknown, fmt.Errorf("could not access service: %v", err)
|
|
}
|
|
defer service.Close()
|
|
status, err := service.Query()
|
|
if err != nil {
|
|
return StatusUnknown, fmt.Errorf("could not query service status: %v", err)
|
|
}
|
|
switch status.State {
|
|
case svc.StartPending:
|
|
return StatusStartPending, nil
|
|
case svc.Running:
|
|
return StatusRunning, nil
|
|
case svc.PausePending:
|
|
return StatusPausePending, nil
|
|
case svc.Paused:
|
|
return StatusPaused, nil
|
|
case svc.ContinuePending:
|
|
return StatusContinuePending, nil
|
|
case svc.StopPending:
|
|
return StatusStopPending, nil
|
|
case svc.Stopped:
|
|
return StatusStopped, nil
|
|
default:
|
|
return StatusUnknown, fmt.Errorf("unknown status %v", status)
|
|
}
|
|
}
|
|
|
|
func (s *WindowsService) getExePath() (string, error) {
|
|
return os.Executable()
|
|
}
|