add Windows Service support

This commit is contained in:
Nicola Murino 2019-09-16 08:52:58 +02:00
parent bba78763e1
commit f3f38f5f09
12 changed files with 739 additions and 167 deletions

View file

@ -76,6 +76,13 @@ Alternately you can use distro packages:
For macOS a `launchd` sample [service](https://github.com/drakkan/sftpgo/tree/master/init/com.github.drakkan.sftpgo.plist "launchd plist") can be found inside the source tree. The `launchd` plist assumes that `sftpgo` has `/usr/local/opt/sftpgo` as base directory.
On Windows you can install and run `SFTPGo` as Windows Service, take a look at the CLI usage to learn how:
```bash
sftpgo.exe service --help
sftpgo.exe service install --help
```
## Configuration
The `sftpgo` executable can be used this way:

52
cmd/install_windows.go Normal file
View file

@ -0,0 +1,52 @@
package cmd
import (
"fmt"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
)
var (
installCmd = &cobra.Command{
Use: "install",
Short: "Install SFTPGo as Windows Service",
Long: `To install the SFTPGo Windows Service with the default values for the command line flags simply use:
sftpgo service install
Please take a look at the usage below to customize the startup options`,
Run: func(cmd *cobra.Command, args []string) {
s := service.Service{
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Shutdown: make(chan bool),
}
winService := service.WindowsService{
Service: s,
}
serviceArgs := []string{"service", "start"}
customFlags := getCustomServeFlags()
if len(customFlags) > 0 {
serviceArgs = append(serviceArgs, customFlags...)
}
err := winService.Install(serviceArgs...)
if err != nil {
fmt.Printf("Error installing service: %v\r\n", err)
} else {
fmt.Printf("Service installed!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(installCmd)
addServeFlags(installCmd)
}

View file

@ -3,16 +3,52 @@ package cmd
import (
"fmt"
"os"
"strconv"
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/utils"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
logSender = "cmd"
logSender = "cmd"
configDirFlag = "config-dir"
configDirKey = "config_dir"
configFileFlag = "config-file"
configFileKey = "config_file"
logFilePathFlag = "log-file-path"
logFilePathKey = "log_file_path"
logMaxSizeFlag = "log-max-size"
logMaxSizeKey = "log_max_size"
logMaxBackupFlag = "log-max-backups"
logMaxBackupKey = "log_max_backups"
logMaxAgeFlag = "log-max-age"
logMaxAgeKey = "log_max_age"
logCompressFlag = "log-compress"
logCompressKey = "log_compress"
logVerboseFlag = "log-verbose"
logVerboseKey = "log_verbose"
defaultConfigDir = "."
defaultConfigName = config.DefaultConfigName
defaultLogFile = "sftpgo.log"
defaultLogMaxSize = 10
defaultLogMaxBackup = 5
defaultLogMaxAge = 28
defaultLogCompress = false
defaultLogVerbose = true
)
var (
configDir string
configFile string
logFilePath string
logMaxSize int
logMaxBackups int
logMaxAge int
logCompress bool
logVerbose bool
rootCmd = &cobra.Command{
Use: "sftpgo",
Short: "Full featured and highly configurable SFTP server",
@ -35,3 +71,98 @@ func Execute() {
os.Exit(1)
}
}
func addServeFlags(cmd *cobra.Command) {
viper.SetDefault(configDirKey, defaultConfigDir)
viper.BindEnv(configDirKey, "SFTPGO_CONFIG_DIR")
cmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey),
"Location for SFTPGo config dir. This directory should contain the \"sftpgo\" configuration file or the configured "+
"config-file and it is used as the base for files with a relative path (eg. the private keys for the SFTP server, "+
"the SQLite database if you use SQLite as data provider). This flag can be set using SFTPGO_CONFIG_DIR env var too.")
viper.BindPFlag(configDirKey, cmd.Flags().Lookup(configDirFlag))
viper.SetDefault(configFileKey, defaultConfigName)
viper.BindEnv(configFileKey, "SFTPGO_CONFIG_FILE")
cmd.Flags().StringVarP(&configFile, configFileFlag, "f", viper.GetString(configFileKey),
"Name for SFTPGo configuration file. It must be the name of a file stored in config-dir not the absolute path to the "+
"configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and "+
"Java properties. Therefore if you set \"sftpgo\" then \"sftpgo.json\", \"sftpgo.yaml\" and so on are searched. "+
"This flag can be set using SFTPGO_CONFIG_FILE env var too.")
viper.BindPFlag(configFileKey, cmd.Flags().Lookup(configFileFlag))
viper.SetDefault(logFilePathKey, defaultLogFile)
viper.BindEnv(logFilePathKey, "SFTPGO_LOG_FILE_PATH")
cmd.Flags().StringVarP(&logFilePath, logFilePathFlag, "l", viper.GetString(logFilePathKey),
"Location for the log file. Leave empty to write logs to the standard output. This flag can be set using SFTPGO_LOG_FILE_PATH "+
"env var too.")
viper.BindPFlag(logFilePathKey, cmd.Flags().Lookup(logFilePathFlag))
viper.SetDefault(logMaxSizeKey, defaultLogMaxSize)
viper.BindEnv(logMaxSizeKey, "SFTPGO_LOG_MAX_SIZE")
cmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, "s", viper.GetInt(logMaxSizeKey),
"Maximum size in megabytes of the log file before it gets rotated. This flag can be set using SFTPGO_LOG_MAX_SIZE "+
"env var too. It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxSizeKey, cmd.Flags().Lookup(logMaxSizeFlag))
viper.SetDefault(logMaxBackupKey, defaultLogMaxBackup)
viper.BindEnv(logMaxBackupKey, "SFTPGO_LOG_MAX_BACKUPS")
cmd.Flags().IntVarP(&logMaxBackups, "log-max-backups", "b", viper.GetInt(logMaxBackupKey),
"Maximum number of old log files to retain. This flag can be set using SFTPGO_LOG_MAX_BACKUPS env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxBackupKey, cmd.Flags().Lookup(logMaxBackupFlag))
viper.SetDefault(logMaxAgeKey, defaultLogMaxAge)
viper.BindEnv(logMaxAgeKey, "SFTPGO_LOG_MAX_AGE")
cmd.Flags().IntVarP(&logMaxAge, "log-max-age", "a", viper.GetInt(logMaxAgeKey),
"Maximum number of days to retain old log files. This flag can be set using SFTPGO_LOG_MAX_AGE env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxAgeKey, cmd.Flags().Lookup(logMaxAgeFlag))
viper.SetDefault(logCompressKey, defaultLogCompress)
viper.BindEnv(logCompressKey, "SFTPGO_LOG_COMPRESS")
cmd.Flags().BoolVarP(&logCompress, logCompressFlag, "z", viper.GetBool(logCompressKey), "Determine if the rotated "+
"log files should be compressed using gzip. This flag can be set using SFTPGO_LOG_COMPRESS env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logCompressKey, cmd.Flags().Lookup(logCompressFlag))
viper.SetDefault(logVerboseKey, defaultLogVerbose)
viper.BindEnv(logVerboseKey, "SFTPGO_LOG_VERBOSE")
cmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+
"This flag can be set using SFTPGO_LOG_VERBOSE env var too.")
viper.BindPFlag(logVerboseKey, cmd.Flags().Lookup(logVerboseFlag))
}
func getCustomServeFlags() []string {
result := []string{}
if configDir != defaultConfigDir {
result = append(result, "--"+configDirFlag)
result = append(result, configDir)
}
if configFile != defaultConfigName {
result = append(result, "--"+configFileFlag)
result = append(result, configFile)
}
if logFilePath != defaultLogFile {
result = append(result, "--"+logFilePathFlag)
result = append(result, logFilePath)
}
if logMaxSize != defaultLogMaxSize {
result = append(result, "--"+logMaxSizeFlag)
result = append(result, strconv.Itoa(logMaxSize))
}
if logMaxBackups != defaultLogMaxBackup {
result = append(result, "--"+logMaxBackupFlag)
result = append(result, strconv.Itoa(logMaxBackups))
}
if logMaxAge != defaultLogMaxAge {
result = append(result, "--"+logMaxAgeFlag)
result = append(result, strconv.Itoa(logMaxAge))
}
if logVerbose != defaultLogVerbose {
result = append(result, "--"+logVerboseFlag+"=false")
}
if logCompress != defaultLogCompress {
result = append(result, "--"+logCompressFlag+"=true")
}
return result
}

View file

@ -1,185 +1,39 @@
package cmd
import (
"fmt"
"net/http"
"os"
"time"
"github.com/drakkan/sftpgo/api"
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/rs/zerolog"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const (
configDirFlag = "config-dir"
configDirKey = "config_dir"
configFileFlag = "config-file"
configFileKey = "config_file"
logFilePathFlag = "log-file-path"
logFilePathKey = "log_file_path"
logMaxSizeFlag = "log-max-size"
logMaxSizeKey = "log_max_size"
logMaxBackupFlag = "log-max-backups"
logMaxBackupKey = "log_max_backups"
logMaxAgeFlag = "log-max-age"
logMaxAgeKey = "log_max_age"
logCompressFlag = "log-compress"
logCompressKey = "log_compress"
logVerboseFlag = "log-verbose"
logVerboseKey = "log_verbose"
)
var (
configDir string
configFile string
logFilePath string
logMaxSize int
logMaxBackups int
logMaxAge int
logCompress bool
logVerbose bool
testVar string
serveCmd = &cobra.Command{
serveCmd = &cobra.Command{
Use: "serve",
Short: "Start the SFTP Server",
Long: `To start the SFTP Server with the default values for the command line flags simply use:
Long: `To start the SFTPGo with the default values for the command line flags simply use:
sftpgo serve
Please take a look at the usage below to customize the startup options`,
Run: func(cmd *cobra.Command, args []string) {
startServe()
service := service.Service{
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Shutdown: make(chan bool),
}
if err := service.Start(); err == nil {
service.Wait()
}
},
}
)
func init() {
rootCmd.AddCommand(serveCmd)
viper.SetDefault(configDirKey, ".")
viper.BindEnv(configDirKey, "SFTPGO_CONFIG_DIR")
serveCmd.Flags().StringVarP(&configDir, configDirFlag, "c", viper.GetString(configDirKey),
"Location for SFTPGo config dir. This directory should contain the \"sftpgo\" configuration file or the configured "+
"config-file and it is used as the base for files with a relative path (eg. the private keys for the SFTP server, "+
"the SQLite database if you use SQLite as data provider). This flag can be set using SFTPGO_CONFIG_DIR env var too.")
viper.BindPFlag(configDirKey, serveCmd.Flags().Lookup(configDirFlag))
viper.SetDefault(configFileKey, config.DefaultConfigName)
viper.BindEnv(configFileKey, "SFTPGO_CONFIG_FILE")
serveCmd.Flags().StringVarP(&configFile, configFileFlag, "f", viper.GetString(configFileKey),
"Name for SFTPGo configuration file. It must be the name of a file stored in config-dir not the absolute path to the "+
"configuration file. The specified file name must have no extension we automatically load JSON, YAML, TOML, HCL and "+
"Java properties. Therefore if you set \"sftpgo\" then \"sftpgo.json\", \"sftpgo.yaml\" and so on are searched. "+
"This flag can be set using SFTPGO_CONFIG_FILE env var too.")
viper.BindPFlag(configFileKey, serveCmd.Flags().Lookup(configFileFlag))
viper.SetDefault(logFilePathKey, "sftpgo.log")
viper.BindEnv(logFilePathKey, "SFTPGO_LOG_FILE_PATH")
serveCmd.Flags().StringVarP(&logFilePath, logFilePathFlag, "l", viper.GetString(logFilePathKey),
"Location for the log file. Leave empty to write logs to the standard output. This flag can be set using SFTPGO_LOG_FILE_PATH "+
"env var too.")
viper.BindPFlag(logFilePathKey, serveCmd.Flags().Lookup(logFilePathFlag))
viper.SetDefault(logMaxSizeKey, 10)
viper.BindEnv(logMaxSizeKey, "SFTPGO_LOG_MAX_SIZE")
serveCmd.Flags().IntVarP(&logMaxSize, logMaxSizeFlag, "s", viper.GetInt(logMaxSizeKey),
"Maximum size in megabytes of the log file before it gets rotated. This flag can be set using SFTPGO_LOG_MAX_SIZE "+
"env var too. It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxSizeKey, serveCmd.Flags().Lookup(logMaxSizeFlag))
viper.SetDefault(logMaxBackupKey, 5)
viper.BindEnv(logMaxBackupKey, "SFTPGO_LOG_MAX_BACKUPS")
serveCmd.Flags().IntVarP(&logMaxBackups, "log-max-backups", "b", viper.GetInt(logMaxBackupKey),
"Maximum number of old log files to retain. This flag can be set using SFTPGO_LOG_MAX_BACKUPS env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxBackupKey, serveCmd.Flags().Lookup(logMaxBackupFlag))
viper.SetDefault(logMaxAgeKey, 28)
viper.BindEnv(logMaxAgeKey, "SFTPGO_LOG_MAX_AGE")
serveCmd.Flags().IntVarP(&logMaxAge, "log-max-age", "a", viper.GetInt(logMaxAgeKey),
"Maximum number of days to retain old log files. This flag can be set using SFTPGO_LOG_MAX_AGE env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logMaxAgeKey, serveCmd.Flags().Lookup(logMaxAgeFlag))
viper.SetDefault(logCompressKey, false)
viper.BindEnv(logCompressKey, "SFTPGO_LOG_COMPRESS")
serveCmd.Flags().BoolVarP(&logCompress, logCompressFlag, "z", viper.GetBool(logCompressKey), "Determine if the rotated "+
"log files should be compressed using gzip. This flag can be set using SFTPGO_LOG_COMPRESS env var too. "+
"It is unused if log-file-path is empty.")
viper.BindPFlag(logCompressKey, serveCmd.Flags().Lookup(logCompressFlag))
viper.SetDefault(logVerboseKey, true)
viper.BindEnv(logVerboseKey, "SFTPGO_LOG_VERBOSE")
serveCmd.Flags().BoolVarP(&logVerbose, logVerboseFlag, "v", viper.GetBool(logVerboseKey), "Enable verbose logs. "+
"This flag can be set using SFTPGO_LOG_VERBOSE env var too.")
viper.BindPFlag(logVerboseKey, serveCmd.Flags().Lookup(logVerboseFlag))
}
func startServe() {
logLevel := zerolog.DebugLevel
if !logVerbose {
logLevel = zerolog.InfoLevel
}
logger.InitLogger(logFilePath, logMaxSize, logMaxBackups, logMaxAge, logCompress, logLevel)
logger.Info(logSender, "", "starting SFTPGo, config dir: %v, config file: %v, log max size: %v log max backups: %v "+
"log max age: %v log verbose: %v, log compress: %v", configDir, configFile, logMaxSize, logMaxBackups, logMaxAge,
logVerbose, logCompress)
config.LoadConfig(configDir, configFile)
providerConf := config.GetProviderConf()
err := dataprovider.Initialize(providerConf, configDir)
if err != nil {
logger.Error(logSender, "", "error initializing data provider: %v", err)
logger.ErrorToConsole("error initializing data provider: %v", err)
os.Exit(1)
}
dataProvider := dataprovider.GetProvider()
sftpdConf := config.GetSFTPDConfig()
httpdConf := config.GetHTTPDConfig()
sftpd.SetDataProvider(dataProvider)
shutdown := make(chan bool)
go func() {
logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf)
if err := sftpdConf.Initialize(configDir); err != nil {
logger.Error(logSender, "", "could not start SFTP server: %v", err)
logger.ErrorToConsole("could not start SFTP server: %v", err)
}
shutdown <- true
}()
if httpdConf.BindPort > 0 {
router := api.GetHTTPRouter()
api.SetDataProvider(dataProvider)
go func() {
logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf)
s := &http.Server{
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
Handler: router,
ReadTimeout: 300 * time.Second,
WriteTimeout: 300 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
if err := s.ListenAndServe(); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
logger.ErrorToConsole("could not start HTTP server: %v", err)
}
shutdown <- true
}()
} else {
logger.Debug(logSender, "", "HTTP server not started, disabled in config file")
logger.DebugToConsole("HTTP server not started, disabled in config file")
}
<-shutdown
addServeFlags(serveCmd)
}

16
cmd/service_windows.go Normal file
View file

@ -0,0 +1,16 @@
package cmd
import (
"github.com/spf13/cobra"
)
var (
serviceCmd = &cobra.Command{
Use: "service",
Short: "Install, Uninstall, Start, Stop and retrieve status for SFTPGo Windows Service",
}
)
func init() {
rootCmd.AddCommand(serviceCmd)
}

42
cmd/start_windows.go Normal file
View file

@ -0,0 +1,42 @@
package cmd
import (
"fmt"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
)
var (
startCmd = &cobra.Command{
Use: "start",
Short: "Start SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.Service{
ConfigDir: configDir,
ConfigFile: configFile,
LogFilePath: logFilePath,
LogMaxSize: logMaxSize,
LogMaxBackups: logMaxBackups,
LogMaxAge: logMaxAge,
LogCompress: logCompress,
LogVerbose: logVerbose,
Shutdown: make(chan bool),
}
winService := service.WindowsService{
Service: s,
}
err := winService.RunService()
if err != nil {
fmt.Printf("Error starting service: %v\r\n", err)
} else {
fmt.Printf("Service started!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(startCmd)
addServeFlags(startCmd)
}

32
cmd/status_windows.go Normal file
View file

@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
)
var (
statusCmd = &cobra.Command{
Use: "status",
Short: "Retrieve the status for the SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
status, err := s.Status()
if err != nil {
fmt.Printf("Error querying service status: %v\r\n", err)
} else {
fmt.Printf("Service status: %#v\r\n", status.String())
}
},
}
)
func init() {
serviceCmd.AddCommand(statusCmd)
}

32
cmd/stop_windows.go Normal file
View file

@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
)
var (
stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.Stop()
if err != nil {
fmt.Printf("Error stopping service: %v\r\n", err)
} else {
fmt.Printf("Service stopped!\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(stopCmd)
}

32
cmd/uninstall_windows.go Normal file
View file

@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"github.com/drakkan/sftpgo/service"
"github.com/spf13/cobra"
)
var (
uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall SFTPGo Windows Service",
Run: func(cmd *cobra.Command, args []string) {
s := service.WindowsService{
Service: service.Service{
Shutdown: make(chan bool),
},
}
err := s.Uninstall()
if err != nil {
fmt.Printf("Error removing service: %v\r\n", err)
} else {
fmt.Printf("Service uninstalled\r\n")
}
},
}
)
func init() {
serviceCmd.AddCommand(uninstallCmd)
}

2
go.mod
View file

@ -26,7 +26,7 @@ require (
go.etcd.io/bbolt v1.3.3
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7 // indirect
golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7
google.golang.org/appengine v1.6.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)

103
service/service.go Normal file
View file

@ -0,0 +1,103 @@
package service
import (
"fmt"
"net/http"
"time"
"github.com/drakkan/sftpgo/api"
"github.com/drakkan/sftpgo/config"
"github.com/drakkan/sftpgo/dataprovider"
"github.com/drakkan/sftpgo/logger"
"github.com/drakkan/sftpgo/sftpd"
"github.com/rs/zerolog"
)
const (
logSender = "service"
)
// Service defines the SFTPGo service
type Service struct {
ConfigDir string
ConfigFile string
LogFilePath string
LogMaxSize int
LogMaxBackups int
LogMaxAge int
LogCompress bool
LogVerbose bool
Shutdown chan bool
}
// Start initializes the service
func (s *Service) Start() error {
logLevel := zerolog.DebugLevel
if !s.LogVerbose {
logLevel = zerolog.InfoLevel
}
logger.InitLogger(s.LogFilePath, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge, s.LogCompress, logLevel)
logger.Info(logSender, "", "starting SFTPGo, config dir: %v, config file: %v, log max size: %v log max backups: %v "+
"log max age: %v log verbose: %v, log compress: %v", s.ConfigDir, s.ConfigFile, s.LogMaxSize, s.LogMaxBackups, s.LogMaxAge,
s.LogVerbose, s.LogCompress)
config.LoadConfig(s.ConfigDir, s.ConfigFile)
providerConf := config.GetProviderConf()
err := dataprovider.Initialize(providerConf, s.ConfigDir)
if err != nil {
logger.Error(logSender, "", "error initializing data provider: %v", err)
logger.ErrorToConsole("error initializing data provider: %v", err)
return err
}
dataProvider := dataprovider.GetProvider()
sftpdConf := config.GetSFTPDConfig()
httpdConf := config.GetHTTPDConfig()
sftpd.SetDataProvider(dataProvider)
go func() {
logger.Debug(logSender, "", "initializing SFTP server with config %+v", sftpdConf)
if err := sftpdConf.Initialize(s.ConfigDir); err != nil {
logger.Error(logSender, "", "could not start SFTP server: %v", err)
logger.ErrorToConsole("could not start SFTP server: %v", err)
}
s.Shutdown <- true
}()
if httpdConf.BindPort > 0 {
router := api.GetHTTPRouter()
api.SetDataProvider(dataProvider)
go func() {
logger.Debug(logSender, "", "initializing HTTP server with config %+v", httpdConf)
httpServer := &http.Server{
Addr: fmt.Sprintf("%s:%d", httpdConf.BindAddress, httpdConf.BindPort),
Handler: router,
ReadTimeout: 300 * time.Second,
WriteTimeout: 300 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
if err := httpServer.ListenAndServe(); err != nil {
logger.Error(logSender, "", "could not start HTTP server: %v", err)
logger.ErrorToConsole("could not start HTTP server: %v", err)
}
s.Shutdown <- true
}()
} else {
logger.Debug(logSender, "", "HTTP server not started, disabled in config file")
logger.DebugToConsole("HTTP server not started, disabled in config file")
}
return nil
}
// Wait blocks until the service exits
func (s *Service) Wait() {
<-s.Shutdown
}
// Stop terminates the service and unblocks the Wait method
func (s *Service) Stop() {
close(s.Shutdown)
logger.Debug(logSender, "", "Service stopped")
}

271
service/service_windows.go Normal file
View file

@ -0,0 +1,271 @@
package service
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"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
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
default:
continue loop
}
}
return false, 0
}
func (s *WindowsService) RunService() error {
exepath, err := s.getExecPath()
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) Install(args ...string) error {
exepath, err := s.getExecPath()
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{
mgr.RecoveryAction{
Type: mgr.ServiceRestart,
Delay: 0,
},
mgr.RecoveryAction{
Type: mgr.ServiceRestart,
Delay: 60 * time.Second,
},
mgr.RecoveryAction{
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) getExecPath() (string, error) {
return os.Executable()
}