{-# LANGUAGE ScopedTypeVariables #-}
module System.FSNotify.Polling
( createPollManager
, PollManager(..)
, FileListener(..)
) where
import Control.Concurrent
import Control.Exception
import Control.Monad (forM_)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.Time.Clock (UTCTime, getCurrentTime)
import Data.Time.Clock.POSIX
import Prelude hiding (FilePath)
import System.Directory (doesDirectoryExist)
import System.FSNotify.Listener
import System.FSNotify.Path (findFilesAndDirs, canonicalizeDirPath)
import System.FSNotify.Types
import System.FilePath
import System.PosixCompat.Files
import System.PosixCompat.Types
data EventType = AddedEvent
| ModifiedEvent
| RemovedEvent
newtype WatchKey = WatchKey ThreadId deriving (Eq, Ord)
data WatchData = WatchData FilePath EventChannel
type WatchMap = Map WatchKey WatchData
newtype PollManager = PollManager (MVar WatchMap)
generateEvent :: UTCTime -> Bool -> EventType -> FilePath -> Maybe Event
generateEvent timestamp isDir AddedEvent filePath = Just (Added filePath timestamp isDir)
generateEvent timestamp isDir ModifiedEvent filePath = Just (Modified filePath timestamp isDir)
generateEvent timestamp isDir RemovedEvent filePath = Just (Removed filePath timestamp isDir)
generateEvents :: UTCTime -> EventType -> [(FilePath, Bool)] -> [Event]
generateEvents timestamp eventType = mapMaybe (\(path, isDir) -> generateEvent timestamp isDir eventType path)
handleEvent :: EventChannel -> ActionPredicate -> Event -> IO ()
handleEvent _ _ (Modified _ _ True) = return ()
handleEvent chan actPred event
| actPred event = writeChan chan event
| otherwise = return ()
pathModMap :: Bool -> FilePath -> IO (Map FilePath (UTCTime, Bool))
pathModMap recursive path = findFilesAndDirs recursive path >>= pathModMap'
where
pathModMap' :: [FilePath] -> IO (Map FilePath (UTCTime, Bool))
pathModMap' files = (Map.fromList . catMaybes) <$> mapM pathAndInfo files
pathAndInfo :: FilePath -> IO (Maybe (FilePath, (UTCTime, Bool)))
pathAndInfo path = handle (\(_ :: IOException) -> return Nothing) $ do
modTime <- getModificationTime path
isDir <- doesDirectoryExist path
return $ Just (path, (modTime, isDir))
pollPath :: Int -> Bool -> EventChannel -> FilePath -> ActionPredicate -> Map FilePath (UTCTime, Bool) -> IO ()
pollPath interval recursive chan filePath actPred oldPathMap = do
threadDelay interval
maybeNewPathMap <- handle (\(_ :: IOException) -> return Nothing) (Just <$> pathModMap recursive filePath)
case maybeNewPathMap of
Nothing -> pollPath interval recursive chan filePath actPred oldPathMap
Just newPathMap -> do
currentTime <- getCurrentTime
let deletedMap = Map.difference oldPathMap newPathMap
createdMap = Map.difference newPathMap oldPathMap
modifiedAndCreatedMap = Map.differenceWith modifiedDifference newPathMap oldPathMap
modifiedMap = Map.difference modifiedAndCreatedMap createdMap
generateEvents' = generateEvents currentTime
handleEvents $ generateEvents' AddedEvent [(path, isDir) | (path, (_, isDir)) <- Map.toList createdMap]
handleEvents $ generateEvents' ModifiedEvent [(path, isDir) | (path, (_, isDir)) <- Map.toList modifiedMap]
handleEvents $ generateEvents' RemovedEvent [(path, isDir) | (path, (_, isDir)) <- Map.toList deletedMap]
pollPath interval recursive chan filePath actPred newPathMap
where
modifiedDifference :: (UTCTime, Bool) -> (UTCTime, Bool) -> Maybe (UTCTime, Bool)
modifiedDifference (newTime, isDir1) (oldTime, isDir2)
| oldTime /= newTime || isDir1 /= isDir2 = Just (newTime, isDir1)
| otherwise = Nothing
handleEvents :: [Event] -> IO ()
handleEvents = mapM_ (handleEvent chan actPred)
createPollManager :: IO PollManager
createPollManager = PollManager <$> newMVar Map.empty
killWatchingThread :: WatchKey -> IO ()
killWatchingThread (WatchKey threadId) = killThread threadId
killAndUnregister :: MVar WatchMap -> WatchKey -> IO ()
killAndUnregister mvarMap wk = do
_ <- withMVar mvarMap $ \m -> do
killWatchingThread wk
return $ Map.delete wk m
return ()
listen' :: Bool -> WatchConfig -> PollManager -> FilePath -> ActionPredicate -> EventChannel -> IO (IO ())
listen' isRecursive conf (PollManager mvarMap) path actPred chan = do
path' <- canonicalizeDirPath path
pmMap <- pathModMap isRecursive path'
threadId <- forkIO $ pollPath (confPollInterval conf) isRecursive chan path' actPred pmMap
let wk = WatchKey threadId
modifyMVar_ mvarMap $ return . Map.insert wk (WatchData path' chan)
return $ killAndUnregister mvarMap wk
instance FileListener PollManager where
initSession = fmap Just createPollManager
killSession (PollManager mvarMap) = do
watchMap <- readMVar mvarMap
forM_ (Map.keys watchMap) killWatchingThread
listen = listen' False
listenRecursive = listen' True
usesPolling = const True
getModificationTime :: FilePath -> IO UTCTime
getModificationTime p = fromEpoch . modificationTime <$> getFileStatus p
fromEpoch :: EpochTime -> UTCTime
fromEpoch = posixSecondsToUTCTime . realToFrac