packages feed

project-m36-0.9.6: src/lib/ProjectM36/Server.hs

{-# LANGUAGE ScopedTypeVariables #-}
module ProjectM36.Server where

import ProjectM36.Client
import ProjectM36.Server.EntryPoints 
import ProjectM36.Server.RemoteCallTypes
import ProjectM36.Server.Config (ServerConfig(..))
import ProjectM36.FSType

import Control.Concurrent.MVar (MVar)
import System.IO (stderr, hPutStrLn)
import System.FilePath (takeDirectory)
import System.Directory (doesDirectoryExist)
import Network.RPC.Curryer.Server
import Network.Socket
import qualified StmContainers.Map as StmMap
import Control.Concurrent.STM

type TestMode = Bool

requestHandlers :: TestMode -> Maybe Timeout -> RequestHandlers ServerState
requestHandlers testFlag ti =
  [
    RequestHandler (\sState (Login dbName) -> do
                       addClientLogin dbName sState
                       conn <- getConn sState
                       handleLogin conn (connectionSocket sState)),
     RequestHandler (\sState Logout -> do
                        conn <- getConn sState                        
                        handleLogout ti conn),
    RequestHandler $ \sState (ExecuteHeadName sessionId) -> do
      --socket -> dbname --maybe create a socket->client state mapping in the server state, too
      conn <- getConn sState
      handleExecuteHeadName ti sessionId conn,
    RequestHandler (\sState (ExecuteRelationalExpr sessionId expr) -> do
                       conn <- getConn sState                        
                       handleExecuteRelationalExpr ti sessionId conn expr),
     RequestHandler (\sState (ExecuteDataFrameExpr sessionId expr) -> do
                        conn <- getConn sState
                        handleExecuteDataFrameExpr ti sessionId conn expr),     
     RequestHandler (\sState (ExecuteDatabaseContextExpr sessionId expr) -> do
                        conn <- getConn sState
                        handleExecuteDatabaseContextExpr ti sessionId conn expr),
     RequestHandler (\sState (ExecuteDatabaseContextIOExpr sessionId expr) -> do
                        conn <- getConn sState
                        handleExecuteDatabaseContextIOExpr ti sessionId conn expr),
     RequestHandler (\sState (ExecuteGraphExpr sessionId expr) -> do
                        conn <- getConn sState
                        handleExecuteGraphExpr ti sessionId conn expr),
     RequestHandler (\sState (ExecuteTransGraphRelationalExpr sessionId expr) -> do
                       conn <- getConn sState
                       handleExecuteTransGraphRelationalExpr ti sessionId conn expr),
     RequestHandler (\sState (ExecuteTypeForRelationalExpr sessionId expr) -> do
                       conn <- getConn sState                        
                       handleExecuteTypeForRelationalExpr ti sessionId conn expr),
     RequestHandler (\sState (RetrieveInclusionDependencies sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveInclusionDependencies ti sessionId conn),
     RequestHandler (\sState (RetrievePlanForDatabaseContextExpr sessionId dbExpr) -> do
                       conn <- getConn sState                        
                       handleRetrievePlanForDatabaseContextExpr ti sessionId conn dbExpr),
     RequestHandler (\sState (RetrieveHeadTransactionId sessionId) -> do
                       conn <- getConn sState                        
                       handleRetrieveHeadTransactionId ti sessionId conn),
     RequestHandler (\sState (RetrieveTransactionGraph sessionId) -> do
                       conn <- getConn sState                        
                       handleRetrieveTransactionGraph ti sessionId conn),
     RequestHandler (\sState (CreateSessionAtHead headn) -> do
                       conn <- getConn sState                        
                       handleCreateSessionAtHead ti conn headn),
     RequestHandler (\sState (CreateSessionAtCommit commitId) -> do
                        conn <- getConn sState
                        handleCreateSessionAtCommit ti conn commitId),
     RequestHandler (\sState (CloseSession sessionId) -> do
                        conn <- getConn sState                 
                        handleCloseSession sessionId conn),
     RequestHandler (\sState (RetrieveAtomTypesAsRelation sessionId) -> do
                        conn <- getConn sState                                         
                        handleRetrieveAtomTypesAsRelation ti sessionId conn),
     RequestHandler (\sState (RetrieveRelationVariableSummary sessionId) -> do
                        conn <- getConn sState                        
                        handleRetrieveRelationVariableSummary ti sessionId conn),
     RequestHandler (\sState (RetrieveAtomFunctionSummary sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveAtomFunctionSummary ti sessionId conn),
     RequestHandler (\sState (RetrieveDatabaseContextFunctionSummary sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveDatabaseContextFunctionSummary ti sessionId conn),     RequestHandler (\sState (RetrieveCurrentSchemaName sessionId) -> do
                       conn <- getConn sState
                       handleRetrieveCurrentSchemaName ti sessionId conn),
     RequestHandler (\sState (ExecuteSchemaExpr sessionId schemaExpr) -> do
                        conn <- getConn sState
                        handleExecuteSchemaExpr ti sessionId conn schemaExpr),
     RequestHandler (\sState (RetrieveSessionIsDirty sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveSessionIsDirty ti sessionId conn),
     RequestHandler (\sState (ExecuteAutoMergeToHead sessionId strat headName') -> do
                        conn <- getConn sState                        
                        handleExecuteAutoMergeToHead ti sessionId conn strat headName'),
     RequestHandler (\sState (RetrieveTypeConstructorMapping sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveTypeConstructorMapping ti sessionId conn),
     RequestHandler (\sState (ExecuteValidateMerkleHashes sessionId) -> do
                        conn <- getConn sState                        
                        handleValidateMerkleHashes ti sessionId conn),
     RequestHandler (\sState (GetDDLHash sessionId) -> do
                        conn <- getConn sState
                        handleGetDDLHash ti sessionId conn),
     RequestHandler (\sState (RetrieveDDLAsRelation sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveDDLAsRelation ti sessionId conn),
     RequestHandler (\sState (RetrieveRegisteredQueries sessionId) -> do
                        conn <- getConn sState
                        handleRetrieveRegisteredQueries ti sessionId conn)
     ] ++ if testFlag then testModeHandlers ti else []

getConn :: ConnectionState ServerState -> IO Connection
getConn connState = do
  let sock = lockless (connectionSocket connState)
      sState = connectionServerState connState
  mConn <- connectionForClient sock sState
  case mConn of
    Nothing -> error "failed to find socket in client map"
    Just conn -> pure conn

testModeHandlers :: Maybe Timeout -> RequestHandlers ServerState
testModeHandlers ti = [RequestHandler (\sState (TestTimeout sessionId) -> do
                                          conn <- getConn sState
                                          handleTestTimeout ti sessionId conn)]

                 
-- | A notification callback which logs the notification to stderr and does nothing else.
loggingNotificationCallback :: NotificationCallback
loggingNotificationCallback notName evaldNot = hPutStrLn stderr $ "Notification received \"" ++ show notName ++ "\": " ++ show evaldNot

checkFSType :: Bool -> PersistenceStrategy -> IO Bool  
checkFSType performCheck strat = 
  case strat of 
    NoPersistence -> pure True
    MinimalPersistence _ -> pure True
    CrashSafePersistence path -> 
      if performCheck then do
        -- if the path does not (yet) exist, then walk back a step- the db directory may not yet have been created
        fullpathexists <- doesDirectoryExist path
        let fscheckpath = if fullpathexists then
                           path
                          else
                           takeDirectory path
        fsTypeSupportsJournaling fscheckpath
      else
        pure True
        
checkFSErrorMsg :: String        
checkFSErrorMsg = "The filesystem does not support journaling so writes may not be crash-safe. Use --disable-fscheck to disable this fatal error."

-- Sockets do not implement hashable, so we just use their string values as keys
type SocketString = String

data ServerState =
  ServerState {
  --map available databases to local database configurations
  stateDBMap :: StmMap.Map DatabaseName Connection,
  --map clients to database names- after logging in, clients are afixed to specific database names
  stateClientMap :: StmMap.Map SocketString DatabaseName
  }

-- add a client socket to the database mapping
addClientLogin :: DatabaseName -> ConnectionState ServerState -> IO ()
addClientLogin dbName cState = do
  let clientMap = stateClientMap (connectionServerState cState)
      sock = lockless (connectionSocket cState)
  atomically $ do
    mVal <- StmMap.lookup (show sock) clientMap
    case mVal of
      Nothing -> StmMap.insert dbName (show sock) clientMap
      Just _ -> pure () --TODO: throw exception- user already logged in
  
connectionForClient :: Socket -> ServerState -> IO (Maybe Connection)
connectionForClient sock sState =
  atomically $ do
    mdbname <- StmMap.lookup (show sock) (stateClientMap sState)
    case mdbname of
      Nothing -> pure Nothing
      Just dbname -> 
        StmMap.lookup dbname (stateDBMap sState)

initialServerState :: DatabaseName -> Connection -> IO ServerState
initialServerState dbName conn = 
  atomically $ do
  dbmap <- StmMap.new
  clientMap <- StmMap.new
  StmMap.insert conn dbName dbmap
  pure (ServerState { stateDBMap = dbmap, stateClientMap = clientMap })
-- | A synchronous function to start the project-m36 daemon given an appropriate 'ServerConfig'. Note that this function only returns if the server exits. Returns False if the daemon exited due to an error. If the second argument is not Nothing, the port is put after the server is ready to service the port.
launchServer :: ServerConfig -> Maybe (MVar SockAddr) -> IO Bool
launchServer daemonConfig mAddr = do
  checkFSResult <- checkFSType (checkFS daemonConfig) (persistenceStrategy daemonConfig)
  if not checkFSResult then do
    hPutStrLn stderr checkFSErrorMsg
    pure False
    else do
      econn <- connectProjectM36 (InProcessConnectionInfo (persistenceStrategy daemonConfig) loggingNotificationCallback (ghcPkgPaths daemonConfig))
      case econn of 
        Left err -> do      
          hPutStrLn stderr ("Failed to create database connection: " ++ show err)
          pure False
        Right conn -> do
          let hostname = bindHost daemonConfig
              port = fromIntegral (bindPort daemonConfig)


          --curryer only supports IPv4 for now
          let addrHints = defaultHints { addrSocketType = Stream, addrFamily = AF_INET }
          hostAddrs <- getAddrInfo (Just addrHints) (Just hostname) Nothing
          case hostAddrs of
            [] -> hPutStrLn stderr ("Failed to resolve: " <> hostname) >> pure False
            (AddrInfo _ _ _ _ (SockAddrInet _ addr32) _):_ -> do
              let hostAddr = hostAddressToTuple addr32
                  mTimeout = fromIntegral <$> case perRequestTimeout daemonConfig of
                                              0 -> Nothing
                                              v -> Just v
                  
              sState <- initialServerState (databaseName daemonConfig) conn
              serve (requestHandlers (testMode daemonConfig) mTimeout) sState hostAddr port mAddr
            _ -> error "unsupported socket addressing mode (IPv4 only currently)"