packages feed

zyre2-0.1.1.0: src/Network/Zyre2/Zyre.hs

-- | Zyre provides reliable group messaging over local area networks. It has these key characteristics:
--
-- * Zyre needs no administration or configuration.
-- * Peers may join and leave the network at any time.
-- * Peers talk to each other without any central brokers or servers.
-- * Peers can talk directly to each other.
-- * Peers can join groups, and then talk to groups.
-- * Zyre is reliable, and loses no messages even when the network is heavily loaded.
-- * Zyre is fast and has low latency, requiring no consensus protocols.
-- * Zyre is designed for WiFi networks, yet also works well on Ethernet networks.
-- * Time for a new peer to join a network is about one second.
--
-- Typical use cases for Zyre are:
--
-- * Local service discovery.
-- * Clustering of a set of services on the same Ethernet network.
-- * Controlling a network of smart devices (Internet of Things).
-- * Multi-user mobile applications (like smart classrooms).
--
-- This package provides a haskell interface to the Zyre 2.0 API. The
-- package requires the c libraries czmq and zyre to be installed on the
-- system. See https://github.com/zeromq/zyre for specifics.
module Network.Zyre2.Zyre
  ( name,
    new,
    start,
    stop,
    destroy,
    join,
    leave,
    uuid,
    version,
    shout,
    shouts,
    whisper,
    whispers,
    recv,
    peers,
    peerAddress,
    peerName,
    peerHeaderValue,
    peersByGroup,
    ownGroups,
    peerGroups,
  )
where

import Control.Exception (throw)
import Control.Monad (forM, forM_, unless, void)
import qualified Data.ByteString as BS
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word64)
import Foreign.C.String (newCString, peekCString)
import Foreign.Marshal.Alloc (free)
import Foreign.Ptr (Ptr, nullPtr)
import qualified Network.Zyre2.Bindings as ZB
import Network.Zyre2.Types
  ( ZCreated,
    ZDestroyed,
    ZRunning,
    ZStopped,
    ZyreContext (ZyreContext),
    unlessStale,
  )
import qualified Network.Zyre2.ZMsg as ZM

-- | Create a new Zyre instance/context.
-- All created contexts must be manually cleaned up with 'destroy' to avoid leaks.
-- Takes a node name, or if 'Nothing' will auto-generate a name from the node UUID.
new :: Maybe Text -> IO (ZyreContext ZCreated)
new name = do
  cname <- case name of
    Just t -> newCString (T.unpack t)
    Nothing -> pure nullPtr
  ptr <- ZB.zyreNew cname
  case name of
    Just _ -> free cname
    Nothing -> pure ()
  stale <- newIORef False
  nameMap <- newIORef Map.empty
  let ctx = ZyreContext ptr stale nameMap :: ZyreContext ZCreated
  pure ctx

-- | Start the zyre instance. Starts UDP beaconing and joins the
-- peer network. Generates an 'Enter' message for other participants.
start :: ZyreContext ZCreated -> IO (ZyreContext ZRunning)
start zctx@(ZyreContext ptr stale nameMap) = unlessStale zctx $ do
  void . ZB.zyreStart $ ptr
  newStale <- newIORef False
  let ctx = ZyreContext ptr newStale nameMap :: ZyreContext ZRunning
  atomicModifyIORef' stale (const (True, ()))
  pure ctx

-- | Stop the zyre instance, leaving the peer network.
-- Generates a 'Exit' message for the other participants.
stop :: ZyreContext ZRunning -> IO (ZyreContext ZStopped)
stop zctx@(ZyreContext ptr stale nameMap) = unlessStale zctx $ do
  void . ZB.zyreStop $ ptr
  newStale <- newIORef False
  let ctx = ZyreContext ptr newStale nameMap :: ZyreContext ZStopped
  atomicModifyIORef' stale (const (True, ()))
  pure ctx

-- | Destroy the given zyre context, freeing its resources.
-- Once it has been destroyed, it can no longer be used.
-- Returns a ZyreContext tagged as destroyed to maintain an API
-- similar to the rest of the interface.
destroy :: ZyreContext ZStopped -> IO (ZyreContext ZDestroyed)
destroy zctx@(ZyreContext ptr stale nameMap) = unlessStale zctx $ do
  ZB.zyreDestroy ptr
  let ctx = ZyreContext ptr stale nameMap :: ZyreContext ZDestroyed
  atomicModifyIORef' stale (const (True, ()))
  pure ctx

-- | Join a peer group, to start receiving and be able to send
-- messages from that group. Generates a 'Join' message for
-- the other participants in the group.
join :: ZyreContext ZRunning -> Text -> IO Int
join zctx@(ZyreContext ptr _ _) name = unlessStale zctx $ do
  cname <- newCString (T.unpack name)
  cint <- ZB.zyreJoin ptr cname
  free cname
  pure $ fromIntegral cint

