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
|
2013-07-10 15:09:14 +04:00
|
|
|
log $ UnpackingBundle bundle
|
2013-07-28 14:41:42 +04:00
|
|
|
unpackTempTar muid tf bundle appname $ \dir -> do
|
2013-07-10 15:09:14 +04:00
|
|
|
let configFP = dir F.</> "config" F.</> "keter.yaml"
|
2013-07-14 14:02:18 +04:00
|
|
|
mconfig <- decodeFileRelative configFP
|
2013-07-10 15:09:14 +04:00
|
|
|
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))
|
2012-11-19 12:31:35 +04:00
|
|
|
-> ProcessTracker
|
2013-07-25 14:07:48 +04:00
|
|
|
-> HostManager
|
2013-07-10 14:26:37 +04:00
|
|
|
-> Plugins
|
2013-07-10 13:48:46 +04:00
|
|
|
-> 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)
|
2012-11-19 12:31:35 +04:00
|
|
|
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)
|
2013-07-10 13:48:46 +04:00
|
|
|
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
|
2012-08-06 18:44:41 +04:00
|
|
|
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
|
|
|
|
-}
|