{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module Erb.Compute (
computeTemplate
, initTemplateDaemon
) where
import Control.Concurrent
import Control.Exception
import Control.Monad.Except
import Data.Aeson.Lens
import qualified Data.Either.Strict as S
import Data.FileCache
import qualified Data.Text as T
import Data.String
import qualified Data.Vector as V
import Debug.Trace
import Erb.Evaluate
import Erb.Parser
import Erb.Ruby
import Paths_language_puppet (getDataFileName)
import System.Environment
import qualified System.Log.Logger as LOG
import System.Posix.Files
import Text.Parsec hiding (string)
import Text.Parsec.Error
import Text.Parsec.Pos
import Control.Lens
import Data.Tuple.Strict
import qualified Foreign.Ruby.Helpers as FR
import qualified Foreign.Ruby.Bindings as FR
import Foreign.Ruby
import GHC.Conc (labelThread)
import Puppet.Interpreter.Types
import Puppet.Interpreter.IO
import Puppet.Interpreter.Resolve
import Puppet.PP
import Puppet.Preferences
import Puppet.Stats
import Puppet.Utils
instance IsString TemplateParseError where
fromString s = TemplateParseError $ newErrorMessage (Message s) (initialPos "dummy")
newtype TemplateParseError = TemplateParseError { tgetError :: ParseError }
type TemplateQuery = (Chan TemplateAnswer, Either T.Text T.Text, InterpreterState, InterpreterReader IO)
type TemplateAnswer = S.Either PrettyError T.Text
showRubyError :: RubyError -> PrettyError
showRubyError (Stack msg stk) = PrettyError $ dullred (string msg) </> dullyellow (string stk)
showRubyError (WithOutput str _) = PrettyError $ dullred (string str)
showRubyError (OtherError rr) = PrettyError (dullred (text rr))
initTemplateDaemon :: RubyInterpreter -> Preferences IO -> MStats -> IO (Either T.Text T.Text -> InterpreterState -> InterpreterReader IO -> IO (S.Either PrettyError T.Text))
initTemplateDaemon intr prefs mvstats = do
controlchan <- newChan
templatecache <- newFileCache
let returnError rs = return $ \_ _ _ -> return (S.Left (showRubyError rs))
x <- runExceptT $ do
liftIO (getRubyScriptPath "hrubyerb.rb") >>= ExceptT . loadFile intr
ExceptT (registerGlobalFunction4 intr "varlookup" hrresolveVariable)
ExceptT (registerGlobalFunction5 intr "callextfunc" hrcallfunction)
liftIO $ void $ forkIO $ templateDaemon intr
(T.pack (prefs ^. prefPuppetPaths.modulesPath))
(T.pack (prefs ^. prefPuppetPaths.templatesPath))
controlchan
mvstats
templatecache
return (templateQuery controlchan)
either returnError return x
templateQuery :: Chan TemplateQuery -> Either T.Text T.Text -> InterpreterState -> InterpreterReader IO -> IO (S.Either PrettyError T.Text)
templateQuery qchan filename stt rdr = do
rchan <- newChan
writeChan qchan (rchan, filename, stt, rdr)
readChan rchan
templateDaemon :: RubyInterpreter -> T.Text -> T.Text -> Chan TemplateQuery -> MStats -> FileCacheR TemplateParseError [RubyStatement] -> IO ()
templateDaemon intr modpath templatepath qchan mvstats filecache = do
let nameThread :: String -> IO ()
nameThread n = myThreadId >>= flip labelThread n
nameThread "RubyTemplateDaemon"
(respchan, fileinfo, stt, rdr) <- readChan qchan
case fileinfo of
Right filename -> do
let prts = T.splitOn "/" filename
searchpathes | length prts > 1 = [modpath <> "/" <> head prts <> "/templates/" <> T.intercalate "/" (tail prts), templatepath <> "/" <> filename]
| otherwise = [templatepath <> "/" <> filename]
acceptablefiles <- filterM (fileExist . T.unpack) searchpathes
if null acceptablefiles
then writeChan respchan (S.Left $ PrettyError $ "Can't find template file for" <+> ttext filename <+> ", looked in" <+> list (map ttext searchpathes))
else measure mvstats filename (computeTemplate intr (Right (head acceptablefiles)) stt rdr mvstats filecache) >>= writeChan respchan
Left _ -> measure mvstats "inline" (computeTemplate intr fileinfo stt rdr mvstats filecache) >>= writeChan respchan
templateDaemon intr modpath templatepath qchan mvstats filecache
computeTemplate :: RubyInterpreter -> Either T.Text T.Text -> InterpreterState -> InterpreterReader IO -> MStats -> FileCacheR TemplateParseError [RubyStatement] -> IO TemplateAnswer
computeTemplate intr fileinfo stt rdr mstats filecache = do
let (curcontext, fvariables) = case extractFromState stt of
Nothing -> (mempty, mempty)
Just (c,v) -> (c,v)
let (filename, ufilename) = case fileinfo of
Left _ -> ("inline", "inline")
Right x -> (x, T.unpack x)
mkSafe a = makeSafe intr a >>= \case
Left rr -> return (S.Left (showRubyError rr))
Right x -> return x
encapsulateError = _Left %~ TemplateParseError
variables = fvariables & traverse . scopeVariables . traverse . _1 . _1 %~ toStr
toStr (PNumber n) = PString (scientific2text n)
toStr x = x
traceEventIO ("START template " ++ T.unpack filename)
parsed <- case fileinfo of
Right _ -> measure mstats ("parsing - " <> filename) $ lazyQuery filecache ufilename $ fmap encapsulateError (parseErbFile ufilename)
Left content -> measure mstats ("parsing - " <> filename) $ return $ encapsulateError (runParser erbparser () "inline" (T.unpack content))
o <- case parsed of
Left err -> do
let !msg = "template " ++ ufilename ++ " could not be parsed " ++ show (tgetError err)
traceEventIO msg
LOG.debugM "Erb.Compute" msg
measure mstats ("ruby - " <> filename) $ mkSafe $ computeTemplateWRuby fileinfo curcontext variables stt rdr
Right ast -> case rubyEvaluate variables curcontext ast of
Right ev -> return (S.Right ev)
Left err -> do
let !msg = "template " ++ ufilename ++ " evaluation failed " ++ show err
traceEventIO msg
LOG.debugM "Erb.Compute" msg
measure mstats ("ruby efail - " <> filename) $ mkSafe $ computeTemplateWRuby fileinfo curcontext variables stt rdr
traceEventIO ("STOP template " ++ T.unpack filename)
return o
getRubyScriptPath :: String -> IO String
getRubyScriptPath rubybin = do
let checkpath :: FilePath -> IO FilePath -> IO FilePath
checkpath fp nxt = do
e <- fileExist fp
if e
then return fp
else nxt
withExecutablePath = do
path <- fmap (T.unpack . takeDirectory . T.pack) getExecutablePath
let fullpath = path <> "/" <> rubybin
checkpath fullpath $ checkpath ("/usr/local/bin/" <> rubybin) (return rubybin)
cabalPath <- getDataFileName $ "ruby/" ++ rubybin :: IO FilePath
checkpath cabalPath withExecutablePath
-- This must be called from the proper thread. As this is callback, this
-- should be ok.
hrresolveVariable :: RValue -> RValue -> RValue -> RValue -> IO RValue
-- T.Text -> Container PValue -> RValue -> RValue -> IO RValue
hrresolveVariable _ rscp rvariables rtoresolve = do
scope <- FR.extractHaskellValue rscp
variables <- FR.extractHaskellValue rvariables
toresolve <- FR.fromRuby rtoresolve
let answer = case toresolve of
Right "~g~e~t_h~a~s~h~" ->
let getvars ctx = (variables ^. ix ctx . scopeVariables) & traverse %~ view (_1 . _1)
vars = getvars "::" <> getvars scope
in Right (PHash vars)
Right t -> getVariable variables scope t
Left rr -> Left ("The variable name is not a string" <+> text rr)
case answer of
Left _ -> getSymbol "undef"
Right r -> FR.toRuby r
hrcallfunction :: RValue -> RValue -> RValue -> RValue -> RValue -> IO RValue
hrcallfunction _ rfname rargs rstt rrdr = do
efname <- FR.fromRuby rfname
eargs <- FR.fromRuby rargs
rdr <- FR.extractHaskellValue rrdr
stt <- FR.extractHaskellValue rstt
let err :: String -> IO RValue
err rr = fmap (either Prelude.snd id) (FR.toRuby (T.pack rr) >>= FR.safeMethodCall "MyError" "new" . (:[]))
case (,) <$> efname <*> eargs of
Right (fname, varray) | fname `elem` ["template", "inline_template"] -> err "Can't call template from a Ruby function, as this will stall (yes it sucks ...)"
| otherwise -> do
let args = case varray of
[PArray vargs] -> V.toList vargs
_ -> varray
(x,_,_) <- interpretMonad rdr stt (resolveFunction' fname args)
case x of
Right o -> case o ^? _Number of
Just n -> FR.toRuby n
Nothing -> FR.toRuby o
Left rr -> err (show rr)
Left rr -> err rr
computeTemplateWRuby :: Either T.Text T.Text -> T.Text -> Container ScopeInformation -> InterpreterState -> InterpreterReader IO -> IO TemplateAnswer
computeTemplateWRuby fileinfo curcontext variables stt rdr = FR.freezeGC $ eitherDocIO $ do
rscp <- FR.embedHaskellValue curcontext
rvariables <- FR.embedHaskellValue variables
rstt <- FR.embedHaskellValue stt
rrdr <- FR.embedHaskellValue rdr
let varlist = variables ^. ix curcontext . scopeVariables
-- must be called from a "makeSafe" thingie
contentinfo <- case fileinfo of
Right fname -> FR.toRuby fname
Left _ -> FR.toRuby ("-" :: T.Text)
let withBinding f = do
erbBinding <- FR.safeMethodCall "ErbBinding" "new" [rscp,rvariables,rstt,rrdr,contentinfo]
case erbBinding of
Left x -> return (Left x)
Right v -> do
forM_ (itoList varlist) $ \(varname, varval :!: _ :!: _) -> FR.toRuby varval >>= FR.rb_iv_set v (T.unpack varname)
f v
o <- case fileinfo of
Right fname -> do
rfname <- FR.toRuby fname
withBinding $ \v -> FR.safeMethodCall "Controller" "runFromFile" [rfname,v]
Left content -> withBinding $ \v -> FR.toRuby content >>= FR.safeMethodCall "Controller" "runFromContent" . (:[v])
FR.freeHaskellValue rrdr
FR.freeHaskellValue rstt
FR.freeHaskellValue rvariables
FR.freeHaskellValue rscp
case o of
Left (rr, _) ->
let fname = case fileinfo of
Right f -> T.unpack f
Left _ -> "inline_template"
in return (S.Left $ PrettyError (dullred (text rr) <+> "in" <+> dullgreen (text fname)))
Right r -> FR.fromRuby r >>= \case
Right result -> return (S.Right result)
Left rr -> return (S.Left $ PrettyError ("Could not deserialiaze ruby output" <+> text rr))
eitherDocIO :: IO (S.Either PrettyError a) -> IO (S.Either PrettyError a)
eitherDocIO computation = (computation >>= check) `catch` (\e -> return $ S.Left $ PrettyError $ dullred $ text $ show (e :: SomeException))
where
check (S.Left r) = return (S.Left r)
check (S.Right x) = return (S.Right x)