packages feed

ipc-0.0.2: System/IPC.hs

-- |The Inter-Process Communication (IPC) library makes the task of setting
-- up datagram sockets between processes trivial.  The simplest method is:
--
-- > outputQueue <- channelConnectSimple "channelName"
--
-- > send outputQueue someInstanceOfBinary
--
--  and
--  
-- > inputQueue <- channelAcceptSimple "channelName"
--
-- > val <- recv inputQueue
--
-- The Queues are TVars from Data.Queue, which can be imported for fine-tuned
-- control.
--
-- If you find this useful then e-mail me and I'll be more likely to maintain
-- it and add features.  Potential new features include:
--
--     * Remove the static maximum message size ('maxBytes') limitation.
--     
--     * Fix connection capability (ex: if the sender closes, receiver rebinds and continues)
--
--     * Testing on other platforms
--
--     * Better exception handling
--
--     * Input buffer size limitations (for memory concious apps)

module System.IPC
    ( channelAccept
    , channelConnect
    , channelAcceptSimple
    , channelConnectSimple
    , D.send
    , D.recv
    , maxBytes
    , waitTillEmpty
    ) where

import qualified Data.Queue as D
import Network.Socket hiding (recv, send)
import Network.Socket.ByteString
import Data.Binary
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Control.Concurrent.STM
import Control.Concurrent (forkIO, threadDelay)
import Control.Monad (forever)
import Control.Exception as X

-- |The maximum bytes per datagram.  Any data structure with an encoding greater than
-- this size stands to be truncated (and probably non-decodable).
maxBytes = 4096

-- |A wrapper for 'channelAccept'.  Creates a new queue and sets conversion == return.
channelAcceptSimple :: (Binary i) => String -> IO (TVar (D.Queue D.Input i))
channelAcceptSimple tag = do
    q <- newTVarIO D.empty
    forkIO $ channelAccept tag return q
    return q

-- |A wrapper for 'channelConnect'.  Creates a new queue and sets conversion == return.
channelConnectSimple :: (Binary o) => String -> IO (TVar (D.Queue D.Output o))
channelConnectSimple tag = withSocketsDo $ do
    q <- newTVarIO D.empty
    forkIO $ channelConnect tag return q
    return q

-- |Sets up an input channel to receive data.
-- Uses the provided queue.  Converts all data from the deserialized format before enqueuing.
channelAccept :: (Binary a) => String -> (a -> IO input) -> TVar (D.Queue D.Input input) -> IO ()
channelAccept tag convIn q = withSocketsDo $ do
    sock <- socket AF_UNIX Datagram defaultProtocol
    bindSocket sock (SockAddrUnix ('\0' : tag))
    receiver sock convIn q

-- |Connects to an open channel to send data.
-- Uses the provided queue.  Converts all data from the queue formate before serializing / sending.
channelConnect :: (Binary b) => String -> (output -> IO b) -> TVar (D.Queue D.Output output) -> IO ()
channelConnect tag convOut q = withSocketsDo $ do
    sock <- socket AF_UNIX Datagram defaultProtocol
    makeConnection sock 0
    sender sock convOut q
  where
  makeConnection s t = X.catchJust ioErrors (doConnect s)
                               (\_ -> threadDelay t >> makeConnection s (newDelay t))
  doConnect sock = connect sock (SockAddrUnix ('\0' : tag))
  newDelay 0 = 1000
  newDelay x = min (x * 2) (10^6)

-- |Receives data from the socket, deserializes, converts, enqueues.
receiver :: (Binary b) => 
            Socket ->                     -- ^ input socket
            (b -> IO a) ->                -- ^ conversion function
            TVar (D.Queue D.Input a) ->   -- ^ buffer
            IO ()
receiver sock conv q = forever $ do
    bs  <- recv sock maxBytes
    val <- conv (decode (toLazy bs))
    atomically ( readTVar q >>= writeTVar q . (flip D.snoc) val)

-- |Removes data from the buffer, converts, serializes and sends.
sender :: (Binary b) =>
          Socket ->                      -- ^ output socket
          (a -> IO b) ->                 -- ^ conversion function
          TVar (D.Queue D.Output a) ->   -- ^ buffer
          IO ()
sender sock conv tv = forever $ do
    v <- atomically ( do
       q <- readTVar tv
       case D.pop q of
           (Just e,rest) -> writeTVar tv rest >> return e
           (Nothing,  _) -> retry )
    val <- conv v
    send sock (fromLazy (encode val))

-- |Will block until the queue is empty.  This is useful for delaying program
-- shutdown till _most_ of the data has been sent.  Items are removed just
-- before sending, so an immediate shutdown could fail to send the final item.
waitTillEmpty :: TVar (D.Queue d a) -> IO ()
waitTillEmpty tv = atomically (do
    q <- readTVar tv
    if D.isEmpty q then return () else retry )

toLazy :: S.ByteString -> L.ByteString
toLazy bs = L.fromChunks [bs]

fromLazy :: L.ByteString -> S.ByteString
fromLazy bs = S.concat $ L.toChunks bs