{-# LANGUAGE NamedFieldPuns #-} module Festung.Vault.VaultManager ( newManager , stopManager , VaultManager , query , parametrizedQuery , deleteVault , ManagerError(..) ) where import Control.Concurrent import Control.Monad import qualified Data.HashMap.Strict as M import Data.Either (isRight) import Control.Monad.Trans.Either import System.FilePath ((), normalise) import System.Directory import Festung.Concurrency.Job import Festung.Concurrency.Utils import Festung.Config import qualified Festung.Vault.VaultHandler as V import qualified Festung.Vault.Persistence as P import Festung.Utils (hoistMEither, mapLeft, whenJust) type VaultManager = Job VaultManagerCommand data ManagerError = VaultError V.VaultError | CouldNotReachManager deriving (Show) type TimeoutSubscriber = MVar () data VaultManagerCommand = OpenVault (Command V.VaultOpener (Either ManagerError V.Vault)) | VaultTimedOut (Command V.VaultOpener (Either ManagerError ())) | GetSubscription (Command V.VaultOpener (Either ManagerError TimeoutSubscriber)) | DeleteVault (Command V.VaultOpener (Either ManagerError ())) type VaultMapKey = FilePath data VaultMapValue = VaultMapValue { opener :: V.VaultOpener , vault :: V.Vault , subscriptions :: [TimeoutSubscriber] } type VaultMap = M.HashMap VaultMapKey VaultMapValue data ManagerState = ManagerState { registry :: VaultMap , timeouts :: Chan V.VaultOpener } -- | Initial manager state (for internal use only) newInitialState :: IO ManagerState newInitialState = do timeouts <- newChan return ManagerState { registry = M.empty, timeouts = timeouts } addSubscription :: TimeoutSubscriber -> VaultMapValue -> VaultMapValue addSubscription s val@VaultMapValue{subscriptions} = let subscriptions' = s:subscriptions in val{subscriptions=subscriptions'} -- | Create a timeout consumer -- -- Process that reads a @'Chan' of vault opener and sends a VaultTimoedOut command -- to the manager whan a new value is sent to the @'Chan' in question. -- -- This is for internal use only newTimeoutConsumer :: VaultManager -> Chan V.VaultOpener -> IO ThreadId newTimeoutConsumer manager timeouts = let sendTimeout = sendMappedCommand VaultTimedOut in forkIOForSure $ forever $ do opener <- readChan timeouts sendTimeout manager opener newManager :: Config -> IO VaultManager newManager config = do state <- newInitialState manager <- newJob state $ handlerLoop config timeoutConsumer <- newTimeoutConsumer manager (timeouts state) onExit manager $ killThread timeoutConsumer return manager stopManager :: VaultManager -> IO () stopManager = killJob updateStateRegistry :: (VaultMap -> VaultMap) -> ManagerState -> ManagerState updateStateRegistry f ManagerState{registry,timeouts} = ManagerState { registry = f registry, timeouts = timeouts } -- | Internal function to check the opener when canAccess :: VaultMap -> VaultMapKey -> V.VaultOpener -> IO Bool canAccess mapping key opener = case M.lookup key mapping of Just VaultMapValue{opener=opener'} -> isRight <$> V.checkOpener opener opener' Nothing -> return True -- | Manager intenal loop (for internal use only) handlerLoop :: Config -> ManagerState -> VaultManagerCommand -> IO (JobStatus, ManagerState) handlerLoop config state cmd = do let opener@(path, _, _) = getOpener cmd key = normalise path canAccess' <- canAccess (registry state) key opener state' <- if canAccess' -- The opener is wrong, we stupidly say "vault does not exist" because you can't -- access this vault with these parameters. then handleCommand config state key cmd else handleError cmd (VaultError V.CouldNotOpen) >> return state return (KeepGoing, state') -- FIXME(Antoine): This code is totally copy pasted. This needs to use polymorphism. handleError :: VaultManagerCommand -> ManagerError -> IO () handleError (OpenVault (Command _ responder)) err = putMVar responder (Left err) handleError (VaultTimedOut (Command _ responder)) err = putMVar responder (Left err) handleError (DeleteVault (Command _ responder)) err = putMVar responder (Left err) getOpener :: VaultManagerCommand -> V.VaultOpener getOpener (OpenVault (Command o _)) = o getOpener (VaultTimedOut (Command o _)) = o getOpener (DeleteVault (Command o _)) = o getOpener (GetSubscription (Command o _)) = o -- XXX(Antoine): This is very very bad code handleCommand :: Config -> ManagerState -> VaultMapKey -> VaultManagerCommand -> IO ManagerState handleCommand config state key (OpenVault (Command opener responder)) = case M.lookup key (registry state) of Just VaultMapValue{vault} -> do putMVar responder (Right vault) return state Nothing -> do res <- wrapManagerError <$> V.newHandler config opener -- FIXME(Antoine): Nesting!!! case res of Left err -> do putMVar responder (Left err) return state Right vault -> do onExit vault $ writeChan (timeouts state) opener putMVar responder (Right vault) let value = VaultMapValue {opener=opener, vault=vault, subscriptions=[]} return $ updateStateRegistry (M.insert key value) state handleCommand _config state key (VaultTimedOut (Command _opener responder)) = do whenJust (M.lookup key (registry state)) $ \ VaultMapValue{vault,subscriptions} -> do killJob vault -- Just making sure the vault is dead forM_ subscriptions $ \ s -> void $ tryPutMVar s () putMVar responder $ Right () return $ updateStateRegistry (M.delete key) state handleCommand _config state key (GetSubscription (Command _opener responder)) = let noop = do subscription <- newMVar () putMVar responder $ Right subscription return state in case M.lookup key (registry state) of Just VaultMapValue{vault} -> do running <- isJobRunning vault if running then noop else do subscription <- newEmptyMVar let addSubscription' = addSubscription subscription putMVar responder $ Right subscription return $ updateStateRegistry (M.adjust addSubscription' key) state Nothing -> noop handleCommand Config{dataDirectory} state key (DeleteVault (Command _opener responder)) = do let deleteVault = do filename <- canonicalizePath $ dataDirectory key exists <- doesFileExist filename when exists $ removeFile filename case M.lookup key (registry state) of Just VaultMapValue{vault, subscriptions} -> do killJob vault forM_ subscriptions $ \s -> void $ tryPutMVar s () deleteVault Nothing -> deleteVault putMVar responder $ Right () return $ updateStateRegistry (M.delete key) state wrapManagerError :: Either V.VaultError a -> Either ManagerError a wrapManagerError = mapLeft VaultError flattenError :: Maybe (Either ManagerError a) -> Either ManagerError a flattenError Nothing = Left CouldNotReachManager flattenError (Just e) = e -- | This is for internal use only getVault :: V.VaultOpener -> VaultManager -> EitherT ManagerError IO V.Vault getVault opener manager = hoistMEither $ flattenError <$> sendMappedCommand OpenVault manager opener -- | This is for internal use only waitForTimeout :: V.VaultOpener -> VaultManager -> IO (Either ManagerError ()) waitForTimeout opener manager = waitOnSubscription =<< flattenError <$> sendMappedCommand GetSubscription manager opener where waitOnSubscription (Left err) = return $ Left err waitOnSubscription (Right mvar) = readMVar mvar >> return (Right ()) retryWhenTimedOut :: V.VaultOpener -> VaultManager -> IO (Either ManagerError a) -> IO (Either ManagerError a) retryWhenTimedOut opener manager action = do res <- action case res of Left (VaultError V.CouldNotReach) -> do timeout <- waitForTimeout opener manager case timeout of Left err -> return $ Left err Right _ -> action _ -> return res -- | Execute a query on a vault query :: V.VaultOpener -> VaultManager -> String -> IO (Either ManagerError P.QueryResult) query opener manager query_ = retryWhenTimedOut opener manager $ runEitherT $ do vault <- getVault opener manager hoistMEither $ wrapManagerError <$> V.query opener vault query_ -- | Execute a parametrized query parametrizedQuery :: V.VaultOpener -> VaultManager -> String -> [P.Value] -> IO (Either ManagerError P.QueryResult) parametrizedQuery opener manager query_ params = retryWhenTimedOut opener manager $ runEitherT $ do vault <- getVault opener manager hoistMEither $ wrapManagerError <$> V.parametrizedQuery opener vault query_ params deleteVault :: V.VaultOpener -> VaultManager -> IO (Either ManagerError ()) deleteVault opener manager = flattenError <$> sendMappedCommand DeleteVault manager opener