main.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "html/template"
  6. "io"
  7. "log"
  8. "os"
  9. "os/signal"
  10. "strings"
  11. "sync"
  12. "syscall"
  13. "time"
  14. "github.com/jmoiron/sqlx"
  15. "github.com/knadh/koanf"
  16. "github.com/knadh/koanf/providers/env"
  17. "github.com/knadh/listmonk/internal/bounce"
  18. "github.com/knadh/listmonk/internal/buflog"
  19. "github.com/knadh/listmonk/internal/i18n"
  20. "github.com/knadh/listmonk/internal/manager"
  21. "github.com/knadh/listmonk/internal/media"
  22. "github.com/knadh/listmonk/internal/messenger"
  23. "github.com/knadh/listmonk/internal/subimporter"
  24. "github.com/knadh/stuffbin"
  25. )
  26. const (
  27. emailMsgr = "email"
  28. )
  29. // App contains the "global" components that are
  30. // passed around, especially through HTTP handlers.
  31. type App struct {
  32. fs stuffbin.FileSystem
  33. db *sqlx.DB
  34. queries *Queries
  35. constants *constants
  36. manager *manager.Manager
  37. importer *subimporter.Importer
  38. messengers map[string]messenger.Messenger
  39. media media.Store
  40. i18n *i18n.I18n
  41. bounce *bounce.Manager
  42. notifTpls *template.Template
  43. log *log.Logger
  44. bufLog *buflog.BufLog
  45. // Channel for passing reload signals.
  46. sigChan chan os.Signal
  47. // Global variable that stores the state indicating that a restart is required
  48. // after a settings update.
  49. needsRestart bool
  50. // Global state that stores data on an available remote update.
  51. update *AppUpdate
  52. sync.Mutex
  53. }
  54. var (
  55. // Buffered log writer for storing N lines of log entries for the UI.
  56. bufLog = buflog.New(5000)
  57. lo = log.New(io.MultiWriter(os.Stdout, bufLog), "",
  58. log.Ldate|log.Ltime|log.Lshortfile)
  59. ko = koanf.New(".")
  60. fs stuffbin.FileSystem
  61. db *sqlx.DB
  62. queries *Queries
  63. // Compile-time variables.
  64. buildString string
  65. versionString string
  66. // If these are set in build ldflags and static assets (*.sql, config.toml.sample. ./frontend)
  67. // are not embedded (in make dist), these paths are looked up. The default values before, when not
  68. // overridden by build flags, are relative to the CWD at runtime.
  69. appDir string = "."
  70. frontendDir string = "frontend"
  71. )
  72. func init() {
  73. initFlags()
  74. // Display version.
  75. if ko.Bool("version") {
  76. fmt.Println(buildString)
  77. os.Exit(0)
  78. }
  79. lo.Println(buildString)
  80. // Generate new config.
  81. if ko.Bool("new-config") {
  82. path := ko.Strings("config")[0]
  83. if err := newConfigFile(path); err != nil {
  84. lo.Println(err)
  85. os.Exit(1)
  86. }
  87. lo.Printf("generated %s. Edit and run --install", path)
  88. os.Exit(0)
  89. }
  90. // Load config files to pick up the database settings first.
  91. initConfigFiles(ko.Strings("config"), ko)
  92. // Load environment variables and merge into the loaded config.
  93. if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
  94. return strings.Replace(strings.ToLower(
  95. strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
  96. }), nil); err != nil {
  97. lo.Fatalf("error loading config from env: %v", err)
  98. }
  99. // Connect to the database, load the filesystem to read SQL queries.
  100. db = initDB()
  101. fs = initFS(appDir, frontendDir, ko.String("static-dir"), ko.String("i18n-dir"))
  102. // Installer mode? This runs before the SQL queries are loaded and prepared
  103. // as the installer needs to work on an empty DB.
  104. if ko.Bool("install") {
  105. // Save the version of the last listed migration.
  106. install(migList[len(migList)-1].version, db, fs, !ko.Bool("yes"), ko.Bool("idempotent"))
  107. os.Exit(0)
  108. }
  109. // Check if the DB schema is installed.
  110. if ok, err := checkSchema(db); err != nil {
  111. log.Fatalf("error checking schema in DB: %v", err)
  112. } else if !ok {
  113. lo.Fatal("the database does not appear to be setup. Run --install.")
  114. }
  115. if ko.Bool("upgrade") {
  116. upgrade(db, fs, !ko.Bool("yes"))
  117. os.Exit(0)
  118. }
  119. // Before the queries are prepared, see if there are pending upgrades.
  120. checkUpgrade(db)
  121. // Load the SQL queries from the filesystem.
  122. _, queries := initQueries(queryFilePath, db, fs, true)
  123. // Load settings from DB.
  124. initSettings(queries.GetSettings)
  125. }
  126. func main() {
  127. // Initialize the main app controller that wraps all of the app's
  128. // components. This is passed around HTTP handlers.
  129. app := &App{
  130. fs: fs,
  131. db: db,
  132. constants: initConstants(),
  133. media: initMediaStore(),
  134. messengers: make(map[string]messenger.Messenger),
  135. log: lo,
  136. bufLog: bufLog,
  137. }
  138. // Load i18n language map.
  139. app.i18n = initI18n(app.constants.Lang, fs)
  140. _, app.queries = initQueries(queryFilePath, db, fs, true)
  141. app.manager = initCampaignManager(app.queries, app.constants, app)
  142. app.importer = initImporter(app.queries, db, app)
  143. app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.i18n, app.constants)
  144. if ko.Bool("bounce.enabled") {
  145. app.bounce = initBounceManager(app)
  146. go app.bounce.Run()
  147. }
  148. // Initialize the default SMTP (`email`) messenger.
  149. app.messengers[emailMsgr] = initSMTPMessenger(app.manager)
  150. // Initialize any additional postback messengers.
  151. for _, m := range initPostbackMessengers(app.manager) {
  152. app.messengers[m.Name()] = m
  153. }
  154. // Attach all messengers to the campaign manager.
  155. for _, m := range app.messengers {
  156. app.manager.AddMessenger(m)
  157. }
  158. // Start the campaign workers. The campaign batches (fetch from DB, push out
  159. // messages) get processed at the specified interval.
  160. go app.manager.Run(time.Second * 5)
  161. // Start the app server.
  162. srv := initHTTPServer(app)
  163. // Star the update checker.
  164. if ko.Bool("app.check_updates") {
  165. go checkUpdates(versionString, time.Hour*24, app)
  166. }
  167. // Wait for the reload signal with a callback to gracefully shut down resources.
  168. // The `wait` channel is passed to awaitReload to wait for the callback to finish
  169. // within N seconds, or do a force reload.
  170. app.sigChan = make(chan os.Signal)
  171. signal.Notify(app.sigChan, syscall.SIGHUP)
  172. closerWait := make(chan bool)
  173. <-awaitReload(app.sigChan, closerWait, func() {
  174. // Stop the HTTP server.
  175. ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
  176. defer cancel()
  177. srv.Shutdown(ctx)
  178. // Close the campaign manager.
  179. app.manager.Close()
  180. // Close the DB pool.
  181. app.db.DB.Close()
  182. // Close the messenger pool.
  183. for _, m := range app.messengers {
  184. m.Close()
  185. }
  186. // Signal the close.
  187. closerWait <- true
  188. })
  189. }