2012-05-11 08:38:05 +04:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
module Keter.App
|
|
|
|
( App
|
|
|
|
, start
|
|
|
|
, reload
|
|
|
|
, Keter.App.terminate
|
|
|
|
) where
|
|
|
|
|
|
|
|
import Keter.TempFolder
|
|
|
|
import Keter.Postgres
|
|
|
|
import Keter.Process
|
|
|
|
import Keter.Nginx hiding (start)
|
2012-05-14 11:15:50 +04:00
|
|
|
import qualified Keter.Prelude
|
2012-05-11 08:38:05 +04:00
|
|
|
import qualified Codec.Archive.Tar as Tar
|
|
|
|
import Codec.Compression.GZip (decompress)
|
2012-05-14 11:15:50 +04:00
|
|
|
import qualified Filesystem.Path.CurrentOS as F
|
2012-05-11 08:38:05 +04:00
|
|
|
import Data.Yaml
|
|
|
|
import Control.Applicative ((<$>), (<*>))
|
|
|
|
import System.PosixCompat.Files
|
|
|
|
import qualified Control.Concurrent.Chan as C
|
|
|
|
import Control.Concurrent (forkIO, threadDelay)
|
|
|
|
import System.Timeout (timeout)
|
|
|
|
import qualified Network
|
|
|
|
import Data.Maybe (fromMaybe)
|
2012-05-14 12:03:57 +04:00
|
|
|
import Control.Exception (try, SomeException, onException, throwIO)
|
2012-05-11 08:38:05 +04:00
|
|
|
import System.IO (hClose)
|
|
|
|
import System.Directory (removeDirectoryRecursive)
|
|
|
|
import Control.Monad (when)
|
2012-05-14 11:15:50 +04:00
|
|
|
import Data.Text (Text, pack)
|
2012-05-11 08:38:05 +04:00
|
|
|
|
|
|
|
data Config = Config
|
2012-05-14 11:15:50 +04:00
|
|
|
{ configExec :: F.FilePath
|
|
|
|
, configArgs :: [Text]
|
2012-05-11 08:38:05 +04:00
|
|
|
, configHost :: String
|
2012-05-11 12:42:56 +04:00
|
|
|
, configPostgres :: Bool
|
2012-05-11 08:38:05 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON Config where
|
|
|
|
parseJSON (Object o) = Config
|
2012-05-14 11:15:50 +04:00
|
|
|
<$> (F.fromText <$> o .: "exec")
|
2012-05-11 08:38:05 +04:00
|
|
|
<*> o .:? "args" .!= []
|
|
|
|
<*> o .: "host"
|
2012-05-11 12:42:56 +04:00
|
|
|
<*> o .:? "postgres" .!= False
|
2012-05-11 08:38:05 +04:00
|
|
|
parseJSON _ = fail "Wanted an object"
|
|
|
|
|
|
|
|
data Command = Reload | Terminate
|
|
|
|
newtype App = App (Command -> IO ())
|
|
|
|
|
|
|
|
tryM :: IO a -> IO (Maybe a)
|
|
|
|
tryM f = do
|
|
|
|
res <- try f
|
|
|
|
case res of
|
|
|
|
Left (e :: SomeException) -> do
|
|
|
|
putStrLn $ "Exception received: " ++ show e
|
|
|
|
return Nothing
|
|
|
|
Right x -> return $ Just x
|
|
|
|
|
|
|
|
unpackBundle :: TempFolder
|
2012-05-14 11:15:50 +04:00
|
|
|
-> F.FilePath
|
2012-05-11 08:38:05 +04:00
|
|
|
-> Appname
|
|
|
|
-> IO (Maybe (FilePath, Config))
|
|
|
|
unpackBundle tf bundle appname = tryM $ do
|
2012-05-14 12:03:57 +04:00
|
|
|
elbs <- runKIO $ Keter.Prelude.readFileLBS bundle
|
|
|
|
lbs <- either throwIO return elbs
|
2012-05-14 12:18:09 +04:00
|
|
|
edir <- runKIO $ getFolder tf appname
|
|
|
|
dir <- either throwIO return edir
|
|
|
|
putStrLn $ "Unpacking bundle to: " ++ show dir
|
2012-05-11 08:38:05 +04:00
|
|
|
let rest = do
|
2012-05-14 12:18:09 +04:00
|
|
|
Tar.unpack (F.encodeString dir) $ Tar.read $ decompress lbs
|
|
|
|
let configFP = dir F.</> "config" F.</> "keter.yaml"
|
2012-05-14 11:15:50 +04:00
|
|
|
Just config <- decodeFile $ F.encodeString configFP
|
2012-05-14 12:18:09 +04:00
|
|
|
return (F.encodeString dir, config)
|
|
|
|
rest `onException` Keter.Prelude.removeTree dir
|
2012-05-11 08:38:05 +04:00
|
|
|
|
|
|
|
start :: TempFolder
|
|
|
|
-> Nginx
|
2012-05-11 12:42:56 +04:00
|
|
|
-> Postgres
|
2012-05-11 12:29:25 +04:00
|
|
|
-> Appname
|
2012-05-14 11:15:50 +04:00
|
|
|
-> F.FilePath -- ^ app bundle
|
2012-05-11 08:38:05 +04:00
|
|
|
-> IO () -- ^ action to perform to remove this App from list of actives
|
|
|
|
-> IO (App, IO ())
|
2012-05-11 12:42:56 +04:00
|
|
|
start tf nginx postgres appname bundle removeFromList = do
|
2012-05-11 08:38:05 +04:00
|
|
|
chan <- C.newChan
|
|
|
|
return (App $ C.writeChan chan, rest chan)
|
|
|
|
where
|
|
|
|
void f = f >> return ()
|
|
|
|
|
|
|
|
runApp port dir config = do
|
2012-05-14 11:15:50 +04:00
|
|
|
setFileMode (F.encodeString $ F.decodeString dir F.</> "config" F.</> configExec config) ownerExecuteMode
|
2012-05-14 13:26:20 +04:00
|
|
|
otherEnv <- do
|
|
|
|
mdbi <-
|
|
|
|
if configPostgres config
|
|
|
|
then do
|
|
|
|
edbi <- Keter.Prelude.runKIO print $ getInfo postgres appname
|
|
|
|
case edbi of
|
|
|
|
Left e -> do
|
|
|
|
Keter.Prelude.runKIO print $ Keter.Prelude.log $ Keter.Prelude.ExceptionThrown e
|
|
|
|
return Nothing
|
|
|
|
Right dbi -> return $ Just dbi
|
|
|
|
else return Nothing
|
|
|
|
return $ case mdbi of
|
|
|
|
Just dbi ->
|
|
|
|
[ ("PGHOST", "localhost")
|
|
|
|
, ("PGPORT", "5432")
|
|
|
|
, ("PGUSER", dbiUser dbi)
|
|
|
|
, ("PGPASS", dbiPass dbi)
|
|
|
|
, ("PGDATABASE", dbiName dbi)
|
|
|
|
]
|
|
|
|
Nothing -> []
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO $ run
|
2012-05-11 08:38:05 +04:00
|
|
|
("config" F.</> configExec config)
|
2012-05-14 11:15:50 +04:00
|
|
|
(F.decodeString dir)
|
2012-05-11 08:38:05 +04:00
|
|
|
(configArgs config)
|
2012-05-14 11:15:50 +04:00
|
|
|
$ ("PORT", pack $ show port)
|
|
|
|
: ("APPROOT", pack $ "http://" ++ configHost config)
|
2012-05-11 12:42:56 +04:00
|
|
|
: otherEnv
|
2012-05-11 08:38:05 +04:00
|
|
|
|
|
|
|
rest chan = void $ forkIO $ do
|
|
|
|
mres <- unpackBundle tf bundle appname
|
|
|
|
case mres of
|
|
|
|
Nothing -> removeFromList
|
|
|
|
Just (dir, config) -> do
|
|
|
|
port <- getPort nginx
|
|
|
|
process <- runApp port dir config
|
|
|
|
b <- testApp port
|
|
|
|
if b
|
|
|
|
then do
|
|
|
|
addEntry nginx (configHost config) $ AppEntry port
|
|
|
|
loop chan dir process port config
|
|
|
|
else do
|
|
|
|
removeFromList
|
|
|
|
releasePort nginx port
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO $ Keter.Process.terminate process
|
2012-05-11 08:38:05 +04:00
|
|
|
|
|
|
|
loop chan dirOld processOld portOld configOld = do
|
|
|
|
command <- C.readChan chan
|
|
|
|
case command of
|
|
|
|
Terminate -> do
|
|
|
|
removeFromList
|
|
|
|
removeEntry nginx $ configHost configOld
|
2012-05-11 12:29:25 +04:00
|
|
|
putStrLn $ "Received terminate signal for app: " ++ show appname
|
2012-05-11 08:38:05 +04:00
|
|
|
terminateOld
|
|
|
|
Reload -> do
|
|
|
|
mres <- unpackBundle tf bundle appname
|
|
|
|
case mres of
|
|
|
|
Nothing -> do
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO $ Keter.Prelude.log $ Keter.Prelude.InvalidBundle bundle
|
2012-05-11 08:38:05 +04:00
|
|
|
loop chan dirOld processOld portOld configOld
|
|
|
|
Just (dir, config) -> do
|
|
|
|
port <- getPort nginx
|
|
|
|
process <- runApp port dir config
|
|
|
|
b <- testApp port
|
|
|
|
if b
|
|
|
|
then do
|
|
|
|
addEntry nginx (configHost config) $ AppEntry port
|
|
|
|
when (configHost config /= configHost configOld) $
|
|
|
|
removeEntry nginx $ configHost configOld
|
2012-05-11 12:29:25 +04:00
|
|
|
putStrLn $ "Finished reloading: " ++ show appname
|
2012-05-11 08:38:05 +04:00
|
|
|
terminateOld
|
|
|
|
loop chan dir process port config
|
|
|
|
else do
|
|
|
|
releasePort nginx port
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO $ Keter.Process.terminate process
|
|
|
|
runKIO $ Keter.Prelude.log $ Keter.Prelude.ProcessDidNotStart bundle
|
2012-05-11 08:38:05 +04:00
|
|
|
loop chan dirOld processOld portOld configOld
|
|
|
|
where
|
|
|
|
terminateOld = void $ forkIO $ do
|
|
|
|
threadDelay $ 20 * 1000 * 1000
|
|
|
|
putStrLn $ "Terminating old process for: " ++ show appname
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO $ Keter.Process.terminate processOld
|
2012-05-11 08:38:05 +04:00
|
|
|
threadDelay $ 60 * 1000 * 1000
|
|
|
|
putStrLn $ "Removing folder: " ++ dirOld
|
|
|
|
removeDirectoryRecursive dirOld
|
|
|
|
|
2012-05-14 12:03:57 +04:00
|
|
|
runKIO :: Keter.Prelude.KIO a -> IO a -- FIXME remove this
|
|
|
|
runKIO = Keter.Prelude.runKIO print
|
|
|
|
|
2012-05-11 08:38:05 +04:00
|
|
|
testApp :: Port -> IO Bool
|
|
|
|
testApp port = do
|
|
|
|
putStrLn $ "Testing app on port: " ++ show port
|
|
|
|
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 (e :: SomeException) -> do
|
|
|
|
putStrLn $ "Connection failed: " ++ show e
|
|
|
|
testApp'
|
|
|
|
Right handle -> do
|
|
|
|
putStrLn $ "App is running on port: " ++ show port
|
|
|
|
hClose handle
|
|
|
|
return True
|
|
|
|
|
|
|
|
reload :: App -> IO ()
|
|
|
|
reload (App f) = f Reload
|
|
|
|
|
|
|
|
terminate :: App -> IO ()
|
|
|
|
terminate (App f) = f Terminate
|