// Package server implements the main HTTP server for the chat application. package server import ( "context" "errors" "fmt" "log/slog" "net/http" "os" "os/signal" "syscall" "time" "git.eeqj.de/sneak/chat/internal/config" "git.eeqj.de/sneak/chat/internal/globals" "git.eeqj.de/sneak/chat/internal/handlers" "git.eeqj.de/sneak/chat/internal/logger" "git.eeqj.de/sneak/chat/internal/middleware" "go.uber.org/fx" "github.com/getsentry/sentry-go" "github.com/go-chi/chi" _ "github.com/joho/godotenv/autoload" // loads .env file ) const ( shutdownTimeout = 5 * time.Second sentryFlushTime = 2 * time.Second ) // Params defines the dependencies for creating a Server. type Params struct { fx.In Logger *logger.Logger Globals *globals.Globals Config *config.Config Middleware *middleware.Middleware Handlers *handlers.Handlers } // Server is the main HTTP server. // It manages routing, middleware, and lifecycle. type Server struct { startupTime time.Time exitCode int sentryEnabled bool log *slog.Logger ctx context.Context //nolint:containedctx // signal handling pattern cancelFunc context.CancelFunc httpServer *http.Server router *chi.Mux params Params mw *middleware.Middleware handlers *handlers.Handlers } // New creates a new Server and registers its lifecycle hooks. func New( lifecycle fx.Lifecycle, params Params, ) (*Server, error) { srv := &Server{ //nolint:exhaustruct // fields set during lifecycle params: params, mw: params.Middleware, handlers: params.Handlers, log: params.Logger.Get(), } lifecycle.Append(fx.Hook{ OnStart: func(_ context.Context) error { srv.startupTime = time.Now() go srv.Run() //nolint:contextcheck return nil }, OnStop: func(_ context.Context) error { return nil }, }) return srv, nil } // Run starts the server configuration, Sentry, and begins serving. func (srv *Server) Run() { srv.configure() srv.enableSentry() srv.serve() } // ServeHTTP delegates to the chi router. func (srv *Server) ServeHTTP( writer http.ResponseWriter, request *http.Request, ) { srv.router.ServeHTTP(writer, request) } // MaintenanceMode reports whether the server is in maintenance mode. func (srv *Server) MaintenanceMode() bool { return srv.params.Config.MaintenanceMode } func (srv *Server) enableSentry() { srv.sentryEnabled = false if srv.params.Config.SentryDSN == "" { return } err := sentry.Init(sentry.ClientOptions{ //nolint:exhaustruct // only essential fields Dsn: srv.params.Config.SentryDSN, Release: fmt.Sprintf( "%s-%s", srv.params.Globals.Appname, srv.params.Globals.Version, ), }) if err != nil { srv.log.Error("sentry init failure", "error", err) os.Exit(1) } srv.log.Info("sentry error reporting activated") srv.sentryEnabled = true } func (srv *Server) serve() int { srv.ctx, srv.cancelFunc = context.WithCancel( context.Background(), ) go func() { sigCh := make(chan os.Signal, 1) signal.Ignore(syscall.SIGPIPE) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) sig := <-sigCh srv.log.Info("signal received", "signal", sig) if srv.cancelFunc != nil { srv.cancelFunc() } }() go srv.serveUntilShutdown() <-srv.ctx.Done() srv.cleanShutdown() return srv.exitCode } func (srv *Server) cleanupForExit() { srv.log.Info("cleaning up") } func (srv *Server) cleanShutdown() { srv.exitCode = 0 ctxShutdown, shutdownCancel := context.WithTimeout( context.Background(), shutdownTimeout, ) err := srv.httpServer.Shutdown(ctxShutdown) if err != nil { srv.log.Error( "server clean shutdown failed", "error", err, ) } if shutdownCancel != nil { shutdownCancel() } srv.cleanupForExit() if srv.sentryEnabled { sentry.Flush(sentryFlushTime) } } func (srv *Server) configure() { // Server configuration placeholder. } func (srv *Server) serveUntilShutdown() { listenAddr := fmt.Sprintf( ":%d", srv.params.Config.Port, ) srv.httpServer = &http.Server{ //nolint:exhaustruct // optional fields Addr: listenAddr, ReadTimeout: httpReadTimeout, WriteTimeout: httpWriteTimeout, MaxHeaderBytes: maxHeaderBytes, Handler: srv, } srv.SetupRoutes() srv.log.Info( "http begin listen", "listenaddr", listenAddr, ) err := srv.httpServer.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { srv.log.Error("listen error", "error", err) if srv.cancelFunc != nil { srv.cancelFunc() } } }