{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Houseman where import Control.Concurrent import Control.Monad import Control.Monad.Cont import Data.Function import Data.List import System.Directory import System.Environment import System.Exit import System.Posix.Signals import System.Process import qualified Configuration.Dotenv as Dotenv import Data.Streaming.Process (StreamingProcessHandle) import Houseman.Internal (terminateAndWaitForProcess, withAllExit, withAnyExit, withProcess) import Houseman.Logger (installLogger, newLogger, runLogger, stopLogger) import Houseman.Types import Procfile.Types -- | Runs `App` in `Procfile` with given name. run :: String -> Procfile -> IO ExitCode run cmd' apps = case find (\App{cmd} -> cmd == cmd') apps of Just app -> Houseman.start [app] -- TODO Remove color in run command Nothing -> die ("Command '" ++ cmd' ++ "' not found in Procfile") -- | Starts all `App`s in given `Procfile`. start :: Procfile -> IO ExitCode start apps = do print apps -- Allocate logger logger <- newLogger -- Run apps (`runContT` return) $ do phs <- mapM (ContT . withApp logger) apps liftIO $ do -- Get a MVar to detect termination of a process readyToTerminate <- newEmptyMVar -- Output logs to stdout runLogger logger -- Fill MVar with signal [sigINT, sigTERM, keyboardSignal] `forM_` \signal -> installHandler signal (Catch (putMVar readyToTerminate ())) Nothing -- Fill MVar with any failure _ <- forkIO $ withAnyExit (/= ExitSuccess) phs (putMVar readyToTerminate ()) -- Fill MVar with all success _ <- forkIO $ withAllExit (== ExitSuccess) phs (putMVar readyToTerminate ()) -- Wait for the termination takeMVar readyToTerminate -- Terminate all and exit mapM_ terminateAndWaitForProcess phs stopLogger logger putStrLn "bye" return ExitSuccess -- | Runs given `App` with given `Logger`, invokes action with the process -- handle of the `App`, and returns result of the action. withApp :: Logger -> App -> (StreamingProcessHandle -> IO a) -> IO a withApp logger App {name,cmd} action = do -- Build environment variables to run app. -- .env supersedes environment from current process. envs <- nubBy ((==) `on` fst) . mconcat <$> sequence [getEnvsInDotEnvFile, getEnvironment] withProcess (shell cmd) { env = Just envs } $ \(out,err,ph) -> do _ <- forkIO $ forM_ [out,err] (installLogger name logger) action ph where getEnvsInDotEnvFile :: IO [Env] getEnvsInDotEnvFile = do d <- doesFileExist ".env" if d then Dotenv.parseFile ".env" else return []