{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module Erb.Compute (
initTemplateDaemon
) where
import Puppet.Prelude
import Data.Aeson.Lens (_Number)
import qualified Data.Either.Strict as S
import qualified Data.FileCache as Cache
import qualified Data.List as List
import Data.String
import qualified Data.Text as Text
import Data.Tuple.Strict (Pair (..))
import qualified Data.Vector as V
import Debug.Trace
import Foreign.Ruby
import qualified Foreign.Ruby.Bindings as FR
import qualified Foreign.Ruby.Helpers as FR
import GHC.Conc (labelThread)
import Paths_language_puppet (getDataFileName)
import System.Environment (getExecutablePath)
import System.Posix.Files
import Text.Parsec hiding (string)
import Text.Parsec.Error
import Text.Parsec.Pos
import Erb.Evaluate
import Erb.Parser
import Erb.Ruby
import Puppet.Interpreter.IO
import Puppet.Interpreter.Resolve
import Puppet.Interpreter.Types
import Puppet.PP
import Puppet.Preferences
import Puppet.Stats
instance IsString TemplateParseError where
fromString s = TemplateParseError $ newErrorMessage (Message s) (initialPos "dummy")
newtype TemplateParseError = TemplateParseError { tgetError :: ParseError }
type TemplateQuery = (Chan TemplateAnswer, Either Text Text, InterpreterState, InterpreterReader IO)
type TemplateAnswer = S.Either PrettyError 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 Text Text -> InterpreterState -> InterpreterReader IO -> IO (S.Either PrettyError Text))
initTemplateDaemon intr prefs mvstats = do
controlchan <- newChan
templatecache <- Cache.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
(Text.pack (prefs ^. prefPuppetPaths.modulesPath))
(Text.pack (prefs ^. prefPuppetPaths.templatesPath))
controlchan
mvstats
templatecache
pure $! templateQuery controlchan
either returnError return x
templateQuery :: Chan TemplateQuery -> Either Text Text -> InterpreterState -> InterpreterReader IO -> IO (S.Either PrettyError Text)
templateQuery qchan filename stt rdr = do
rchan <- newChan
writeChan qchan (rchan, filename, stt, rdr)
readChan rchan
templateDaemon :: RubyInterpreter -> Text -> Text -> Chan TemplateQuery -> MStats -> Cache.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 = Text.splitOn "/" filename
searchpathes | length prts > 1 = [modpath <> "/" <> List.head prts <> "/templates/" <> Text.intercalate "/" (List.tail prts), templatepath <> "/" <> filename]
| otherwise = [templatepath <> "/" <> filename]
acceptablefiles <- filterM (fileExist . Text.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 (List.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 Text Text -> InterpreterState -> InterpreterReader IO -> MStats -> Cache.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, Text.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 " ++ Text.unpack filename)
parsed <- case fileinfo of
Right _ -> measure mstats ("parsing - " <> filename) $ Cache.lazyQuery filecache ufilename $ fmap encapsulateError (parseErbFile ufilename)
Left content -> measure mstats ("parsing - " <> filename) $ return $ encapsulateError (runParser erbparser () "inline" (Text.unpack content))
o <- case parsed of
Left err -> do
let msg = "Template '" <> toS ufilename <> "' could not be parsed " <> show (tgetError err)
logDebug 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 '" <> toS ufilename <> "' evaluation failed with: " <> show err
logDebug msg
measure mstats ("ruby efail - " <> filename) $ mkSafe $ computeTemplateWRuby fileinfo curcontext variables stt rdr
traceEventIO ("STOP template " ++ Text.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 (Text.unpack . takeDirectory . Text.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
-- 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 snd identity) (FR.toRuby (Text.pack rr) >>= FR.safeMethodCall "MyError" "new" . (:[]))
case (,) <$> efname <*> eargs of
Right (fname, varray) | fname `elem` ["template", "inline_template"] -> do
logError $ "Can't parse a call to the external ruby function '" <> fname <> "' n an erb file.\n\tIt is not possible to call it from a Ruby function. It would stall (yes it sucks ...).\n\tChoosing to output \"undef\" !"
getSymbol "undef"
| 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 Text Text -> 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 ("-" :: 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 (Text.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 -> Text.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)