{-# LANGUAGE IncoherentInstances #-} module IHP.Server (run, application) where import IHP.Prelude import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp.Systemd as Systemd import Network.Wai import Network.Wai.Middleware.MethodOverridePost (methodOverridePost) import Network.Wai.Session (withSession) import Network.Wai.Session.ClientSession (clientsessionStore) import qualified Network.Wai.Middleware.HealthCheckEndpoint as HealthCheckEndpoint import qualified Web.ClientSession as ClientSession import IHP.Controller.Session (sessionVaultKey) import IHP.ApplicationContext import qualified IHP.Environment as Env import qualified IHP.PGListener as PGListener import IHP.FrameworkConfig import IHP.RouterSupport (frontControllerToWAIApp, FrontController) import qualified IHP.AutoRefresh as AutoRefresh import qualified IHP.AutoRefresh.Types as AutoRefresh import qualified IHP.Job.Runner as Job import qualified IHP.Job.Types as Job import qualified Data.ByteString.Char8 as ByteString import qualified Network.Wai.Middleware.Cors as Cors import qualified Network.Wai.Middleware.Approot as Approot import qualified Control.Exception as Exception import qualified System.Directory as Directory import qualified GHC.IO.Encoding as IO import qualified System.IO as IO import qualified Network.Wai.Application.Static as Static import qualified WaiAppStatic.Types as Static import qualified IHP.EnvVar as EnvVar import qualified Network.Wreq as Wreq import qualified Data.Function as Function import IHP.Controller.NotFound (handleNotFound) import Paths_ihp (getDataFileName) run :: (FrontController RootApplication, Job.Worker RootApplication) => ConfigBuilder -> IO () run configBuilder = do -- We cannot use 'Main.Utf8.withUtf8' here, as this for some reason breaks live reloading -- in the dev server. So we switch the file handles to utf8 manually IO.setLocaleEncoding IO.utf8 withFrameworkConfig configBuilder \frameworkConfig -> do modelContext <- IHP.FrameworkConfig.initModelContext frameworkConfig approotMiddleware <- Approot.envFallback withInitalizers frameworkConfig modelContext do PGListener.withPGListener modelContext \pgListener -> do autoRefreshServer <- newIORef (AutoRefresh.newAutoRefreshServer pgListener) let ?modelContext = modelContext let ?applicationContext = ApplicationContext { modelContext = ?modelContext, autoRefreshServer, frameworkConfig, pgListener } sessionMiddleware <- initSessionMiddleware frameworkConfig staticApp <- initStaticApp frameworkConfig let corsMiddleware = initCorsMiddleware frameworkConfig let requestLoggerMiddleware = frameworkConfig.requestLoggerMiddleware let CustomMiddleware customMiddleware = frameworkConfig.customMiddleware useSystemd <- EnvVar.envOrDefault "IHP_SYSTEMD" False withBackgroundWorkers pgListener frameworkConfig . runServer frameworkConfig useSystemd . (if useSystemd then HealthCheckEndpoint.healthCheck else Function.id) . customMiddleware . corsMiddleware . methodOverridePost . sessionMiddleware . approotMiddleware $ application staticApp requestLoggerMiddleware {-# INLINABLE run #-} withBackgroundWorkers :: (Job.Worker RootApplication, ?modelContext :: ModelContext) => PGListener.PGListener -> FrameworkConfig -> IO () -> IO () withBackgroundWorkers pgListener frameworkConfig app = do let jobWorkers = Job.workers RootApplication let isDevelopment = frameworkConfig.environment == Env.Development if isDevelopment && not (isEmpty jobWorkers) then race_ (Job.devServerMainLoop frameworkConfig pgListener jobWorkers) app else app -- | Returns a WAI app that servers files stored in the app's @static/@ directory and IHP's own @static/@ directory -- -- HTTP Cache headers are set automatically. This includes Cache-Control, Last-Mofified and ETag -- -- The cache strategy works like this: -- - In dev mode we disable the browser cache for the app's @static/@ directory to make sure that always the latest CSS and JS is used -- - In production mode: We cache files forever. IHP's 'assetPath' helper will add a hash to files to cache bust when something has changed. initStaticApp :: FrameworkConfig -> IO Application initStaticApp frameworkConfig = do frameworkStaticDir <- getDataFileName "static" let maxAge = case frameworkConfig.environment of Env.Development -> Static.MaxAgeSeconds 0 Env.Production -> Static.MaxAgeForever frameworkSettings = (Static.defaultWebAppSettings frameworkStaticDir) { Static.ss404Handler = Just (frameworkConfig.requestLoggerMiddleware handleNotFound) , Static.ssMaxAge = maxAge } appSettings = (Static.defaultWebAppSettings "static/") { Static.ss404Handler = Just (Static.staticApp frameworkSettings) , Static.ssMaxAge = maxAge } pure (Static.staticApp appSettings) initSessionMiddleware :: FrameworkConfig -> IO Middleware initSessionMiddleware FrameworkConfig { sessionCookie } = do let path = "Config/client_session_key.aes" hasSessionSecretEnvVar <- EnvVar.hasEnvVar "IHP_SESSION_SECRET" doesConfigDirectoryExist <- Directory.doesDirectoryExist "Config" store <- clientsessionStore <$> if hasSessionSecretEnvVar || not doesConfigDirectoryExist then ClientSession.getKeyEnv "IHP_SESSION_SECRET" else ClientSession.getKey path let sessionMiddleware :: Middleware = withSession store "SESSION" sessionCookie sessionVaultKey pure sessionMiddleware initCorsMiddleware :: FrameworkConfig -> Middleware initCorsMiddleware FrameworkConfig { corsResourcePolicy } = case corsResourcePolicy of Just corsResourcePolicy -> Cors.cors (const (Just corsResourcePolicy)) Nothing -> id application :: (FrontController RootApplication, ?applicationContext :: ApplicationContext) => Application -> Middleware -> Application application staticApp middleware request respond = do frontControllerToWAIApp @RootApplication @AutoRefresh.AutoRefreshWSApp middleware RootApplication staticApp request respond {-# INLINABLE application #-} runServer :: (?applicationContext :: ApplicationContext) => FrameworkConfig -> Bool -> Application -> IO () runServer config@FrameworkConfig { environment = Env.Development, appPort } useSystemd = Warp.runSettings $ Warp.defaultSettings |> Warp.setBeforeMainLoop (do ByteString.putStrLn "Server started" IO.hFlush IO.stdout ) |> Warp.setPort appPort runServer FrameworkConfig { environment = Env.Production, appPort, exceptionTracker } useSystemd = let warpSettings = Warp.defaultSettings |> Warp.setPort appPort |> Warp.setOnException exceptionTracker.onException |> Warp.setGracefulShutdownTimeout (Just 6) |> Warp.setFdCacheDuration (5 * 60) |> Warp.setFileInfoCacheDuration (5 * 60) heartbeatCheck = do response <- Wreq.get ("http://127.0.0.1:" <> cs (show appPort) <> "/_healthz") pure () systemdSettings = Systemd.defaultSystemdSettings |> Systemd.setRequireSocketActivation True |> Systemd.setHeartbeatInterval (Just 30) |> Systemd.setHeartbeatCheck heartbeatCheck in if useSystemd then Systemd.runSystemdWarp systemdSettings warpSettings else Warp.runSettings warpSettings withInitalizers :: FrameworkConfig -> ModelContext -> IO () -> IO () withInitalizers frameworkConfig modelContext continue = do let ?context = frameworkConfig let ?modelContext = modelContext withInitalizers' frameworkConfig.initializers where withInitalizers' :: (?context :: FrameworkConfig, ?modelContext :: ModelContext) => [Initializer] -> IO () withInitalizers' (Initializer { onStartup } : rest) = withAsync onStartup (\async -> link async >> withInitalizers' rest) withInitalizers' [] = continue