packages feed

amqp-utils-0.6.3.2: Network/AMQP/Utils/Helpers.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}

module Network.AMQP.Utils.Helpers where

import           Control.Concurrent
import qualified Control.Exception                as X
import           Control.Monad
import qualified Data.ByteString.Char8            as BS
import qualified Data.ByteString.Lazy.Char8       as BL
import qualified Data.ByteString.UTF8             as BU
import           Data.Int                         (Int64)
import           Data.List
import qualified Data.Map                         as M
import           Data.Maybe
import qualified Data.Text                        as T
import           Data.Time
import           Data.Time.Clock.POSIX
import           Data.Word                        (Word16)
import           Network.AMQP
import           Network.AMQP.Types
import           Network.AMQP.Utils.Options
import           Network.Socket                   (PortNumber)
import           System.Directory                 (removeFile)
import           System.Environment               (getEnvironment)
import           System.Exit
import           System.FilePath.Posix.ByteString (RawFilePath)
import           System.IO
import           System.Posix.IO.ByteString
import           System.Process

-- | print config parameters
class (Show a) =>
      Flexprint a
  where
  flexprint :: a -> IO ()
  flexprint = (hPutStrLn stderr) . show
  empty :: a -> Bool
  empty _ = False
  printparam :: String -> a -> IO ()
  printparam label x =
    if empty x
      then return ()
      else do
        mapM_ (hPutStr stderr) [" --- ", label, ": "]
        flexprint x
        hFlush stderr

instance (Flexprint a) => Flexprint (Maybe a) where
  empty = isNothing
  printparam _ Nothing  = return ()
  printparam x (Just y) = printparam x y

instance Flexprint BS.ByteString where
  flexprint = BS.hPutStrLn stderr
  empty = BS.null

instance Flexprint String where
  flexprint = hPutStrLn stderr
  empty = null

instance Flexprint [BS.ByteString] where
  flexprint = flexprint . BS.unwords
  empty = null

instance Flexprint [String] where
  flexprint = flexprint . unwords
  empty = null

instance Flexprint [Maybe BS.ByteString] where
  flexprint = flexprint . catMaybes
  empty = null . catMaybes

instance Flexprint [Maybe String] where
  flexprint = flexprint . catMaybes
  empty = null . catMaybes

instance Flexprint T.Text where
  flexprint = flexprint . T.unpack
  empty = T.null

instance Flexprint BL.ByteString where
  flexprint x = hPutStrLn stderr "" >> BL.hPut stderr x >> hPutStrLn stderr ""
  empty = BL.null

instance Flexprint Bool where
  empty = not

instance Flexprint Int

instance Flexprint Int64

instance Flexprint Word16

instance Flexprint ExitCode

instance Flexprint X.SomeException

instance Flexprint X.IOException

instance Flexprint AMQPException

instance Flexprint ConfirmationResult

instance Flexprint PortNumber

-- | log marker
hr :: String -> IO ()
hr x = hPutStrLn stderr hr' >> hFlush stderr
  where
    hr' = take 72 $ (take 25 hr'') ++ " " ++ x ++ " " ++ hr''
    hr'' = repeat '-'

-- | format headers for printing
formatheaders :: ((T.Text, FieldValue) -> [a]) -> FieldTable -> [a]
formatheaders f (FieldTable ll) = concat $ map f $ M.toList ll

-- | format headers for setting environment variables
formatheadersEnv ::
     ((Int, (T.Text, FieldValue)) -> [(String, String)])
  -> FieldTable
  -> [(String, String)]
formatheadersEnv f (FieldTable ll) = concat $ map f $ zip [0 ..] $ M.toList ll

-- | log formatting
fieldshow :: (T.Text, FieldValue) -> String
fieldshow (k, v) = "\n        " ++ T.unpack k ++ ": " ++ valueshow v

fieldshow' :: (T.Text, FieldValue) -> String
fieldshow' (k, v) = "\n           " ++ T.unpack k ++ ": " ++ valueshow v

-- | callback cmdline formatting
fieldshowOpt :: (T.Text, FieldValue) -> [String]
fieldshowOpt (k, v) = ["-h", T.unpack k ++ "=" ++ valueshow v]

-- | environment variable formatting
fieldshowEnv :: (Int, (T.Text, FieldValue)) -> [(String, String)]
fieldshowEnv (n, (k, v)) =
  [ ("AMQP_HEADER_KEY_" ++ nn, T.unpack k)
  , ("AMQP_HEADER_VALUE_" ++ nn, valueshow v)
  ]
  where
    nn = show n

