1 {-# LANGUAGE FlexibleInstances #-}
3 module Network.AMQP.Utils.Helpers where
5 import Control.Concurrent
6 import qualified Control.Exception as X
8 import qualified Data.ByteString.Lazy.Char8 as BL
9 import qualified Data.ByteString.UTF8 as BS
10 import Data.Int (Int64)
12 import qualified Data.Map as M
14 import qualified Data.Text as T
16 import Data.Time.Clock.POSIX
17 import Data.Word (Word16)
19 import Network.AMQP.Types
20 import Network.AMQP.Utils.Options
21 import Network.Socket (PortNumber)
22 import System.Directory (removeFile)
23 import System.Environment (getEnvironment)
28 -- | print config parameters
32 flexprint :: a -> IO ()
33 flexprint = (hPutStrLn stderr) . show
36 printparam :: String -> a -> IO ()
41 mapM_ (hPutStr stderr) [" --- ", label, ": "]
45 instance (Flexprint a) => Flexprint (Maybe a) where
47 printparam _ Nothing = return ()
48 printparam x (Just y) = printparam x y
50 instance Flexprint String where
51 flexprint = hPutStrLn stderr
54 instance Flexprint [String] where
55 flexprint = flexprint . unwords
58 instance Flexprint [Maybe String] where
59 flexprint = flexprint . catMaybes
60 empty = null . catMaybes
62 instance Flexprint T.Text where
63 flexprint = flexprint . T.unpack
66 instance Flexprint BL.ByteString where
67 flexprint x = hPutStrLn stderr "" >> BL.hPut stderr x >> hPutStrLn stderr ""
70 instance Flexprint Bool where
73 instance Flexprint Int
75 instance Flexprint Int64
77 instance Flexprint Word16
79 instance Flexprint ExitCode
81 instance Flexprint X.SomeException
83 instance Flexprint X.IOException
85 instance Flexprint AMQPException
87 instance Flexprint ConfirmationResult
89 instance Flexprint PortNumber
93 hr x = hPutStrLn stderr hr' >> hFlush stderr
95 hr' = take 72 $ (take 25 hr'') ++ " " ++ x ++ " " ++ hr''
98 -- | format headers for printing
99 formatheaders :: ((T.Text, FieldValue) -> [a]) -> FieldTable -> [a]
100 formatheaders f (FieldTable ll) = concat $ map f $ M.toList ll
102 -- | format headers for setting environment variables
104 ((Int, (T.Text, FieldValue)) -> [(String, String)])
106 -> [(String, String)]
107 formatheadersEnv f (FieldTable ll) = concat $ map f $ zip [0 ..] $ M.toList ll
110 fieldshow :: (T.Text, FieldValue) -> String
111 fieldshow (k, v) = "\n " ++ T.unpack k ++ ": " ++ valueshow v
113 fieldshow' :: (T.Text, FieldValue) -> String
114 fieldshow' (k, v) = "\n " ++ T.unpack k ++ ": " ++ valueshow v
116 -- | callback cmdline formatting
117 fieldshowOpt :: (T.Text, FieldValue) -> [String]
118 fieldshowOpt (k, v) = ["-h", T.unpack k ++ "=" ++ valueshow v]
120 -- | environment variable formatting
121 fieldshowEnv :: (Int, (T.Text, FieldValue)) -> [(String, String)]
122 fieldshowEnv (n, (k, v)) =
123 [ ("AMQP_HEADER_KEY_" ++ nn, T.unpack k)
124 , ("AMQP_HEADER_VALUE_" ++ nn, valueshow v)
129 -- | showing a FieldValue
130 valueshow :: FieldValue -> String
131 valueshow (FVString value) = BS.toString value
132 valueshow (FVInt8 value) = show value
133 valueshow (FVInt16 value) = show value
134 valueshow (FVInt32 value) = show value
135 valueshow (FVInt64 value) = show value
136 valueshow (FVFloat value) = show value
137 valueshow (FVDouble value) = show value
138 valueshow (FVBool value) = show value
139 valueshow (FVFieldTable value) = (formatheaders fieldshow') value
140 valueshow value = show value
142 -- | skip showing body head if binary type
143 isimage :: Maybe String -> Bool
144 isimage Nothing = False
146 | isPrefixOf "application/xml" ctype = False
147 | isPrefixOf "application/json" ctype = False
148 | otherwise = any (flip isPrefixOf ctype) ["application", "image"]
150 -- | show the first bytes of message body
151 anriss' :: Maybe Int64 -> BL.ByteString -> BL.ByteString
157 -- | callback cmdline with optional parameters
158 printopt :: (String, Maybe String) -> [String]
159 printopt (_, Nothing) = []
160 printopt (opt, Just s) = [opt, s]
162 -- | prints header and head on stderr and returns
163 -- cmdline options and environment variables to callback
166 -> (Message, Envelope)
169 -> IO ([String], [(String, String)])
170 printmsg h (msg, envi) anR now = do
173 [ ("routing key", rkey)
174 , ("message-id", messageid)
175 , ("headers", headers)
176 , ("content-type", ctype)
177 , ("content-encoding", cenc)
178 , ("redelivered", redeliv)
179 , ("timestamp", timestamp'')
184 , ("user id", muserid)
185 , ("application id", mappid)
186 , ("cluster id", mclusterid)
187 , ("reply to", mreplyto)
188 , ("correlation id", mcorrid)
189 , ("expiration", mexp)
190 , ("delivery mode", mdelivmode)
192 printparam label anriss
193 mapM_ (\hdl -> BL.hPut hdl body >> hFlush hdl) h
194 oldenv <- getEnvironment
199 [ ("ROUTINGKEY", rkey)
200 , ("CONTENTTYPE", ctype)
202 , ("MSGID", messageid)
203 , ("TIMESTAMP", timestamp)
205 , ("REDELIVERED", redeliv)
208 , ("USERID", muserid)
210 , ("CLUSTERID", mclusterid)
211 , ("REPLYTO", mreplyto)
212 , ("CORRID", mcorrid)
213 , ("EXPIRATION", mexp)
214 , ("DELIVERYMODE", mdelivmode)
217 return (cmdline, environment)
219 step (_, Nothing) xs = xs
220 step (k, Just v) xs = ("AMQP_" ++ k, v) : xs
234 headers = fmap (formatheaders fieldshow) $ msgHeaders msg
236 maybeToList $ fmap (formatheaders fieldshowOpt) $ msgHeaders msg
238 concat . maybeToList $
239 fmap (formatheadersEnv fieldshowEnv) $ msgHeaders msg
244 else Just (anriss' anR body) :: Maybe BL.ByteString
245 anriss'' = maybe "" (\a -> "first " ++ (show a) ++ " bytes of ") anR
246 label = anriss'' ++ "body"
247 ctype = fmap T.unpack $ msgContentType msg
248 cenc = fmap T.unpack $ msgContentEncoding msg
249 rkey = Just . T.unpack $ envRoutingKey envi
250 messageid = fmap T.unpack $ msgID msg
251 pri = fmap show $ msgPriority msg
252 mtype = fmap T.unpack $ msgType msg
253 muserid = fmap T.unpack $ msgUserID msg
254 mappid = fmap T.unpack $ msgApplicationID msg
255 mclusterid = fmap T.unpack $ msgClusterID msg
256 mreplyto = fmap T.unpack $ msgReplyTo msg
257 mcorrid = fmap T.unpack $ msgCorrelationID msg
258 mexp = fmap T.unpack $ msgExpiration msg
259 mdelivmode = fmap show $ msgDeliveryMode msg
260 size = Just . show $ BL.length body
262 if envRedelivered envi
265 tz = zonedTimeZone now
266 nowutc = zonedTimeToUTCFLoor now
267 msgtime = msgTimestamp msg
268 msgtimeutc = fmap (posixSecondsToUTCTime . realToFrac) msgtime
269 timestamp = fmap show msgtime
270 timediff = fmap (difftime nowutc) msgtimeutc
273 Just "now" -> Nothing
274 _ -> showtime tz $ Just nowutc
275 timestamp' = showtime tz msgtimeutc
278 (\a b c -> a ++ " (" ++ b ++ ") (" ++ c ++ ")")
283 -- | timestamp conversion
284 zonedTimeToUTCFLoor :: ZonedTime -> UTCTime
285 zonedTimeToUTCFLoor x =
286 posixSecondsToUTCTime $
287 realToFrac ((floor . utcTimeToPOSIXSeconds . zonedTimeToUTC) x :: Timestamp)
289 -- | show the timestamp
290 showtime :: TimeZone -> Maybe UTCTime -> Maybe String
291 showtime tz = fmap (show . (utcToZonedTime tz))
293 -- | show difference between two timestamps
294 difftime :: UTCTime -> UTCTime -> String
297 | now > msg = diff ++ " ago"
298 | otherwise = diff ++ " in the future"
300 diff = show (diffUTCTime now msg)
302 -- | if the message is to be saved
303 -- and maybe processed further
311 -> Maybe (ExitCode -> BL.ByteString -> IO ())
312 -> [(String, String)]
314 optionalFileStuff (msg, envi) callbackoptions addi numstring a tid action environment = do
315 path <- saveFile (tempDir a) numstring (msgBody msg)
316 printparam "saved to" path
317 let callbackcmdline =
319 (constructCallbackCmdLine (simple a) callbackoptions addi numstring)
322 printparam "calling" callbackcmdline
327 (doProc a numstring envi c action path environment)
328 (either (throwTo tid) return) >>
332 -- | save message into temp file
333 saveFile :: Maybe String -> String -> BL.ByteString -> IO (Maybe String)
334 saveFile Nothing _ _ = return Nothing
335 saveFile (Just tempD) numstring body = do
337 openBinaryTempFileWithDefaultPermissions
339 ("amqp-utils-" ++ numstring ++ "-.tmp")
344 -- | construct cmdline for callback script
345 constructCallbackCmdLine ::
346 Bool -> [String] -> [String] -> String -> String -> String -> [String]
347 constructCallbackCmdLine True _ addi _ exe path = exe : addi ++ path : []
348 constructCallbackCmdLine False opts addi num exe path =
349 exe : "-f" : path : "-n" : num : opts ++ addi
351 -- | call callback script
357 -> Maybe (ExitCode -> BL.ByteString -> IO ())
359 -> [(String, String)]
361 doProc a numstring envi (exe:args) action path environment = do
362 (_, h, _, processhandle) <-
365 {std_out = out, std_err = Inherit, env = Just environment'}
366 sout <- mapM BL.hGetContents h
368 maybe 0 id (fmap BL.length sout) `seq` waitForProcess processhandle
369 printparam (numstring ++ " call returned") exitcode
370 if isJust action && isJust sout
371 then ((fromJust action $ exitcode) (fromJust sout)) >> acke envi a
372 else case exitcode of
373 ExitSuccess -> acke envi a
374 ExitFailure _ -> reje envi a
375 if (cleanupTmpFile a)
377 (maybe (return ()) removeFile path)
378 (\e -> printparam "error removing temp file" (e :: X.IOException))
386 ("AMQP_NUMBER", numstring) : ("AMQP_FILE", fromJust path) : environment
387 doProc _ _ _ _ _ _ _ = return ()
390 acke :: Envelope -> Args -> IO ()
392 | (ack a) = ackEnv envi
393 | otherwise = return ()
396 reje :: Envelope -> Args -> IO ()
398 | (ack a) = rejectEnv envi (requeuenack a)
399 | otherwise = return ()
401 -- | main loop: sleep forever or wait for an exception
402 sleepingBeauty :: IO (X.SomeException)
405 (forever (threadDelay 600000000) >>
406 return (X.toException $ X.ErrorCall "not reached"))
409 -- | extract first input file in case only one is needed
410 firstInputFile :: [(String,String,String)] -> String
411 firstInputFile [] = "-"
412 firstInputFile ((x,_,_):_) = x