diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-06-12 08:55:05 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-06-12 08:55:05 -0700 |
| commit | 4912c3a7c20d765692993f6caaa98a94f5ae28cb (patch) | |
| tree | f2944e4c1bd72c38d3ad90713200e0984d2ca3fc /cmd/mobius-hotline-server | |
| parent | c15f8510fbd5ccf9a122f88d6e975d1ef69e00b3 (diff) | |
Add Reloader interface to remove type assertions from reload path
main.go's reloadFunc reached through the server's interface fields with
concrete type assertions (srv.MessageBoard.(*mobius.FlatNews), etc.) to
trigger SIGHUP/API reloads, leaking storage implementation details past
the manager interfaces.
Storage backends now implement a one-method Reloader interface, with
compile-time assertions for FlatNews, BanFile, ThreadedNewsYAML, and
Agreement. BanFile.Load and ThreadedNewsYAML.Load are renamed Reload
for a uniform method set, matching FlatNews's existing convention of
using Reload for the initial load as well.
main.go registers each backend in a named reloader list as it is
constructed, and reloadFunc iterates the list. The banner reload is a
ReloaderFunc that also performs the initial load, and the Redis-backed
ban list simply registers no reloader, replacing the old type-switch
special case.
Diffstat (limited to 'cmd/mobius-hotline-server')
| -rw-r--r-- | cmd/mobius-hotline-server/main.go | 70 |
1 files changed, 37 insertions, 33 deletions
diff --git a/cmd/mobius-hotline-server/main.go b/cmd/mobius-hotline-server/main.go index 117cb2a..ba7924d 100644 --- a/cmd/mobius-hotline-server/main.go +++ b/cmd/mobius-hotline-server/main.go @@ -109,11 +109,17 @@ func main() { os.Exit(1) } - srv.MessageBoard, err = mobius.NewFlatNews(path.Join(*configDir, "MessageBoard.txt")) + // reloaders collects the storage backends whose state is reloaded on SIGHUP or via the + // reload API endpoint. + var reloaders []namedReloader + + messageBoard, err := mobius.NewFlatNews(path.Join(*configDir, "MessageBoard.txt")) if err != nil { slogger.Error("Error loading message board", "err", err) os.Exit(1) } + srv.MessageBoard = messageBoard + reloaders = append(reloaders, namedReloader{"message board", messageBoard}) // Initialize ban list - use Redis if configured, otherwise use file-based storage if *redisAddr != "" { @@ -133,18 +139,23 @@ func main() { srv.BanList = mobius.NewRedisBanMgr(redisClient, slogger) slogger.Debug("Using Redis for ban management", "addr", *redisAddr) } else { - srv.BanList, err = mobius.NewBanFile(path.Join(*configDir, "Banlist.yaml")) + banFile, err := mobius.NewBanFile(path.Join(*configDir, "Banlist.yaml")) if err != nil { slogger.Error("Error loading ban list", "err", err) os.Exit(1) } + srv.BanList = banFile + // The Redis-backed ban list needs no reload, so only the file-backed one registers. + reloaders = append(reloaders, namedReloader{"ban list", banFile}) } - srv.ThreadedNewsMgr, err = mobius.NewThreadedNewsYAML(path.Join(*configDir, "ThreadedNews.yaml")) + threadedNews, err := mobius.NewThreadedNewsYAML(path.Join(*configDir, "ThreadedNews.yaml")) if err != nil { slogger.Error("Error loading news", "err", err) os.Exit(1) } + srv.ThreadedNewsMgr = threadedNews + reloaders = append(reloaders, namedReloader{"threaded news", threadedNews}) srv.AccountManager, err = mobius.NewYAMLAccountManager(path.Join(*configDir, "Users/")) if err != nil { @@ -152,48 +163,35 @@ func main() { os.Exit(1) } - srv.Agreement, err = mobius.NewAgreement(*configDir, "\r") + agreement, err := mobius.NewAgreement(*configDir, "\r") if err != nil { slogger.Error("Error loading agreement", "err", err) os.Exit(1) } + srv.Agreement = agreement + reloaders = append(reloaders, namedReloader{"agreement", agreement}) - bannerPath := path.Join(*configDir, config.BannerFile) - banner, err := os.ReadFile(bannerPath) - if err != nil { + // On reload failure, the previous banner is kept because SetBanner is only called on success. + reloadBanner := mobius.ReloaderFunc(func() error { + banner, err := os.ReadFile(path.Join(*configDir, config.BannerFile)) + if err != nil { + return err + } + srv.SetBanner(banner) + return nil + }) + if err := reloadBanner.Reload(); err != nil { slogger.Error("Error loading banner", "err", err) os.Exit(1) } - srv.SetBanner(banner) + reloaders = append(reloaders, namedReloader{"banner", reloadBanner}) reloadFunc := func() { - if err := srv.MessageBoard.(*mobius.FlatNews).Reload(); err != nil { - slogger.Error("Error reloading news", "err", err) - } - - // Only reload ban list if using file-based storage (Redis doesn't need reload) - if banFile, ok := srv.BanList.(*mobius.BanFile); ok { - if err := banFile.Load(); err != nil { - slogger.Error("Error reloading ban list", "err", err) + for _, item := range reloaders { + if err := item.reloader.Reload(); err != nil { + slogger.Error("Error reloading "+item.name, "err", err) } } - - if err := srv.ThreadedNewsMgr.(*mobius.ThreadedNewsYAML).Load(); err != nil { - slogger.Error("Error reloading threaded news list", "err", err) - } - - if err := srv.Agreement.(*mobius.Agreement).Reload(); err != nil { - slogger.Error("Error reloading agreement", "err", err) - } - - // Let's try to reload the banner. On failure, keep serving the previous banner. - bannerPath := path.Join(*configDir, config.BannerFile) - banner, err := os.ReadFile(bannerPath) - if err != nil { - slogger.Error("Error reloading banner", "err", err) - } else { - srv.SetBanner(banner) - } } if *apiAddr != "" { @@ -248,6 +246,12 @@ func main() { slogger.Info("Server shut down") } +// namedReloader pairs a Reloader with a human-readable name for reload error logging. +type namedReloader struct { + name string + reloader mobius.Reloader +} + // findConfigPath searches for an existing config directory from the predefined search order. // Returns the first directory that exists, or falls back to "config" as the default. func findConfigPath() string { |