main.go 6.4 KB

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