add Windows Service support
This commit is contained in:
parent
bba78763e1
commit
f3f38f5f09
12 changed files with 739 additions and 167 deletions
|
@ -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
52
cmd/install_windows.go
Normal 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)
|
||||
}
|
133
cmd/root.go
133
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
|
||||
}
|
||||
|
|
184
cmd/serve.go
184
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)
|
||||
}
|
||||
|
|
16
cmd/service_windows.go
Normal file
16
cmd/service_windows.go
Normal 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
42
cmd/start_windows.go
Normal 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
32
cmd/status_windows.go
Normal 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
32
cmd/stop_windows.go
Normal 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
32
cmd/uninstall_windows.go
Normal 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
2
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
|
||||
)
|
||||
|
|
103
service/service.go
Normal file
103
service/service.go
Normal 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
271
service/service_windows.go
Normal 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()
|
||||
}
|
Loading…
Reference in a new issue