all repos — weimar @ afea02cbeb79ac199b088f364eeadd21c25e0ca2

Unnamed repository; edit this file 'description' to name the repository.

fix: graceful SIGINT shutdown and SIGHUP config reload

SIGINT now exits cleanly with code 0 instead of propagating
http.ErrServerClosed as a fatal error. SIGHUP re-reads the config file
and atomically swaps the config pointer — settings like site_title,
favicon_path, behind_proxy, and require_auth_for_browse take effect
immediately without restarting. Config is stored as *config.Config
throughout so handlers and middleware read the live value at request time.

Co-authored-by: Crush
Maxwell Jensen maxwelljensen@posteo.net
Wed, 13 May 2026 20:10:38 +0200
commit

afea02cbeb79ac199b088f364eeadd21c25e0ca2

parent

b26a020c4b21a1c0d9a62bc6afdf405ea2097d82

4 files changed, 35 insertions(+), 14 deletions(-)

jump to
M cmd/weimar/serve.gocmd/weimar/serve.go

@@ -5,6 +5,7 @@ "embed"

"fmt" "io/fs" "log/slog" + "os" "os/signal" "syscall"

@@ -57,20 +58,31 @@ slog.Info("created initial admin user", "username", adminEmail)

} } - srv, err := server.New(cfg, webSub, database) + srv, err := server.New(&cfg, webSub, database) if err != nil { return fmt.Errorf("creating server: %w", err) } + // Handle SIGHUP for config reload. + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGHUP) + go func() { + for range sig { + slog.Info("received SIGHUP, reloading config") + if err := initConfig(); err != nil { + slog.Warn("config reload failed", "error", err) + continue + } + srv.ReloadConfig(config.FromViper()) + slog.Info("config reloaded") + } + }() + ctx, stop := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) defer stop() slog.Info("starting weimar", "host", cfg.Server.Host, "port", cfg.Server.Port) - if err := srv.Start(ctx); err != nil { - return fmt.Errorf("server error: %w", err) - } - - return nil + return srv.Start(ctx) }, }
M internal/api/api.gointernal/api/api.go

@@ -26,10 +26,10 @@ type API struct {

DB *db.DB Auth *auth.SessionManager Storage *storage.Storage - Cfg config.Config + Cfg *config.Config } -func New(database *db.DB, sm *auth.SessionManager, st *storage.Storage, cfg config.Config) *API { +func New(database *db.DB, sm *auth.SessionManager, st *storage.Storage, cfg *config.Config) *API { return &API{ DB: database, Auth: sm,
M internal/api/api_test.gointernal/api/api_test.go

@@ -50,7 +50,7 @@ if err != nil {

t.Fatalf("login: %v", err) } - return New(database, sm, st, cfg), token + return New(database, sm, st, &cfg), token } // authenticatedRequest creates a request with the session cookie.

@@ -100,7 +100,7 @@ cfg := config.Config{

Auth: config.AuthConfig{AllowRegistration: allowRegistration}, Upload: config.UploadConfig{MaxSizeMB: 10}, } - return New(database, sm, st, cfg) + return New(database, sm, st, &cfg) } func TestHandleRegister_Success(t *testing.T) {

@@ -359,7 +359,7 @@ sm := auth.NewSessionManager(database)

auth.Register(context.Background(), database, "alice", "password123", false) t.Cleanup(func() { database.Close() }) st, _ := storage.New(t.TempDir()) - a := New(database, sm, st, config.Config{ + a := New(database, sm, st, &config.Config{ Upload: config.UploadConfig{MaxSizeMB: 0}, // 0 = no max size, so no rejection })
M internal/server/server.gointernal/server/server.go

@@ -13,7 +13,7 @@ "github.com/mjensen/weimar/internal/db"

"github.com/mjensen/weimar/internal/storage" ) -func New(cfg config.Config, webFS fs.FS, database *db.DB) (*Server, error) { +func New(cfg *config.Config, webFS fs.FS, database *db.DB) (*Server, error) { sm := auth.NewSessionManager(database) st, err := storage.New(cfg.Storage.Path) if err != nil {

@@ -73,7 +73,7 @@ return s, nil

} type Server struct { - cfg config.Config + cfg *config.Config mux *http.ServeMux auth *auth.SessionManager }

@@ -91,7 +91,16 @@ <-ctx.Done()

srv.Shutdown(context.Background()) }() - return srv.ListenAndServe() + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + return err + } + return nil +} + +// ReloadConfig atomically swaps the server config. Handlers and middleware +// read from the pointer at request time and will pick up the new values. +func (s *Server) ReloadConfig(cfg config.Config) { + *s.cfg = cfg } func itoa(n int) string {