packages feed

language-puppet-1.3.13: src/Erb/Compute.hs

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