From f3f38f5f09801aaf3f70893c9d9bd7c3ab391e68 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 16 Sep 2019 08:52:58 +0200 Subject: [PATCH] add Windows Service support --- README.md | 7 + cmd/install_windows.go | 52 +++++++ cmd/root.go | 133 +++++++++++++++++- cmd/serve.go | 184 +++---------------------- cmd/service_windows.go | 16 +++ cmd/start_windows.go | 42 ++++++ cmd/status_windows.go | 32 +++++ cmd/stop_windows.go | 32 +++++ cmd/uninstall_windows.go | 32 +++++ go.mod | 2 +- service/service.go | 103 ++++++++++++++ service/service_windows.go | 271 +++++++++++++++++++++++++++++++++++++ 12 files changed, 739 insertions(+), 167 deletions(-) create mode 100644 cmd/install_windows.go create mode 100644 cmd/service_windows.go create mode 100644 cmd/start_windows.go create mode 100644 cmd/status_windows.go create mode 100644 cmd/stop_windows.go create mode 100644 cmd/uninstall_windows.go create mode 100644 service/service.go create mode 100644 service/service_windows.go diff --git a/README.md b/README.md index d6996ef3..d381222f 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/cmd/install_windows.go b/cmd/install_windows.go new file mode 100644 index 00000000..cb463057 --- /dev/null +++ b/cmd/install_windows.go @@ -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) +} diff --git a/cmd/root.go b/cmd/root.go index 92414f05..b8cdde07 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 +} diff --git a/cmd/serve.go b/cmd/serve.go index e103bf64..897b5cfa 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -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) } diff --git a/cmd/service_windows.go b/cmd/service_windows.go new file mode 100644 index 00000000..8ca44aed --- /dev/null +++ b/cmd/service_windows.go @@ -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) +} diff --git a/cmd/start_windows.go b/cmd/start_windows.go new file mode 100644 index 00000000..34e19a6e --- /dev/null +++ b/cmd/start_windows.go @@ -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) +} diff --git a/cmd/status_windows.go b/cmd/status_windows.go new file mode 100644 index 00000000..5b281dc3 --- /dev/null +++ b/cmd/status_windows.go @@ -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) +} diff --git a/cmd/stop_windows.go b/cmd/stop_windows.go new file mode 100644 index 00000000..657f7226 --- /dev/null +++ b/cmd/stop_windows.go @@ -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) +} diff --git a/cmd/uninstall_windows.go b/cmd/uninstall_windows.go new file mode 100644 index 00000000..669b6dec --- /dev/null +++ b/cmd/uninstall_windows.go @@ -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) +} diff --git a/go.mod b/go.mod index fed27f9c..a637ca05 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/service/service.go b/service/service.go new file mode 100644 index 00000000..0c04b8be --- /dev/null +++ b/service/service.go @@ -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") +} diff --git a/service/service_windows.go b/service/service_windows.go new file mode 100644 index 00000000..892103b0 --- /dev/null +++ b/service/service_windows.go @@ -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() +}