packages feed

pms-infra-agent-server-0.0.1.0: src/PMS/Infra/Agent/Server/ApplicationBase/Control.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ScopedTypeVariables #-}

module PMS.Infra.Agent.Server.ApplicationBase.Control where

import Control.Lens ((^.))
import Control.Monad (void)
import Control.Monad.Except (catchError, liftEither, runExceptT)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Reader (runReaderT)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ask)
import Control.Monad.Trans.State.Lazy (get, runStateT)
import Data.Aeson (eitherDecode)
import Data.Conduit (ConduitT, await, awaitForever, runConduit, yield, (.|))
import Data.Void (Void)
import System.Exit (ExitCode(..))
import System.IO (hPutStrLn, stderr)
import System.Log.FastLogger (TimedFastLogger)
import qualified Control.Concurrent.STM as STM
import qualified Control.Exception.Safe as E
import qualified Data.Map.Strict as Map
import qualified PMS.Domain.Model.DM.Type as DM
import qualified PMS.Domain.Model.DS.Utility as DM

import PMS.Infra.Agent.Server.CoreModel.Constant
import PMS.Infra.Agent.Server.CoreModel.TH
import PMS.Infra.Agent.Server.CoreModel.Type
import PMS.Infra.Agent.Server.ApplicationBase.State.Connected ()
import PMS.Infra.Agent.Server.ApplicationBase.State.Idle ()
import PMS.Infra.Agent.Server.ApplicationBase.State.Listening ()
import PMS.Infra.Agent.Server.Interface.Network

$(funcTH_transit)

runAppStateContext
  :: DM.DomainData
  -> AppData
  -> AppStateW
  -> TimedFastLogger
  -> AppStateContext a
  -> IO (Either DM.ErrorData (a, AppStateW))
runAppStateContext domDat appDat initSt logger ctx = do
  (result, nextSt) <-
    DM.runFastLoggerT domDat logger
      $ flip runReaderT domDat
      $ flip runReaderT appDat
      $ runStateT (runExceptT ctx) initSt
  return $ case result of
    Left e -> Left e
    Right a -> Right (a, nextSt)

run :: DM.DomainContext ()
run domDat = do
  hPutStrLn stderr "[INFO] PMS.Infra.Agent.Server.ApplicationBase.Control.run called."
  appDat <- defaultAppData defaultServerPortFunc
  runWithAppData appDat domDat

defaultAppData :: ServerPortFunc -> IO AppData
defaultAppData portFunc = AppData portFunc
  <$> STM.newTMVarIO Nothing
  <*> STM.newTMVarIO Map.empty

runWithAppData :: AppData -> DM.DomainContext ()
runWithAppData appDat domDat = do
  logDat <- DM.createLogger domDat _LOG_FILE_NAME
  runWithLogger logDat appDat domDat

runWithLogger :: (TimedFastLogger, IO ()) -> AppData -> DM.DomainContext ()
runWithLogger (logger, finalizeLogger) appDat domDat =
  flip E.catchAny exception
    $ flip E.finally finalize
    $ runAppStateContext domDat appDat (AppStateW IdleState) logger app
    >>= \case
      Right (x, _) -> return x
      Left e -> errorEnd e
  where
    finalize = do
      hPutStrLn stderr "-----------------------------------------------------------------------------"
      hPutStrLn stderr "[INFO] PMS.Infra.Agent.Server.ApplicationBase.Control.run finalize called."
      finalizeLogger
      hPutStrLn stderr "-----------------------------------------------------------------------------"

    exception e = do
      hPutStrLn stderr "-----------------------------------------------------------------------------"
      hPutStrLn stderr "[ERROR] PMS.Infra.Agent.Server.ApplicationBase.Control.run exception occurred."
      hPutStrLn stderr $ show e
      hPutStrLn stderr "-----------------------------------------------------------------------------"
      E.throwIO e

    errorEnd e = do
      hPutStrLn stderr "-----------------------------------------------------------------------------"
      hPutStrLn stderr "[ERROR] PMS.Infra.Agent.Server.ApplicationBase.Control.run end with error."
      hPutStrLn stderr $ show e
      hPutStrLn stderr "-----------------------------------------------------------------------------"

app :: AppStateContext ()
app = do
  void $ actionSW (AppStateW IdleState) (EventW EntryEvent)
  runConduit pipeline
  st <- lift get
  void $ actionSW st (EventW ExitEvent)

pipeline :: ConduitT () Void AppStateContext ()
pipeline = src .| work .| sink

