1 -- SPDX-FileCopyrightText: 2022 Frank Doepper
3 -- SPDX-License-Identifier: GPL-3.0-only
5 {-# LANGUAGE FlexibleInstances #-}
6 {-# LANGUAGE OverloadedStrings #-}
8 module Network.AMQP.Utils.Helpers where
10 import Control.Concurrent
11 import qualified Control.Exception as X
13 import qualified Data.ByteString.Char8 as BS
14 import qualified Data.ByteString.Lazy.Char8 as BL
15 import qualified Data.ByteString.UTF8 as BU
16 import Data.Int (Int64)
18 import qualified Data.Map as M
20 import qualified Data.Text as T
22 import Data.Time.Clock.POSIX
23 import Data.Word (Word16)
25 import Network.AMQP.Types
26 import Network.AMQP.Utils.Options
27 import Network.Socket (PortNumber)
28 import System.Directory (removeFile)
29 import System.Environment (getEnvironment)
31 import System.FilePath.Posix.ByteString (RawFilePath)
33 import System.Posix.IO.ByteString
36 -- | print config parameters
40 flexprint :: a -> IO ()
41 flexprint = (hPutStrLn stderr) . show
44 printparam :: String -> a -> IO ()
49 mapM_ (hPutStr stderr) [" --- ", label, ": "]
53 instance (Flexprint a) => Flexprint (Maybe a) where
55 printparam _ Nothing = return ()
56 printparam x (Just y) = printparam x y
58 instance Flexprint BS.ByteString where
59 flexprint = BS.hPutStrLn stderr
62 instance Flexprint String where
63 flexprint = hPutStrLn stderr
66 instance Flexprint [BS.ByteString] where
67 flexprint = flexprint . BS.unwords
70 instance Flexprint [String] where
71 flexprint = flexprint . unwords
74 instance Flexprint [Maybe BS.ByteString] where
75 flexprint = flexprint . catMaybes
76 empty = null . catMaybes
78 instance Flexprint [Maybe String] where
79 flexprint = flexprint . catMaybes
80 empty = null . catMaybes
82 instance Flexprint T.Text where
83 flexprint = flexprint . T.unpack
86 instance Flexprint BL.ByteString where
87 flexprint x = hPutStrLn stderr "" >> BL.hPut stderr x >> hPutStrLn stderr ""
90 instance Flexprint Bool where
93 instance Flexprint Int
95 instance Flexprint Int64
97 instance Flexprint Word16
99 instance Flexprint ExitCode
101 instance Flexprint X.SomeException
103 instance Flexprint X.IOException
105 instance Flexprint AMQPException
107 instance Flexprint ConfirmationResult
109 instance Flexprint PortNumber
112 hr :: String -> IO ()
113 hr x = hPutStrLn stderr hr' >> hFlush stderr
115 hr' = take 72 $ (take 25 hr'') ++ " " ++ x ++ " " ++ hr''
118 -- | format headers for printing
119 formatheaders :: ((T.Text, FieldValue) -> [a]) -> FieldTable -> [a]
120 formatheaders f (FieldTable ll) = concat $ map f $ M.toList ll
122 -- | format headers for setting environment variables
124 ((Int, (T.Text, FieldValue)) -> [(String, String)])
126 -> [(String, String)]
127 formatheadersEnv f (FieldTable ll) = concat $ map f $ zip [0 ..] $ M.toList ll
130 fieldshow :: (T.Text, FieldValue) -> String
131 fieldshow (k, v) = "\n " ++ T.unpack k ++ ": " ++ valueshow v
133 fieldshow' :: (T.Text, FieldValue) -> String
134 fieldshow' (k, v) = "\n " ++ T.unpack k ++ ": " ++ valueshow v
136 -- | callback cmdline formatting
137 fieldshowOpt :: (T.Text, FieldValue) -> [String]
138 fieldshowOpt (k, v) = ["-h", T.unpack k ++ "=" ++ valueshow v]
140 -- | environment variable formatting
141 fieldshowEnv :: (Int, (T.Text, FieldValue)) -> [(String, String)]
142 fieldshowEnv (n, (k, v)) =
143 [ ("AMQP_HEADER_KEY_" ++ nn, T.unpack k)
144 , ("AMQP_HEADER_VALUE_" ++ nn, valueshow v)
149 -- | showing a FieldValue
150 valueshow :: FieldValue -> String
151 valueshow (FVString value) = BU.toString value
152 valueshow (FVInt8 value) = show value
153 valueshow (FVInt16 value) = show value
154 valueshow (FVInt32 value) = show value
155 valueshow (FVInt64 value) = show value
156 valueshow (FVFloat value) = show value
157 valueshow (FVDouble value) = show value
158 valueshow (FVBool value) = show value
159 valueshow (FVFieldTable value) = (formatheaders fieldshow') value
160 valueshow value = show value
162 -- | skip showing body head if binary type
163 isimage :: Maybe String -> Bool
164 isimage Nothing = False
166 | isPrefixOf "application/xml" ctype = False
167 | isPrefixOf "application/json" ctype = False
168 | otherwise = any (flip isPrefixOf ctype) ["application", "image"]
170 -- | show the first bytes of message body
171 anriss' :: Maybe Int64 -> BL.ByteString -> BL.ByteString
177 -- | callback cmdline with optional parameters
178 printopt :: (String, Maybe String) -> [String]
179 printopt (_, Nothing) = []
180 printopt (opt, Just s) = [opt, s]
182 -- | prints header and head on stderr and returns
183 -- cmdline options and environment variables to callback
186 -> (Message, Envelope)
189 -> IO ([String], [(String, String)])
190 printmsg h (msg, envi) anR now = do
193 [ ("routing key", rkey)
194 , ("message-id", messageid)
195 , ("headers", headers)
196 , ("content-type", ctype)
197 , ("content-encoding", cenc)
198 , ("redelivered", redeliv)
199 , ("timestamp", timestamp'')
204 , ("user id", muserid)
205 , ("application id", mappid)
206 , ("cluster id", mclusterid)
207 , ("reply to", mreplyto)
208 , ("correlation id", mcorrid)
209 , ("expiration", mexp)
210 , ("delivery mode", mdelivmode)
212 printparam label anriss
213 mapM_ (\hdl -> BL.hPut hdl body >> hFlush hdl) h
214 oldenv <- getEnvironment
219 [ ("ROUTINGKEY", rkey)
220 , ("CONTENTTYPE", ctype)
222 , ("MSGID", messageid)
223 , ("TIMESTAMP", timestamp)
225 , ("REDELIVERED", redeliv)
228 , ("USERID", muserid)
230 , ("CLUSTERID", mclusterid)
231 , ("REPLYTO", mreplyto)
232 , ("CORRID", mcorrid)
233 , ("EXPIRATION", mexp)
234 , ("DELIVERYMODE", mdelivmode)
237 return (cmdline, environment)
239 step (_, Nothing) xs = xs
240 step (k, Just v) xs = ("AMQP_" ++ k, v) : xs
254 headers = fmap (formatheaders fieldshow) $ msgHeaders msg
256 maybeToList $ fmap (formatheaders fieldshowOpt) $ msgHeaders msg
258 concat . maybeToList $
259 fmap (formatheadersEnv fieldshowEnv) $ msgHeaders msg
264 else Just (anriss' anR body) :: Maybe BL.ByteString
265 anriss'' = maybe "" (\a -> "first " ++ (show a) ++ " bytes of ") anR
266 label = anriss'' ++ "body"
267 ctype = fmap T.unpack $ msgContentType msg
268 cenc = fmap T.unpack $ msgContentEncoding msg
269 rkey = Just . T.unpack $ envRoutingKey envi
270 messageid = fmap T.unpack $ msgID msg
271 pri = fmap show $ msgPriority msg
272 mtype = fmap T.unpack $ msgType msg
273 muserid = fmap T.unpack $ msgUserID msg
274 mappid = fmap T.unpack $ msgApplicationID msg
275 mclusterid = fmap T.unpack $ msgClusterID msg
276 mreplyto = fmap T.unpack $ msgReplyTo msg
277 mcorrid = fmap T.unpack $ msgCorrelationID msg
278 mexp = fmap T.unpack $ msgExpiration msg
279 mdelivmode = fmap show $ msgDeliveryMode msg
280 size = Just . show $ BL.length body
282 if envRedelivered envi
285 tz = zonedTimeZone now
286 nowutc = zonedTimeToUTCFLoor now
287 msgtime = msgTimestamp msg
288 msgtimeutc = fmap (posixSecondsToUTCTime . realToFrac) msgtime
289 timestamp = fmap show msgtime
290 timediff = fmap (difftime nowutc) msgtimeutc
293 Just "now" -> Nothing
294 _ -> showtime tz $ Just nowutc
295 timestamp' = showtime tz msgtimeutc
298 (\a b c -> a ++ " (" ++ b ++ ") (" ++ c ++ ")")
303 -- | timestamp conversion
304 zonedTimeToUTCFLoor :: ZonedTime -> UTCTime
305 zonedTimeToUTCFLoor x =
306 posixSecondsToUTCTime $
307 realToFrac ((floor . utcTimeToPOSIXSeconds . zonedTimeToUTC) x :: Timestamp)
309 -- | show the timestamp
310 showtime :: TimeZone -> Maybe UTCTime -> Maybe String
311 showtime tz = fmap (show . (utcToZonedTime tz))
313 -- | show difference between two timestamps
314 difftime :: UTCTime -> UTCTime -> String
317 | now > msg = diff ++ " ago"
318 | otherwise = diff ++ " in the future"
320 diff = show (diffUTCTime now msg)
322 -- | if the message is to be saved
323 -- and maybe processed further
331 -> Maybe (ExitCode -> BL.ByteString -> IO ())
332 -> [(String, String)]
334 optionalFileStuff (msg, envi) callbackoptions addi numstring a tid action environment = do
335 path <- saveFile (tempDir a) numstring (msgBody msg)
336 printparam "saved to" path
337 let callbackcmdline =
339 (constructCallbackCmdLine (simple a) callbackoptions addi numstring)
342 printparam "calling" callbackcmdline
347 (doProc a numstring envi c action path environment)
348 (either (throwTo tid) return) >>
352 -- | save message into temp file
353 saveFile :: Maybe String -> String -> BL.ByteString -> IO (Maybe String)
354 saveFile Nothing _ _ = return Nothing
355 saveFile (Just tempD) numstring body = do
357 openBinaryTempFileWithDefaultPermissions
359 ("amqp-utils-" ++ numstring ++ "-.tmp")
364 -- | construct cmdline for callback script
365 constructCallbackCmdLine ::
366 Bool -> [String] -> [String] -> String -> String -> String -> [String]
367 constructCallbackCmdLine True _ addi _ exe path = exe : addi ++ path : []
368 constructCallbackCmdLine False opts addi num exe path =
369 exe : "-f" : path : "-n" : num : opts ++ addi
371 -- | call callback script
377 -> Maybe (ExitCode -> BL.ByteString -> IO ())
379 -> [(String, String)]
381 doProc a numstring envi (exe:args) action path environment = do
382 (_, h, _, processhandle) <-
385 {std_out = out, std_err = Inherit, env = Just environment'}
386 sout <- mapM BL.hGetContents h
388 maybe 0 id (fmap BL.length sout) `seq` waitForProcess processhandle
389 printparam (numstring ++ " call returned") exitcode
390 if isJust action && isJust sout
391 then ((fromJust action $ exitcode) (fromJust sout)) >> acke envi a
392 else case exitcode of
393 ExitSuccess -> acke envi a
394 ExitFailure _ -> reje envi a
395 if (cleanupTmpFile a)
397 (maybe (return ()) removeFile path)
398 (\e -> printparam "error removing temp file" (e :: X.IOException))
406 ("AMQP_NUMBER", numstring) : ("AMQP_FILE", fromJust path) : environment
407 doProc _ _ _ _ _ _ _ = return ()
410 acke :: Envelope -> Args -> IO ()
412 | (ack a) = ackEnv envi
413 | otherwise = return ()
416 reje :: Envelope -> Args -> IO ()
418 | (ack a) = rejectEnv envi (requeuenack a)
419 | otherwise = return ()
421 -- | main loop: sleep forever or wait for an exception
422 sleepingBeauty :: IO (X.SomeException)
425 (forever (threadDelay 600000000) >>
426 return (X.toException $ X.ErrorCall "not reached"))
429 -- | extract first input file in case only one is needed
430 firstInputFile :: [(RawFilePath,String,String)] -> RawFilePath
431 firstInputFile [] = "-"
432 firstInputFile ((x,_,_):_) = x
434 -- | read RawFilePath to Lazy ByteString
435 readFileRawLazy :: RawFilePath -> IO BL.ByteString
436 readFileRawLazy path = do
437 h <- openFd path ReadOnly defaultFlags >>= fdToHandle
438 hSetBinaryMode h True
441 defaultFlags = defaultFileFlags { noctty = True }