mirror of
https://github.com/snoyberg/keter.git
synced 2024-12-13 19:28:17 +03:00
250 lines
10 KiB
Haskell
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
|