module System.KQueue.HighLevel
( watchFile
, stopWatching
, EventType (..)
, Watcher
) where
import Control.Concurrent (ThreadId, forkIO, killThread)
import Control.Monad.State (StateT, evalStateT, forever, get, liftIO, liftM, put, when)
import Data.List (intersect)
import Foreign.Ptr (nullPtr)
import System.Directory (canonicalizePath, doesFileExist)
import System.FilePath (takeDirectory)
import System.Posix.IO (OpenMode (ReadOnly), defaultFileFlags, openFd)
import System.Posix.Types (Fd)
import System.KQueue
data EventType = Changed | Created | Deleted deriving Show
newtype Watcher = Watcher ThreadId
watchFile :: FilePath -> (EventType -> IO ()) -> IO Watcher
watchFile file callback =
do kq <- kqueue
dir <- takeDirectory `liftM` canonicalizePath file
tid <- forkIO $ watchDirectoryForFile kq dir file callback
return $ Watcher tid
stopWatching :: Watcher -> IO ()
stopWatching (Watcher tid) = killThread tid
watchDirectoryForFile :: KQueue -> FilePath -> FilePath -> (EventType -> IO ()) -> IO ()
watchDirectoryForFile kq dir file callback =
do
dfd <- openFd dir ReadOnly Nothing defaultFileFlags
let dirEvent = KEvent
{ ident = fromIntegral dfd
, evfilter = EvfiltVnode
, flags = [EvOneshot]
, fflags = [NoteWrite]
, data_ = 0
, udata = nullPtr
}
mkFileEvent ffd = KEvent
{ ident = fromIntegral ffd
, evfilter = EvfiltVnode
, flags = [EvOneshot]
, fflags = [NoteDelete, NoteWrite, NoteRename]
, data_ = 0
, udata = nullPtr
}
exists <- doesFileExist file
mFd <-
if exists
then Just `liftM` openFd file ReadOnly Nothing defaultFileFlags
else return Nothing
let eventsToAdd = dirEvent : maybe [] (return . mkFileEvent) mFd
_ <- kevent kq (map (setFlag EvAdd) eventsToAdd) 0 Nothing
flip evalStateT mFd . forever $ monitorChanges kq dirEvent mkFileEvent callback file
monitorChanges :: KQueue -> KEvent -> (Fd -> KEvent) -> (EventType -> IO ()) -> FilePath -> StateT (Maybe Fd) IO ()
monitorChanges kq dirEvent mkFileEvent callback file =
do mFd <- get
newMFd <- liftIO $ monitorChangesIO kq dirEvent mkFileEvent callback file mFd
put newMFd
monitorChangesIO :: KQueue -> KEvent -> (Fd -> KEvent) -> (EventType -> IO ()) -> FilePath -> Maybe Fd -> IO (Maybe Fd)
monitorChangesIO kq dirEvent mkFileEvent callback file mFd =
do
let eventsToMonitor = dirEvent : maybe [] (return . mkFileEvent) mFd
[firstChg] <- kevent kq eventsToMonitor 1 Nothing
otherChgs <- getAllEvents kq (map (setFlag EvAdd) eventsToMonitor)
let chgs = firstChg : otherChgs
when (NoteWrite `elem` [ fflag | fileChg <- chgs, ident fileChg /= ident dirEvent, fflag <- fflags fileChg]) $
callback Changed
exists <- doesFileExist file
case (exists, mFd) of
(True, Nothing) ->
do callback Created
fd <- openFd file ReadOnly Nothing defaultFileFlags
_ <- kevent kq [setFlag EvAdd (mkFileEvent fd)] 0 Nothing
return (Just fd)
(False, Just fd) ->
do callback Deleted
_ <- kevent kq [setFlag EvDelete (mkFileEvent fd)] 0 Nothing
return Nothing
(True, Just fd) | not . null . filter (isDeleteOrRename fd) $ chgs ->
do callback Changed
_ <- kevent kq [setFlag EvDelete (mkFileEvent fd)] 0 Nothing
newFd <- openFd file ReadOnly Nothing defaultFileFlags
_ <- kevent kq [setFlag EvAdd (mkFileEvent newFd)] 0 Nothing
return (Just newFd)
(_, _ ) -> return mFd
isDeleteOrRename :: Fd -> KEvent -> Bool
isDeleteOrRename fd evt = fromIntegral fd == ident evt
&& (not . null . intersect [NoteDelete, NoteRename] . fflags) evt
getAllEvents :: KQueue -> [KEvent] -> IO [KEvent]
getAllEvents kq evts = go []
where
go collectedChgs =
do chgs <- kevent kq evts 10 (Just 0.1)
if null chgs
then return collectedChgs
else go (collectedChgs ++ chgs)
setFlag :: (Flag -> KEvent -> KEvent)
setFlag flag ev = ev { flags = flag : flags ev }