mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
255985b7b0
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
403 lines
10 KiB
Go
403 lines
10 KiB
Go
// Copyright (C) 2019-2023 Nicola Murino
|
|
//
|
|
// 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 service
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/sys/windows/svc"
|
|
"golang.org/x/sys/windows/svc/eventlog"
|
|
"golang.org/x/sys/windows/svc/mgr"
|
|
|
|
"github.com/drakkan/sftpgo/v2/internal/common"
|
|
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
|
"github.com/drakkan/sftpgo/v2/internal/ftpd"
|
|
"github.com/drakkan/sftpgo/v2/internal/httpd"
|
|
"github.com/drakkan/sftpgo/v2/internal/logger"
|
|
"github.com/drakkan/sftpgo/v2/internal/plugin"
|
|
"github.com/drakkan/sftpgo/v2/internal/sftpd"
|
|
"github.com/drakkan/sftpgo/v2/internal/telemetry"
|
|
"github.com/drakkan/sftpgo/v2/internal/webdavd"
|
|
)
|
|
|
|
const (
|
|
serviceName = "SFTPGo"
|
|
serviceDesc = "Fully featured and highly configurable SFTP server with optional FTP/S and WebDAV support"
|
|
rotateLogCmd = svc.Cmd(128)
|
|
acceptRotateLog = svc.Accepted(rotateLogCmd)
|
|
)
|
|
|
|
// 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) handleExit(wasStopped chan bool) {
|
|
s.Service.Wait()
|
|
|
|
select {
|
|
case <-wasStopped:
|
|
// the service was stopped nothing to do
|
|
logger.Info(logSender, "", "Windows Service was stopped")
|
|
return
|
|
default:
|
|
// the server failed while running, we must be sure to exit the process.
|
|
// The defined recovery action will be executed.
|
|
logger.Info(logSender, "", "Service wait ended, error: %v", s.Service.Error)
|
|
if s.Service.Error == nil {
|
|
os.Exit(0)
|
|
} else {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
|
changes <- svc.Status{State: svc.StartPending}
|
|
|
|
go func() {
|
|
if err := s.Service.Start(false); err != nil {
|
|
logger.Error(logSender, "", "Windows service failed to start, error: %v", err)
|
|
s.Service.Error = err
|
|
s.Service.Shutdown <- true
|
|
return
|
|
}
|
|
logger.Info(logSender, "", "Windows service started")
|
|
cmdsAccepted := svc.AcceptStop | svc.AcceptShutdown | svc.AcceptParamChange | acceptRotateLog
|
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
|
}()
|
|
|
|
wasStopped := make(chan bool, 1)
|
|
|
|
go s.handleExit(wasStopped)
|
|
|
|
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
|
|
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}
|
|
wasStopped <- true
|
|
s.Service.Stop()
|
|
plugin.Handler.Cleanup()
|
|
common.WaitForTransfers(graceTime)
|
|
break loop
|
|
case svc.ParamChange:
|
|
logger.Debug(logSender, "", "Received reload request")
|
|
err := dataprovider.ReloadConfig()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading dataprovider configuration: %v", err)
|
|
}
|
|
err = httpd.ReloadCertificateMgr()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading cert manager: %v", err)
|
|
}
|
|
err = ftpd.ReloadCertificateMgr()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading FTPD cert manager: %v", err)
|
|
}
|
|
err = webdavd.ReloadCertificateMgr()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading WebDAV cert manager: %v", err)
|
|
}
|
|
err = telemetry.ReloadCertificateMgr()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading telemetry cert manager: %v", err)
|
|
}
|
|
err = common.Reload()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading common configs: %v", err)
|
|
}
|
|
err = sftpd.Reload()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error reloading sftpd revoked certificates: %v", err)
|
|
}
|
|
case rotateLogCmd:
|
|
logger.Debug(logSender, "", "Received log file rotation request")
|
|
err := logger.RotateLogFile()
|
|
if err != nil {
|
|
logger.Warn(logSender, "", "error rotating log file: %v", err)
|
|
}
|
|
default:
|
|
continue loop
|
|
}
|
|
}
|
|
|
|
return false, 0
|
|
}
|
|
|
|
func (s *WindowsService) RunService() error {
|
|
exePath, err := s.getExePath()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isService, err := svc.IsWindowsService()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.isInteractive = !isService
|
|
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) RotateLogFile() 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(rotateLogCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("could not send control=%d: %v", rotateLogCmd, 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: 5 * time.Second,
|
|
},
|
|
{
|
|
Type: mgr.ServiceRestart,
|
|
Delay: 60 * time.Second,
|
|
},
|
|
{
|
|
Type: mgr.ServiceRestart,
|
|
Delay: 90 * time.Second,
|
|
},
|
|
}
|
|
err = service.SetRecoveryActions(recoveryActions, 300)
|
|
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()
|
|
}
|