-- | showing a FieldValue
valueshow :: FieldValue -> String
valueshow (FVString value)     = BU.toString value
valueshow (FVInt8 value)       = show value
valueshow (FVInt16 value)      = show value
valueshow (FVInt32 value)      = show value
valueshow (FVInt64 value)      = show value
valueshow (FVFloat value)      = show value
valueshow (FVDouble value)     = show value
valueshow (FVBool value)       = show value
valueshow (FVFieldTable value) = (formatheaders fieldshow') value
valueshow value                = show value

-- | skip showing body head if binary type
isimage :: Maybe String -> Bool
isimage Nothing = False
isimage (Just ctype)
  | isPrefixOf "application/xml" ctype = False
  | isPrefixOf "application/json" ctype = False
  | otherwise = any (flip isPrefixOf ctype) ["application", "image"]

-- | show the first bytes of message body
anriss' :: Maybe Int64 -> BL.ByteString -> BL.ByteString
anriss' x =
  case x of
    Nothing -> id
    Just y  -> BL.take y

-- | callback cmdline with optional parameters
printopt :: (String, Maybe String) -> [String]
printopt (_, Nothing)  = []
printopt (opt, Just s) = [opt, s]

-- | prints header and head on stderr and returns
-- cmdline options and environment variables to callback
printmsg ::
     Maybe Handle
  -> (Message, Envelope)
  -> Maybe Int64
  -> ZonedTime
  -> IO ([String], [(String, String)])
printmsg h (msg, envi) anR now = do
  mapM_
    (uncurry printparam)
    [ ("routing key", rkey)
    , ("message-id", messageid)
    , ("headers", headers)
    , ("content-type", ctype)
    , ("content-encoding", cenc)
    , ("redelivered", redeliv)
    , ("timestamp", timestamp'')
    , ("time now", now')
    , ("size", size)
    , ("priority", pri)
    , ("type", mtype)
    , ("user id", muserid)
    , ("application id", mappid)
    , ("cluster id", mclusterid)
    , ("reply to", mreplyto)
    , ("correlation id", mcorrid)
    , ("expiration", mexp)
    , ("delivery mode", mdelivmode)
    ]
  printparam label anriss
  mapM_ (\hdl -> BL.hPut hdl body >> hFlush hdl) h
  oldenv <- getEnvironment
  let environment =
        foldr
          step
          oldenv
          [ ("ROUTINGKEY", rkey)
          , ("CONTENTTYPE", ctype)
          , ("ENCODING", cenc)
          , ("MSGID", messageid)
          , ("TIMESTAMP", timestamp)
          , ("PRIORITY", pri)
          , ("REDELIVERED", redeliv)
          , ("SIZE", size)
          , ("TYPE", mtype)
          , ("USERID", muserid)
          , ("APPID", mappid)
          , ("CLUSTERID", mclusterid)
          , ("REPLYTO", mreplyto)
          , ("CORRID", mcorrid)
          , ("EXPIRATION", mexp)
          , ("DELIVERYMODE", mdelivmode)
          ] ++
        headersEnv
  return (cmdline, environment)
  where
    step (_, Nothing) xs = xs
    step (k, Just v) xs  = ("AMQP_" ++ k, v) : xs
    cmdline =
      concat
        (map
           printopt
           [ ("-r", rkey)
           , ("-m", ctype)
           , ("-e", cenc)
           , ("-i", messageid)
           , ("-t", timestamp)
           , ("-p", pri)
           , ("-R", redeliv)
           ] ++
         headersOpt)
    headers = fmap (formatheaders fieldshow) $ msgHeaders msg
    headersOpt =
      maybeToList $ fmap (formatheaders fieldshowOpt) $ msgHeaders msg
    headersEnv =
      concat . maybeToList $
      fmap (formatheadersEnv fieldshowEnv) $ msgHeaders msg
    body = msgBody msg
    anriss =
      if isimage ctype
        then Nothing
        else Just (anriss' anR body) :: Maybe BL.ByteString
    anriss'' = maybe "" (\a -> "first " ++ (show a) ++ " bytes of ") anR
    label = anriss'' ++ "body"
    ctype = fmap T.unpack $ msgContentType msg
    cenc = fmap T.unpack $ msgContentEncoding msg
    rkey = Just . T.unpack $ envRoutingKey envi
    messageid = fmap T.unpack $ msgID msg
    pri = fmap show $ msgPriority msg
    mtype = fmap T.unpack $ msgType msg
    muserid = fmap T.unpack $ msgUserID msg
    mappid = fmap T.unpack $ msgApplicationID msg
    mclusterid = fmap T.unpack $ msgClusterID msg
    mreplyto = fmap T.unpack $ msgReplyTo msg
    mcorrid = fmap T.unpack $ msgCorrelationID msg
    mexp = fmap T.unpack $ msgExpiration msg
    mdelivmode = fmap show $ msgDeliveryMode msg
    size = Just . show $ BL.length body
    redeliv =
      if envRedelivered envi
        then Just "YES"
        else Nothing
    tz = zonedTimeZone now
    nowutc = zonedTimeToUTCFLoor now
    msgtime = msgTimestamp msg
    msgtimeutc = fmap (posixSecondsToUTCTime . realToFrac) msgtime
    timestamp = fmap show msgtime
    timediff = fmap (difftime nowutc) msgtimeutc
    now' =
      case timediff of
        Just "now" -> Nothing
        _          -> showtime tz $ Just nowutc
    timestamp' = showtime tz msgtimeutc
    timestamp'' =
      liftM3
        (\a b c -> a ++ " (" ++ b ++ ") (" ++ c ++ ")")
        timestamp
        timestamp'
        timediff

-- | timestamp conversion
zonedTimeToUTCFLoor :: ZonedTime -> UTCTime
zonedTimeToUTCFLoor x =
  posixSecondsToUTCTime $
  realToFrac ((floor . utcTimeToPOSIXSeconds . zonedTimeToUTC) x :: Timestamp)

-- | show the timestamp
showtime :: TimeZone -> Maybe UTCTime -> Maybe String
showtime tz = fmap (show . (utcToZonedTime tz))

-- | show difference between two timestamps
difftime :: UTCTime -> UTCTime -> String
difftime now msg
  | now == msg = "now"
  | now > msg = diff ++ " ago"
  | otherwise = diff ++ " in the future"
  where
    diff = show (diffUTCTime now msg)

-- | if the message is to be saved
-- and maybe processed further
optionalFileStuff ::
     (Message, Envelope)
  -> [String]
  -> [String]
  -> String
  -> Args
  -> ThreadId
  -> Maybe (ExitCode -> BL.ByteString -> IO ())
  -> [(String, String)]
  -> IO ()
optionalFileStuff (msg, envi) callbackoptions addi numstring a tid action environment = do
  path <- saveFile (tempDir a) numstring (msgBody msg)
  printparam "saved to" path
  let callbackcmdline =
        liftM2
          (constructCallbackCmdLine (simple a) callbackoptions addi numstring)
          (fileProcess a)
          path
  printparam "calling" callbackcmdline
  maybe
    (acke envi a)
    (\c ->
       forkFinally
         (doProc a numstring envi c action path environment)
         (either (throwTo tid) return) >>
       return ())
    callbackcmdline

-- | save message into temp file
saveFile :: Maybe String -> String -> BL.ByteString -> IO (Maybe String)
saveFile Nothing _ _ = return Nothing
saveFile (Just tempD) numstring body = do
  (p, h) <-
    openBinaryTempFileWithDefaultPermissions
      tempD
      ("amqp-utils-" ++ numstring ++ "-.tmp")
  BL.hPut h body
  hClose h
  return $ Just p

-- | construct cmdline for callback script
constructCallbackCmdLine ::
     Bool -> [String] -> [String] -> String -> String -> String -> [String]
constructCallbackCmdLine True _ addi _ exe path = exe : addi ++ path : []
constructCallbackCmdLine False opts addi num exe path =
  exe : "-f" : path : "-n" : num : opts ++ addi

-- | call callback script
doProc ::
     Args
  -> String
  -> Envelope
  -> [String]
  -> Maybe (ExitCode -> BL.ByteString -> IO ())
  -> Maybe String
  -> [(String, String)]
  -> IO ()
doProc a numstring envi (exe:args) action path environment = do
  (_, h, _, processhandle) <-
    createProcess
      (proc exe args)
        {std_out = out, std_err = Inherit, env = Just environment'}
  sout <- mapM BL.hGetContents h
  exitcode <-
    maybe 0 id (fmap BL.length sout) `seq` waitForProcess processhandle
  printparam (numstring ++ " call returned") exitcode
  if isJust action && isJust sout
    then ((fromJust action $ exitcode) (fromJust sout)) >> acke envi a
    else case exitcode of
           ExitSuccess   -> acke envi a
           ExitFailure _ -> reje envi a
  if (cleanupTmpFile a)
    then X.catch
           (maybe (return ()) removeFile path)
           (\e -> printparam "error removing temp file" (e :: X.IOException))
    else return ()
  where
    out =
      if isJust action
        then CreatePipe
        else Inherit
    environment' =
      ("AMQP_NUMBER", numstring) : ("AMQP_FILE", fromJust path) : environment
doProc _ _ _ _ _ _ _ = return ()

-- | ack
acke :: Envelope -> Args -> IO ()
acke envi a
  | (ack a) = ackEnv envi
  | otherwise = return ()

-- | reject
reje :: Envelope -> Args -> IO ()
reje envi a
  | (ack a) = rejectEnv envi (requeuenack a)
  | otherwise = return ()

-- | main loop: sleep forever or wait for an exception
sleepingBeauty :: IO (X.SomeException)
sleepingBeauty =
  X.catch
    (forever (threadDelay 600000000) >>
     return (X.toException $ X.ErrorCall "not reached"))
    return

-- | extract first input file in case only one is needed
firstInputFile :: [(RawFilePath,String,String)] -> RawFilePath
firstInputFile []          = "-"
firstInputFile ((x,_,_):_) = x

-- | read RawFilePath to Lazy ByteString
readFileRawLazy :: RawFilePath -> IO BL.ByteString
readFileRawLazy path = do
  h <- openFd path ReadOnly Nothing defaultFlags >>= fdToHandle
  hSetBinaryMode h True
  BL.hGetContents h
  where
    defaultFlags = OpenFileFlags { System.Posix.IO.ByteString.append = False
                                 , exclusive = False
                                 , noctty = True
                                 , nonBlock = False
                                 , trunc = False }