src :: ConduitT () DM.AgentServerCommand AppStateContext ()
src = do
  domDat <- lift (lift (lift (lift ask)))
  let queue = domDat^.DM.agentServerQueueDomainData
  cmd <- liftIO $ STM.atomically $ STM.readTQueue queue
  yield cmd
  src

work :: ConduitT DM.AgentServerCommand EventW AppStateContext ()
work = awaitForever $ \cmd ->
  flip catchError (errHdl cmd) $ do
    ev <- lift (toEventW cmd)
    yield ev
  where
    errHdl :: DM.AgentServerCommand
           -> DM.ErrorData
           -> ConduitT DM.AgentServerCommand EventW AppStateContext ()
    errHdl cmd msg = do
      let jsonRpc = DM.getJsonRpcAgentServerCommand cmd
      lift $ errorToolsCallResponse jsonRpc msg

toEventW :: DM.AgentServerCommand -> AppStateContext EventW
toEventW (DM.AgentServerListenCommand dat) = do
  params <- liftEither $ eitherDecode $ DM.unRawJsonByteString
              $ dat^.DM.argumentsAgentServerListenCommandData
  return $ EventW $ ServerListenEvent
    $ ServerListenEventData (dat^.DM.jsonrpcAgentServerListenCommandData) params
toEventW (DM.AgentServerCloseCommand dat) =
  return $ EventW $ ServerCloseEvent
    $ ServerCloseEventData (dat^.DM.jsonrpcAgentServerCloseCommandData)
toEventW (DM.AgentServerStatusCommand dat) =
  return $ EventW $ ServerStatusEvent
    $ ServerStatusEventData (dat^.DM.jsonrpcAgentServerStatusCommandData)
toEventW (DM.AgentServerEventsCommand dat) = do
  params <- liftEither $ eitherDecode $ DM.unRawJsonByteString
              $ dat^.DM.argumentsAgentServerEventsCommandData
  return $ EventW $ ServerEventsEvent
    $ ServerEventsEventData (dat^.DM.jsonrpcAgentServerEventsCommandData) params
toEventW (DM.AgentServerReadCommand dat) =
  return $ EventW $ ServerReadEvent
    $ ServerReadEventData (dat^.DM.jsonrpcAgentServerReadCommandData)
toEventW (DM.AgentServerWriteCommand dat) = do
  params <- liftEither $ eitherDecode $ DM.unRawJsonByteString
              $ dat^.DM.argumentsAgentServerWriteCommandData
  return $ EventW $ ServerWriteEvent
    $ ServerWriteEventData (dat^.DM.jsonrpcAgentServerWriteCommandData) params
toEventW (DM.AgentServerReadByteCommand dat) =
  return $ EventW $ ServerReadByteEvent
    $ ServerReadByteEventData (dat^.DM.jsonrpcAgentServerReadByteCommandData)
toEventW (DM.AgentServerWriteByteCommand dat) = do
  params <- liftEither $ eitherDecode $ DM.unRawJsonByteString
              $ dat^.DM.argumentsAgentServerWriteByteCommandData
  return $ EventW $ ServerWriteByteEvent
    $ ServerWriteByteEventData (dat^.DM.jsonrpcAgentServerWriteByteCommandData) params
toEventW (DM.AgentServerInternalClientConnectedCommand dat) =
  return $ EventW $ InternalClientConnectedEvent
    $ InternalClientConnectedEventData
        (dat^.DM.handleNameAgentServerInternalClientConnectedCommandData)
        (dat^.DM.remoteAddrAgentServerInternalClientConnectedCommandData)
toEventW (DM.AgentServerInternalClientDisconnectedCommand _) =
  return $ EventW $ InternalClientDisconnectedEvent
    $ InternalClientDisconnectedEventData

errorToolsCallResponse :: DM.JsonRpcRequest -> DM.ErrorData -> AppStateContext ()
errorToolsCallResponse jsonRpc msg = do
  domDat <- lift (lift (lift ask))
  let resQ = domDat^.DM.responseQueueDomainData
  liftIO $ DM.toolsCallResponse resQ jsonRpc (ExitFailure 1) "" msg

sink :: ConduitT EventW Void AppStateContext ()
sink = await >>= \case
  Nothing -> return ()
  Just ev -> flip catchError errHdl $ do
    lift (go ev)
    sink
  where
    errHdl :: DM.ErrorData -> ConduitT EventW Void AppStateContext ()
    errHdl msg = do
      msg `seq` return ()
      sink

    go :: EventW -> AppStateContext ()
    go ev = do
      st <- lift get
      result <- actionSW st ev
      case result of
        Nothing -> return ()
        Just t -> transit t