fsnotify-0.2.1/src/0000755000000000000000000000000012554771233012341 5ustar0000000000000000fsnotify-0.2.1/src/System/0000755000000000000000000000000012554771233013625 5ustar0000000000000000fsnotify-0.2.1/src/System/FSNotify/0000755000000000000000000000000012556340013015314 5ustar0000000000000000fsnotify-0.2.1/test/0000755000000000000000000000000012554771233012531 5ustar0000000000000000fsnotify-0.2.1/src/System/FSNotify.hs0000644000000000000000000001755012554771233015672 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- {-# LANGUAGE CPP, ScopedTypeVariables, ExistentialQuantification, RankNTypes #-} -- | NOTE: This library does not currently report changes made to directories, -- only files within watched directories. -- -- Minimal example: -- -- >{-# LANGUAGE OverloadedStrings #-} -- for FilePath literals -- > -- >import System.FSNotify -- >import Control.Concurrent (threadDelay) -- >import Control.Monad (forever) -- > -- >main = -- > withManager $ \mgr -> do -- > -- start a watching job (in the background) -- > watchDir -- > mgr -- manager -- > "." -- directory to watch -- > (const True) -- predicate -- > print -- action -- > -- > -- sleep forever (until interrupted) -- > forever $ threadDelay 1000000 module System.FSNotify ( -- * Events Event(..) , EventChannel , eventTime , eventPath , Action , ActionPredicate -- * Starting/Stopping , WatchManager , withManager , startManager , stopManager , defaultConfig , WatchConfig(..) , Debounce(..) , withManagerConf , startManagerConf , StopListening , isPollingManager -- * Watching , watchDir , watchDirChan , watchTree , watchTreeChan ) where import Prelude hiding (FilePath) import Data.Maybe import Control.Concurrent import Control.Concurrent.Async import Control.Exception import Control.Applicative import Control.Monad import System.FilePath import System.FSNotify.Polling import System.FSNotify.Types import System.FSNotify.Listener (StopListening) #ifdef OS_Linux import System.FSNotify.Linux #else # ifdef OS_Win32 import System.FSNotify.Win32 # else # ifdef OS_Mac import System.FSNotify.OSX # else type NativeManager = PollManager # endif # endif #endif -- | Watch manager. You need one in order to create watching jobs. data WatchManager = forall manager . FileListener manager => WatchManager WatchConfig manager (MVar (Maybe (IO ()))) -- cleanup action, or Nothing if the manager is stopped -- | Default configuration -- -- * Debouncing is enabled with a time interval of 1 millisecond -- -- * Polling is disabled -- -- * The polling interval defaults to 1 second defaultConfig :: WatchConfig defaultConfig = WatchConfig { confDebounce = DebounceDefault , confPollInterval = 10^(6 :: Int) -- 1 second , confUsePolling = False } -- | Perform an IO action with a WatchManager in place. -- Tear down the WatchManager after the action is complete. withManager :: (WatchManager -> IO a) -> IO a withManager = withManagerConf defaultConfig -- | Start a file watch manager. -- Directories can only be watched when they are managed by a started watch -- watch manager. -- When finished watching. you must release resources via 'stopManager'. -- It is preferrable if possible to use 'withManager' to handle this -- automatically. startManager :: IO WatchManager startManager = startManagerConf defaultConfig -- | Stop a file watch manager. -- Stopping a watch manager will immediately stop -- watching for files and free resources. stopManager :: WatchManager -> IO () stopManager (WatchManager _ wm cleanupVar) = do mbCleanup <- swapMVar cleanupVar Nothing fromMaybe (return ()) mbCleanup killSession wm -- | Like 'withManager', but configurable withManagerConf :: WatchConfig -> (WatchManager -> IO a) -> IO a withManagerConf conf = bracket (startManagerConf conf) stopManager -- | Like 'startManager', but configurable startManagerConf :: WatchConfig -> IO WatchManager startManagerConf conf | confUsePolling conf = pollingManager | otherwise = initSession >>= createManager where createManager :: Maybe NativeManager -> IO WatchManager createManager (Just nativeManager) = WatchManager conf nativeManager <$> cleanupVar createManager Nothing = pollingManager pollingManager = WatchManager conf <$> createPollManager <*> cleanupVar cleanupVar = newMVar (Just (return ())) -- | Does this manager use polling? isPollingManager :: WatchManager -> Bool isPollingManager (WatchManager _ wm _) = usesPolling wm -- | Watch the immediate contents of a directory by streaming events to a Chan. -- Watching the immediate contents of a directory will only report events -- associated with files within the specified directory, and not files -- within its subdirectories. watchDirChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening watchDirChan (WatchManager db wm _) = listen db wm -- | Watch all the contents of a directory by streaming events to a Chan. -- Watching all the contents of a directory will report events associated with -- files within the specified directory and its subdirectories. watchTreeChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening watchTreeChan (WatchManager db wm _) = listenRecursive db wm -- | Watch the immediate contents of a directory by committing an Action for each event. -- Watching the immediate contents of a directory will only report events -- associated with files within the specified directory, and not files -- within its subdirectories. No two events pertaining to the same FilePath will -- be executed concurrently. watchDir :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening watchDir wm = threadChan listen wm -- | Watch all the contents of a directory by committing an Action for each event. -- Watching all the contents of a directory will report events associated with -- files within the specified directory and its subdirectories. No two events -- pertaining to the same FilePath will be executed concurrently. watchTree :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening watchTree wm = threadChan listenRecursive wm threadChan :: (forall sessionType . FileListener sessionType => WatchConfig -> sessionType -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening) -- (^ this is the type of listen and listenRecursive) -> WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening threadChan listenFn (WatchManager db listener cleanupVar) path actPred action = modifyMVar cleanupVar $ \mbCleanup -> case mbCleanup of -- check if we've been stopped Nothing -> return (Nothing, return ()) -- or throw an exception? Just cleanup -> do chan <- newChan asy <- async $ readEvents chan action -- Ideally, the the asy thread should be linked to the current one -- (@link asy@), so that it doesn't die quietly. -- However, if we do that, then cancelling asy will also kill -- ourselves. I haven't figured out how to do this (probably we -- should just abandon async and use lower-level primitives). For now -- we don't link the thread. stopListener <- listenFn db listener path actPred chan let cleanThisUp = cancel asy return ( Just $ cleanup >> cleanThisUp , stopListener >> cleanThisUp ) readEvents :: EventChannel -> Action -> IO () readEvents chan action = forever $ do event <- readChan chan us <- myThreadId -- Execute the event handler in a separate thread, but throw any -- exceptions back to us. -- -- Note that there's a possibility that we may miss some exceptions, if -- an event handler finishes after the listen is cancelled (and so this -- thread is dead). How bad is that? The alternative is to kill the -- handler anyway when we're cancelling. forkFinally (action event) $ either (throwTo us) (const $ return ()) #if !MIN_VERSION_base(4,6,0) forkFinally :: IO a -> (Either SomeException a -> IO ()) -> IO ThreadId forkFinally action and_then = mask $ \restore -> forkIO $ try (restore action) >>= and_then #endif fsnotify-0.2.1/src/System/FSNotify/Devel.hs0000644000000000000000000000463412554771233016730 0ustar0000000000000000-- | Some additional functions on top of "System.FSNotify". -- -- Example of compiling scss files with compass -- -- @ -- compass :: WatchManager -> FilePath -> IO () -- compass man dir = do -- putStrLn $ "compass " ++ encodeString dir -- treeExtExists man dir "scss" $ \fp -> -- when ("deploy" `notElem` splitDirectories fp) $ do -- let d = encodeString $ head (splitDirectories rel) -- system "cd " ++ d ++ "&& bundle exec compass compile" -- return () -- @ module System.FSNotify.Devel ( treeExtAny, treeExtExists, doAllEvents, allEvents, existsEvents ) where import Prelude hiding (FilePath) import Data.Text import System.FilePath import System.FSNotify import System.FSNotify.Path (hasThisExtension) -- | In the given directory tree, watch for any 'Added' and 'Modified' -- events (but ignore 'Removed' events) for files with the given file -- extension treeExtExists :: WatchManager -> FilePath -- ^ Directory to watch -> Text -- ^ extension -> (FilePath -> IO ()) -- ^ action to run on file -> IO StopListening treeExtExists man dir ext action = watchTree man dir (existsEvents $ flip hasThisExtension ext) (doAllEvents action) -- | In the given directory tree, watch for any events for files with the -- given file extension treeExtAny :: WatchManager -> FilePath -- ^ Directory to watch -> Text -- ^ extension -> (FilePath -> IO ()) -- ^ action to run on file -> IO StopListening treeExtAny man dir ext action = watchTree man dir (allEvents $ flip hasThisExtension ext) (doAllEvents action) -- | Turn a 'FilePath' callback into an 'Event' callback that ignores the -- 'Event' type and timestamp doAllEvents :: Monad m => (FilePath -> m ()) -> Event -> m () doAllEvents action event = case event of Added f _ -> action f Modified f _ -> action f Removed f _ -> action f -- | Turn a 'FilePath' predicate into an 'Event' predicate that accepts -- only 'Added' and 'Modified' event types existsEvents :: (FilePath -> Bool) -> (Event -> Bool) existsEvents filt event = case event of Added f _ -> filt f Modified f _ -> filt f Removed _ _ -> False -- | Turn a 'FilePath' predicate into an 'Event' predicate that accepts -- any event types allEvents :: (FilePath -> Bool) -> (Event -> Bool) allEvents filt event = case event of Added f _ -> filt f Modified f _ -> filt f Removed f _ -> filt f fsnotify-0.2.1/src/System/FSNotify/Listener.hs0000644000000000000000000000630712554771233017455 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- module System.FSNotify.Listener ( debounce , epsilonDefault , FileListener(..) , StopListening , newDebouncePayload ) where import Prelude hiding (FilePath) import Data.IORef (newIORef) import Data.Time (diffUTCTime, NominalDiffTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import System.FilePath import System.FSNotify.Types -- | An action that cancels a watching/listening job type StopListening = IO () -- | A typeclass that imposes structure on watch managers capable of listening -- for events, or simulated listening for events. class FileListener sessionType where -- | Initialize a file listener instance. initSession :: IO (Maybe sessionType) -- ^ Just an initialized file listener, -- or Nothing if this file listener -- cannot be supported. -- | Kill a file listener instance. -- This will immediately stop acting on events for all directories being -- watched. killSession :: sessionType -> IO () -- | Listen for file events associated with the immediate contents of a directory. -- Listening for events associated with immediate contents of a directory will -- only report events associated with files within the specified directory, and -- not files within its subdirectories. listen :: WatchConfig -> sessionType -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening -- | Listen for file events associated with all the contents of a directory. -- Listening for events associated with all the contents of a directory will -- report events associated with files within the specified directory and its -- subdirectories. listenRecursive :: WatchConfig -> sessionType -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening -- | Does this manager use polling? usesPolling :: sessionType -> Bool -- | The default maximum difference (exclusive, in seconds) for two -- events to be considered as occuring "at the same time". epsilonDefault :: NominalDiffTime epsilonDefault = 0.001 -- | The default event that provides a basis for comparison. eventDefault :: Event eventDefault = Added "" (posixSecondsToUTCTime 0) -- | A predicate indicating whether two events may be considered "the same -- event". This predicate is applied to the most recent dispatched event and -- the current event after the client-specified ActionPredicate is applied, -- before the event is dispatched. debounce :: NominalDiffTime -> Event -> Event -> Bool debounce epsilon e1 e2 = eventPath e1 == eventPath e2 && timeDiff > -epsilon && timeDiff < epsilon where timeDiff = diffUTCTime (eventTime e2) (eventTime e1) -- | Produces a fresh data payload used for debouncing events in a -- handler. newDebouncePayload :: Debounce -> IO DebouncePayload newDebouncePayload DebounceDefault = newIORef eventDefault >>= return . Just . DebounceData epsilonDefault newDebouncePayload (Debounce epsilon) = newIORef eventDefault >>= return . Just . DebounceData epsilon newDebouncePayload NoDebounce = return Nothing fsnotify-0.2.1/src/System/FSNotify/Path.hs0000644000000000000000000000555512554771233016570 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- {-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-} module System.FSNotify.Path ( findFiles , findDirs , canonicalizeDirPath , canonicalizePath , hasThisExtension ) where import Prelude hiding (FilePath) import Control.Applicative import Control.Monad -- import Filesystem -- import Filesystem.Path hiding (concat) import qualified Data.Text as T import qualified System.Directory as D import System.PosixCompat.Files as PF import System.FilePath getDirectoryContentsPath :: FilePath -> IO [FilePath] getDirectoryContentsPath path = (map (path )) . filter (not . dots) <$> D.getDirectoryContents path where dots "." = True dots ".." = True dots _ = False fileDirContents :: FilePath -> IO ([FilePath],[FilePath]) fileDirContents path = do contents <- getDirectoryContentsPath path stats <- mapM getFileStatus contents let pairs = zip stats contents let files = [ f | (s, f) <- pairs, PF.isRegularFile s] let dirs = [ d | (s, d) <- pairs, PF.isDirectory s] return (files, dirs) findAllFiles :: FilePath -> IO [FilePath] findAllFiles path = do (files, dirs) <- fileDirContents path nestedFiles <- mapM findAllFiles dirs return (files ++ concat nestedFiles) findImmediateFiles, findImmediateDirs :: FilePath -> IO [FilePath] findImmediateFiles = fileDirContents >=> mapM D.canonicalizePath . fst findImmediateDirs = fileDirContents >=> mapM D.canonicalizePath . snd findAllDirs :: FilePath -> IO [FilePath] findAllDirs path = do dirs <- findImmediateDirs path nestedDirs <- mapM findAllDirs dirs return (dirs ++ concat nestedDirs) findFiles :: Bool -> FilePath -> IO [FilePath] findFiles True path = findAllFiles =<< canonicalizeDirPath path findFiles False path = findImmediateFiles =<< canonicalizeDirPath path findDirs :: Bool -> FilePath -> IO [FilePath] findDirs True path = findAllDirs =<< canonicalizeDirPath path findDirs False path = findImmediateDirs =<< canonicalizeDirPath path -- | add a trailing slash to ensure the path indicates a directory addTrailingSlash :: FilePath -> FilePath addTrailingSlash = addTrailingPathSeparator canonicalizeDirPath :: FilePath -> IO FilePath canonicalizeDirPath path = addTrailingSlash `fmap` D.canonicalizePath path -- | bugfix older version of canonicalizePath (system-fileio <= 0.3.7) loses trailing slash canonicalizePath :: FilePath -> IO FilePath canonicalizePath path = let was_dir = null (takeFileName path) in if not was_dir then D.canonicalizePath path else canonicalizeDirPath path hasThisExtension :: FilePath -> T.Text -> Bool hasThisExtension p ext = takeExtension p == T.unpack ext fsnotify-0.2.1/src/System/FSNotify/Polling.hs0000644000000000000000000001171512554771233017273 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- module System.FSNotify.Polling ( createPollManager , PollManager(..) , FileListener(..) ) where import Prelude hiding (FilePath) import Control.Applicative import Control.Concurrent import Data.Map (Map) import Data.Maybe import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Time.Clock.POSIX -- import Debug.Trace (trace) import System.FilePath import System.FSNotify.Listener import System.FSNotify.Path (findFiles, canonicalizeDirPath) import System.FSNotify.Types import System.PosixCompat.Files import System.PosixCompat.Types import qualified Data.Map as Map import Control.Monad (forM_) data EventType = AddedEvent | ModifiedEvent | RemovedEvent data WatchKey = WatchKey ThreadId deriving (Eq, Ord) data WatchData = WatchData FilePath EventChannel type WatchMap = Map WatchKey WatchData data PollManager = PollManager (MVar WatchMap) generateEvent :: UTCTime -> EventType -> FilePath -> Maybe Event generateEvent timestamp AddedEvent filePath = Just (Added filePath timestamp) generateEvent timestamp ModifiedEvent filePath = Just (Modified filePath timestamp) generateEvent timestamp RemovedEvent filePath = Just (Removed filePath timestamp) generateEvents :: UTCTime -> EventType -> [FilePath] -> [Event] generateEvents timestamp eventType = mapMaybe (generateEvent timestamp eventType) handleEvent :: EventChannel -> ActionPredicate -> Event -> IO () handleEvent chan actPred event | actPred event = writeChan chan event | otherwise = return () pathModMap :: Bool -> FilePath -> IO (Map FilePath UTCTime) pathModMap True path = findFiles True path >>= pathModMap' pathModMap False path = findFiles False path >>= pathModMap' pathModMap' :: [FilePath] -> IO (Map FilePath UTCTime) pathModMap' files = fmap Map.fromList $ mapM pathAndTime files where pathAndTime :: FilePath -> IO (FilePath, UTCTime) pathAndTime path = do modTime <- getModificationTime path return (path, modTime) pollPath :: Int -> Bool -> EventChannel -> FilePath -> ActionPredicate -> Map FilePath UTCTime -> IO () pollPath interval recursive chan filePath actPred oldPathMap = do threadDelay interval newPathMap <- pathModMap recursive filePath 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 $ Map.keys createdMap handleEvents $ generateEvents' ModifiedEvent $ Map.keys modifiedMap handleEvents $ generateEvents' RemovedEvent $ Map.keys deletedMap pollPath' newPathMap where modifiedDifference :: UTCTime -> UTCTime -> Maybe UTCTime modifiedDifference newTime oldTime | oldTime /= newTime = Just newTime | otherwise = Nothing handleEvents :: [Event] -> IO () handleEvents = mapM_ (handleEvent chan actPred) pollPath' :: Map FilePath UTCTime -> IO () pollPath' = pollPath interval recursive chan filePath actPred -- Additional init funciton exported to allow startManager to unconditionally -- create a poll manager as a fallback when other managers will not instantiate. createPollManager :: IO PollManager createPollManager = fmap 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 () instance FileListener PollManager where initSession = fmap Just createPollManager killSession (PollManager mvarMap) = do watchMap <- readMVar mvarMap forM_ (Map.keys watchMap) killWatchingThread listen conf (PollManager mvarMap) path actPred chan = do path' <- canonicalizeDirPath path pmMap <- pathModMap False path' threadId <- forkIO $ pollPath (confPollInterval conf) False chan path' actPred pmMap let wk = WatchKey threadId modifyMVar_ mvarMap $ return . Map.insert wk (WatchData path' chan) return $ killAndUnregister mvarMap wk listenRecursive conf (PollManager mvarMap) path actPred chan = do path' <- canonicalizeDirPath path pmMap <- pathModMap True path' threadId <- forkIO $ pollPath (confPollInterval conf) True chan path' actPred pmMap let wk = WatchKey threadId modifyMVar_ mvarMap $ return . Map.insert wk (WatchData path' chan) return $ killAndUnregister mvarMap wk usesPolling = const True getModificationTime :: FilePath -> IO UTCTime getModificationTime p = fromEpoch . modificationTime <$> getFileStatus p fromEpoch :: EpochTime -> UTCTime fromEpoch = posixSecondsToUTCTime . realToFrac fsnotify-0.2.1/src/System/FSNotify/Types.hs0000644000000000000000000000677412554771233017004 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- module System.FSNotify.Types ( act , ActionPredicate , Action , WatchConfig(..) , Debounce(..) , DebounceData(..) , DebouncePayload , Event(..) , EventChannel , eventPath , eventTime , IOEvent ) where import Prelude hiding (FilePath) import Control.Concurrent.Chan import Data.IORef (IORef) import Data.Time (NominalDiffTime) import Data.Time.Clock (UTCTime) import System.FilePath -- | A file event reported by a file watcher. Each event contains the -- canonical path for the file and a timestamp guaranteed to be after the -- event occurred (timestamps represent current time when FSEvents receives -- it from the OS and/or platform-specific Haskell modules). data Event = Added FilePath UTCTime | Modified FilePath UTCTime | Removed FilePath UTCTime deriving (Eq, Show) -- | Helper for extracting the path associated with an event. eventPath :: Event -> FilePath eventPath (Added path _) = path eventPath (Modified path _) = path eventPath (Removed path _) = path -- | Helper for extracting the time associated with an event. eventTime :: Event -> UTCTime eventTime (Added _ timestamp) = timestamp eventTime (Modified _ timestamp) = timestamp eventTime (Removed _ timestamp) = timestamp type EventChannel = Chan Event -- | Watch configuration data WatchConfig = WatchConfig { confDebounce :: Debounce -- ^ Debounce configuration , confPollInterval :: Int -- ^ Polling interval if polling is used (microseconds) , confUsePolling :: Bool -- ^ Force use of polling, even if a more effective method may be -- available. This is mostly for testing purposes. } -- | This specifies whether multiple events from the same file should be -- collapsed together, and how close is close enough. -- -- This is performed by ignoring any event that occurs to the same file -- until the specified time interval has elapsed. -- -- Note that the current debouncing logic may fail to report certain changes -- to a file, potentially leaving your program in a state that is not -- consistent with the filesystem. -- -- Make sure that if you are using this feature, all changes you make as a -- result of an 'Event' notification are both non-essential and idempotent. data Debounce = DebounceDefault -- ^ perform debouncing based on the default time interval of 1 millisecond | Debounce NominalDiffTime -- ^ perform debouncing based on the specified time interval | NoDebounce -- ^ do not perform debouncing type IOEvent = IORef Event -- | DebouncePayload contents. Contains epsilon value for debouncing -- near-simultaneous events and an IORef of the latest Event. Difference in -- arrival time is measured according to Event value timestamps. data DebounceData = DebounceData NominalDiffTime IOEvent -- | Data "payload" passed to event handlers to enable debouncing. This value -- is automatically derived from a 'WatchConfig' value. A value of Just -- DebounceData results in debouncing according to the given epsilon and -- IOEvent. A value of Nothing results in no debouncing. type DebouncePayload = Maybe DebounceData -- | A predicate used to determine whether to act on an event. type ActionPredicate = Event -> Bool -- | An action to be performed in response to an event. type Action = Event -> IO () -- | Predicate to always act. act :: ActionPredicate act _ = True fsnotify-0.2.1/src/System/FSNotify/Linux.hs0000644000000000000000000001235212554771233016764 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- {-# LANGUAGE DeriveDataTypeable #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module System.FSNotify.Linux ( FileListener(..) , NativeManager ) where import Prelude hiding (FilePath) import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Exception import Control.Monad (when) import Data.IORef (atomicModifyIORef, readIORef) import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Typeable -- import Debug.Trace (trace) import System.FilePath import System.FSNotify.Listener import System.FSNotify.Path (findDirs, canonicalizeDirPath) import System.FSNotify.Types import qualified System.INotify as INo type NativeManager = INo.INotify data EventVarietyMismatchException = EventVarietyMismatchException deriving (Show, Typeable) instance Exception EventVarietyMismatchException -- Note that INo.Closed in this context is "modified" because we listen to -- CloseWrite events. fsnEvent :: FilePath -> UTCTime -> INo.Event -> Maybe Event fsnEvent basePath timestamp (INo.Created False name ) = Just (Added (basePath name) timestamp) fsnEvent basePath timestamp (INo.Closed False (Just name) _) = Just (Modified (basePath name) timestamp) fsnEvent basePath timestamp (INo.MovedOut False name _) = Just (Removed (basePath name) timestamp) fsnEvent basePath timestamp (INo.MovedIn False name _) = Just (Added (basePath name) timestamp) fsnEvent basePath timestamp (INo.Deleted False name ) = Just (Removed (basePath name) timestamp) fsnEvent _ _ _ = Nothing handleInoEvent :: ActionPredicate -> EventChannel -> FilePath -> DebouncePayload -> INo.Event -> IO () -- handleInoEvent _ _ basePath _ inoEvent | trace ("Linux: handleInoEvent " ++ show basePath ++ " " ++ show inoEvent) False = undefined handleInoEvent actPred chan basePath dbp inoEvent = do currentTime <- getCurrentTime let maybeFsnEvent = fsnEvent basePath currentTime inoEvent handleEvent actPred chan dbp maybeFsnEvent handleEvent :: ActionPredicate -> EventChannel -> DebouncePayload -> Maybe Event -> IO () -- handleEvent actPred _ _ (Just event) | trace ("Linux: handleEvent " ++ show (actPred event) ++ " " ++ show event) False = undefined handleEvent actPred chan dbp (Just event) = when (actPred event) $ case dbp of (Just (DebounceData epsilon ior)) -> do lastEvent <- readIORef ior when (not $ debounce epsilon lastEvent event) writeToChan atomicModifyIORef ior (\_ -> (event, ())) Nothing -> writeToChan where writeToChan = writeChan chan event -- handleEvent _ _ _ Nothing | trace ("Linux handleEvent Nothing") False = undefined handleEvent _ _ _ Nothing = return () varieties :: [INo.EventVariety] varieties = [INo.Create, INo.Delete, INo.MoveIn, INo.MoveOut, INo.CloseWrite] instance FileListener INo.INotify where initSession = fmap Just INo.initINotify killSession = INo.killINotify listen conf iNotify path actPred chan = do path' <- canonicalizeDirPath path dbp <- newDebouncePayload $ confDebounce conf wd <- INo.addWatch iNotify varieties path' (handler path' dbp) return $ INo.removeWatch wd where handler :: FilePath -> DebouncePayload -> INo.Event -> IO () handler = handleInoEvent actPred chan listenRecursive conf iNotify initialPath actPred chan = do -- wdVar stores the list of created watch descriptors. We use it to -- cancel the whole recursive listening task. -- -- To avoid a race condition (when a new watch is added right after -- we've stopped listening), we replace the MVar contents with Nothing -- to signify that the listening task is cancelled, and no new watches -- should be added. wdVar <- newMVar (Just []) let stopListening = do modifyMVar_ wdVar $ \mbWds -> do maybe (return ()) (mapM_ INo.removeWatch) mbWds return Nothing listenRec initialPath wdVar return stopListening where listenRec :: FilePath -> MVar (Maybe [INo.WatchDescriptor]) -> IO () listenRec path wdVar = do path' <- canonicalizeDirPath path paths <- findDirs True path' mapM_ (pathHandler wdVar) (path':paths) pathHandler :: MVar (Maybe [INo.WatchDescriptor]) -> FilePath -> IO () pathHandler wdVar filePath = do dbp <- newDebouncePayload $ confDebounce conf modifyMVar_ wdVar $ \mbWds -> -- Atomically add a watch and record its descriptor. Also, check -- if the listening task is cancelled, in which case do nothing. case mbWds of Nothing -> return mbWds Just wds -> do wd <- INo.addWatch iNotify varieties filePath (handler filePath dbp) return $ Just (wd:wds) where handler :: FilePath -> DebouncePayload -> INo.Event -> IO () handler baseDir _ (INo.Created True dirPath) = do listenRec (baseDir dirPath) wdVar handler baseDir dbp event = handleInoEvent actPred chan baseDir dbp event usesPolling = const False fsnotify-0.2.1/src/System/FSNotify/Win32.hs0000644000000000000000000000657712556340013016571 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- {-# OPTIONS_GHC -fno-warn-orphans #-} module System.FSNotify.Win32 ( FileListener(..) , NativeManager ) where import Prelude import Control.Concurrent.Chan import Control.Monad (when) import Data.IORef (atomicModifyIORef, readIORef) import Data.Time (getCurrentTime, UTCTime) import System.FSNotify.Listener import System.FSNotify.Path (canonicalizeDirPath) import System.FSNotify.Types import System.FilePath import qualified System.Win32.Notify as WNo type NativeManager = WNo.WatchManager -- | Apparently Win32 gives back relative paths, so we pass around the base -- directory to turn them into absolute ones type BaseDir = FilePath -- NEXT TODO: Need to ensure we use properly canonalized paths as -- event paths. In Linux this required passing the base dir to -- handle[native]Event. -- Win32-notify has (temporarily?) dropped support for Renamed events. fsnEvent :: BaseDir -> UTCTime -> WNo.Event -> Maybe Event fsnEvent basedir timestamp ev = case ev of WNo.Created False name -> Just $ Added (basedir name) timestamp WNo.Modified False name -> Just $ Modified (basedir name) timestamp WNo.Deleted False name -> Just $ Removed (basedir name) timestamp _ -> Nothing {- fsnEvents timestamp (WNo.Renamed False (Just oldName) newName) = [Removed (fp oldName) timestamp, Added (fp newName) timestamp] fsnEvents timestamp (WNo.Renamed False Nothing newName) = [Added (fp newName) timestamp] -} handleWNoEvent :: BaseDir -> ActionPredicate -> EventChannel -> DebouncePayload -> WNo.Event -> IO () handleWNoEvent basedir actPred chan dbp inoEvent = do currentTime <- getCurrentTime let maybeEvent = fsnEvent basedir currentTime inoEvent case maybeEvent of Just evt -> handleEvent actPred chan dbp evt Nothing -> return () handleEvent :: ActionPredicate -> EventChannel -> DebouncePayload -> Event -> IO () handleEvent actPred chan dbp event = when (actPred event) $ case dbp of (Just (DebounceData epsilon ior)) -> do lastEvent <- readIORef ior when (not $ debounce epsilon lastEvent event) writeToChan atomicModifyIORef ior (\_ -> (event, ())) Nothing -> writeToChan where writeToChan = writeChan chan event instance FileListener WNo.WatchManager where -- TODO: This should actually lookup a Windows API version and possibly return -- Nothing the calls we need are not available. This will require that API -- version information be exposed by Win32-notify. initSession = fmap Just WNo.initWatchManager killSession = WNo.killWatchManager listen conf watchManager path actPred chan = do path' <- canonicalizeDirPath path dbp <- newDebouncePayload $ confDebounce conf wid <- WNo.watchDirectory watchManager path' False varieties (handleWNoEvent path' actPred chan dbp) return $ WNo.killWatch wid listenRecursive conf watchManager path actPred chan = do path' <- canonicalizeDirPath path dbp <- newDebouncePayload $ confDebounce conf wid <- WNo.watchDirectory watchManager path' True varieties (handleWNoEvent path' actPred chan dbp) return $ WNo.killWatch wid usesPolling = const False varieties :: [WNo.EventVariety] varieties = [WNo.Create, WNo.Delete, WNo.Move, WNo.Modify] fsnotify-0.2.1/src/System/FSNotify/OSX.hs0000644000000000000000000001333612556340013016327 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- module System.FSNotify.OSX ( FileListener(..) , NativeManager ) where import Prelude hiding (FilePath) import Control.Concurrent.Chan import Control.Concurrent.MVar import Control.Monad import Data.Bits import Data.IORef (atomicModifyIORef, readIORef) import Data.Map (Map) import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Word import Data.Unique import System.FilePath import System.Directory import System.FSNotify.Listener import System.FSNotify.Path (canonicalizeDirPath) import System.FSNotify.Types import qualified Data.Map as Map import qualified System.OSX.FSEvents as FSE data ListenType = NonRecursive | Recursive data WatchData = WatchData FSE.EventStream ListenType EventChannel type WatchMap = Map Unique WatchData data OSXManager = OSXManager (MVar WatchMap) type NativeManager = OSXManager nil :: Word64 nil = 0x00 -- OS X reports the absolute (canonical) path without a trailing slash. Add -- the trailing slash when the path refers to a directory canonicalEventPath :: FSE.Event -> FilePath canonicalEventPath event = if flags .&. dirFlag /= nil then addTrailingPathSeparator path else path where flags = FSE.eventFlags event dirFlag = FSE.eventFlagItemIsDir path = FSE.eventPath event fsnEvents :: UTCTime -> FSE.Event -> IO [Event] fsnEvents timestamp fseEvent = liftM concat . sequence $ map (\f -> f fseEvent) (eventFunctions timestamp) where eventFunctions :: UTCTime -> [FSE.Event -> IO [Event]] eventFunctions t = [addedFn t, modifFn t, removFn t, renamFn t] addedFn t e = if hasFlag e FSE.eventFlagItemCreated then return [Added (path e) t] else return [] modifFn t e = if (hasFlag e FSE.eventFlagItemModified || hasFlag e FSE.eventFlagItemInodeMetaMod) then return [Modified (path e) t] else return [] removFn t e = if hasFlag e FSE.eventFlagItemRemoved then return [Removed (path e) t] else return [] renamFn t e = if hasFlag e FSE.eventFlagItemRenamed then doesFileExist (path e) >>= \exists -> if exists then return [Added (path e) t] else return [Removed (path e) t] else return [] path = canonicalEventPath hasFlag event flag = FSE.eventFlags event .&. flag /= 0 -- Separate logic is needed for non-recursive events in OSX because the -- hfsevents package doesn't support non-recursive event reporting. handleNonRecursiveFSEEvent :: ActionPredicate -> EventChannel -> FilePath -> DebouncePayload -> FSE.Event -> IO () handleNonRecursiveFSEEvent actPred chan dirPath dbp fseEvent = do currentTime <- getCurrentTime events <- fsnEvents currentTime fseEvent handleNonRecursiveEvents actPred chan dirPath dbp events handleNonRecursiveEvents :: ActionPredicate -> EventChannel -> FilePath -> DebouncePayload -> [Event] -> IO () handleNonRecursiveEvents actPred chan dirPath dbp (event:events) | takeDirectory dirPath == takeDirectory (eventPath event) && actPred event = do case dbp of (Just (DebounceData epsilon ior)) -> do lastEvent <- readIORef ior when (not $ debounce epsilon lastEvent event) (writeChan chan event) atomicModifyIORef ior (\_ -> (event, ())) Nothing -> writeChan chan event handleNonRecursiveEvents actPred chan dirPath dbp events | otherwise = handleNonRecursiveEvents actPred chan dirPath dbp events handleNonRecursiveEvents _ _ _ _ [] = return () handleFSEEvent :: ActionPredicate -> EventChannel -> DebouncePayload -> FSE.Event -> IO () handleFSEEvent actPred chan dbp fseEvent = do currentTime <- getCurrentTime events <- fsnEvents currentTime fseEvent handleEvents actPred chan dbp events handleEvents :: ActionPredicate -> EventChannel -> DebouncePayload -> [Event] -> IO () handleEvents actPred chan dbp (event:events) = do when (actPred event) $ case dbp of (Just (DebounceData epsilon ior)) -> do lastEvent <- readIORef ior when (not $ debounce epsilon lastEvent event) (writeChan chan event) atomicModifyIORef ior (\_ -> (event, ())) Nothing -> writeChan chan event handleEvents actPred chan dbp events handleEvents _ _ _ [] = return () listenFn :: (ActionPredicate -> EventChannel -> FilePath -> DebouncePayload -> FSE.Event -> IO a) -> WatchConfig -> OSXManager -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening listenFn handler conf (OSXManager mvarMap) path actPred chan = do path' <- canonicalizeDirPath path dbp <- newDebouncePayload $ confDebounce conf unique <- newUnique eventStream <- FSE.eventStreamCreate [path'] 0.0 True False True (handler actPred chan path' dbp) modifyMVar_ mvarMap $ \watchMap -> return (Map.insert unique (WatchData eventStream NonRecursive chan) watchMap) return $ do FSE.eventStreamDestroy eventStream modifyMVar_ mvarMap $ \watchMap -> return $ Map.delete unique watchMap instance FileListener OSXManager where initSession = do (v1, v2, _) <- FSE.osVersion if not $ v1 > 10 || (v1 == 10 && v2 > 6) then return Nothing else fmap (Just . OSXManager) $ newMVar Map.empty killSession (OSXManager mvarMap) = do watchMap <- readMVar mvarMap forM_ (Map.elems watchMap) eventStreamDestroy' where eventStreamDestroy' :: WatchData -> IO () eventStreamDestroy' (WatchData eventStream _ _) = FSE.eventStreamDestroy eventStream listen = listenFn handleNonRecursiveFSEEvent listenRecursive = listenFn $ \actPred chan _ -> handleFSEEvent actPred chan usesPolling = const False fsnotify-0.2.1/test/test.hs0000644000000000000000000000545112554771233014051 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, ImplicitParams #-} import Prelude hiding ( FilePath ) import Control.Applicative import Test.Tasty import Test.Tasty.HUnit import System.Directory import System.FilePath import System.FSNotify import System.IO.Error import System.IO.Temp import System.PosixCompat.Files import Control.Monad import Control.Exception import Control.Concurrent import EventUtils nativeMgrSupported :: IO Bool nativeMgrSupported = do mgr <- startManager stopManager mgr return $ not $ isPollingManager mgr main :: IO () main = do hasNative <- nativeMgrSupported unless hasNative $ putStrLn "WARNING: native manager cannot be used or tested on this platform" defaultMain $ withResource (createDirectoryIfMissing True testDirPath) (const $ removeDirectoryRecursive testDirPath) $ const $ tests hasNative tests :: Bool -> TestTree tests hasNative = testGroup "Tests" $ do poll <- if hasNative then [False, True] else [True] let ?timeInterval = if poll then 2*10^(6 :: Int) else 5*10^(5 :: Int) return $ testGroup (if poll then "Polling" else "Native") $ do recursive <- [False, True] return $ testGroup (if recursive then "Recursive" else "Non-recursive") $ do nested <- [False, True] return $ testGroup (if nested then "In a subdirectory" else "Right here") $ do t <- [ mkTest "new file" (if poll then [evAdded] else [evAdded, evModified]) (const $ return ()) (\f -> writeFile f "foo") , mkTest "modify file" [evModified] (\f -> writeFile f "") (\f -> when poll (threadDelay $ 10^(6 :: Int)) >> writeFile f "foo") , mkTest "delete file" [evRemoved] (\f -> writeFile f "") (\f -> removeFile f) , mkTest "directories are ignored" [] (const $ return ()) (\f -> createDirectory f >> removeDirectory f) ] return $ t nested recursive poll where mkTest title evs prepare action nested recursive poll = testCase title $ withTempDirectory testDirPath "test." $ \watchedDir -> do let baseDir = if nested then watchedDir "subdir" else watchedDir f = baseDir fileName expect = expectEvents poll (if recursive then watchTree else watchDir) watchedDir createDirectoryIfMissing True baseDir (prepare f >> expect (if not nested || recursive then map ($ f) evs else []) (action f)) `finally` (isFile f >>= \b -> when b (removeFile f)) fileName = "testfile" ------------------------------------------------------------------------------- isFile :: FilePath -> IO Bool isFile p = handleJust h return checkFile where h e = if isDoesNotExistError e then Just False else Nothing checkFile = isRegularFile <$> getFileStatus p fsnotify-0.2.1/test/EventUtils.hs0000644000000000000000000000570412554771233015175 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, ImplicitParams #-} module EventUtils where import Prelude hiding (FilePath) import Test.Tasty.HUnit import Control.Concurrent import Control.Concurrent.Async hiding (poll) import Control.Applicative import Control.Monad import Data.IORef import Data.List (sortBy) import Data.Ord (comparing) import System.FilePath import System.FSNotify import System.IO.Unsafe import System.Directory import Text.Printf delay :: (?timeInterval :: Int) => IO () delay = threadDelay ?timeInterval -- event patterns data EventPattern = EventPattern { patFile :: FilePath , patName :: String , patPredicate :: Event -> Bool } evAdded, evRemoved, evModified :: FilePath -> EventPattern evAdded path = EventPattern path "Added" (\x -> case x of Added path' _ -> path == path'; _ -> False) evRemoved path = EventPattern path "Removed" (\x -> case x of Removed path' _ -> path == path'; _ -> False) evModified path = EventPattern path "Modified" (\x -> case x of Modified path' _ -> path == path'; _ -> False) matchEvents :: [EventPattern] -> [Event] -> Assertion matchEvents expected actual = do unless (length expected == length actual) $ assertFailure $ printf "Unexpected number of events.\n Expected: %s\n Actual: %s\n" (show expected) (show actual) sequence_ $ (\f -> zipWith f expected actual) $ \pat ev -> assertBool (printf "Unexpected event.\n Expected :%s\n Actual: %s\n" (show expected) (show actual)) (patPredicate pat ev) instance Show EventPattern where show p = printf "%s %s" (patName p) (show $ patFile p) gatherEvents :: (?timeInterval :: Int) => Bool -- use polling? -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -- (^ this is the type of watchDir/watchTree) -> FilePath -> IO (Async [Event]) gatherEvents poll watch path = do mgr <- startManagerConf defaultConfig { confDebounce = NoDebounce , confUsePolling = poll , confPollInterval = 2 * 10^(5 :: Int) } eventsVar <- newIORef [] stop <- watch mgr path (const True) (\ev -> atomicModifyIORef eventsVar (\evs -> (ev:evs, ()))) async $ do delay stop reverse <$> readIORef eventsVar expectEvents :: (?timeInterval :: Int) => Bool -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -> FilePath -> [EventPattern] -> IO () -> Assertion expectEvents poll w path pats action = do a <- gatherEvents poll w path action evs <- wait a matchEvents pats $ sortBy (comparing eventTime) evs testDirPath :: FilePath testDirPath = (unsafePerformIO getCurrentDirectory) "testdir" expectEventsHere :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHere poll = expectEvents poll watchDir testDirPath expectEventsHereRec :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHereRec poll = expectEvents poll watchTree testDirPath fsnotify-0.2.1/LICENSE0000644000000000000000000000276212554771233012566 0ustar0000000000000000Copyright (c) 2012, Mark Dittmer All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Mark Dittmer nor the names of other contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. fsnotify-0.2.1/Setup.hs0000644000000000000000000000031412554771233013204 0ustar0000000000000000-- -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org -- import Distribution.Simple main = defaultMain fsnotify-0.2.1/fsnotify.cabal0000644000000000000000000000521612556340043014374 0ustar0000000000000000Name: fsnotify Version: 0.2.1 Author: Mark Dittmer Maintainer: Greg Weber , Roman Cheplyaka License: BSD3 License-File: LICENSE Synopsis: Cross platform library for file change notification. Description: Cross platform library for file creation, modification, and deletion notification. This library builds upon existing libraries for platform-specific Windows, Mac, and Linux filesystem event notification. Category: Filesystem Cabal-Version: >= 1.8 Build-Type: Simple Homepage: https://github.com/haskell-fswatch/hfsnotify Extra-Source-Files: README.md CHANGELOG.md test/test.hs test/EventUtils.hs Library Build-Depends: base >= 4.3.1 && < 5 , containers >= 0.4 , directory >= 1.1.0.0 , filepath >= 1.3.0.0 , text >= 0.11.0 , time >= 1.1 , async >= 2.0.1 , unix-compat >= 0.2 Exposed-Modules: System.FSNotify , System.FSNotify.Devel Other-Modules: System.FSNotify.Listener , System.FSNotify.Path , System.FSNotify.Polling , System.FSNotify.Types Hs-Source-Dirs: src GHC-Options: -Wall if os(linux) CPP-Options: -DOS_Linux Other-Modules: System.FSNotify.Linux Build-Depends: hinotify >= 0.3.7 else if os(windows) CPP-Options: -DOS_Win32 Other-Modules: System.FSNotify.Win32 Build-Depends: Win32-notify >= 0.3 else if os(darwin) CPP-Options: -DOS_Mac Other-Modules: System.FSNotify.OSX Build-Depends: hfsevents >= 0.1.3 Test-Suite test Type: exitcode-stdio-1.0 Main-Is: test.hs Other-modules: EventUtils Hs-Source-Dirs: test GHC-Options: -Wall -threaded Build-depends: base >= 4.3.1.0 , tasty >= 0.5 , tasty-hunit , directory , filepath , unix-compat , fsnotify , async >= 2 , temporary-rc Source-Repository head Type: git Location: git://github.com/haskell-fswatch/hfsnotify fsnotify-0.2.1/README.md0000644000000000000000000000050612554771233013032 0ustar0000000000000000hfsnotify ========= Unified Haskell interface for basic file system notifications. This is a library. There are executables built on top of it. * [spy](https://hackage.haskell.org/package/spy) * [steeloverseer](https://github.com/schell/steeloverseer) Requirements ============ Windows ------- compile with -fthreaded fsnotify-0.2.1/CHANGELOG.md0000644000000000000000000000216612554771567013402 0ustar0000000000000000Changes ======= Version 0.2 ----------- Use filepath instead of deprecated system-filepath Version 0.1.0.3 --------------- * Fix the tests Version 0.1.0.2 --------------- * Restore compatibility with GHC 7.4 * Fix a bug in `treeExtAny`, which previously work identically to `treeExtExists` * Improve documentation Version 0.1.0.1 --------------- Include CHANGELOG.md and README.md in the source distribution. Version 0.1 ----------- * Allow to stop a listening job. Note this changes the return type of watching functions from `()` to `IO ()`. * Previously, some care was taken to prevent multiple callbacks from running simultaneously. It is now the user's responsibility. See [#43](https://github.com/haskell-fswatch/hfsnotify/issues/43) for details. * Previously, paths returned to callbacks were relative on Windows. Now they are absolute, like on the other platforms. * The `WatchConfig` type has changed. Previously, it only specified debouncing parameters. Now it also contains polling parameters. * The `isPollingManager` function is added to determine, at runtime, whether the polling implementation is used. fsnotify-0.2.1/test/test.hs0000644000000000000000000000545112554771233014051 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, ImplicitParams #-} import Prelude hiding ( FilePath ) import Control.Applicative import Test.Tasty import Test.Tasty.HUnit import System.Directory import System.FilePath import System.FSNotify import System.IO.Error import System.IO.Temp import System.PosixCompat.Files import Control.Monad import Control.Exception import Control.Concurrent import EventUtils nativeMgrSupported :: IO Bool nativeMgrSupported = do mgr <- startManager stopManager mgr return $ not $ isPollingManager mgr main :: IO () main = do hasNative <- nativeMgrSupported unless hasNative $ putStrLn "WARNING: native manager cannot be used or tested on this platform" defaultMain $ withResource (createDirectoryIfMissing True testDirPath) (const $ removeDirectoryRecursive testDirPath) $ const $ tests hasNative tests :: Bool -> TestTree tests hasNative = testGroup "Tests" $ do poll <- if hasNative then [False, True] else [True] let ?timeInterval = if poll then 2*10^(6 :: Int) else 5*10^(5 :: Int) return $ testGroup (if poll then "Polling" else "Native") $ do recursive <- [False, True] return $ testGroup (if recursive then "Recursive" else "Non-recursive") $ do nested <- [False, True] return $ testGroup (if nested then "In a subdirectory" else "Right here") $ do t <- [ mkTest "new file" (if poll then [evAdded] else [evAdded, evModified]) (const $ return ()) (\f -> writeFile f "foo") , mkTest "modify file" [evModified] (\f -> writeFile f "") (\f -> when poll (threadDelay $ 10^(6 :: Int)) >> writeFile f "foo") , mkTest "delete file" [evRemoved] (\f -> writeFile f "") (\f -> removeFile f) , mkTest "directories are ignored" [] (const $ return ()) (\f -> createDirectory f >> removeDirectory f) ] return $ t nested recursive poll where mkTest title evs prepare action nested recursive poll = testCase title $ withTempDirectory testDirPath "test." $ \watchedDir -> do let baseDir = if nested then watchedDir "subdir" else watchedDir f = baseDir fileName expect = expectEvents poll (if recursive then watchTree else watchDir) watchedDir createDirectoryIfMissing True baseDir (prepare f >> expect (if not nested || recursive then map ($ f) evs else []) (action f)) `finally` (isFile f >>= \b -> when b (removeFile f)) fileName = "testfile" ------------------------------------------------------------------------------- isFile :: FilePath -> IO Bool isFile p = handleJust h return checkFile where h e = if isDoesNotExistError e then Just False else Nothing checkFile = isRegularFile <$> getFileStatus p fsnotify-0.2.1/test/EventUtils.hs0000644000000000000000000000570412554771233015175 0ustar0000000000000000{-# LANGUAGE OverloadedStrings, ImplicitParams #-} module EventUtils where import Prelude hiding (FilePath) import Test.Tasty.HUnit import Control.Concurrent import Control.Concurrent.Async hiding (poll) import Control.Applicative import Control.Monad import Data.IORef import Data.List (sortBy) import Data.Ord (comparing) import System.FilePath import System.FSNotify import System.IO.Unsafe import System.Directory import Text.Printf delay :: (?timeInterval :: Int) => IO () delay = threadDelay ?timeInterval -- event patterns data EventPattern = EventPattern { patFile :: FilePath , patName :: String , patPredicate :: Event -> Bool } evAdded, evRemoved, evModified :: FilePath -> EventPattern evAdded path = EventPattern path "Added" (\x -> case x of Added path' _ -> path == path'; _ -> False) evRemoved path = EventPattern path "Removed" (\x -> case x of Removed path' _ -> path == path'; _ -> False) evModified path = EventPattern path "Modified" (\x -> case x of Modified path' _ -> path == path'; _ -> False) matchEvents :: [EventPattern] -> [Event] -> Assertion matchEvents expected actual = do unless (length expected == length actual) $ assertFailure $ printf "Unexpected number of events.\n Expected: %s\n Actual: %s\n" (show expected) (show actual) sequence_ $ (\f -> zipWith f expected actual) $ \pat ev -> assertBool (printf "Unexpected event.\n Expected :%s\n Actual: %s\n" (show expected) (show actual)) (patPredicate pat ev) instance Show EventPattern where show p = printf "%s %s" (patName p) (show $ patFile p) gatherEvents :: (?timeInterval :: Int) => Bool -- use polling? -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -- (^ this is the type of watchDir/watchTree) -> FilePath -> IO (Async [Event]) gatherEvents poll watch path = do mgr <- startManagerConf defaultConfig { confDebounce = NoDebounce , confUsePolling = poll , confPollInterval = 2 * 10^(5 :: Int) } eventsVar <- newIORef [] stop <- watch mgr path (const True) (\ev -> atomicModifyIORef eventsVar (\evs -> (ev:evs, ()))) async $ do delay stop reverse <$> readIORef eventsVar expectEvents :: (?timeInterval :: Int) => Bool -> (WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening) -> FilePath -> [EventPattern] -> IO () -> Assertion expectEvents poll w path pats action = do a <- gatherEvents poll w path action evs <- wait a matchEvents pats $ sortBy (comparing eventTime) evs testDirPath :: FilePath testDirPath = (unsafePerformIO getCurrentDirectory) "testdir" expectEventsHere :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHere poll = expectEvents poll watchDir testDirPath expectEventsHereRec :: (?timeInterval::Int) => Bool -> [EventPattern] -> IO () -> Assertion expectEventsHereRec poll = expectEvents poll watchTree testDirPath