packages feed

keter-2.3.0: src/Keter/App.hs

{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

module Keter.App
    ( start
    , reload
    , getTimestamp
    , Keter.App.terminate
    ) where

import Control.Arrow ((***))
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Exception
       (IOException, SomeException, bracketOnError, catch, throwIO, try)
import Control.Monad (void, when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.IO.Unlift (withRunInIO)
import Control.Monad.Logger
import Control.Monad.Reader (ask)
import Data.CaseInsensitive qualified as CI
import Data.Foldable (for_)
import Data.IORef
import Data.Map (Map)
import Data.Map qualified as Map
import Data.Maybe (fromMaybe, isJust)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (encodeUtf8)
import Data.Vector qualified as V
import Data.Yaml
import Keter.Common
import Keter.Conduit.Process.Unix
       ( monitorProcess
       , printStatus
       , terminateMonitoredProcess
       )
import Keter.Config
import Keter.Context
import Keter.HostManager hiding (start)
import Keter.Logger (Logger)
import Keter.Logger qualified as Log
import Keter.PortPool (getPort, releasePort)
import Keter.Rewrite (ReverseProxyConfig(..))
import Keter.SharedData.App
import Keter.SharedData.AppManager (AppState(..))
import Keter.TempTarball
import Keter.Yaml.FilePath
import Network.Socket
import Network.TLS qualified as TLS
import Prelude hiding (FilePath)
import System.Directory
       (canonicalizePath, doesFileExist, removeDirectoryRecursive)
import System.Environment (getEnvironment)
import System.FilePath (FilePath, (</>))
import System.IO (IOMode(..), hClose)
import System.Log.FastLogger qualified as FL
import System.Posix.Files (fileAccess)
import System.Posix.Types (EpochTime)
import System.Timeout (timeout)

import Keter.Config.Middleware (processMiddlewareIO, MiddlewareConfig)
import Network.Wai (Middleware)

unpackBundle :: FilePath
             -> AppId
             -> KeterM AppStartConfig (FilePath, BundleConfig)
unpackBundle bundle aid = do
    AppStartConfig{..} <- ask
    $logInfo $ pack $ "Unpacking bundle '" <> show bundle <> "'"
    liftIO $ unpackTempTar (fmap snd ascSetuid) ascTempFolder bundle folderName $ \dir -> do
        -- Get the FilePath for the keter yaml configuration. Tests for
        -- keter.yml and defaults to keter.yaml.
        let yml = dir </> "config" </> "keter.yml"
        exists <- doesFileExist yml
        let configFP = if exists then yml else dir </> "config" </> "keter.yaml"
        mconfig <- decodeFileRelative configFP
        config <-
            case mconfig of
                Right config -> return config
                Left e -> throwIO $ InvalidConfigFile e
        return (dir, config)
  where
    folderName =
        case aid of
            AIBuiltin -> "__builtin__"
            AINamed x -> x

withConfig :: AppId
           -> AppInput
           -> (Maybe FilePath -> BundleConfig -> Maybe EpochTime -> KeterM AppStartConfig a)
           -> KeterM AppStartConfig a
withConfig _aid (AIData bconfig) f = f Nothing bconfig Nothing
withConfig aid (AIBundle fp modtime) f = do
    withRunInIO $ \rio ->
        bracketOnError (rio $ unpackBundle fp aid) (\(newdir, _) -> removeDirectoryRecursive newdir) $ \(newdir, bconfig) ->
            rio $ f (Just newdir) bconfig (Just modtime)

withReservations :: AppId
                 -> BundleConfig
                 -> ([WebAppConfig Port] -> [BackgroundConfig] -> Map Host (ProxyAction, TLS.Credentials) -> KeterM AppStartConfig a)
                 -> KeterM AppStartConfig a
withReservations aid bconfig f = do
    AppStartConfig{..} <- ask
    withActions bconfig $ \wacs backs actions ->
        withRunInIO $ \rio ->
            bracketOnError
              (rio $ withMappedConfig (const ascHostManager) $ reserveHosts aid $ Map.keysSet actions)
              (rio . withMappedConfig (const ascHostManager) . forgetReservations aid)
              (\_ -> rio $ f wacs backs actions)

withActions :: BundleConfig
            -> ([ WebAppConfig Port] -> [BackgroundConfig] -> Map Host (ProxyAction, TLS.Credentials) -> KeterM AppStartConfig a)
            -> KeterM AppStartConfig a
withActions bconfig f =
    loop (V.toList $ bconfigStanzas bconfig) [] [] Map.empty
  where
    -- todo: add loading from relative location
    loadCert (SSL certFile chainCertFiles keyFile) =
         either (const mempty) (TLS.Credentials . (:[]))
            <$> TLS.credentialLoadX509Chain certFile (V.toList chainCertFiles) keyFile
    loadCert _ = return mempty

    setupMW :: [Keter.Config.Middleware.MiddlewareConfig] -> IO Middleware
    setupMW = processMiddlewareIO

    loop [] wacs backs actions = f wacs backs actions
    loop (Stanza (StanzaWebApp wac) rs:stanzas) wacs backs actions = do
      AppStartConfig{..} <- ask
      withRunInIO $ \rio ->
        liftIO $ bracketOnError
          (rio (getPort ascPortPool) >>= either throwIO pure)
          (\port -> releasePort ascPortPool port)
          (\port -> do
              mw <- setupMW (waconfigMiddleware wac)
              let wac' = wac { waconfigPort = port }
                  action = PAPort port mw (waconfigTimeout wac)
                  hosts = Set.toList $ Set.insert (waconfigApprootHost wac) (waconfigHosts wac)
              cert <- loadCert (waconfigSsl wac)
              rio $ loop
                stanzas
                (wac' : wacs)
                backs
                (Map.unions $ actions : map (\host -> Map.singleton host ((action, rs), cert)) hosts)
          )
    loop (Stanza (StanzaStaticFiles sfc) rs:stanzas) wacs backs actions0 = do
        cert <- liftIO $ loadCert $ sfconfigSsl sfc
        mw <- liftIO $ setupMW (sfconfigMiddleware sfc)
        let action = PAStatic sfc mw
        loop stanzas wacs backs (actions action cert)
      where
        actions action cert =
          Map.unions $ actions0 :
            map (\host -> Map.singleton host ((action, rs), cert))
                (Set.toList (sfconfigHosts sfc))
    loop (Stanza (StanzaRedirect red) rs:stanzas) wacs backs actions0 = do
        cert <- liftIO $ loadCert $ redirconfigSsl red
        let action = PARedirect red
        loop stanzas wacs backs (actions action cert)
      where
        actions action cert =
          Map.unions $ actions0 :
            map (\host -> Map.singleton host ((action, rs), cert))
                (Set.toList (redirconfigHosts red))
    loop (Stanza (StanzaReverseProxy rev mid to) rs:stanzas) wacs backs actions0 = do
        cert <- liftIO $ loadCert $ reversingUseSSL rev
        mw <- liftIO $ setupMW mid
        let action = PAReverseProxy rev mw to
        loop stanzas wacs backs (Map.insert (CI.mk $ reversingHost rev) ((action, rs), cert) actions0)
    loop (Stanza (StanzaBackground back) _:stanzas) wacs backs actions =
        loop stanzas wacs (back:backs) actions

-- | Gives the log file or log tag name for a given 'AppId'
appLogName :: AppId -> String
appLogName AIBuiltin = "__builtin__"
appLogName (AINamed x) = "app-" <> unpack x

withLogger :: AppId
           -> Maybe (TVar (Maybe Logger))
           -> (TVar (Maybe Logger) -> Logger -> KeterM AppStartConfig a)
           -> KeterM AppStartConfig a
withLogger aid Nothing f = do
    var <- liftIO $ newTVarIO Nothing
    withLogger aid (Just var) f
withLogger aid (Just var) f = do
    AppStartConfig{..} <- ask
    mappLogger <- liftIO $ readTVarIO var
    case mappLogger of
        Nothing -> withRunInIO $ \rio ->
          bracketOnError (Log.createLoggerViaConfig ascKeterConfig (appLogName aid))
                         Log.loggerClose
                       $ \appLogger -> do
                           atomically $ writeTVar var $ Just appLogger
                           rio $ f var appLogger
        Just appLogger ->  f var appLogger

withSanityChecks :: BundleConfig -> KeterM AppStartConfig a -> KeterM AppStartConfig a
withSanityChecks BundleConfig{..} f = do
    liftIO $ V.mapM_ go bconfigStanzas
    $logInfo "Sanity checks passed"
    f
  where
    go (Stanza (StanzaWebApp WebAppConfig {..}) _) = do
      isExec waconfigExec
      for_ waconfigEnsureAliveTimeout
        $ \x -> when (x < 1) $ throwIO $ EnsureAliveShouldBeBiggerThenZero x
    go (Stanza (StanzaBackground BackgroundConfig {..}) _) = isExec bgconfigExec
    go _ = return ()

    isExec fp = do
        exists <- doesFileExist fp
        if exists
            then do
                canExec <- fileAccess fp True False True
                if canExec
                    then return ()
                    else throwIO $ FileNotExecutable fp
            else throwIO $ ExecutableNotFound fp

start :: AppId
      -> AppInput
      -> TVar AppState
      -> KeterM AppStartConfig App
start aid input tstate =
    withLogger aid Nothing $ \tAppLogger appLogger ->
    withConfig aid input $ \newdir bconfig mmodtime ->
    withSanityChecks bconfig $ 
    withReservations aid bconfig $ \webapps backs actions ->
    withBackgroundApps aid bconfig newdir appLogger backs $ \runningBacks ->
    withWebApps aid bconfig newdir appLogger webapps $ \runningWebapps -> do
        asc@AppStartConfig{..} <- ask
        liftIO $ mapM_ (ensureAlive tstate) runningWebapps
        withMappedConfig (const ascHostManager) $ activateApp aid actions
        liftIO $
          App
            <$> newTVarIO mmodtime
            <*> newTVarIO runningWebapps
            <*> newTVarIO runningBacks
            <*> return aid
            <*> newTVarIO (Map.keysSet actions)
            <*> newTVarIO newdir
            <*> return asc
            <*> return tAppLogger

bracketedMap :: (a -> (b -> IO c) -> IO c)
             -> ([b] -> IO c)
             -> [a]
             -> IO c
bracketedMap with inside =
    loop id
  where
    loop front [] = inside $ front []
    loop front (c:cs) = with c $ \x -> loop (front . (x:)) cs

withWebApps :: AppId
            -> BundleConfig
            -> Maybe FilePath
            -> Logger
            -> [WebAppConfig Port]
            -> ([RunningWebApp] -> KeterM AppStartConfig a)
            -> KeterM AppStartConfig a
withWebApps aid bconfig mdir appLogger configs0 f =
    withRunInIO $ \rio ->
      bracketedMap (\wac f' -> rio $ alloc wac (liftIO <$> f')) (rio . f) configs0
  where
    alloc = launchWebApp aid bconfig mdir appLogger

-- | Format a log message for an app by tagging it with 'app-$name>' (only when it is being logged to stderr)
formatAppLog :: AppId -> FL.LogType -> LogStr -> LogStr
formatAppLog aid (FL.LogStderr _) msg = toLogStr (appLogName aid) <> "> " <> msg
formatAppLog _ _ msg = msg

launchWebApp :: AppId
             -> BundleConfig
             -> Maybe FilePath
             -> Logger
             -> WebAppConfig Port
             -> (RunningWebApp -> KeterM AppStartConfig a)
             -> KeterM AppStartConfig a
launchWebApp aid BundleConfig {..} mdir appLogger WebAppConfig {..} f = do
    AppStartConfig{..} <- ask
    otherEnv <- liftIO $ pluginsGetEnv ascPlugins name bconfigPlugins
    forwardedEnv <- liftIO $ getForwardedEnv waconfigForwardEnv
    let httpPort  = kconfigExternalHttpPort  ascKeterConfig
        httpsPort = kconfigExternalHttpsPort ascKeterConfig
        (scheme, extport) =
            if waconfigSsl == SSLFalse
                then ("http://",  if httpPort  ==  80 then "" else ':' : show httpPort)
                else ("https://", if httpsPort == 443 then "" else ':' : show httpsPort)
        env = Map.toList $ Map.unions
            -- Ordering chosen specifically to precedence rules: app specific,
            -- plugins, global, and then auto-set Keter variables.
            [ waconfigEnvironment
            , forwardedEnv
            , Map.fromList otherEnv
            , kconfigEnvironment ascKeterConfig
            , Map.singleton "PORT" $ pack $ show waconfigPort
            , Map.singleton "APPROOT" $ scheme <> CI.original waconfigApprootHost <> pack extport
            ]
    exec <- liftIO $ canonicalizePath waconfigExec
    withRunInIO $ \rio -> bracketOnError
        (rio $ monitorProcess
            ascProcessTracker
            (encodeUtf8 . fst <$> ascSetuid)
            (encodeUtf8 $ pack exec)
            (maybe "/tmp" (encodeUtf8 . pack) mdir)
            (map encodeUtf8 $ V.toList waconfigArgs)
            (map (encodeUtf8 *** encodeUtf8) env)
            (Log.loggerLog appLogger . formatAppLog aid (Log.loggerType appLogger) . toLogStr)
            (const $ return True))
        terminateMonitoredProcess
        $ \mp -> rio $ f RunningWebApp
            { rwaProcess = mp
            , rwaPort = waconfigPort
            , rwaEnsureAliveTimeOut = fromMaybe (90 * 1000 * 1000) waconfigEnsureAliveTimeout
            }
  where
    name =
        case aid of
            AIBuiltin -> "__builtin__"
            AINamed x -> x

killWebApp :: RunningWebApp -> KeterM cfg ()
killWebApp RunningWebApp {..} = do
    status <- liftIO $ printStatus rwaProcess
    $logInfo $ pack $ "Killing " <> unpack status <> " running on port: "  <> show rwaPort
    liftIO $ terminateMonitoredProcess rwaProcess

ensureAlive :: TVar AppState -> RunningWebApp -> IO ()
ensureAlive tstate RunningWebApp {..} = do
    didAnswer <- testApp rwaPort
    if didAnswer
        then return ()
        else error $ "ensureAlive failed, this means keter couldn't " <>
                      "detect your app at port " <> show rwaPort <>
                      ", check your app logs detailed errors. " <>
                      " Also make sure your app binds to the PORT environment variable (not YESOD_PORT for example)." -- TODO domain name would be good to add as well
  where
    testApp :: Port -> IO Bool
    testApp port = do
        res <- timeout rwaEnsureAliveTimeOut testApp'
        return $ fromMaybe False res
      where
        testApp' = do
            threadDelay $ 2 * 1000 * 1000
            eres <- try $ connectTo "127.0.0.1" $ show port
            case eres of
                Left (_ :: IOException) -> do
                    testApp'required <- not <$> hasNextStartingAction
                    if testApp'required then testApp' else return False
                Right handle -> do
                    hClose handle
                    return True
        hasNextStartingAction :: IO Bool
        hasNextStartingAction = do
            state <- readTVarIO tstate
            case state of
                ASStarting _app _time tmaction -> isJust <$> readTVarIO tmaction
                _ -> return False
        connectTo host serv = do
            let hints = defaultHints { addrFlags = [AI_ADDRCONFIG]
                                     , addrSocketType = Stream }
            addrs <- getAddrInfo (Just hints) (Just host) (Just serv)
            firstSuccessful $ map tryToConnect addrs
            where
              tryToConnect addr =
                bracketOnError
                  (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
                  close  -- only done if there's an error
                  (\sock -> do
                    connect sock (addrAddress addr)
                    socketToHandle sock ReadWriteMode
                  )
              firstSuccessful = go Nothing
                where
                  go _ (p:ps) = do
                    r <- tryIO p
                    case r of
                          Right x -> return x
                          Left  e -> go (Just e) ps
                 -- All operations failed, throw error if one exists
                  go Nothing  [] = ioError $ userError "connectTo firstSuccessful: empty list"
                  go (Just e) [] = throwIO e
                  tryIO :: IO a -> IO (Either IOException a)
                  tryIO m = catch (fmap Right m) (return . Left)


withBackgroundApps :: AppId
                   -> BundleConfig
                   -> Maybe FilePath
                   -> Logger
                   -> [BackgroundConfig]
                   -> ([RunningBackgroundApp] -> KeterM AppStartConfig a)
                   -> KeterM AppStartConfig a
withBackgroundApps aid bconfig mdir appLogger configs f =
    withRunInIO $ \rio -> bracketedMap (\cfg f' -> rio $ alloc cfg (liftIO <$> f')) (rio . f) configs
  where
    alloc = launchBackgroundApp aid bconfig mdir appLogger

launchBackgroundApp :: AppId
                    -> BundleConfig
                    -> Maybe FilePath
                    -> Logger
                    -> BackgroundConfig
                    -> (RunningBackgroundApp -> IO a)
                    -> KeterM AppStartConfig a
launchBackgroundApp aid BundleConfig {..} mdir appLogger BackgroundConfig {..} f = do
    AppStartConfig{..} <- ask
    otherEnv <- liftIO $ pluginsGetEnv ascPlugins name bconfigPlugins
    forwardedEnv <- liftIO $ getForwardedEnv bgconfigForwardEnv
    let env = Map.toList $ Map.unions
            -- Order matters as in launchWebApp
            [ bgconfigEnvironment
            , forwardedEnv
            , Map.fromList otherEnv
            , kconfigEnvironment ascKeterConfig
            ]
    exec <- liftIO $ canonicalizePath bgconfigExec

    let delay = threadDelay $ fromIntegral $ bgconfigRestartDelaySeconds * 1000 * 1000
    shouldRestart <-
        case bgconfigRestartCount of
            UnlimitedRestarts -> return $ do
                delay
                return True
            LimitedRestarts maxCount -> do
                icount <- liftIO $ newIORef 0
                return $ do
                    res <- atomicModifyIORef icount $ \count ->
                        (count + 1, count < maxCount)
                    when res delay
                    return res
    withRunInIO $ \rio -> bracketOnError
        (rio $ monitorProcess
            ascProcessTracker
            (encodeUtf8 . fst <$> ascSetuid)
            (encodeUtf8 $ pack exec)
            (maybe "/tmp" (encodeUtf8 . pack) mdir)
            (map encodeUtf8 $ V.toList bgconfigArgs)
            (map (encodeUtf8 *** encodeUtf8) env)
            (Log.loggerLog appLogger . formatAppLog aid (Log.loggerType appLogger) . toLogStr)
            (const shouldRestart))
        terminateMonitoredProcess
        (f . RunningBackgroundApp)
  where
    name =
        case aid of
            AIBuiltin -> "__builtin__"
            AINamed x -> x

killBackgroundApp :: RunningBackgroundApp -> IO ()
killBackgroundApp RunningBackgroundApp {..} = do
    terminateMonitoredProcess rbaProcess

    {-
start :: TempFolder
      -> Maybe (Text, (UserID, GroupID))
      -> ProcessTracker
      -> HostManager
      -> Plugins
      -> RotatingLog
      -> Appname
      -> (Maybe BundleConfig)
      -> KIO () -- ^ action to perform to remove this App from list of actives
      -> KIO (App, KIO ())
start tf muid processTracker portman plugins appLogger appname bundle removeFromList = do
    Prelude.error "FIXME Keter.App.start"
    chan <- newChan
    return (App $ writeChan chan, rest chan)
  where

    rest chan = forkKIO $ do
        mres <- unpackBundle tf (snd <$> muid) bundle appname
        case mres of
            Left e -> do
                $logEx e
                removeFromList
            Right (dir, config) -> do
                let common = do
                        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
                    Nothing -> do
                        common
                        loop chan dir config Nothing
                    Just appconfig -> do
                        eport <- getPort portman
                        case eport of
                            Left e -> do
                                $logEx e
                                removeFromList
                            Right port -> do
                                eprocess <- runApp port dir appconfig
                                case eprocess of
                                    Left e -> do
                                        $logEx e
                                        removeFromList
                                    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

    loop chan dirOld configOld mprocPortOld = do
        command <- readChan chan
        case command of
            Terminate -> do
                removeFromList
                case bconfigApp configOld of
                    Nothing -> return ()
                    Just appconfig -> do
                        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
                log $ TerminatingApp appname
                terminateOld
            Reload -> do
                mres <- unpackBundle tf (snd <$> muid) bundle appname
                case mres of
                    Left e -> do
                        log $ InvalidBundle bundle e
                        loop chan dirOld configOld mprocPortOld
                    Right (dir, config) -> do
                        eport <- getPort portman
                        case eport of
                            Left e -> $logEx e
                            Right port -> do
                                let common = do
                                        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
                                    Nothing -> do
                                        common
                                        loop chan dir config Nothing
                                    Just appconfig -> do
                                        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
                                                addEntry portman (aconfigHost appconfig) $ PEPort port
                                                mapM_ (flip (addEntry portman) $ PEPort port) $ Set.toList $ aconfigExtraHosts appconfig
                                                common
                                                case bconfigApp configOld of
                                                    Just appconfigOld | aconfigHost appconfig /= aconfigHost appconfigOld ->
                                                        removeEntry portman $ aconfigHost appconfigOld
                                                    _ -> return ()
                                                log $ FinishedReloading appname
                                                terminateOld
                                                loop chan dir config $ Just (process, port)
                                            Nothing -> do
                                                releasePort portman port
                                                case eprocess of
                                                    Left _ -> return ()
                                                    Right process -> void $ liftIO $ terminateMonitoredProcess process
                                                log $ ProcessDidNotStart bundle
                                                loop chan dirOld configOld mprocPortOld
      where
        terminateOld = forkKIO $ do
    -}

reload :: AppInput -> TVar AppState -> KeterM App ()
reload input tstate = do
    App{..} <- ask
    withMappedConfig (const appAsc) $
      withLogger appId (Just appLog) $ \_ appLogger ->
      withConfig appId input $ \newdir bconfig mmodtime ->
      withSanityChecks bconfig $
      withReservations appId bconfig $ \webapps backs actions -> 
      withBackgroundApps appId bconfig newdir appLogger backs $ \runningBacks ->
      withWebApps appId bconfig newdir appLogger webapps $ \runningWebapps -> do
          liftIO $ mapM_ (ensureAlive tstate) runningWebapps
          liftIO (readTVarIO appHosts) >>= \hosts ->
            withMappedConfig (const $ ascHostManager appAsc) $
              reactivateApp appId actions hosts
          (oldApps, oldBacks, oldDir, oldRlog) <- liftIO $ atomically $ do
              oldApps <- readTVar appRunningWebApps
              oldBacks <- readTVar appBackgroundApps
              oldDir <- readTVar appDir
              oldRlog <- readTVar appLog

              writeTVar appModTime mmodtime
              writeTVar appRunningWebApps runningWebapps
              writeTVar appBackgroundApps runningBacks
              writeTVar appHosts $ Map.keysSet actions
              writeTVar appDir newdir
              return (oldApps, oldBacks, oldDir, oldRlog)
          void $ withRunInIO $ \rio ->
            forkIO $ rio $ terminateHelper appId oldApps oldBacks oldDir oldRlog

terminate :: KeterM App ()
terminate = do
    App{..} <- ask
    let AppStartConfig {..} = appAsc
    (hosts, apps, backs, mdir, appLogger) <- liftIO $ atomically $ do
        hosts <- readTVar appHosts
        apps <- readTVar appRunningWebApps
        backs <- readTVar appBackgroundApps
        mdir <- readTVar appDir
        appLogger <- readTVar appLog

        writeTVar appModTime Nothing
        writeTVar appRunningWebApps []
        writeTVar appBackgroundApps []
        writeTVar appHosts Set.empty
        writeTVar appDir Nothing
        writeTVar appLog Nothing

        return (hosts, apps, backs, mdir, appLogger)

    withMappedConfig (const ascHostManager) $
        deactivateApp appId hosts

    void $ withRunInIO $ \rio ->
      forkIO $ rio $ withMappedConfig (const appAsc) $
        terminateHelper appId apps backs mdir appLogger
    liftIO $ maybe (return ()) Log.loggerClose appLogger

terminateHelper :: AppId
                -> [RunningWebApp]
                -> [RunningBackgroundApp]
                -> Maybe FilePath
                -> Maybe Logger
                -> KeterM AppStartConfig ()
terminateHelper aid apps backs mdir _appLogger = do
    AppStartConfig{..} <- ask
    let preDelay = maybe (20 * 1000 * 1000) id (kconfigGracefulDrainMicros ascKeterConfig)
    liftIO $ threadDelay preDelay
    $logInfo $ pack $
        "Sending old process TERM signal: "
          ++ case aid of { AINamed t -> unpack t; AIBuiltin -> "builtin" }
    mapM_ killWebApp apps
    liftIO $ do
        mapM_ killBackgroundApp backs
        threadDelay $ 60 * 1000 * 1000
    case mdir of
        Nothing -> return ()
        Just dir -> do
            $logInfo $ pack $ "Removing unneeded folder: " ++ dir
            res <- liftIO $ try @SomeException $ removeDirectoryRecursive dir
            case res of
                Left e -> $logError $ pack $ show e
                Right () -> return ()

-- | Get the modification time of the bundle file this app was launched from,
-- if relevant.
getTimestamp :: App -> STM (Maybe EpochTime)
getTimestamp = readTVar . appModTime

pluginsGetEnv :: Plugins -> Appname -> Object -> IO [(Text, Text)]
pluginsGetEnv ps app o = concat <$> mapM (\p -> pluginGetEnv p app o) ps

-- | For the forward-env option. From a Set of desired variables, create a
-- Map pulled from the system environment.
getForwardedEnv :: Set Text -> IO (Map Text Text)
getForwardedEnv vars = filterEnv <$> getEnvironment
  where
    filterEnv = Map.filterWithKey (\k _ -> Set.member k vars)
              . Map.fromList
              . map (pack *** pack)


    {- 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)
    -}

{- 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
                        mappLogger <- do
                            let dirout = kconfigDir </> "log" </> fromText ("app-" ++ appname)
                                direrr = dirout </> "err"
                            eappLogger <- liftIO $ Log.openRotatingLog
                                (F.encodeString dirout)
                                Log.defaultMaxTotal
                            case eappLogger of
                                Left e -> do
                                    $logEx e
                                    return Nothing
                                Right appLogger -> return (Just appLogger)
                        let appLogger = fromMaybe Log.dummy mappLogger
                        (app, rest) <- App.start
                            tf
                            muid
                            processTracker
                            hostman
                            plugins
                            appLogger
                            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
            -}