packages feed

cachix-1.7.5: src/Cachix/Daemon/Types/Daemon.hs

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeApplications #-}

module Cachix.Daemon.Types.Daemon
  ( -- * Daemon
    DaemonEvent (..),
    DaemonEnv (..),
    Daemon,
    runDaemon,
  )
where

import Cachix.Client.Config.Orphans ()
import Cachix.Client.Env as Env
import Cachix.Client.OptionsParser (PushOptions)
import Cachix.Daemon.Log qualified as Log
import Cachix.Daemon.Protocol qualified as Protocol
import Cachix.Daemon.ShutdownLatch (ShutdownLatch)
import Cachix.Daemon.Subscription (SubscriptionManager)
import Cachix.Daemon.Types.Error (DaemonError (DaemonUnhandledException), UnhandledException (..))
import Cachix.Daemon.Types.EventLoop (EventLoop)
import Cachix.Daemon.Types.Log (Logger)
import Cachix.Daemon.Types.PushEvent (PushEvent)
import Cachix.Daemon.Types.PushManager (PushManagerEnv (..))
import Cachix.Daemon.Types.SocketStore (SocketId, SocketStore)
import Cachix.Daemon.Worker qualified as Worker
import Cachix.Types.BinaryCache (BinaryCache, BinaryCacheName)
import Control.Exception.Safe qualified as Safe
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
import Control.Monad.IO.Unlift (MonadUnliftIO)
import Katip qualified
import Network.Socket (Socket)
import Protolude hiding (bracketOnError)
import System.Posix.Types (ProcessID)

-- | Daemon events that are handled by the 'EventLoop'.
data DaemonEvent
  = -- | Shut down the daemon gracefully.
    ShutdownGracefully
  | -- | Re-establish the daemon socket
    ReconnectSocket
  | -- | Add a new client socket connection.
    AddSocketClient Socket
  | -- | Remove an existing client socket connection. For example, after it is closed.
    RemoveSocketClient SocketId
  | -- | Handle a new message from a client.
    ReceivedMessage Protocol.ClientMessage

data DaemonEnv = DaemonEnv
  { -- | Cachix client env
    daemonEnv :: Env,
    -- | The main event loop
    daemonEventLoop :: EventLoop DaemonEvent (Either DaemonError ()),
    -- | Push options, like compression settings and number of jobs
    daemonPushOptions :: PushOptions,
    -- | Path to the socket that the daemon listens on
    daemonSocketPath :: FilePath,
    -- | Main inbound socket thread
    daemonSocketThread :: MVar (Async ()),
    -- | The name of the binary cache to push to
    daemonCacheName :: BinaryCacheName,
    -- | The binary cache to push to
    daemonBinaryCache :: BinaryCache,
    -- | The state of active push requests
    daemonPushManager :: PushManagerEnv,
    -- | Connected clients over the socket
    daemonClients :: SocketStore,
    -- | A pool of worker threads
    daemonWorkerThreads :: MVar [Worker.Thread],
    -- | A multiplexer for push events
    daemonSubscriptionManager :: SubscriptionManager Protocol.PushRequestId PushEvent,
    -- | A thread handle for the subscription manager
    daemonSubscriptionManagerThread :: MVar (Async ()),
    -- | Logging env
    daemonLogger :: Logger,
    -- | Shutdown latch
    daemonShutdownLatch :: ShutdownLatch,
    -- | The PID of the daemon process
    daemonPid :: ProcessID
  }

newtype Daemon a = Daemon
  { unDaemon :: ReaderT DaemonEnv IO a
  }
  deriving newtype
    ( Functor,
      Applicative,
      Monad,
      MonadIO,
      MonadReader DaemonEnv,
      MonadUnliftIO,
      MonadCatch,
      MonadMask,
      MonadThrow
    )

instance Katip.Katip Daemon where
  getLogEnv = Log.getKatipLogEnv <$> asks daemonLogger
  localLogEnv f (Daemon m) = Daemon (local (\s -> s {daemonLogger = Log.localLogEnv f (daemonLogger s)}) m)

instance Katip.KatipContext Daemon where
  getKatipContext = Log.getKatipContext <$> asks daemonLogger
  localKatipContext f (Daemon m) = Daemon (local (\s -> s {daemonLogger = Log.localKatipContext f (daemonLogger s)}) m)

  getKatipNamespace = Log.getKatipNamespace <$> asks daemonLogger
  localKatipNamespace f (Daemon m) = Daemon (local (\s -> s {daemonLogger = Log.localKatipNamespace f (daemonLogger s)}) m)

-- | Run a pre-configured daemon.
runDaemon :: DaemonEnv -> Daemon a -> IO (Either DaemonError a)
runDaemon env f = tryDaemon $ do
  Log.withLogger (daemonLogger env) $ \logger -> do
    let pushManagerEnv = (daemonPushManager env) {pmLogger = logger}
    unDaemon f `runReaderT` env {daemonLogger = logger, daemonPushManager = pushManagerEnv}
  where
    tryDaemon :: IO a -> IO (Either DaemonError a)
    tryDaemon a =
      Safe.catch
        (a >>= \v -> return (Right v))
        (return . Left . wrapUnhandledErrors)

    -- Wrap errors in a DaemonError
    wrapUnhandledErrors :: SomeException -> DaemonError
    wrapUnhandledErrors e | Just daemonError <- fromException @DaemonError e = daemonError
    wrapUnhandledErrors e = DaemonUnhandledException (UnhandledException e)