epoll-0.2: src/System/Linux/Epoll/EventLoop.hs
-- |
-- Module : System.Linux.Epoll.EventLoop
-- Copyright : (c) 2009 Toralf Wittner
-- License : LGPL
-- Maintainer : toralf.wittner@gmail.com
-- Stability : experimental
-- Portability : non-portable
--
-- EventLoop's can be used to get notified when certain events occur on a file
-- descriptor. One can add callback functions for any 'EventType' combination.
module System.Linux.Epoll.EventLoop (
Data,
Callback,
EventLoop,
CallbackFn,
EventMap,
createEventLoop,
stopEventLoop,
addCallback,
removeCallback,
reEnableCallback,
closeEvents
) where
import Util
import Control.Monad
import Control.Concurrent
import Control.Concurrent.MVar
import Data.Maybe
import System.Posix.Types (Fd)
import System.Linux.Epoll.Base
import System.Posix.IO (setFdOption, FdOption (NonBlockingRead))
-- | Callback function type
type CallbackFn = Device -> Event Data -> IO ()
type EventMap = [(EventType, CallbackFn)]
-- Used to queue up deleted descriptors to pass them to freeDesc.
--
-- The reason is that freeing descriptors is only safe while no event processing
-- takes place, otherwise delayed handler thread might still use the descriptor
-- or when free directly in 'doClose', it might be part of the next event set
-- still which would cause a segfault.
type Garbage = MVar [Descriptor Data]
data Data = Data {
cbMap :: EventMap,
cbDirty :: MVar Bool -- True when closed.
}
-- | Abstract data type holding bookeeping info.
data Callback = Callback {
cbData :: Data,
cbDesc :: Descriptor Data
}
-- | Abstract data type.
data EventLoop = EventLoop {
elDevice :: Device, -- Epoll Device
elLoop :: ThreadId, -- Epoll wait loop thread
elGarbage :: Garbage -- Epoll Descriptors to be deleted
}
-- | Create one event loop which handles up to 'Size' events per call to epoll's
-- 'wait'. An event loop runs until 'stopEventLoop' is invoked, calling 'wait'
-- with a max timeout of 500ms before it waits again.
createEventLoop :: Size -> IO EventLoop
createEventLoop s = do
dev <- create s
bin <- newMVar []
lop <- forkIO $ runLoop dev bin
return $ EventLoop dev lop bin
-- | Terminates the event loop and cleans resources. Note that one can only
-- remove callbacks from an eventloop while it is running, so make sure you call
-- this function after all 'removeCallback' calls.
stopEventLoop :: EventLoop -> IO ()
stopEventLoop el = do
killThread (elLoop el)
close (elDevice el)
-- | Adds a callback for the given file descriptor to this event loop. The event
-- map specifies for each event type which function to call. event types might
-- be combined using 'combineEvents'.
addCallback :: EventLoop -> Fd -> EventMap -> IO Callback
addCallback el fd emp = do
dirty <- newMVar False
let ety = map fst emp
dat = Data emp dirty
setFdOption fd NonBlockingRead True
desc <- add (elDevice el) dat ety fd
return $ Callback dat desc
-- | Removes the callback obtained from 'addCallback' from this event loop. Note
-- that you must not call 'stopEventLoop' before invoking this function.
removeCallback :: EventLoop -> Callback -> IO ()
removeCallback el cb = do
doClose (elDevice el) (elGarbage el) (cbDesc cb) (cbData cb)
return ()
-- | In case you use 'oneShotEvent' you can re-enable a callback after the event
-- occured. Otherwise no further events will be reported. Cf. epoll(7) for
-- details.
reEnableCallback :: Device -> Data -> Descriptor Data -> IO ()
reEnableCallback dev dat des =
withMVar (cbDirty dat) $ \dirty ->
unless dirty $
modify dev (map fst (cbMap dat)) des
-- The heart of the event loop. Runs forever, dispatching events.
runLoop :: Device -> Garbage -> IO ()
runLoop dev bin = do
let dur = fromJust . toDuration $ 500
forever $ wait dur dev >>= mapM_ dispatch >> gc bin >> yield
where
dispatch e = case eventType e of
t | t =~ closeEvents -> handleClose dev bin e
| otherwise -> handleEvent dev e
-- garbage collection, i.e. free old descriptors
gc b = modifyMVar b (\v -> return ([], v)) >>= mapM freeDesc
-- Invokes 'doClose' and potentially 'handleEvent' with 'closeEvents',
-- if the callback has not been closed before.
handleClose :: Device -> Garbage -> Event Data -> IO ()
handleClose dev bin e = do
isDirty <- doClose dev bin (eventDesc e) (eventRef e)
unless isDirty $ handleEvent dev (e { eventType = closeEvents })
-- Marks callback data as dirty and deletes descriptor, i.e. removes it from
-- epoll and schedules it for freeDesc.
doClose :: Device -> Garbage -> Descriptor Data -> Data -> IO Bool
doClose dev bin des dat =
modifyMVar (cbDirty dat) $ \dirty -> do
unless dirty $ do
delete dev des
modifyMVar_ bin $ return . (des:)
return (True, dirty)
-- Forks each callback function which is applicable for the current
-- event into its own thread.
handleEvent :: Device -> Event Data -> IO ()
handleEvent dev e = do
let fs = lookupCB (cbMap . eventRef $ e) (eventType e)
forM_ fs $ \f -> forkIO_ $ f dev e
-- Matches the given event type against the event map.
lookupCB :: EventMap -> EventType -> [CallbackFn]
lookupCB emap ety = map snd $ filter ((=~ ety) . fst) emap
-- | A combination of 'peerCloseEvent', 'errorEvent', 'hangupEvent'.
closeEvents :: EventType
closeEvents = combineEvents [peerCloseEvent, errorEvent, hangupEvent]