-- | Leave a peer group, and stop receiving updates from that group.
-- Generates a 'Leave' message for the other participants in the
-- group network.
leave :: ZyreContext ZRunning -> Text -> IO Int
leave zctx@(ZyreContext ptr _ _) name = unlessStale zctx $ do
  cname <- newCString (T.unpack name)
  cint <- ZB.zyreLeave ptr cname
  free cname
  pure $ fromIntegral cint

-- | Retrieve the UUID generated for the context.
uuid :: ZyreContext a -> IO Text
uuid zctx@(ZyreContext ptr _ _) = unlessStale zctx $ do
  cstring <- ZB.zyreUuid ptr
  str <- peekCString cstring <* free cstring
  pure $ T.pack str

-- | Retrieve the version of underlying zyre library.
version :: IO Word64
version = ZB.zyreVersion

-- | Retrieve the name of our node after initialization. Either set by 'new'
-- or automatically generated by zyre from the nodes UUID.
name :: ZyreContext a -> IO Text
name zctx@(ZyreContext ptr _ _) = unlessStale zctx $ do
  cstring <- ZB.zyreName ptr
  str <- peekCString cstring <* free cstring
  pure $ T.pack str

-- | Shout a 'Shout' to a group. Sends data frames.
--
-- > ctx <- new "my-node"
-- > ctx <- start ctx
-- > join ctx "my-group"
-- > let msg = addString "My message" msgShout
-- > shout ctx "my-group" msg
--
-- You can also send multiple frames in the same message.
--
-- > let msg = addString "Frame2" . addString "Frame1" $ msgShout
shout :: ZyreContext ZRunning -> Text -> ZM.ZMsg -> IO Int
shout zctx@(ZyreContext ptr _ _) room zmsg@ZM.Shout {} = unlessStale zctx $ do
  msg_ptr <- ZB.zyreNewZMsg
  croom <- newCString (T.unpack room)
  forM_ (ZM._zmsgMessage zmsg) $ \frame -> ZB.zyreAddFrame msg_ptr (ZM.frameData frame)
  cint <- ZB.zyreShout ptr croom msg_ptr
  free croom
  pure $ fromIntegral cint
shout _ _ _ = pure (-1)

-- | Shout a 'Shout' to a group. Sends some 'Text' value encoded as a 'Data.ByteString.ByteString'.
--
-- > ctx <- new "my-node"
-- > ctx <- start ctx
-- > join ctx "my-group"
-- > shout ctx "my-group" "My message"
shouts :: ZyreContext ZRunning -> Text -> Text -> IO Int
shouts zctx@(ZyreContext ptr _ _) room msg = unlessStale zctx $ do
  croom <- newCString (T.unpack room)
  cmsg <- newCString (T.unpack msg)
  cint <- ZB.zyreShouts ptr croom cmsg
  free croom
  free cmsg
  pure $ fromIntegral cint

-- | Whisper a 'Whisper' to a specific peer. Takes a node id and a 'ZMsg'. Sends data frames.
whisper :: ZyreContext ZRunning -> Text -> ZM.ZMsg -> IO Int
whisper zctx@(ZyreContext ptr _ _) peer zmsg@ZM.Whisper {} = unlessStale zctx $ do
  msg_ptr <- ZB.zyreNewZMsg
  cpeer <- newCString (T.unpack peer)
  forM_ (ZM._zmsgMessage zmsg) $ \frame -> ZB.zyreAddFrame msg_ptr (ZM.frameData frame)
  cint <- ZB.zyreWhisper ptr cpeer msg_ptr
  free cpeer
  pure $ fromIntegral cint
whisper _ _ _ = pure (-1)

-- | Whisper a 'Whisper' to a specific peer. Sends some 'Text' value encoded as a 'Data.ByteString.ByteString'.
whispers :: ZyreContext ZRunning -> Text -> Text -> IO Int
whispers zctx@(ZyreContext ptr _ _) peer msg = unlessStale zctx $ do
  cpeer <- newCString (T.unpack peer)
  cmsg <- newCString (T.unpack msg)
  cint <- ZB.zyreWhispers ptr cpeer cmsg
  free cpeer
  free cmsg
  pure $ fromIntegral cint

