keter/Keter/Main.hs

243 lines
9.9 KiB
Haskell
Raw Normal View History

2012-05-14 12:18:09 +04:00
{-# LANGUAGE OverloadedStrings #-}
2013-06-03 15:15:13 +04:00
{-# LANGUAGE ViewPatterns #-}
2012-05-15 12:38:54 +04:00
{-# LANGUAGE NoImplicitPrelude #-}
2012-05-17 10:32:11 +04:00
{-# LANGUAGE TemplateHaskell #-}
2012-05-29 06:30:59 +04:00
{-# LANGUAGE RecordWildCards #-}
2012-10-24 18:31:18 +04:00
{-# LANGUAGE ScopedTypeVariables #-}
2012-05-11 12:29:25 +04:00
module Keter.Main
( keter
) where
2012-05-17 11:59:17 +04:00
import Keter.Prelude hiding (getCurrentTime)
2012-05-11 12:29:25 +04:00
import qualified Keter.TempFolder as TempFolder
import qualified Keter.App as App
import qualified Keter.ProcessTracker as ProcessTracker
2012-05-11 12:42:56 +04:00
import qualified Keter.Postgres as Postgres
2012-05-17 08:15:25 +04:00
import qualified Keter.LogFile as LogFile
2012-05-17 10:32:11 +04:00
import qualified Keter.Logger as Logger
import qualified Keter.PortManager as PortMan
import qualified Keter.Proxy as Proxy
import qualified Keter.ReverseProxy as ReverseProxy
2013-05-29 10:43:52 +04:00
import System.Posix.Files (modificationTime, getFileStatus)
import System.Posix.Signals (sigHUP, installHandler, Handler (Catch))
2012-05-11 12:29:25 +04:00
2013-06-04 10:42:54 +04:00
import Data.Conduit.Network (HostPreference)
2012-05-11 12:29:25 +04:00
import qualified Control.Concurrent.MVar as M
import Control.Concurrent (forkIO)
2012-05-11 12:29:25 +04:00
import qualified Data.Map as Map
2013-06-03 14:34:47 +04:00
import qualified System.FSNotify as FSN
2013-05-29 10:43:52 +04:00
import Control.Monad (forever, mzero, forM)
2012-05-14 11:15:50 +04:00
import qualified Filesystem.Path.CurrentOS as F
2012-05-29 06:30:59 +04:00
import qualified Filesystem as F
2012-10-24 18:31:18 +04:00
import Control.Exception (throwIO, try)
2012-05-15 12:38:54 +04:00
import qualified Prelude as P
2012-05-17 08:15:25 +04:00
import Data.Text.Encoding (encodeUtf8)
import Data.Time (getCurrentTime)
import qualified Data.Text as T
2013-05-29 10:43:52 +04:00
import Data.Maybe (fromMaybe, catMaybes)
2012-05-29 06:30:59 +04:00
import Data.Yaml (decodeFile, FromJSON (parseJSON), Value (Object), (.:), (.:?), (.!=))
import Control.Applicative ((<$>), (<*>))
import Data.String (fromString)
2012-10-24 18:31:18 +04:00
import System.Posix.User (userID, userGroupID, getUserEntryForName, getUserEntryForID, userName)
import qualified Data.Text.Read
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Network.HTTP.Conduit as HTTP (newManager)
2013-06-03 15:15:13 +04:00
import qualified Network.Wai.Handler.Warp as Warp
2012-05-11 12:29:25 +04:00
2012-05-29 06:30:59 +04:00
data Config = Config
{ configDir :: F.FilePath
, configPortMan :: PortMan.Settings
, configHost :: HostPreference
, configPort :: PortMan.Port
, configSsl :: Maybe Proxy.TLSConfigNoDir
2012-10-24 18:31:18 +04:00
, configSetuid :: Maybe Text
, configReverseProxy :: Set ReverseProxy.ReverseProxyConfig
2013-06-03 15:45:52 +04:00
, configIpFromHeader :: Bool
2012-05-29 06:30:59 +04:00
}
instance Default Config where
def = Config
{ configDir = "."
, configPortMan = def
, configHost = "*"
, configPort = 80
2012-08-09 19:12:32 +04:00
, configSsl = Nothing
2012-10-24 18:31:18 +04:00
, configSetuid = Nothing
, configReverseProxy = Set.empty
2013-06-03 15:45:52 +04:00
, configIpFromHeader = False
}
2012-05-29 06:30:59 +04:00
instance FromJSON Config where
parseJSON (Object o) = Config
<$> (F.fromText <$> o .: "root")
<*> o .:? "port-manager" .!= def
<*> (fmap fromString <$> o .:? "host") .!= configHost def
<*> o .:? "port" .!= configPort def
2012-08-09 19:12:32 +04:00
<*> o .:? "ssl"
2012-10-24 18:31:18 +04:00
<*> o .:? "setuid"
<*> o .:? "reverse-proxy" .!= Set.empty
2013-06-03 15:45:52 +04:00
<*> o .:? "ip-from-header" .!= False
2012-05-29 06:30:59 +04:00
parseJSON _ = mzero
keter :: P.FilePath -- ^ root directory or config file
2012-05-15 12:38:54 +04:00
-> P.IO ()
2012-05-29 06:30:59 +04:00
keter input' = do
exists <- F.isFile input
Config{..} <-
if exists
then decodeFile input' >>= maybe (P.error "Invalid config file") return
else return def { configDir = input }
2012-05-29 06:30:59 +04:00
let dir = F.directory input F.</> configDir
2012-10-24 18:31:18 +04:00
muid <-
case configSetuid 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))
processTracker <- ProcessTracker.initProcessTracker
portman <- runThrow $ PortMan.start configPortMan
2012-05-15 12:38:54 +04:00
tf <- runThrow $ TempFolder.setup $ dir </> "temp"
postgres <- runThrow $ Postgres.load def $ dir </> "etc" </> "postgres.yaml"
2012-05-17 08:15:25 +04:00
mainlog <- runThrow $ LogFile.start $ dir </> "log" </> "keter"
let runKIO' = runKIO $ \ml -> do
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"
]
runKIOPrint $ LogFile.addChunk mainlog bs
runKIOPrint = runKIO P.print
2012-05-11 12:29:25 +04:00
2013-06-03 15:15:13 +04:00
manager <- HTTP.newManager def
_ <- forkIO $ Proxy.reverseProxy
2013-06-03 15:45:52 +04:00
configIpFromHeader
2013-06-03 15:15:13 +04:00
manager
Warp.defaultSettings
{ Warp.settingsPort = configPort
, Warp.settingsHost = configHost
}
(runKIOPrint . PortMan.lookupPort portman)
2012-08-09 19:12:32 +04:00
case configSsl of
Nothing -> return ()
2013-06-03 15:15:13 +04:00
Just (Proxy.setDir dir -> (s, ts)) -> do
2012-08-09 19:12:32 +04:00
_ <- forkIO $ Proxy.reverseProxySsl
2013-06-03 15:45:52 +04:00
configIpFromHeader
2013-06-03 15:15:13 +04:00
manager
ts
s
2012-08-09 19:12:32 +04:00
(runKIOPrint . PortMan.lookupPort portman)
return ()
2012-05-11 12:29:25 +04:00
mappMap <- M.newMVar Map.empty
2012-05-15 12:19:03 +04:00
let removeApp appname = Keter.Prelude.modifyMVar_ mappMap $ return . Map.delete appname
2012-05-11 12:29:25 +04:00
addApp bundle = do
let appname = getAppname bundle
2012-05-15 12:38:54 +04:00
rest <- modifyMVar mappMap $ \appMap ->
2012-05-11 12:29:25 +04:00
case Map.lookup appname appMap of
2013-05-29 10:43:52 +04:00
Just (app, _time) -> do
2012-05-15 12:38:54 +04:00
App.reload app
2013-05-29 10:43:52 +04:00
etime <- liftIO $ modificationTime <$> getFileStatus (F.encodeString bundle)
let time = either (P.const 0) id etime
return (Map.insert appname (app, time) appMap, return ())
2012-05-11 12:29:25 +04:00
Nothing -> do
2012-05-17 10:32:11 +04:00
mlogger <- do
let dirout = dir </> "log" </> fromText ("app-" ++ appname)
direrr = dirout </> "err"
elfout <- LogFile.start dirout
case elfout of
Left e -> do
$logEx e
return Nothing
Right lfout -> do
elferr <- LogFile.start direrr
case elferr of
Left e -> do
$logEx e
LogFile.close lfout
return Nothing
Right lferr -> fmap Just $ Logger.start lfout lferr
let logger = fromMaybe Logger.dummy mlogger
2012-05-15 12:38:54 +04:00
(app, rest) <- App.start
2012-05-14 11:15:50 +04:00
tf
2012-10-24 18:31:18 +04:00
muid
processTracker
portman
2012-05-14 11:15:50 +04:00
postgres
2012-05-17 10:32:11 +04:00
logger
2012-05-14 11:15:50 +04:00
appname
2012-05-15 12:38:54 +04:00
bundle
2012-05-14 11:15:50 +04:00
(removeApp appname)
2013-05-29 10:43:52 +04:00
etime <- liftIO $ modificationTime <$> getFileStatus (F.encodeString bundle)
let time = either (P.const 0) id etime
let appMap' = Map.insert appname (app, time) appMap
2012-05-11 12:29:25 +04:00
return (appMap', rest)
2012-05-15 12:38:54 +04:00
rest
2012-05-11 12:29:25 +04:00
terminateApp appname = do
2013-05-29 10:43:52 +04:00
-- FIXME why not remove it from the map?
2012-05-11 12:29:25 +04:00
appMap <- M.readMVar mappMap
case Map.lookup appname appMap of
Nothing -> return ()
2013-05-29 10:43:52 +04:00
Just (app, _) -> runKIO' $ App.terminate app
2012-05-11 12:29:25 +04:00
let incoming = dir </> "incoming"
2012-05-15 12:38:54 +04:00
isKeter fp = hasExtension fp "keter"
2012-05-17 12:47:54 +04:00
createTree incoming
2013-05-29 10:43:52 +04:00
bundles0 <- fmap (filter isKeter) $ listDirectory incoming
runKIO' $ mapM_ addApp bundles0
2012-05-11 12:29:25 +04:00
let staticReverse r = do
2013-06-03 15:15:13 +04:00
PortMan.addEntry portman (ReverseProxy.reversingHost r)
$ PortMan.PEReverseProxy
$ ReverseProxy.RPEntry r manager
runKIO' $ mapM_ staticReverse (Set.toList configReverseProxy)
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
Left fp -> when (isKeter fp) $ terminateApp $ getAppname fp
Right fp -> when (isKeter fp) $ runKIO' $ addApp $ 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.
_ <- flip (installHandler sigHUP) Nothing $ Catch $ do
actions <- M.withMVar mappMap $ \appMap -> do
bundles <- fmap (filter isKeter) $ F.listDirectory incoming
newMap <- fmap Map.fromList $ forM bundles $ \bundle -> do
time <- modificationTime <$> getFileStatus (F.encodeString bundle)
return (getAppname' $ F.encodeString bundle, (bundle, time))
let apps = Set.toList $ Set.fromList (Map.keys newMap)
`Set.union` Set.fromList (Map.keys appMap)
fmap catMaybes $ forM apps $ \appname -> return $
case (Map.lookup appname appMap, Map.lookup appname newMap) of
(Nothing, Nothing) -> Nothing -- should never happen
(Just _, Nothing) -> Just $ terminateApp appname
(Nothing, Just (bundle, _)) -> Just $ runKIO' $ addApp bundle
(Just (_, oldTime), Just (bundle, newTime))
| newTime /= oldTime -> Just $ runKIO' $ addApp bundle
| otherwise -> Nothing
P.sequence_ actions
2012-05-15 12:38:54 +04:00
runKIO' $ forever $ threadDelay $ 60 * 1000 * 1000
2012-05-11 12:29:25 +04:00
where
2012-05-15 12:38:54 +04:00
getAppname = either id id . toText . basename
getAppname' = getAppname . F.decodeString
2012-05-17 08:15:25 +04:00
runThrow f = runKIO P.print f >>= either throwIO return
2012-05-29 06:30:59 +04:00
input = F.decodeString input'