keter/Keter/Main.hs

195 lines
7.5 KiB
Haskell
Raw Normal View History

2013-07-26 12:17:05 +04:00
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
2012-10-24 18:31:18 +04:00
{-# LANGUAGE ScopedTypeVariables #-}
2013-07-26 12:17:05 +04:00
{-# LANGUAGE TemplateHaskell #-}
2012-05-11 12:29:25 +04:00
module Keter.Main
( keter
) where
2013-07-10 15:15:18 +04:00
import qualified Codec.Archive.TempTarball as TempFolder
2013-07-26 12:17:05 +04:00
import Control.Concurrent.Async (waitAny, withAsync)
import Control.Monad (unless)
import qualified Data.Conduit.LogFile as LogFile
import Data.Monoid (mempty)
import qualified Data.Vector as V
import Keter.App (AppStartConfig (..))
import qualified Keter.AppManager as AppMan
import qualified Keter.HostManager as HostMan
import qualified Keter.PortPool as PortPool
import Keter.Prelude hiding (getCurrentTime, runKIO)
import qualified Keter.Proxy as Proxy
import Keter.Types
import System.Posix.Files (getFileStatus, modificationTime)
import System.Posix.Signals (Handler (Catch), installHandler,
sigHUP)
import Control.Applicative ((<$>))
import Control.Exception (throwIO, try)
import Control.Exception (Exception)
import Control.Monad (forM)
import Data.Conduit.Process.Unix (initProcessTracker)
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
2012-10-24 18:31:18 +04:00
import qualified Data.Text.Read
2013-07-26 12:17:05 +04:00
import Data.Time (getCurrentTime)
import Data.Typeable (Typeable)
import Data.Yaml (ParseException)
import Data.Yaml.FilePath
import qualified Filesystem as F
import qualified Filesystem.Path.CurrentOS as F
import qualified Keter.Prelude
import qualified Network.HTTP.Conduit as HTTP (newManager)
import Prelude (IO)
import qualified Prelude as P
import qualified System.FSNotify as FSN
import System.Posix.User (getUserEntryForID,
getUserEntryForName, userGroupID,
userID, userName)
2012-05-11 12:29:25 +04:00
2013-07-26 11:46:34 +04:00
keter :: F.FilePath -- ^ root directory or config file
-> [F.FilePath -> P.IO Plugin]
2012-05-15 12:38:54 +04:00
-> P.IO ()
2013-07-26 12:17:05 +04:00
keter input mkPlugins = withManagers input mkPlugins $ \kc hostman appMan -> do
launchInitial kc appMan
startWatching kc appMan
startListening kc hostman
-- | Load up Keter config.
withConfig :: FilePath
-> (KeterConfig -> IO a)
-> IO a
withConfig input f = do
2012-05-29 06:30:59 +04:00
exists <- F.isFile input
2013-07-26 12:17:05 +04:00
config <-
2012-05-29 06:30:59 +04:00
if exists
2013-07-26 12:17:05 +04:00
then do
eres <- decodeFileRelative input
case eres of
Left e -> throwIO $ InvalidKeterConfigFile input e
Right x -> return x
2013-07-14 16:30:55 +04:00
else return def { kconfigDir = input }
2013-07-26 12:17:05 +04:00
f config
2012-05-29 06:30:59 +04:00
2013-07-26 12:17:05 +04:00
withRunner :: FilePath
-> (KeterConfig -> (forall a. KIO a -> IO a) -> IO b)
-> IO b
withRunner fp f = withConfig fp $ \config -> do
2013-07-26 11:27:06 +04:00
mainlog <- LogFile.openRotatingLog
2013-07-26 12:17:05 +04:00
(F.encodeString $ (kconfigDir config) </> "log" </> "keter")
LogFile.defaultMaxTotal
2012-05-17 08:15:25 +04:00
2013-07-26 12:17:05 +04:00
f config $ Keter.Prelude.runKIO $ \ml -> do
2012-05-17 08:15:25 +04:00
now <- getCurrentTime
let bs = encodeUtf8 $ T.concat
2012-05-17 10:32:11 +04:00
[ T.take 22 $ show now
2012-05-17 08:15:25 +04:00
, ": "
, show ml
, "\n"
]
LogFile.addChunk mainlog bs
2012-05-11 12:29:25 +04:00
2013-07-26 12:17:05 +04:00
withManagers :: FilePath
-> [FilePath -> IO Plugin]
-> (KeterConfig -> HostMan.HostManager -> AppMan.AppManager -> IO a)
-> IO a
withManagers input mkPlugins f = withRunner input $ \kc@KeterConfig {..} runKIO -> do
2013-07-26 11:27:06 +04:00
processTracker <- initProcessTracker
hostman <- HostMan.start
portpool <- PortPool.start kconfigPortPool
tf <- TempFolder.setup $ kconfigDir </> "temp"
2013-07-26 11:46:34 +04:00
plugins <- P.sequence $ map ($ kconfigDir) mkPlugins
muid <-
case kconfigSetuid of
Nothing -> return Nothing
Just t -> do
x <- try $
case Data.Text.Read.decimal t of
Right (i, "") -> getUserEntryForID i
_ -> getUserEntryForName $ T.unpack t
case x of
Left (_ :: SomeException) -> P.error $ T.unpack $ "Invalid user ID: " ++ t
Right ue -> return $ Just (T.pack $ userName ue, (userID ue, userGroupID ue))
2013-07-26 11:09:11 +04:00
let appStartConfig = AppStartConfig
{ ascTempFolder = tf
, ascSetuid = muid
, ascProcessTracker = processTracker
, ascHostManager = hostman
, ascPortPool = portpool
, ascPlugins = plugins
}
2013-07-26 12:17:05 +04:00
appMan <- AppMan.initialize (AppMan.RunKIO runKIO) appStartConfig
f kc hostman appMan
2013-07-26 11:09:11 +04:00
2013-07-26 12:17:05 +04:00
data InvalidKeterConfigFile = InvalidKeterConfigFile !FilePath !ParseException
deriving (Show, Typeable)
instance Exception InvalidKeterConfigFile
launchInitial :: KeterConfig -> AppMan.AppManager -> IO ()
launchInitial kc@KeterConfig {..} appMan = do
2012-05-17 12:47:54 +04:00
createTree incoming
2013-05-29 10:43:52 +04:00
bundles0 <- fmap (filter isKeter) $ listDirectory incoming
2013-07-26 12:17:05 +04:00
mapM_ (AppMan.addApp appMan) bundles0
2012-05-11 12:29:25 +04:00
2013-07-25 18:35:16 +04:00
unless (V.null kconfigBuiltinStanzas) $ AppMan.perform
appMan
AppMan.AIBuiltin
(AppMan.Reload $ AppMan.AIData $ BundleConfig kconfigBuiltinStanzas mempty)
2013-07-26 12:17:05 +04:00
where
incoming = getIncoming kc
getIncoming :: KeterConfig -> FilePath
getIncoming kc = kconfigDir kc </> "incoming"
2013-07-10 14:26:37 +04:00
2013-07-26 12:17:05 +04:00
isKeter :: FilePath -> Bool
isKeter fp = hasExtension fp "keter"
startWatching :: KeterConfig -> AppMan.AppManager -> IO ()
startWatching kc@KeterConfig {..} appMan = do
2013-06-03 14:34:47 +04:00
-- File system watching
wm <- FSN.startManager
FSN.watchDir wm incoming (P.const True) $ \e ->
let e' =
case e of
FSN.Removed fp _ -> Left fp
FSN.Added fp _ -> Right fp
FSN.Modified fp _ -> Right fp
in case e' of
2013-07-26 12:17:05 +04:00
Left fp -> when (isKeter fp) $ AppMan.terminateApp appMan $ getAppname fp
Right fp -> when (isKeter fp) $ AppMan.addApp appMan $ incoming </> fp
2012-05-11 12:29:25 +04:00
2013-05-29 10:43:52 +04:00
-- Install HUP handler for cases when inotify cannot be used.
2013-07-26 12:17:05 +04:00
void $ flip (installHandler sigHUP) Nothing $ Catch $ do
2013-07-26 11:09:11 +04:00
bundles <- fmap (filter isKeter) $ F.listDirectory incoming
newMap <- fmap Map.fromList $ forM bundles $ \bundle -> do
time <- modificationTime <$> getFileStatus (F.encodeString bundle)
return (getAppname bundle, (bundle, time))
AppMan.reloadAppList appMan newMap
2013-07-26 12:17:05 +04:00
where
incoming = getIncoming kc
2013-05-29 10:43:52 +04:00
2013-07-26 12:17:05 +04:00
startListening :: KeterConfig -> HostMan.HostManager -> IO ()
startListening KeterConfig {..} hostman = do
manager <- HTTP.newManager def
2013-07-26 11:27:06 +04:00
runAndBlock kconfigListeners $ Proxy.reverseProxy
kconfigIpFromHeader
manager
2013-07-26 12:17:05 +04:00
(HostMan.lookupAction hostman)
2013-07-26 11:27:06 +04:00
runAndBlock :: NonEmptyVector a
-> (a -> P.IO ())
-> P.IO ()
runAndBlock (NonEmptyVector x0 v) f =
loop l0 []
where
l0 = x0 : V.toList v
loop (x:xs) asyncs = withAsync (f x) $ \async -> loop xs $ async : asyncs
-- Once we have all of our asyncs, we wait for /any/ of them to exit. If
-- any listener thread exits, we kill the whole process.
loop [] asyncs = void $ waitAny asyncs