-- | Block and await a message from the peer network.
-- Returns 'Nothing' if it times out or is interruped.
recv :: ZyreContext ZRunning -> IO (Maybe ZM.ZMsg)
recv zctx@(ZyreContext ptr _ nameMap) = unlessStale zctx $ do
  -- Block and listen for a msg, receive a pointer to msg or null.
  ZB.zyreRecv ptr >>= \msg_ptr -> do
    if msg_ptr == nullPtr
      then pure Nothing
      else do
        -- Check first frame for message type
        cmsgType <- ZB.zyrePopStrFrame msg_ptr
        msgType <- peekCString cmsgType <* free cmsgType

        -- Parse remaining frames depending on message type
        maybeMsg <- case msgType of
          "ENTER" -> parseEnterMessage msg_ptr nameMap
          "EVASIVE" -> parseEvasiveMessage msg_ptr
          "SILENT" -> parseSilentMessage msg_ptr
          "EXIT" -> parseExitMessage msg_ptr nameMap
          "JOIN" -> parseJoinMessage msg_ptr
          "LEAVE" -> parseLeaveMessage msg_ptr
          "WHISPER" -> parseWhisperMessage msg_ptr
          "SHOUT" -> parseShoutMessage msg_ptr
          "STOP" -> pure $ Just ZM.Stop

          -- If we encounter an unknown message type
          _ -> do
            putStrLn $ "Unhandled msgType: " <> msgType
            pure Nothing
        -- Clean up zmsg
        ZB.zyreZmsgDestroy msg_ptr
        pure maybeMsg
  where
    parseEnterMessage msg_ptr nameMap = do
      -- Pop the message metadata off the zmsg, marshal into haskell types.
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      cheader_ptr <- ZB.zyrePopFrame msg_ptr
      cipport <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname  <* free cname
      ipport <- peekCString cipport <* free cipport

      -- Unpack the headers
      headersRef <- newIORef []
      headers_ptr <- ZB.zyreUnpackHeaders cheader_ptr
      extractHeaders headers_ptr headersRef
      ZB.zyreDestroyHeaders headers_ptr
      headers <- readIORef headersRef

      -- Release the allocated resources
      ZB.zyreDestroyFrame cheader_ptr

      -- Add the node name to our mapping
      atomicModifyIORef' nameMap (\x -> (Map.insert (T.pack fromnode) (T.pack name) x, ()))

      pure $
        Just $
          ZM.Enter
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name,
              ZM._zmsgHeaders = headers,
              ZM._zmsgIpPort = T.pack ipport
            }

    parseEvasiveMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname

      pure $
        Just $
          ZM.Evasive
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name
            }

    parseSilentMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname

      pure $
        Just $
          ZM.Silent
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name
            }

    parseExitMessage msg_ptr nameMap = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname

      -- Remove the node name from our mapping
      atomicModifyIORef' nameMap (\x -> (Map.delete (T.pack fromnode) x, ()))

      pure $
        Just $
          ZM.Exit
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name
            }

    parseJoinMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      cgroupname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname
      groupname <- peekCString cgroupname <* free cgroupname

      pure $
        Just $
          ZM.Join
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name,
              ZM._zmsgGroupName = T.pack groupname
            }

    parseLeaveMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      cgroupname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname
      groupname <- peekCString cgroupname <* free cgroupname

      pure $
        Just $
          ZM.Leave
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name,
              ZM._zmsgGroupName = T.pack groupname
            }

    parseWhisperMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname

      msgBodyRef <- newIORef []
      extractFrames msg_ptr msgBodyRef
      msgBody <- readIORef msgBodyRef

      pure $
        Just $
          ZM.Whisper
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name,
              ZM._zmsgMessage = msgBody
            }

    parseShoutMessage msg_ptr = do
      cfromnode <- ZB.zyrePopStrFrame msg_ptr
      cname <- ZB.zyrePopStrFrame msg_ptr
      cgroupname <- ZB.zyrePopStrFrame msg_ptr
      fromnode <- peekCString cfromnode <* free cfromnode
      name <- peekCString cname <* free cname
      groupname <- peekCString cgroupname <* free cgroupname

      msgBodyRef <- newIORef []
      extractFrames msg_ptr msgBodyRef
      msgBody <- readIORef msgBodyRef

      pure $
        Just $
          ZM.Shout
            { ZM._zmsgFromNode = T.pack fromnode,
              ZM._zmsgName = T.pack name,
              ZM._zmsgGroupName = T.pack groupname,
              ZM._zmsgMessage = msgBody
            }

-- | List the id of the peers in the peer network.
peers :: ZyreContext ZRunning -> IO [Text]
peers zctx@(ZyreContext ptr _ _) = unlessStale zctx $ do
  list_ptr <- ZB.zyrePeers ptr
  returnRef <- newIORef []
  extractList list_ptr returnRef
  ZB.zyreZListDestroy list_ptr
  readIORef returnRef

