{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Snap.Snaplet.Internal.Initializer
( addPostInitHook
, addPostInitHookBase
, toSnapletHook
, bracketInit
, modifyCfg
, nestSnaplet
, embedSnaplet
, makeSnaplet
, nameSnaplet
, onUnload
, addRoutes
, wrapSite
, runInitializer
, runSnaplet
, combineConfig
, serveSnaplet
, serveSnapletNoArgParsing
, loadAppConfig
, printInfo
, getRoutes
, getEnvironment
, modifyMaster
) where
import Control.Applicative ((<$>))
import Control.Concurrent.MVar (MVar, modifyMVar_, newEmptyMVar,
putMVar, readMVar)
import Control.Exception.Lifted (SomeException, catch, try)
import Control.Lens (ALens', cloneLens, over, set,
storing, (^#))
import Control.Monad (Monad (..), join, liftM, unless,
when, (=<<))
import Control.Monad.Reader (ask)
import Control.Monad.State (get, modify)
import Control.Monad.Trans (lift, liftIO)
import Control.Monad.Trans.Writer hiding (pass)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Configurator (Worth (..), addToConfig, empty,
loadGroups, subconfig)
import qualified Data.Configurator.Types as C
import Data.IORef (IORef, atomicModifyIORef,
newIORef, readIORef)
import Data.Maybe (Maybe (..), fromJust, fromMaybe,
isNothing)
import Data.Text (Text)
import qualified Data.Text as T
import Prelude (Bool (..), Either (..), Eq (..),
String, concat, concatMap,
const, either,
error, filter, flip, fst, id,
map, not, show, ($), ($!), (++),
(.))
import Snap.Core (Snap, liftSnap, route)
import Snap.Http.Server (Config, completeConfig,
getCompression, getErrorHandler,
getOther, getVerbose, httpServe)
import Snap.Util.GZip (withCompression)
import System.Directory (copyFile,
createDirectoryIfMissing,
doesDirectoryExist,
getCurrentDirectory)
import System.Directory.Tree (DirTree (..), FileName, buildL,
dirTree, readDirectoryWith)
import System.FilePath.Posix (dropFileName, makeRelative,
(</>))
import System.IO (FilePath, IO, hPutStrLn, stderr)
import Snap.Snaplet.Config (AppConfig, appEnvironment,
commandLineAppConfig)
import qualified Snap.Snaplet.Internal.Lensed as L
import qualified Snap.Snaplet.Internal.LensT as LT
import Snap.Snaplet.Internal.Types
iGet :: Initializer b v (InitializerState b)
iGet = Initializer $ LT.getBase
iModify :: (InitializerState b -> InitializerState b) -> Initializer b v ()
iModify f = Initializer $ do
b <- LT.getBase
LT.putBase $ f b
iGets :: (InitializerState b -> a) -> Initializer b v a
iGets f = Initializer $ do
b <- LT.getBase
return $ f b
getRoutes :: Initializer b v [ByteString]
getRoutes = liftM (map fst) $ iGets _handlers
getEnvironment :: Initializer b v String
getEnvironment = iGets _environment
toSnapletHook :: (v -> IO (Either Text v))
-> (Snaplet v -> IO (Either Text (Snaplet v)))
toSnapletHook f (Snaplet cfg reset val) = do
val' <- f val
return $! Snaplet cfg reset <$> val'
addPostInitHook :: (v -> IO (Either Text v))
-> Initializer b v ()
addPostInitHook = addPostInitHook' . toSnapletHook
addPostInitHook' :: (Snaplet v -> IO (Either Text (Snaplet v)))
-> Initializer b v ()
addPostInitHook' h = do
h' <- upHook h
addPostInitHookBase h'
addPostInitHookBase :: (Snaplet b -> IO (Either Text (Snaplet b)))
-> Initializer b v ()
addPostInitHookBase = Initializer . lift . tell . Hook
upHook :: (Snaplet v -> IO (Either Text (Snaplet v)))
-> Initializer b v (Snaplet b -> IO (Either Text (Snaplet b)))
upHook h = Initializer $ do
l <- ask
return $ upHook' l h
upHook' :: Monad m => ALens' b a -> (a -> m (Either e a)) -> b -> m (Either e b)
upHook' l h b = do
v <- h (b ^# l)
return $ case v of
Left e -> Left e
Right v' -> Right $ storing l v' b
modifyCfg :: (SnapletConfig -> SnapletConfig) -> Initializer b v ()
modifyCfg f = iModify $ over curConfig $ \c -> f c
setupFilesystem :: Maybe (IO FilePath)
-> FilePath
-> Initializer b v ()
setupFilesystem Nothing _ = return ()
setupFilesystem (Just getSnapletDataDir) targetDir = do
exists <- liftIO $ doesDirectoryExist targetDir
unless exists $ do
printInfo "...setting up filesystem"
liftIO $ createDirectoryIfMissing True targetDir
srcDir <- liftIO getSnapletDataDir
liftIO $ readDirectoryWith (doCopy srcDir targetDir) srcDir
return ()
where
doCopy srcRoot targetRoot filename = do
createDirectoryIfMissing True directory
copyFile filename toDir
where
toDir = targetRoot </> makeRelative srcRoot filename
directory = dropFileName toDir
makeSnaplet :: Text
-> Text
-> Maybe (IO FilePath)
-> Initializer b v v
-> SnapletInit b v
makeSnaplet snapletId desc getSnapletDataDir m = SnapletInit $ do
modifyCfg $ \c -> if isNothing $ _scId c
then set scId (Just snapletId) c else c
sid <- iGets (T.unpack . fromJust . _scId . _curConfig)
topLevel <- iGets _isTopLevel
unless topLevel $ do
modifyCfg $ over scUserConfig (subconfig (T.pack sid))
modifyCfg $ \c -> set scFilePath
(_scFilePath c </> "snaplets" </> sid) c
iModify (set isTopLevel False)
modifyCfg $ set scDescription desc
cfg <- iGets _curConfig
printInfo $ T.pack $ concat
["Initializing "
,sid
," @ /"
,B.unpack $ buildPath $ _scRouteContext cfg
]
setupFilesystem getSnapletDataDir (_scFilePath cfg)
env <- iGets _environment
let configLocation = _scFilePath cfg </> (env ++ ".cfg")
liftIO $ addToConfig [Optional configLocation]
(_scUserConfig cfg)
mkSnaplet m
mkSnaplet :: Initializer b v v -> Initializer b v (Snaplet v)
mkSnaplet m = do
res <- m
cfg <- iGets _curConfig
setInTop <- iGets masterReloader
l <- getLens
let modifier = setInTop . set (cloneLens l . snapletValue)
return $ Snaplet cfg modifier res
bracketInit :: Initializer b v a -> Initializer b v a
bracketInit m = do
s <- iGet
res <- m
iModify (set curConfig (_curConfig s))
return res
setupSnapletCall :: ByteString -> Initializer b v ()
setupSnapletCall rte = do
curId <- iGets (fromJust . _scId . _curConfig)
modifyCfg (over scAncestry (curId:))
modifyCfg (over scId (const Nothing))
unless (B.null rte) $ modifyCfg (over scRouteContext (rte:))
nestSnaplet :: ByteString
-> SnapletLens v v1
-> SnapletInit b v1
-> Initializer b v (Snaplet v1)
nestSnaplet rte l (SnapletInit snaplet) =
with l $ bracketInit $ do
setupSnapletCall rte
snaplet
embedSnaplet :: ByteString
-> SnapletLens v v1
-> SnapletInit v1 v1
-> Initializer b v (Snaplet v1)
embedSnaplet rte l (SnapletInit snaplet) = bracketInit $ do
curLens <- getLens
setupSnapletCall ""
chroot rte (cloneLens curLens . subSnaplet l) snaplet
chroot :: ByteString
-> SnapletLens (Snaplet b) v1
-> Initializer v1 v1 a
-> Initializer b v a
chroot rte l (Initializer m) = do
curState <- iGet
let newSetter f = masterReloader curState (over (cloneLens l) f)
((a,s), (Hook hook)) <- liftIO $ runWriterT $ LT.runLensT m id $
curState {
_handlers = [],
_hFilter = id,
masterReloader = newSetter
}
let handler = chrootHandler l $ _hFilter s $ route $ _handlers s
iModify $ over handlers (++[(rte,handler)])
. set cleanup (_cleanup s)
addPostInitHookBase $ upHook' l hook
return a
chrootHandler :: SnapletLens (Snaplet v) b'
-> Handler b' b' a -> Handler b v a
chrootHandler l (Handler h) = Handler $ do
s <- get
(a, s') <- liftSnap $ L.runLensed h id (s ^# l)
modify $ storing l s'
return a
nameSnaplet :: Text
-> SnapletInit b v
-> SnapletInit b v
nameSnaplet nm (SnapletInit m) = SnapletInit $
modifyCfg (set scId (Just nm)) >> m
addRoutes :: [(ByteString, Handler b v ())]
-> Initializer b v ()
addRoutes rs = do
l <- getLens
ctx <- iGets (_scRouteContext . _curConfig)
let modRoute (r,h) = ( buildPath (r:ctx)
, setPattern r >> withTop' l h)
let rs' = map modRoute rs
iModify (\v -> over handlers (++rs') v)
where
setPattern r = do
p <- getRoutePattern
when (isNothing p) $ setRoutePattern r
wrapSite :: (Handler b v () -> Handler b v ())
-> Initializer b v ()
wrapSite f0 = do
f <- mungeFilter f0
iModify (\v -> over hFilter (f.) v)
mungeFilter :: (Handler b v () -> Handler b v ())
-> Initializer b v (Handler b b () -> Handler b b ())
mungeFilter f = do
myLens <- Initializer ask
return $ \m -> with' myLens $ f' m
where
f' (Handler m) = f $ Handler $ L.withTop id m
onUnload :: IO () -> Initializer b v ()
onUnload m = do
cleanupRef <- iGets _cleanup
liftIO $ atomicModifyIORef cleanupRef f
where
f curCleanup = (curCleanup >> m, ())
logInitMsg :: IORef Text -> Text -> IO ()
logInitMsg ref msg = atomicModifyIORef ref (\cur -> (cur `T.append` msg, ()))
printInfo :: Text -> Initializer b v ()
printInfo msg = do
logRef <- iGets _initMessages
liftIO $ logInitMsg logRef (msg `T.append` "\n")
mkReloader :: FilePath
-> String
-> ((Snaplet b -> Snaplet b) -> IO ())
-> IORef (IO ())
-> Initializer b b (Snaplet b)
-> IO (Either Text Text)
mkReloader cwd env resetter cleanupRef i = do
join $ readIORef cleanupRef
!res <- runInitializer' resetter env i cwd
either (return . Left) good res
where
good (b,is) = do
_ <- resetter (const b)
msgs <- readIORef $ _initMessages is
return $ Right msgs
runBase :: Handler b b a
-> MVar (Snaplet b)
-> Snap a
runBase (Handler m) mvar = do
!b <- liftIO (readMVar mvar)
(!a, _) <- L.runLensed m id b
return $! a
modifyMaster :: v -> Handler b v ()
modifyMaster v = do
modifier <- getsSnapletState _snapletModifier
liftIO $ modifier v
runInitializer :: ((Snaplet b -> Snaplet b) -> IO ())
-> String
-> Initializer b b (Snaplet b)
-> IO (Either Text (Snaplet b, InitializerState b))
runInitializer resetter env b =
getCurrentDirectory >>= runInitializer' resetter env b
runInitializer' :: ((Snaplet b -> Snaplet b) -> IO ())
-> String
-> Initializer b b (Snaplet b)
-> FilePath
-> IO (Either Text (Snaplet b, InitializerState b))
runInitializer' resetter env b@(Initializer i) cwd = do
cleanupRef <- newIORef (return ())
let reloader_ = mkReloader cwd env resetter cleanupRef b
let builtinHandlers = [("/admin/reload", reloadSite)]
let cfg = SnapletConfig [] cwd Nothing "" empty [] Nothing reloader_
logRef <- newIORef ""
let body = do
((res, s), (Hook hook)) <- runWriterT $ LT.runLensT i id $
InitializerState True cleanupRef builtinHandlers id cfg logRef
env resetter
res' <- hook res
return $ (,s) <$> res'
handler e = do
join $ readIORef cleanupRef
logMessages <- readIORef logRef
return $ Left $ T.unlines
[ "Initializer threw an exception..."
, T.pack $ show (e :: SomeException)
, ""
, "...but before it died it generated the following output:"
, logMessages
]
catch body handler
runSnaplet :: Maybe String -> SnapletInit b b -> IO (Text, Snap (), IO ())
runSnaplet env (SnapletInit b) = do
snapletMVar <- newEmptyMVar
let resetter f = modifyMVar_ snapletMVar (return . f)
eRes <- runInitializer resetter (fromMaybe "devel" env) b
let go (siteSnaplet,is) = do
putMVar snapletMVar siteSnaplet
msgs <- liftIO $ readIORef $ _initMessages is
let handler = runBase (_hFilter is $ route $ _handlers is) snapletMVar
cleanupAction <- readIORef $ _cleanup is
return (msgs, handler, cleanupAction)
either (error . ('\n':) . T.unpack) go eRes
combineConfig :: Config Snap a -> Snap () -> IO (Config Snap a, Snap ())
combineConfig config handler = do
conf <- completeConfig config
let catch500 = (flip catch $ fromJust $ getErrorHandler conf)
let compress = if fromJust (getCompression conf)
then withCompression else id
let site = compress $ catch500 handler
return (conf, site)
serveSnaplet :: Config Snap AppConfig
-> SnapletInit b b
-> IO ()
serveSnaplet startConfig initializer = do
config <- commandLineAppConfig startConfig
serveSnapletNoArgParsing config initializer
serveSnapletNoArgParsing :: Config Snap AppConfig
-> SnapletInit b b
-> IO ()
serveSnapletNoArgParsing config initializer = do
let env = appEnvironment =<< getOther config
(msgs, handler, doCleanup) <- runSnaplet env initializer
(conf, site) <- combineConfig config handler
createDirectoryIfMissing False "log"
let serve = httpServe conf
when (loggingEnabled conf) $ liftIO $ hPutStrLn stderr $ T.unpack msgs
_ <- try $ serve $ site
:: IO (Either SomeException ())
doCleanup
where
loggingEnabled = not . (== Just False) . getVerbose
loadAppConfig :: FileName
-> FilePath
-> IO C.Config
loadAppConfig cfg root = do
tree <- buildL root
let groups = loadAppConfig' cfg "" $ dirTree tree
loadGroups groups
loadAppConfig' :: FileName -> Text -> DirTree a -> [(Text, Worth a)]
loadAppConfig' cfg _prefix d@(Dir _ c) =
(map ((_prefix,) . Required) $ getCfg cfg d) ++
concatMap (\a -> loadAppConfig' cfg (nextPrefix $ name a) a) snaplets
where
nextPrefix p = T.concat [_prefix, T.pack p, "."]
snapletsDirs = filter isSnapletsDir c
snaplets = concatMap (filter isDir . contents) snapletsDirs
loadAppConfig' _ _ _ = []
isSnapletsDir :: DirTree t -> Bool
isSnapletsDir (Dir "snaplets" _) = True
isSnapletsDir _ = False
isDir :: DirTree t -> Bool
isDir (Dir _ _) = True
isDir _ = False
isCfg :: FileName -> DirTree t -> Bool
isCfg cfg (File n _) = cfg == n
isCfg _ _ = False
getCfg :: FileName -> DirTree b -> [b]
getCfg cfg (Dir _ c) = map file $ filter (isCfg cfg) c
getCfg _ _ = []