keter/Keter/AppManager.hs
2013-07-28 15:19:08 +03:00

250 lines
10 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
-- | Used for management of applications.
module Keter.AppManager
( -- * Types
AppManager
, Action (..)
-- * Actions
, perform
, reloadAppList
, addApp
, terminateApp
-- * Initialize
, initialize
) where
import Control.Applicative
import Control.Concurrent (forkIO)
import Control.Concurrent.MVar (MVar, newMVar, withMVar)
import Control.Concurrent.STM
import qualified Control.Exception as E
import Control.Monad (void)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe)
import Data.Maybe (catMaybes)
import qualified Data.Set as Set
import qualified Filesystem.Path.CurrentOS as F
import Keter.App (App, AppStartConfig)
import qualified Keter.App as App
import Keter.Types
import Prelude hiding (FilePath, log)
import System.Posix.Files (getFileStatus, modificationTime)
import System.Posix.Types (EpochTime)
data AppManager = AppManager
{ apps :: !(TVar (Map AppId (TVar AppState)))
, appStartConfig :: !AppStartConfig
, mutex :: !(MVar ())
, log :: !(LogMessage -> IO ())
}
data AppState = ASRunning App
| ASStarting
!(Maybe App)
!(TVar (Maybe EpochTime))
!(TVar (Maybe Action)) -- ^ the next one to try
| ASTerminated
data Action = Reload AppInput | Terminate
initialize :: (LogMessage -> IO ())
-> AppStartConfig
-> IO AppManager
initialize log' asc = AppManager
<$> newTVarIO Map.empty
<*> return asc
<*> newMVar ()
<*> return log'
-- | Reset which apps are running.
--
-- * Any app not listed here that is currently running will be terminated.
--
-- * Any app listed here that is currently running will be reloaded.
--
-- * Any app listed here that is not currently running will be started.
reloadAppList :: AppManager
-> Map Appname (FilePath, EpochTime)
-> IO ()
reloadAppList am@AppManager {..} newApps = withMVar mutex $ const $ do
actions <- atomically $ do
m <- readTVar apps
let currentApps = Set.fromList $ mapMaybe toAppName $ Map.keys m
allApps = Set.toList $ Map.keysSet newApps `Set.union` currentApps
fmap catMaybes $ mapM (getAction m) allApps
sequence_ actions
where
toAppName AIBuiltin = Nothing
toAppName (AINamed x) = Just x
getAction currentApps appname = do
case Map.lookup (AINamed appname) currentApps of
Nothing -> return freshLaunch
Just tstate -> do
state <- readTVar tstate
case state of
ASTerminated -> return freshLaunch
ASRunning app ->
case Map.lookup appname newApps of
Nothing -> return terminate
Just (fp, newTimestamp) -> do
moldTimestamp <- App.getTimestamp app
return $ if moldTimestamp == Just newTimestamp
then Nothing
else reload fp newTimestamp
ASStarting _ tmoldTimestamp tmaction ->
case Map.lookup appname newApps of
Nothing -> do
writeTVar tmaction $ Just Terminate
return Nothing
Just (fp, newTimestamp) -> do
moldTimestamp <- readTVar tmoldTimestamp
return $ if moldTimestamp == Just newTimestamp
then Nothing
else reload fp newTimestamp
where
freshLaunch =
case Map.lookup appname newApps of
Nothing -> E.assert False Nothing
Just (fp, timestamp) -> reload fp timestamp
terminate = Just $ performNoLock am (AINamed appname) Terminate
reload fp timestamp = Just $ performNoLock am (AINamed appname) (Reload $ AIBundle fp timestamp)
{-
case (Map.lookup appname currentApps, Map.lookup appname newApps) of
(Nothing, Nothing) -> E.assert False Nothing
(Just _, Nothing) -> Just $ perform am (AINamed appname) Terminate
(Nothing, Just _) -> Just $ perform am (AINamed appname) (Reload AIBundle)
-}
{- FIXME
actions <- do
current <- getAllApps appMan
let apps = Set.toList $ Set.fromList (Map.keys newMap) `Set.union` current
fmap catMaybes $ forM apps $ \appname -> return $
case (Set.member appname current, Map.lookup appname newMap) of
(False, Nothing) -> Nothing -- should never happen
(True, Nothing) -> Just $ terminateApp appname
(False, Just (bundle, _)) -> Just $ runKIO' $ addApp bundle
(Just (_, oldTime), Just (bundle, newTime))
| newTime /= oldTime -> Just $ runKIO' $ addApp bundle
| otherwise -> Nothing
P.sequence_ actions
getAllApps :: AppManager -> IO (Set Appname)
getAllApps AppManager {..} = atomically $ do
m <- readTVar apps
return $ Set.fromList $ mapMaybe toAppName $ Map.keys m
-}
perform :: AppManager -> AppId -> Action -> IO ()
perform am appid action = withMVar (mutex am) $ const $ performNoLock am appid action
performNoLock :: AppManager -> AppId -> Action -> IO ()
performNoLock am@AppManager {..} aid action = E.mask_ $ do
launchWorker' <- atomically $ do
m <- readTVar apps
case Map.lookup aid m of
Just tstate -> do
state <- readTVar tstate
case state of
ASStarting _mcurrent _tmtimestamp tmnext -> do
writeTVar tmnext $ Just action
-- use the previous worker, so nothing to do
return noWorker
ASRunning runningApp -> do
tmnext <- newTVar Nothing
tmtimestamp <- newTVar $
case action of
Reload (AIBundle _fp timestamp) -> Just timestamp
Reload (AIData _) -> Nothing
Terminate -> Nothing
writeTVar tstate $ ASStarting (Just runningApp) tmtimestamp tmnext
return $ launchWorker am aid tstate tmnext (Just runningApp) action
ASTerminated -> onNotRunning
Nothing -> onNotRunning
launchWorker'
where
noWorker = return ()
onNotRunning =
case action of
Reload input -> do
tmnext <- newTVar Nothing
tmtimestamp <- newTVar $
case input of
AIBundle _fp timestamp -> Just timestamp
AIData _ -> Nothing
tstate <- newTVar $ ASStarting Nothing tmtimestamp tmnext
modifyTVar apps $ Map.insert aid tstate
return $ launchWorker am aid tstate tmnext Nothing action
Terminate -> return noWorker
launchWorker :: AppManager
-> AppId
-> TVar AppState
-> TVar (Maybe Action)
-> Maybe App
-> Action
-> IO ()
launchWorker AppManager {..} appid tstate tmnext mcurrentApp0 action0 = void $ forkIO $ do
loop mcurrentApp0 action0
where
loop mcurrentApp action = do
mRunningApp <- processAction mcurrentApp action
mnext <- atomically $ do
mnext <- readTVar tmnext
writeTVar tmnext Nothing
case mnext of
Nothing ->
case mRunningApp of
Nothing -> writeTVar tstate ASTerminated
Just runningApp -> writeTVar tstate $ ASRunning runningApp
Just _next -> do
tmtimestamp <- newTVar $
case action of
Reload (AIBundle _fp timestamp) -> Just timestamp
Reload (AIData _) -> Nothing
Terminate -> Nothing
writeTVar tstate $ ASStarting mRunningApp tmtimestamp tmnext
return mnext
case mnext of
Nothing -> return ()
Just next -> loop mRunningApp next
processAction Nothing Terminate = return Nothing
processAction (Just app) Terminate = do
App.terminate app
return Nothing
processAction Nothing (Reload input) = do
eres <- E.try $ App.start appStartConfig appid input
case eres of
Left e -> do
let name =
case appid of
AIBuiltin -> "<builtin>"
AINamed x -> x
log $ ErrorStartingBundle name e
return Nothing
Right app -> return $ Just app
processAction (Just app) (Reload input) = do
App.reload app input
-- reloading will /always/ result in a valid app, either the old one
-- will continue running or the new one will replace it.
return $ Just app
addApp :: AppManager -> FilePath -> IO ()
addApp appMan bundle = do
(input, action) <- getInputForBundle bundle
perform appMan input action
getInputForBundle :: FilePath -> IO (AppId, Action)
getInputForBundle bundle = do
time <- modificationTime <$> getFileStatus (F.encodeString bundle)
return (AINamed $ getAppname bundle, Reload $ AIBundle bundle time)
terminateApp :: AppManager -> Appname -> IO ()
terminateApp appMan appname = perform appMan (AINamed appname) Terminate