packages feed

icepeak-0.7.1.0: app/Icepeak/Main.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main (main) where

import Control.Exception (fromException, catch, handle, AsyncException, SomeException)
import Control.Monad (forM, void, when)
import Data.Foldable (forM_)
import Data.Semigroup ((<>))
import Options.Applicative (execParser)
import System.Environment (getEnvironment)
import System.IO (BufferMode (..), hSetBuffering, stdout)

import qualified Control.Concurrent.Async as Async
import qualified Data.Text as Text
import qualified Prometheus
import qualified Prometheus.Metric.GHC
import qualified System.Posix.Signals as Signals

import Config (Config (..), configInfo)
import Core (Core (..))
import Persistence (getDataFile, setupStorageBackend)
import Logger (Logger, LogLevel(..), postLog)

import qualified Core
import qualified HttpServer
import qualified Server
import qualified WebsocketServer
import qualified Logger
import qualified Metrics
import qualified MetricsServer

-- Install SIGTERM and SIGINT handlers to do a graceful exit.
installHandlers :: Core -> IO ()
installHandlers core =
  let
    logHandle = do
      postLog (coreLogger core) LogInfo "\nTermination sequence initiated ..."
      Core.postQuit core
    handler = Signals.CatchOnce logHandle
    blockSignals = Nothing
    installHandler signal = Signals.installHandler signal handler blockSignals
  in do
    void $ installHandler Signals.sigTERM
    void $ installHandler Signals.sigINT


main :: IO ()
main = do
  -- make sure output is flushed regularly
  hSetBuffering stdout LineBuffering

  env <- getEnvironment
  config <- execParser (configInfo env)

  -- make sure the storage file exists and that it has the right format - otherwise, fail early
  let dataFile = getDataFile (configStorageBackend config) (configDataFile config)
  setupStorageBackend (configStorageBackend config) dataFile

  -- start logging as early as possible
  logger <- Logger.newLogger config
  loggerThread <- Async.async $ Logger.processLogRecords logger
  handle (\e -> postLog logger LogError . Text.pack . show $ (e :: SomeException)) $ do
    -- setup metrics if enabled
    icepeakMetrics <- forM (configMetricsEndpoint config) $ const $ do
      void $ Prometheus.register Prometheus.Metric.GHC.ghcMetrics
      Metrics.createAndRegisterIcepeakMetrics

    eitherCore <- Core.newCore config logger icepeakMetrics
    either (postLog logger LogError . Text.pack) runCore eitherCore

    -- only stop logging when everything else has stopped
  Logger.postStop logger
  Async.wait loggerThread


runCore :: Core -> IO ()
runCore core = do
  let config = coreConfig core
  let logger = coreLogger core
  httpServer <- HttpServer.new core
  let wsServer = WebsocketServer.acceptConnection core

  -- start threads
  commandLoopThread <- Async.async $ catchRunCoreResult CommandLoopException $ Core.runCommandLoop core
  webSocketThread <- Async.async $ catchRunCoreResult WebSocketsException $ WebsocketServer.processUpdates core
  httpThread <- Async.async $ catchRunCoreResult HttpException $ Server.runServer logger wsServer httpServer (configPort config)
  syncThread <- Async.async $ Core.runSyncTimer core
  metricsThread <- Async.async
    $ forM_ (configMetricsEndpoint config) (MetricsServer.runMetricsServer logger)

  installHandlers core
  logAuthSettings config logger
  logQueueSettings config logger
  logSyncSettings config logger

  postLog logger LogInfo "System online. ** robot sounds **"

  -- Everything should stop when any of these stops
  (_, runCoreResult) <- Async.waitAny [commandLoopThread, webSocketThread, httpThread]
  logRunCoreResult logger runCoreResult

  -- kill all threads when one of the main threads ended
  Core.postQuit core -- should stop commandLoopThread
  Async.cancel webSocketThread
  Async.cancel httpThread
  Async.cancel metricsThread
  Async.cancel syncThread
  void $ Async.wait commandLoopThread

-- | Data type to hold results for the async that finishes first
data RunCoreResult
  = CommandLoopException SomeException
  | WebSocketsException SomeException
  | HttpException SomeException
  | ThreadOk

-- | If a threads fails, catch the error and tag it so we know how to log it
catchRunCoreResult :: (SomeException -> RunCoreResult) -> IO () -> IO RunCoreResult
catchRunCoreResult tag action = catch (action >> pure ThreadOk) $ \exc -> case fromException exc of
    Just (_ :: AsyncException) -> pure ThreadOk
    _ -> pure (tag exc) -- we only worry about non-async exceptions

logRunCoreResult :: Logger -> RunCoreResult -> IO ()
logRunCoreResult logger rcr = do
    case rcr of
      CommandLoopException exc -> handleLog "core" exc
      WebSocketsException exc -> handleLog "web sockets server" exc
      HttpException exc -> handleLog "http server" exc
      ThreadOk -> pure ()
  where
    handleLog name exc
      | Just (_ :: AsyncException) <- fromException exc = pure ()
      | otherwise = do
            Logger.postLog logger LogError $ name <> " stopped with an exception: " <> Text.pack (show exc)

logAuthSettings :: Config -> Logger -> IO ()
logAuthSettings cfg logger
  | configEnableJwtAuth cfg = case configJwtSecret cfg of
      Just _ -> postLog logger LogInfo "JWT authorization enabled and secret provided, tokens will be verified."
      Nothing -> postLog logger LogInfo "JWT authorization enabled but no secret provided, tokens will NOT be verified."
  | otherwise = case configJwtSecret cfg of
      Just _ -> postLog logger LogInfo "WARNING a JWT secret has been provided, but JWT authorization is disabled."
      Nothing -> postLog logger LogInfo "JWT authorization disabled."

logQueueSettings :: Config -> Logger -> IO ()
logQueueSettings cfg logger =
  postLog logger LogInfo ("Queue capacity is set to " <> Text.pack (show (configQueueCapacity cfg)) <> ".")

logSyncSettings :: Config -> Logger -> IO ()
logSyncSettings cfg logger = case configSyncIntervalMicroSeconds cfg of
  Nothing -> do
    postLog logger LogInfo "Sync: Persisting after every modification"
    when (configEnableJournaling cfg) $ do
      postLog logger LogInfo "Journaling has no effect when periodic syncing is disabled"
  Just musecs -> do
    postLog logger LogInfo ("Sync: every " <> Text.pack (show musecs) <> " microseconds.")
    when (configEnableJournaling cfg) $ do
      postLog logger LogInfo "Journaling enabled"