keter/Keter/App.hs

463 lines
19 KiB
Haskell
Raw Normal View History

2013-07-28 16:19:08 +04:00
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
2012-05-11 08:38:05 +04:00
{-# LANGUAGE ScopedTypeVariables #-}
2013-07-28 16:19:08 +04:00
{-# LANGUAGE TemplateHaskell #-}
2012-05-11 08:38:05 +04:00
module Keter.App
( App
2013-07-26 11:09:11 +04:00
, AppStartConfig (..)
2012-05-11 08:38:05 +04:00
, start
, reload
2013-07-26 11:09:11 +04:00
, getTimestamp
2012-05-11 08:38:05 +04:00
, Keter.App.terminate
) where
2013-07-28 16:19:08 +04:00
import Codec.Archive.TempTarball
import Control.Applicative ((<$>))
import Control.Arrow ((***))
import Control.Concurrent.STM
import Control.Exception (bracketOnError, throwIO)
import qualified Data.Conduit.LogFile as LogFile
import Data.Conduit.Process.Unix (MonitoredProcess, ProcessTracker,
RotatingLog, monitorProcess,
terminateMonitoredProcess)
import qualified Data.Map as Map
import Data.Monoid ((<>))
import qualified Data.Set as Set
import Data.Text (pack)
import Data.Text.Encoding (decodeUtf8With, encodeUtf8)
import Data.Text.Encoding.Error (lenientDecode)
import qualified Data.Vector as V
import Data.Yaml
import Data.Yaml.FilePath
2013-07-28 20:34:15 +04:00
import Filesystem (removeTree, isFile, canonicalizePath)
2012-05-14 11:15:50 +04:00
import qualified Filesystem.Path.CurrentOS as F
2013-07-28 16:19:08 +04:00
import Keter.HostManager hiding (start)
import Keter.PortPool (PortPool, getPort, releasePort)
import Keter.Types
import Prelude hiding (FilePath)
import System.Posix.Types (EpochTime)
import System.Posix.Types (GroupID, UserID)
2013-07-28 20:34:15 +04:00
import System.Posix.Files (fileAccess)
import System.IO (hClose)
import qualified Network
import Control.Exception (try, IOException)
import Data.Maybe (fromMaybe)
import Control.Concurrent (threadDelay)
import System.Timeout (timeout)
2012-10-21 09:07:26 +04:00
2013-07-28 16:19:08 +04:00
data App = App
{ appModTime :: !(TVar (Maybe EpochTime))
, appRunningWebApps :: ![RunningWebApp]
}
data RunningWebApp = RunningWebApp
{ rwaProcess :: !MonitoredProcess
2013-07-28 20:34:15 +04:00
, rwaPort :: !Port
2013-07-28 16:19:08 +04:00
}
2012-05-11 08:38:05 +04:00
2013-07-28 16:19:08 +04:00
unpackBundle :: AppStartConfig
2013-07-28 14:41:42 +04:00
-> FilePath
2013-07-28 16:19:08 +04:00
-> AppId
2013-07-28 14:41:42 +04:00
-> IO (FilePath, BundleConfig)
2013-07-28 16:19:08 +04:00
unpackBundle AppStartConfig {..} bundle aid = do
ascLog $ UnpackingBundle bundle
unpackTempTar (fmap snd ascSetuid) ascTempFolder bundle folderName $ \dir -> do
let configFP = dir F.</> "config" F.</> "keter.yaml"
2013-07-14 14:02:18 +04:00
mconfig <- decodeFileRelative configFP
config <-
case mconfig of
2013-07-14 14:02:18 +04:00
Right config -> return config
Left e -> throwIO $ InvalidConfigFile e
2013-07-25 15:10:09 +04:00
return (dir, config)
2013-07-28 16:19:08 +04:00
where
folderName =
case aid of
AIBuiltin -> "__builtin__"
AINamed x -> x
2012-10-12 14:59:46 +04:00
2013-07-26 11:09:11 +04:00
data AppStartConfig = AppStartConfig
2013-07-28 16:19:08 +04:00
{ ascTempFolder :: !TempFolder
, ascSetuid :: !(Maybe (Text, (UserID, GroupID)))
2013-07-26 11:09:11 +04:00
, ascProcessTracker :: !ProcessTracker
2013-07-28 16:19:08 +04:00
, ascHostManager :: !HostManager
, ascPortPool :: !PortPool
, ascPlugins :: !Plugins
, ascLog :: !(LogMessage -> IO ())
, ascKeterConfig :: !KeterConfig
2013-07-26 11:09:11 +04:00
}
2013-07-28 16:19:08 +04:00
withConfig :: AppStartConfig
-> AppId
-> AppInput
-> (FilePath -> BundleConfig -> Maybe EpochTime -> IO a)
-> IO a
withConfig _asc _aid (AIData bconfig) f = f "/tmp" bconfig Nothing
withConfig asc aid (AIBundle fp modtime) f = bracketOnError
(unpackBundle asc fp aid)
(\(newdir, _) -> removeTree newdir)
$ \(newdir, bconfig) -> f newdir bconfig (Just modtime)
2013-07-26 11:09:11 +04:00
2013-07-28 16:19:08 +04:00
withReservations :: AppStartConfig
-> AppId
-> BundleConfig
-> ([WebAppConfig Port] -> Map Host ProxyAction -> IO a)
-> IO a
withReservations asc aid bconfig f = withActions asc bconfig $ \wacs actions -> bracketOnError
(reserveHosts (ascHostManager asc) aid $ Map.keysSet actions)
(forgetReservations (ascHostManager asc) aid)
(const $ f wacs actions)
withActions :: AppStartConfig
-> BundleConfig
-> ([WebAppConfig Port] -> Map Host ProxyAction -> IO a)
-> IO a
withActions asc bconfig f =
loop (V.toList $ bconfigStanzas bconfig) [] Map.empty
where
loop [] wacs actions = f wacs actions
loop (StanzaWebApp wac:stanzas) wacs actions = bracketOnError
(getPort (ascLog asc) (ascPortPool asc) >>= either throwIO return)
(releasePort (ascPortPool asc))
(\port -> loop
stanzas
(wac { waconfigPort = port } : wacs)
(Map.unions $ actions : map (\host -> Map.singleton host $ PAPort port) hosts))
where
hosts = Set.toList $ Set.insert (waconfigApprootHost wac) (waconfigHosts wac)
loop (StanzaStaticFiles sfc:stanzas) wacs actions0 =
loop stanzas wacs actions
where
actions = Map.unions
$ actions0
: map (\host -> Map.singleton host $ PAStatic sfc)
(Set.toList (sfconfigHosts sfc))
loop (StanzaRedirect red:stanzas) wacs actions0 =
loop stanzas wacs actions
where
actions = Map.unions
$ actions0
: map (\host -> Map.singleton host $ PARedirect red)
(Set.toList (redirconfigHosts red))
loop (StanzaReverseProxy rev:stanzas) wacs actions0 =
loop stanzas wacs actions
where
actions = Map.insert (reversingHost rev) (PAReverseProxy rev) actions0
withRotatingLog :: AppStartConfig
-> AppId
-> (RotatingLog -> IO a)
-> IO a
withRotatingLog AppStartConfig {..} aid = bracketOnError
(LogFile.openRotatingLog (F.encodeString dir) LogFile.defaultMaxTotal)
LogFile.close
where
dir = kconfigDir ascKeterConfig F.</> "log" F.</> name
name =
case aid of
AIBuiltin -> "__builtin__"
AINamed x -> F.fromText $ "app-" <> x
2013-07-26 11:09:11 +04:00
2013-07-28 20:34:15 +04:00
withSanityChecks :: AppStartConfig -> BundleConfig -> IO a -> IO a
withSanityChecks AppStartConfig {..} BundleConfig {..} f = do
V.mapM_ go bconfigStanzas
ascLog SanityChecksPassed
f
where
go (StanzaWebApp WebAppConfig {..}) = do
exists <- isFile waconfigExec
if exists
then do
canExec <- fileAccess (F.encodeString waconfigExec) True False True
if canExec
then return ()
else throwIO $ FileNotExecutable waconfigExec
else throwIO $ ExecutableNotFound waconfigExec
go _ = return ()
2013-07-26 11:09:11 +04:00
start :: AppStartConfig
-> AppId
2013-07-28 16:19:08 +04:00
-> AppInput
2013-07-28 14:41:42 +04:00
-> IO App
2013-07-28 16:19:08 +04:00
start asc aid input =
withConfig asc aid input $ \newdir bconfig mmodtime ->
2013-07-28 20:34:15 +04:00
withSanityChecks asc bconfig $
2013-07-28 16:19:08 +04:00
withReservations asc aid bconfig $ \webapps actions ->
withRotatingLog asc aid $ \rlog ->
withWebApps asc aid bconfig newdir rlog webapps $ \runningWebapps -> do
mapM_ ensureAlive runningWebapps
activateApp (ascHostManager asc) aid actions
tmodtime <- newTVarIO mmodtime
return App
{ appModTime = tmodtime
, appRunningWebApps = runningWebapps
}
withWebApps :: AppStartConfig
-> AppId
-> BundleConfig
-> FilePath
-> RotatingLog
-> [WebAppConfig Port]
-> ([RunningWebApp] -> IO a)
-> IO a
withWebApps asc aid bconfig dir rlog configs0 f =
loop configs0 id
where
loop [] front = f $ front []
loop (c:cs) front = bracketOnError
(launchWebApp asc aid bconfig dir rlog c)
killWebApp
(\rwa -> loop cs (front . (rwa:)))
launchWebApp :: AppStartConfig
-> AppId
-> BundleConfig
-> FilePath
-> RotatingLog
-> WebAppConfig Port
-> IO RunningWebApp
launchWebApp AppStartConfig {..} aid BundleConfig {..} dir rlog WebAppConfig {..} = do
otherEnv <- pluginsGetEnv ascPlugins name bconfigRaw
let env = ("PORT", pack $ show waconfigPort)
: ("APPROOT", (if waconfigSsl then "https://" else "http://") <> waconfigApprootHost)
: otherEnv
2013-07-28 20:34:15 +04:00
exec <- canonicalizePath waconfigExec
2013-07-28 16:19:08 +04:00
bracketOnError
(monitorProcess
(ascLog . OtherMessage . decodeUtf8With lenientDecode)
ascProcessTracker
(encodeUtf8 . fst <$> ascSetuid)
2013-07-28 20:34:15 +04:00
(encodeUtf8 $ either id id $ F.toText exec)
2013-07-28 16:19:08 +04:00
(encodeUtf8 $ either id id $ F.toText dir)
(map encodeUtf8 $ V.toList waconfigArgs)
(map (encodeUtf8 *** encodeUtf8) env)
rlog)
terminateMonitoredProcess
$ \mp -> do
return RunningWebApp
{ rwaProcess = mp
2013-07-28 20:34:15 +04:00
, rwaPort = waconfigPort
2013-07-28 16:19:08 +04:00
}
where
name =
case aid of
AIBuiltin -> "__builtin__"
AINamed x -> x
killWebApp :: RunningWebApp -> IO ()
2013-07-28 21:02:28 +04:00
killWebApp RunningWebApp {..} = do
terminateMonitoredProcess rwaProcess
2013-07-28 16:19:08 +04:00
ensureAlive :: RunningWebApp -> IO ()
2013-07-28 20:34:15 +04:00
ensureAlive RunningWebApp {..} = do
didAnswer <- testApp rwaPort
if didAnswer
then return ()
else error "ensureAlive failed"
2013-07-28 16:19:08 +04:00
where
2013-07-28 20:34:15 +04:00
testApp :: Port -> IO Bool
testApp port = do
res <- timeout (90 * 1000 * 1000) testApp'
return $ fromMaybe False res
where
testApp' = do
threadDelay $ 2 * 1000 * 1000
eres <- try $ Network.connectTo "127.0.0.1" $ Network.PortNumber $ fromIntegral port
case eres of
Left (_ :: IOException) -> testApp'
Right handle -> do
hClose handle
return True
2013-07-26 11:09:11 +04:00
{-
2012-05-11 08:38:05 +04:00
start :: TempFolder
2012-10-24 18:31:18 +04:00
-> Maybe (Text, (UserID, GroupID))
-> ProcessTracker
2013-07-25 14:07:48 +04:00
-> HostManager
2013-07-10 14:26:37 +04:00
-> Plugins
-> RotatingLog
2012-05-11 12:29:25 +04:00
-> Appname
2013-07-26 11:09:11 +04:00
-> (Maybe BundleConfig)
2012-05-15 12:19:03 +04:00
-> KIO () -- ^ action to perform to remove this App from list of actives
-> KIO (App, KIO ())
2013-07-10 14:26:37 +04:00
start tf muid processTracker portman plugins rlog appname bundle removeFromList = do
2013-07-25 15:10:09 +04:00
Prelude.error "FIXME Keter.App.start"
2012-05-15 12:19:03 +04:00
chan <- newChan
return (App $ writeChan chan, rest chan)
2012-05-11 08:38:05 +04:00
where
2012-05-15 12:19:03 +04:00
rest chan = forkKIO $ do
2012-10-24 18:31:18 +04:00
mres <- unpackBundle tf (snd <$> muid) bundle appname
2012-05-11 08:38:05 +04:00
case mres of
2012-05-15 12:19:03 +04:00
Left e -> do
2012-05-17 10:32:11 +04:00
$logEx e
2012-05-15 12:19:03 +04:00
removeFromList
Right (dir, config) -> do
2013-01-28 11:09:02 +04:00
let common = do
2013-07-14 16:28:48 +04:00
mapM_ (\StaticHost{..} -> addEntry portman shHost (PEStatic shRoot)) $ Set.toList $ bconfigStaticHosts config
mapM_ (\Redirect{..} -> addEntry portman redFrom (PERedirect $ encodeUtf8 redTo)) $ Set.toList $ bconfigRedirects config
case bconfigApp config of
2013-01-28 11:09:02 +04:00
Nothing -> do
common
loop chan dir config Nothing
Just appconfig -> do
eport <- getPort portman
case eport of
Left e -> do
$logEx e
2012-05-15 11:49:20 +04:00
removeFromList
2013-01-28 11:09:02 +04:00
Right port -> do
2013-07-14 17:28:43 +04:00
eprocess <- runApp port dir appconfig
case eprocess of
Left e -> do
$logEx e
2013-01-28 11:09:02 +04:00
removeFromList
2013-07-14 17:28:43 +04:00
Right process -> do
b <- testApp port
if b
then do
addEntry portman (aconfigHost appconfig) $ PEPort port
mapM_ (flip (addEntry portman) $ PEPort port) $ Set.toList $ aconfigExtraHosts appconfig
common
loop chan dir config $ Just (process, port)
else do
removeFromList
releasePort portman port
void $ liftIO $ terminateMonitoredProcess process
2012-05-11 08:38:05 +04:00
2013-01-28 11:09:02 +04:00
loop chan dirOld configOld mprocPortOld = do
2012-05-15 12:19:03 +04:00
command <- readChan chan
2012-05-11 08:38:05 +04:00
case command of
Terminate -> do
removeFromList
2013-07-14 16:28:48 +04:00
case bconfigApp configOld of
2013-01-28 11:09:02 +04:00
Nothing -> return ()
Just appconfig -> do
2013-07-14 16:28:48 +04:00
removeEntry portman $ aconfigHost appconfig
mapM_ (removeEntry portman) $ Set.toList $ aconfigExtraHosts appconfig
mapM_ (removeEntry portman) $ map shHost $ Set.toList $ bconfigStaticHosts configOld
mapM_ (removeEntry portman) $ map redFrom $ Set.toList $ bconfigRedirects configOld
2012-05-15 12:19:03 +04:00
log $ TerminatingApp appname
2012-05-11 08:38:05 +04:00
terminateOld
Reload -> do
2012-10-24 18:31:18 +04:00
mres <- unpackBundle tf (snd <$> muid) bundle appname
2012-05-11 08:38:05 +04:00
case mres of
2012-05-15 12:19:03 +04:00
Left e -> do
log $ InvalidBundle bundle e
2013-01-28 11:09:02 +04:00
loop chan dirOld configOld mprocPortOld
2012-05-15 12:19:03 +04:00
Right (dir, config) -> do
eport <- getPort portman
2012-05-15 11:49:20 +04:00
case eport of
2012-05-17 10:32:11 +04:00
Left e -> $logEx e
2012-05-15 11:49:20 +04:00
Right port -> do
2013-01-28 11:09:02 +04:00
let common = do
2013-07-14 16:28:48 +04:00
mapM_ (\StaticHost{..} -> addEntry portman shHost (PEStatic shRoot)) $ Set.toList $ bconfigStaticHosts config
mapM_ (\Redirect{..} -> addEntry portman redFrom (PERedirect $ encodeUtf8 redTo)) $ Set.toList $ bconfigRedirects config
case bconfigApp config of
2013-01-28 11:09:02 +04:00
Nothing -> do
common
loop chan dir config Nothing
Just appconfig -> do
2013-07-14 17:28:43 +04:00
eprocess <- runApp port dir appconfig
mprocess <-
case eprocess of
Left _ -> return Nothing
Right process -> do
b <- testApp port
return $ if b
then Just process
else Nothing
case mprocess of
Just process -> do
2013-07-14 16:28:48 +04:00
addEntry portman (aconfigHost appconfig) $ PEPort port
mapM_ (flip (addEntry portman) $ PEPort port) $ Set.toList $ aconfigExtraHosts appconfig
2013-01-28 11:09:02 +04:00
common
2013-07-14 16:28:48 +04:00
case bconfigApp configOld of
Just appconfigOld | aconfigHost appconfig /= aconfigHost appconfigOld ->
removeEntry portman $ aconfigHost appconfigOld
2013-01-28 11:09:02 +04:00
_ -> return ()
log $ FinishedReloading appname
terminateOld
loop chan dir config $ Just (process, port)
2013-07-14 17:28:43 +04:00
Nothing -> do
2013-01-28 11:09:02 +04:00
releasePort portman port
2013-07-14 17:28:43 +04:00
case eprocess of
Left _ -> return ()
Right process -> void $ liftIO $ terminateMonitoredProcess process
2013-01-28 11:09:02 +04:00
log $ ProcessDidNotStart bundle
loop chan dirOld configOld mprocPortOld
2012-05-11 08:38:05 +04:00
where
2012-05-15 12:19:03 +04:00
terminateOld = forkKIO $ do
2012-05-11 08:38:05 +04:00
threadDelay $ 20 * 1000 * 1000
2012-05-15 12:19:03 +04:00
log $ TerminatingOldProcess appname
2013-01-28 11:09:02 +04:00
case mprocPortOld of
Nothing -> return ()
2013-07-14 17:28:43 +04:00
Just (processOld, _) -> void $ liftIO $ terminateMonitoredProcess processOld
2012-05-11 08:38:05 +04:00
threadDelay $ 60 * 1000 * 1000
2012-05-15 12:19:03 +04:00
log $ RemovingOldFolder dirOld
res <- liftIO $ removeTree dirOld
case res of
2012-05-17 10:32:11 +04:00
Left e -> $logEx e
2012-05-15 12:19:03 +04:00
Right () -> return ()
2013-07-25 15:10:09 +04:00
-}
2012-05-14 12:03:57 +04:00
2013-07-28 14:41:42 +04:00
reload :: App -> AppInput -> IO ()
2013-07-28 16:19:08 +04:00
reload = error "FIXME"
2012-05-11 08:38:05 +04:00
2013-07-28 14:41:42 +04:00
terminate :: App -> IO ()
2013-07-28 16:19:08 +04:00
terminate = error "FIXME"
2013-07-10 14:26:37 +04:00
2013-07-26 11:09:11 +04:00
-- | Get the modification time of the bundle file this app was launched from,
-- if relevant.
getTimestamp :: App -> STM (Maybe EpochTime)
getTimestamp _ = return Nothing -- FIXME
2013-07-28 16:19:08 +04:00
pluginsGetEnv :: Plugins -> Appname -> Object -> IO [(Text, Text)]
pluginsGetEnv ps app o = fmap concat $ mapM (\p -> pluginGetEnv p app o) ps
2013-07-25 18:35:16 +04:00
{- FIXME handle static stanzas
let staticReverse r = do
HostMan.addEntry hostman (ReverseProxy.reversingHost r)
$ HostMan.PEReverseProxy
$ ReverseProxy.RPEntry r manager
runKIO' $ mapM_ staticReverse (Set.toList kconfigReverseProxy)
-}
2013-07-26 11:09:11 +04:00
{- FIXME
rest <-
case Map.lookup appname appMap of
Just (app, _time) -> do
App.reload app
etime <- liftIO $ modificationTime <$> getFileStatus (F.encodeString bundle)
let time = either (P.const 0) id etime
return (Map.insert appname (app, time) appMap, return ())
Nothing -> do
mlogger <- do
let dirout = kconfigDir </> "log" </> fromText ("app-" ++ appname)
direrr = dirout </> "err"
erlog <- liftIO $ LogFile.openRotatingLog
(F.encodeString dirout)
LogFile.defaultMaxTotal
case erlog of
Left e -> do
$logEx e
return Nothing
Right rlog -> return (Just rlog)
let logger = fromMaybe LogFile.dummy mlogger
(app, rest) <- App.start
tf
muid
processTracker
hostman
plugins
logger
appname
bundle
(removeApp appname)
etime <- liftIO $ modificationTime <$> getFileStatus (F.encodeString bundle)
let time = either (P.const 0) id etime
let appMap' = Map.insert appname (app, time) appMap
return (appMap', rest)
rest
-}