-- | List the id of the peers in a specific group in the peer network.
peersByGroup :: ZyreContext ZRunning -> Text -> IO (Maybe [Text])
peersByGroup zctx@(ZyreContext ptr _ _) group = unlessStale zctx $ do
  returnRef <- newIORef []
  cgroup <- newCString (T.unpack group)
  list_ptr <- ZB.zyrePeersByGroup ptr cgroup
  if list_ptr /= nullPtr
    then do
      extractList list_ptr returnRef
      ZB.zyreZListDestroy list_ptr
    else pure ()
  free cgroup
  (\xs -> if null xs then Nothing else Just xs) <$> readIORef returnRef

-- | Retrieve the endpoint of a connected peer.
-- Returns 'Nothing' if peer does not exist.
peerAddress :: ZyreContext ZRunning -> Text -> IO (Maybe Text)
peerAddress zctx@(ZyreContext ptr _ _) peer = unlessStale zctx $ do
  cpeer <- newCString (T.unpack peer)
  caddress <- ZB.zyrePeerAddress ptr cpeer
  address <- T.pack <$> peekCString caddress
  free cpeer
  free caddress
  pure $ if T.null address then Nothing else Just address

peerName :: ZyreContext ZRunning -> Text -> IO (Maybe Text)
peerName zctx@(ZyreContext _ _ nameMap) peer = unlessStale zctx $ do
  map <- readIORef nameMap
  pure $ Map.lookup peer map

-- | Retrieve the value of a header of a connected peer.
-- Returns 'Nothing' if peer or key doesn't exist.
peerHeaderValue :: ZyreContext ZRunning -> Text -> Text -> IO (Maybe Text)
peerHeaderValue zctx@(ZyreContext ptr _ _) peer header = unlessStale zctx $ do
  cpeer <- newCString (T.unpack peer)
  cheader <- newCString (T.unpack header)
  cvalue <- ZB.zyrePeerHeaderValue ptr cpeer cheader
  value <- if cvalue == nullPtr then pure T.empty else T.pack <$> peekCString cvalue
  free cpeer
  free cheader
  free cvalue
  pure $ if T.null value then Nothing else Just value

-- | List the groups that you are a part of.
ownGroups :: ZyreContext ZRunning -> IO [Text]
ownGroups zctx@(ZyreContext ptr _ _) = unlessStale zctx $ do
  list_ptr <- ZB.zyreOwnGroups ptr
  returnRef <- newIORef []
  extractList list_ptr returnRef
  ZB.zyreZListDestroy list_ptr
  readIORef returnRef

-- | List groups that are known through connected peers.
peerGroups :: ZyreContext ZRunning -> IO [Text]
peerGroups zctx@(ZyreContext ptr _ _) = unlessStale zctx $ do
  list_ptr <- ZB.zyreOwnGroups ptr
  returnRef <- newIORef []
  extractList list_ptr returnRef
  ZB.zyreZListDestroy list_ptr
  readIORef returnRef

-- | Internal helper function.
-- Traverses a zmsg using next() and accumulates the frames in an IORef.
extractFrames :: Ptr () -> IORef [ZM.ZFrame] -> IO ()
extractFrames msg_ptr framesRef = do
  cursor <- ZB.zyreNextFrame msg_ptr
  if cursor /= nullPtr
    then do
      len <- ZB.zyreFrameSize cursor
      content_ptr <- ZB.zyreFrameData cursor
      packed <- BS.packCStringLen (content_ptr, fromIntegral len)
      atomicModifyIORef' framesRef (\x -> (x <> [ZM.mkFrame packed], ()))
      extractFrames msg_ptr framesRef
    else pure ()

-- | Internal helper function.
-- Traverses an unpacked zhash table, using next() and cursor() to
-- accumulate the stored values into a dictionary in an IORef.
extractHeaders :: Ptr () -> IORef [(Text, Text)] -> IO ()
extractHeaders header_ptr headersRef = do
  cursor <- ZB.zyreNextHeader header_ptr
  if cursor /= nullPtr
    then do
      key_ptr <- ZB.zyreHeaderCursor header_ptr
      key <- peekCString key_ptr
      val <- peekCString cursor

      atomicModifyIORef' headersRef (\x -> (x <> [(T.pack key, T.pack val)], ()))
      extractHeaders header_ptr headersRef
    else pure ()

-- | Internal helper function.
-- Extracts values in a zlist as text values into an IORef.
extractList :: Ptr () -> IORef [Text] -> IO ()
extractList list_ptr accumRef = do
  cursor <- ZB.zyreZListNext list_ptr
  if cursor /= nullPtr
    then do
      val <- peekCString cursor
      atomicModifyIORef' accumRef (\x -> (x <> [T.pack val], ()))
      extractList list_ptr accumRef
    else pure ()