-- | -- 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]