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