aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-06-12 08:55:05 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-06-12 08:55:05 -0700
commit4912c3a7c20d765692993f6caaa98a94f5ae28cb (patch)
treef2944e4c1bd72c38d3ad90713200e0984d2ca3fc /cmd
parentc15f8510fbd5ccf9a122f88d6e975d1ef69e00b3 (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')
-rw-r--r--cmd/mobius-hotline-server/main.go70
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 {