keter/Keter/App.hs

301 lines
13 KiB
Haskell
Raw Normal View History

2012-05-11 08:38:05 +04:00
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
2012-05-17 10:32:11 +04:00
{-# LANGUAGE TemplateHaskell #-}
2012-10-14 20:17:01 +04:00
{-# LANGUAGE RecordWildCards #-}
2012-05-11 08:38:05 +04:00
module Keter.App
( App
2013-07-26 11:09:11 +04:00
, AppStartConfig (..)
, AppId (..)
, AppInput (..)
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-26 11:09:11 +04:00
import System.Posix.Types (EpochTime)
import Control.Concurrent.STM (STM)
2013-07-10 15:15:18 +04:00
import Codec.Archive.TempTarball
2013-07-10 14:26:37 +04:00
import Keter.Types
2013-07-25 14:07:48 +04:00
import Keter.HostManager hiding (start)
2012-05-14 11:15:50 +04:00
import qualified Filesystem.Path.CurrentOS as F
2013-07-14 14:02:18 +04:00
import qualified Filesystem as F
2012-05-11 08:38:05 +04:00
import Data.Yaml
2013-07-14 16:28:48 +04:00
import Control.Applicative ((<$>))
2012-05-11 08:38:05 +04:00
import qualified Network
2013-07-14 14:02:18 +04:00
import Data.Maybe (fromMaybe)
2013-07-28 14:41:42 +04:00
import Control.Exception (throwIO, try, IOException)
2012-05-11 08:38:05 +04:00
import System.IO (hClose)
2012-10-12 14:17:00 +04:00
import qualified Data.Set as Set
2013-07-14 17:28:43 +04:00
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
2012-10-24 18:31:18 +04:00
import System.Posix.Types (UserID, GroupID)
2013-07-14 17:28:43 +04:00
import Data.Conduit.Process.Unix (ProcessTracker, RotatingLog, terminateMonitoredProcess, monitorProcess)
2013-07-14 14:02:18 +04:00
import Data.Yaml.FilePath
2013-07-26 11:09:11 +04:00
import Keter.PortPool (PortPool)
2013-07-28 14:41:42 +04:00
import Control.Concurrent (threadDelay)
import System.Timeout (timeout)
import Prelude hiding (FilePath)
2012-10-21 09:07:26 +04:00
2012-05-11 08:38:05 +04:00
data Command = Reload | Terminate
2013-07-28 14:41:42 +04:00
newtype App = App (Command -> IO ())
2012-05-11 08:38:05 +04:00
2013-07-28 14:41:42 +04:00
unpackBundle :: (LogMessage -> IO ())
-> TempFolder
2012-10-24 18:31:18 +04:00
-> Maybe (UserID, GroupID)
2013-07-28 14:41:42 +04:00
-> FilePath
2012-05-11 08:38:05 +04:00
-> Appname
2013-07-28 14:41:42 +04:00
-> IO (FilePath, BundleConfig)
unpackBundle log tf muid bundle appname = do
log $ UnpackingBundle bundle
2013-07-28 14:41:42 +04:00
unpackTempTar muid tf bundle appname $ \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)
2012-10-12 14:59:46 +04:00
2013-07-26 11:09:11 +04:00
data AppStartConfig = AppStartConfig
{ ascTempFolder :: !TempFolder
, ascSetuid :: !(Maybe (Text, (UserID, GroupID)))
, ascProcessTracker :: !ProcessTracker
, ascHostManager :: !HostManager
, ascPortPool :: !PortPool
, ascPlugins :: !Plugins
}
data AppInput = AIBundle !FilePath !EpochTime
| AIData !BundleConfig
data AppId = AIBuiltin | AINamed !Appname
deriving (Eq, Ord)
start :: AppStartConfig
-> AppId
-> AppInput -- ^ if not provided, we'll extract from the relevant file
2013-07-28 14:41:42 +04:00
-> IO App
start _ _ _ = error "Keter.App.start"
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
runApp port dir config = do
2013-07-25 15:10:09 +04:00
otherEnv <- pluginsGetEnv plugins appname (bconfigRaw config)
2012-05-17 10:32:11 +04:00
let env = ("PORT", show port)
2013-07-14 16:28:48 +04:00
: ("APPROOT", (if aconfigSsl config then "https://" else "http://") ++ aconfigHost config)
2012-05-17 10:32:11 +04:00
: otherEnv
2013-07-14 17:28:43 +04:00
log' <- getIOLogger
liftIO $ monitorProcess
(log' . decodeUtf8With lenientDecode)
processTracker
2013-07-14 17:28:43 +04:00
(encodeUtf8 . fst <$> muid)
(encodeUtf8 $ either id id $ F.toText $ aconfigExec config)
(encodeUtf8 $ either id id $ F.toText dir)
(map encodeUtf8 $ aconfigArgs config)
(map (encodeUtf8 *** encodeUtf8) env)
rlog
2012-05-11 08:38:05 +04:00
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
testApp :: Port -> IO Bool
2012-05-11 08:38:05 +04:00
testApp port = do
res <- timeout (90 * 1000 * 1000) testApp'
return $ fromMaybe False res
where
testApp' = do
threadDelay $ 2 * 1000 * 1000
2013-07-28 14:41:42 +04:00
eres <- try $ Network.connectTo "127.0.0.1" $ Network.PortNumber $ fromIntegral port
2012-05-11 08:38:05 +04:00
case eres of
2013-07-28 14:41:42 +04:00
Left (_ :: IOException) -> testApp'
2012-05-11 08:38:05 +04:00
Right handle -> do
2013-07-28 14:41:42 +04:00
hClose handle
2012-05-11 08:38:05 +04:00
return True
2013-07-28 14:41:42 +04:00
reload :: App -> AppInput -> IO ()
2013-07-26 11:09:11 +04:00
reload (App f) _fixme = f Reload
2012-05-11 08:38:05 +04:00
2013-07-28 14:41:42 +04:00
terminate :: App -> IO ()
2012-05-11 08:38:05 +04:00
terminate (App f) = f Terminate
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 14:41:42 +04:00
pluginsGetEnv :: Plugins -> Appname -> Object -> IO (Either SomeException [(Text, Text)])
pluginsGetEnv ps app o = try $ 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
-}