serve.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "github.com/flashmob/go-guerrilla"
  7. "github.com/flashmob/go-guerrilla/backends"
  8. "github.com/flashmob/go-guerrilla/log"
  9. _ "github.com/flashmob/maildiranasaurus"
  10. "github.com/spf13/cobra"
  11. "io/ioutil"
  12. "os"
  13. "os/exec"
  14. "os/signal"
  15. "reflect"
  16. "strconv"
  17. "strings"
  18. "syscall"
  19. "time"
  20. )
  21. const (
  22. defaultPidFile = "/var/run/go-guerrilla.pid"
  23. )
  24. var (
  25. configPath string
  26. pidFile string
  27. serveCmd = &cobra.Command{
  28. Use: "serve",
  29. Short: "start the small SMTP server",
  30. Run: serve,
  31. }
  32. cmdConfig = CmdConfig{}
  33. signalChannel = make(chan os.Signal, 1) // for trapping SIG_HUP
  34. mainlog log.Logger
  35. )
  36. func init() {
  37. // log to stderr on startup
  38. var logOpenError error
  39. if mainlog, logOpenError = log.GetLogger(log.OutputStderr.String()); logOpenError != nil {
  40. mainlog.WithError(logOpenError).Errorf("Failed creating a logger to %s", log.OutputStderr)
  41. }
  42. serveCmd.PersistentFlags().StringVarP(&configPath, "config", "c",
  43. "goguerrilla.conf", "Path to the configuration file")
  44. // intentionally didn't specify default pidFile; value from config is used if flag is empty
  45. serveCmd.PersistentFlags().StringVarP(&pidFile, "pidFile", "p",
  46. "", "Path to the pid file")
  47. rootCmd.AddCommand(serveCmd)
  48. }
  49. func sigHandler(app guerrilla.Guerrilla) {
  50. // handle SIGHUP for reloading the configuration while running
  51. signal.Notify(signalChannel, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGKILL)
  52. for sig := range signalChannel {
  53. if sig == syscall.SIGHUP {
  54. // save old config & load in new one
  55. oldConfig := cmdConfig
  56. newConfig := CmdConfig{}
  57. err := readConfig(configPath, pidFile, &newConfig)
  58. if err != nil {
  59. // new config will not be applied
  60. mainlog.WithError(err).Error("Error while ReadConfig (reload)")
  61. // re-open logs
  62. cmdConfig.EmitLogReopenEvents(app)
  63. } else {
  64. cmdConfig = newConfig
  65. mainlog.Infof("Configuration was reloaded at %s", guerrilla.ConfigLoadTime)
  66. cmdConfig.emitChangeEvents(&oldConfig, app)
  67. }
  68. } else if sig == syscall.SIGTERM || sig == syscall.SIGQUIT || sig == syscall.SIGINT {
  69. mainlog.Infof("Shutdown signal caught")
  70. app.Shutdown()
  71. mainlog.Infof("Shutdown completed, exiting.")
  72. return
  73. } else {
  74. mainlog.Infof("Shutdown, unknown signal caught")
  75. return
  76. }
  77. }
  78. }
  79. func subscribeBackendEvent(event guerrilla.Event, backend backends.Backend, app guerrilla.Guerrilla) {
  80. app.Subscribe(event, func(cmdConfig *CmdConfig) {
  81. logger, _ := log.GetLogger(cmdConfig.LogFile)
  82. var err error
  83. if err = backend.Shutdown(); err != nil {
  84. logger.WithError(err).Warn("Backend failed to shutdown")
  85. return
  86. }
  87. newBackend, newErr := backends.New(cmdConfig.BackendName, cmdConfig.BackendConfig, logger)
  88. if newErr != nil {
  89. // this will continue using old backend
  90. logger.WithError(newErr).Error("Error while loading the backend")
  91. } else {
  92. // swap to the bew backend (assuming old backend was shutdown so it can be safely swapped)
  93. backend = newBackend
  94. logger.Info("Backend started:", cmdConfig.BackendName)
  95. }
  96. })
  97. }
  98. func serve(cmd *cobra.Command, args []string) {
  99. logVersion()
  100. err := readConfig(configPath, pidFile, &cmdConfig)
  101. if err != nil {
  102. mainlog.WithError(err).Fatal("Error while reading config")
  103. }
  104. mainlog.SetLevel(cmdConfig.LogLevel)
  105. // Check that max clients is not greater than system open file limit.
  106. fileLimit := getFileLimit()
  107. if fileLimit > 0 {
  108. maxClients := 0
  109. for _, s := range cmdConfig.Servers {
  110. maxClients += s.MaxClients
  111. }
  112. if maxClients > fileLimit {
  113. mainlog.Fatalf("Combined max clients for all servers (%d) is greater than open file limit (%d). "+
  114. "Please increase your open file limit or decrease max clients.", maxClients, fileLimit)
  115. }
  116. }
  117. // Backend setup
  118. var backend backends.Backend
  119. backend, err = backends.New(cmdConfig.BackendName, cmdConfig.BackendConfig, mainlog)
  120. if err != nil {
  121. mainlog.WithError(err).Fatalf("Error while loading the backend")
  122. }
  123. /*
  124. // add our custom processor to the backend
  125. backends.Service.AddProcessor("MailDir", maildiranasaurus.MaildirProcessor)
  126. config := guerrilla.AppConfig {
  127. LogFile: log.OutputStderr,
  128. LogLevel: "info",
  129. AllowedHosts: []string{"example.com"},
  130. PidFile: "./pidfile.pid",
  131. Servers: []guerrilla.ServerConfig{
  132. }
  133. }
  134. */
  135. // g := guerrilla.NewSMTPD{config: cmdConfig}
  136. app, err := guerrilla.New(&cmdConfig.AppConfig, backend, mainlog)
  137. if err != nil {
  138. mainlog.WithError(err).Error("Error(s) when creating new server(s)")
  139. }
  140. // start the app
  141. err = app.Start()
  142. if err != nil {
  143. mainlog.WithError(err).Error("Error(s) when starting server(s)")
  144. }
  145. subscribeBackendEvent(guerrilla.EventConfigBackendConfig, backend, app)
  146. subscribeBackendEvent(guerrilla.EventConfigBackendName, backend, app)
  147. // Write out our PID
  148. writePid(cmdConfig.PidFile)
  149. // ...and write out our pid whenever the file name changes in the config
  150. app.Subscribe(guerrilla.EventConfigPidFile, func(ac *guerrilla.AppConfig) {
  151. writePid(ac.PidFile)
  152. })
  153. // change the logger from stdrerr to one from config
  154. mainlog.Infof("main log configured to %s", cmdConfig.LogFile)
  155. var logOpenError error
  156. if mainlog, logOpenError = log.GetLogger(cmdConfig.LogFile); logOpenError != nil {
  157. mainlog.WithError(logOpenError).Errorf("Failed changing to a custom logger [%s]", cmdConfig.LogFile)
  158. }
  159. app.SetLogger(mainlog)
  160. sigHandler(app)
  161. }
  162. // Superset of `guerrilla.AppConfig` containing options specific
  163. // the the command line interface.
  164. type CmdConfig struct {
  165. guerrilla.AppConfig
  166. BackendName string `json:"backend_name"`
  167. BackendConfig backends.BackendConfig `json:"backend_config"`
  168. }
  169. func (c *CmdConfig) load(jsonBytes []byte) error {
  170. err := json.Unmarshal(jsonBytes, &c)
  171. if err != nil {
  172. return fmt.Errorf("Could not parse config file: %s", err.Error())
  173. } else {
  174. // load in guerrilla.AppConfig
  175. return c.AppConfig.Load(jsonBytes)
  176. }
  177. }
  178. func (c *CmdConfig) emitChangeEvents(oldConfig *CmdConfig, app guerrilla.Guerrilla) {
  179. // has backend changed?
  180. if !reflect.DeepEqual((*c).BackendConfig, (*oldConfig).BackendConfig) {
  181. app.Publish(guerrilla.EventConfigBackendConfig, c)
  182. }
  183. if c.BackendName != oldConfig.BackendName {
  184. app.Publish(guerrilla.EventConfigBackendName, c)
  185. }
  186. // call other emitChangeEvents
  187. c.AppConfig.EmitChangeEvents(&oldConfig.AppConfig, app)
  188. }
  189. // ReadConfig which should be called at startup, or when a SIG_HUP is caught
  190. func readConfig(path string, pidFile string, config *CmdConfig) error {
  191. // load in the config.
  192. data, err := ioutil.ReadFile(path)
  193. if err != nil {
  194. return fmt.Errorf("Could not read config file: %s", err.Error())
  195. }
  196. if err := config.load(data); err != nil {
  197. return err
  198. }
  199. // override config pidFile with with flag from the command line
  200. if len(pidFile) > 0 {
  201. config.AppConfig.PidFile = pidFile
  202. } else if len(config.AppConfig.PidFile) == 0 {
  203. config.AppConfig.PidFile = defaultPidFile
  204. }
  205. if len(config.AllowedHosts) == 0 {
  206. return errors.New("Empty `allowed_hosts` is not allowed")
  207. }
  208. guerrilla.ConfigLoadTime = time.Now()
  209. return nil
  210. }
  211. func getFileLimit() int {
  212. cmd := exec.Command("ulimit", "-n")
  213. out, err := cmd.Output()
  214. if err != nil {
  215. return -1
  216. }
  217. limit, err := strconv.Atoi(strings.TrimSpace(string(out)))
  218. if err != nil {
  219. return -1
  220. }
  221. return limit
  222. }
  223. func writePid(pidFile string) {
  224. if len(pidFile) > 0 {
  225. if f, err := os.Create(pidFile); err == nil {
  226. defer f.Close()
  227. pid := os.Getpid()
  228. if _, err := f.WriteString(fmt.Sprintf("%d", pid)); err == nil {
  229. f.Sync()
  230. mainlog.Infof("pid_file (%s) written with pid:%v", pidFile, pid)
  231. } else {
  232. mainlog.WithError(err).Fatalf("Error while writing pidFile (%s)", pidFile)
  233. }
  234. } else {
  235. mainlog.WithError(err).Fatalf("Error while creating pidFile (%s)", pidFile)
  236. }
  237. }
  238. }