packages feed

net-mqtt-0.8.5.0: app/mqtt-watch/Main.hs

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import           Control.Concurrent         (threadDelay)
import           Control.Concurrent.Async   (async, link)
import           Control.Concurrent.STM     (TChan, atomically, newTChanIO, readTChan, writeTChan)
import           Control.Exception          (Handler (..), IOException, catches)
import           Control.Monad              (foldM_, forever, when, (<=<))
import qualified Data.ByteString.Lazy       as BL
import qualified Data.ByteString.Lazy.Char8 as BCS
import qualified Data.IORef                 as R
import           Data.Maybe                 (fromJust)
import qualified Data.Text                  as T
import qualified Data.Text.IO               as TIO
import           Data.Word                  (Word32)
import           Network.MQTT.Client
import           Network.MQTT.Topic         (Filter, mkFilter, unTopic)
import           Network.MQTT.Types         (ConnACKFlags (..), SessionReuse (..), qosFromInt)
import           Network.URI
import           Options.Applicative        (Parser, argument, auto, eitherReader, execParser, fullDesc, help, helper,
                                             info, long, maybeReader, metavar, option, progDesc, short, showDefault,
                                             some, switch, value, (<**>))
import           System.IO                  (stdout)
import           Text.Read                  (readMaybe)

data Msg = Msg Topic BL.ByteString [Property]

data Options = Options {
  optUri           :: URI
  , optHideProps   :: Bool
  , optSessionTime :: Word32
  , optVerbose     :: Bool
  , optQoS         :: QoS
  , optSubResume   :: Bool
  , optTopics      :: [Filter]
  }

options :: Parser Options
options = Options
  <$> option (maybeReader parseURI) (long "mqtt-uri" <> short 'u' <> showDefault <> value (fromJust $ parseURI "mqtt://localhost/") <> help "mqtt broker URI")
  <*> switch (short 'p' <> help "hide properties")
  <*> option auto (long "session-timeout" <> showDefault <> value 300 <> help "mqtt session timeout (0 == clean)")
  <*> switch (short 'v' <> long "verbose" <> help "enable debug logging")
  <*> option (eitherReader pQoS) (long "qos" <> short 'q' <> showDefault <> value QoS0 <> help "QoS level (0-2)")
  <*> switch (long "always-subscribe" <> help "subscribe even when resuming a connection")
  <*> some (argument (maybeReader (mkFilter . T.pack)) (metavar "topics..."))

  where
    pQoS = maybe (Left "Only QoS 0, 1, and 2 are supported") Right . (qosFromInt <=< readMaybe)

printer :: TChan Msg -> Bool -> Bool -> IO ()
printer ch showProps verbose = forever $ do
  (Msg t m props) <- atomically $ readTChan ch
  TIO.putStr $ mconcat [unTopic t, " → "]
  BL.hPut stdout m
  putStrLn ""
  mapM_ (putStrLn . ("  " <>) . drop 4 . show) (filter (viewableProp showProps verbose) props)

    where
      viewableProp False _ _              = False
      viewableProp _ True _               = True
      viewableProp _ _ (PropTopicAlias _) = False
      viewableProp _ _ _                  = True

run :: Options -> IO ()
run Options{..} = do
  ch <- newTChanIO
  async (printer ch (not optHideProps) optVerbose) >>= link
  uref <- R.newIORef optUri

  forever $ catches (go ch uref) [Handler (\(ex :: MQTTException) -> handler (show ex)),
                                  Handler (\(ex :: IOException) -> handler (show ex))]

  where
    go ch uref = do
      uri <- R.readIORef uref
      verbose ("Connecting to " <> show uri)
      mc <- connectURI mqttConfig{_msgCB=SimpleCallback (showme ch), _protocol=Protocol50,
                                  _cleanSession=optSessionTime == 0,
                                  _connProps=[PropReceiveMaximum 65535,
                                              PropTopicAliasMaximum 10000,
                                              PropSessionExpiryInterval optSessionTime,
                                              PropRequestResponseInformation 1,
                                              PropRequestProblemInformation 1]}
        uri

      (ConnACKFlags sp _ props) <- connACK mc
      when (optSessionTime > 0) $ updateURI uref props
      verbose (if sp == ExistingSession then "<resuming session>" else "<new session>")
      verbose ("Properties: " <> show props)
      when (sp == NewSession || optSubResume) $ subscribe mc [(t, subOptions{_subQoS=optQoS}) | t <- optTopics] mempty >>= verboseShow

      print =<< waitForClient mc

    verbose = when optVerbose . putStrLn
    verboseShow = verbose . show

    showme ch _ t m props = atomically $ writeTChan ch $ Msg t m props

    handler e = putStrLn ("ERROR: " <> e) >> threadDelay 1000000

    updateURI uref = foldM_ up ()
      where up _ (PropAssignedClientIdentifier i) = R.modifyIORef uref (\u -> u{uriFragment='#':BCS.unpack i})
            up a _                                = pure a

main :: IO ()
main = run =<< execParser opts

  where opts = info (options <**> helper)
          ( fullDesc <> progDesc "Watch stuff.")