packages feed

fsnotify 0.0.11 → 0.1

raw patch · 16 files changed

+470/−761 lines, 16 filesdep +asyncdep +directorydep +fsnotifydep −Cabaldep −Globdep −QuickCheckdep ~basedep ~hinotifydep ~system-fileionew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: async, directory, fsnotify, tasty, tasty-hunit, temporary-rc

Dependencies removed: Cabal, Glob, QuickCheck, bytestring, ghc, hspec, random, uniqueid

Dependency ranges changed: base, hinotify, system-fileio, system-filepath, text

API changes (from Hackage documentation)

+ System.FSNotify: WatchConfig :: Debounce -> Int -> Bool -> WatchConfig
+ System.FSNotify: confDebounce :: WatchConfig -> Debounce
+ System.FSNotify: confPollInterval :: WatchConfig -> Int
+ System.FSNotify: confUsePolling :: WatchConfig -> Bool
+ System.FSNotify: data Debounce
+ System.FSNotify: isPollingManager :: WatchManager -> Bool
+ System.FSNotify: type StopListening = IO ()
- System.FSNotify: Debounce :: NominalDiffTime -> WatchConfig
+ System.FSNotify: Debounce :: NominalDiffTime -> Debounce
- System.FSNotify: DebounceDefault :: WatchConfig
+ System.FSNotify: DebounceDefault :: Debounce
- System.FSNotify: NoDebounce :: WatchConfig
+ System.FSNotify: NoDebounce :: Debounce
- System.FSNotify: watchDir :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO ()
+ System.FSNotify: watchDir :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening
- System.FSNotify: watchDirChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO ()
+ System.FSNotify: watchDirChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening
- System.FSNotify: watchTree :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO ()
+ System.FSNotify: watchTree :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening
- System.FSNotify: watchTreeChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO ()
+ System.FSNotify: watchTreeChan :: WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO StopListening
- System.FSNotify.Devel: treeExtAny :: WatchManager -> FilePath -> Text -> (FilePath -> IO ()) -> IO ()
+ System.FSNotify.Devel: treeExtAny :: WatchManager -> FilePath -> Text -> (FilePath -> IO ()) -> IO StopListening
- System.FSNotify.Devel: treeExtExists :: WatchManager -> FilePath -> Text -> (FilePath -> IO ()) -> IO ()
+ System.FSNotify.Devel: treeExtExists :: WatchManager -> FilePath -> Text -> (FilePath -> IO ()) -> IO StopListening

Files

fsnotify.cabal view
@@ -1,7 +1,7 @@ Name:                   fsnotify-Version:                0.0.11+Version:                0.1 Author:                 Mark Dittmer <mark.s.dittmer@gmail.com>-Maintainer:             Mark Dittmer <mark.s.dittmer@gmail.com>, Greg Weber <greg@gregweber.info>+Maintainer:             Greg Weber <greg@gregweber.info>, Roman Cheplyaka <roma@ro-che.info> License:                BSD3 License-File:           LICENSE Synopsis:               Cross platform library for file change notification.@@ -12,19 +12,19 @@ Category:               Filesystem Cabal-Version:          >= 1.8 Build-Type:             Simple-Extra-Source-Files:     test/FSNotify.hs-                        test/Path.hs-                        test/Util.hs-                        test/watch-here.hs+Extra-Source-Files:+  test/test.hs+  test/EventUtils.hs   Library-  Build-Depends:          base >= 4.3.1.0 && < 5+  Build-Depends:          base >= 4.3.1 && < 5                         , containers >= 0.4-                        , system-fileio >= 0.3.8 && < 0.4-                        , system-filepath >= 0.4.6 && <= 0.5+                        , system-fileio >= 0.3.8+                        , system-filepath >= 0.4.6                         , text >= 0.11.0                         , time >= 1.1+                        , async >= 2.0.1   Exposed-Modules:        System.FSNotify                         , System.FSNotify.Devel   Other-Modules:          System.FSNotify.Listener@@ -36,7 +36,7 @@   if os(linux)     CPP-Options:        -DOS_Linux     Other-Modules:      System.FSNotify.Linux-    Build-Depends:      hinotify >= 0.3.5 && < 0.4+    Build-Depends:      hinotify >= 0.3.7   else     if os(windows)       CPP-Options:      -DOS_Win32@@ -48,52 +48,22 @@         Other-Modules:  System.FSNotify.OSX         Build-Depends:  hfsevents >= 0.1.3 --- executable watch-here---   hs-source-dirs: src, test---   main-is: watch-here.hs---   build-depends:  base---                 , containers >= 0.4---                 , directory >= 1.1.0.2---                 , filepath >= 1.3.0.0---                 , Glob >= 0.7.1---                 , system-fileio >= 0.3.8---                 , system-filepath >= 0.4.6---                 , text >= 0.11.0---                 , time >= 1.1- Test-Suite test   Type:                 exitcode-stdio-1.0-  Main-Is:              main.hs-  -- Type:                 detailed-0.9-  -- Test-Module:          Tests-  Hs-Source-Dirs:       test, src+  Main-Is:              test.hs+  Other-modules:        EventUtils+  Hs-Source-Dirs:       test   GHC-Options:          -Wall -threaded   Build-depends:          base >= 4.3.1.0-                        , bytestring >= 0.9.2.1-                        , Cabal >= 1.14.0-                        , containers >= 0.4-                        , Glob >= 0.7.1-                        , hspec >= 1.3.0-                        , random >= 1.0.1.1-                        , system-filepath >= 0.4.6-                        , system-fileio >= 0.3.7-                        , text >= 0.10-                        , time >= 1.1-                        , QuickCheck >= 2.4.2-                        , uniqueid >= 0.1.1-  if os(linux)-    CPP-Options:        -DOS_Linux-    Build-Depends:      hinotify >= 0.3.5 && < 0.4-  else-    if os(windows)-      CPP-Options:      -DOS_Win32-      Build-Depends:    Win32-notify >= 0.3, ghc >= 7.4.2-    else-      if os(darwin)-        CPP-Options:    -DOS_Mac-        Build-Depends:  hfsevents >= 0.1.3-+                        , tasty >= 0.5+                        , tasty-hunit+                        , system-filepath >= 0.4.7+                        , system-fileio >= 0.3.11+                        , directory+                        , fsnotify+                        , async >= 2+                        , temporary-rc  Source-Repository head   Type:                 git-  Location:             git://github.com/mdittmer/hfsnotify+  Location:             git://github.com/haskell-fswatch/hfsnotify
src/System/FSNotify.hs view
@@ -2,13 +2,31 @@ -- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org -- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org ---{-# LANGUAGE CPP, ScopedTypeVariables #-}+{-# LANGUAGE CPP, ScopedTypeVariables, ExistentialQuantification, RankNTypes #-} --- | cross-platform file watching.+-- | 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 maxBound  module System.FSNotify        (-       +        -- * Events          Event(..)        , EventChannel@@ -24,8 +42,11 @@        , stopManager        , defaultConfig        , WatchConfig(..)+       , Debounce(..)        , withManagerConf        , startManagerConf+       , StopListening+       , isPollingManager         -- * Watching        , watchDir@@ -34,16 +55,20 @@        , watchTreeChan        ) where -import Prelude hiding (FilePath, catch)+import Prelude hiding (FilePath) +import Data.Maybe import Control.Concurrent+import Control.Concurrent.Async import Control.Exception-import Data.Map (Map)+import Control.Applicative+import Control.Monad import Filesystem.Path.CurrentOS import System.FSNotify.Polling import System.FSNotify.Types-import qualified Data.Map as Map +import System.FSNotify.Listener (StopListening)+ #ifdef OS_Linux import System.FSNotify.Linux #else@@ -58,9 +83,22 @@ #  endif #endif -data WatchManager = WatchManager WatchConfig (Either PollManager NativeManager)+-- | 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 defaultConfig :: WatchConfig-defaultConfig = DebounceDefault+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.@@ -80,81 +118,98 @@ -- Stopping a watch manager will immediately stop -- watching for files and free resources. stopManager :: WatchManager -> IO ()-stopManager (WatchManager _ wm) =-  case wm of-    Right native -> killSession native-    Left poll    -> killSession poll+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 debounce = bracket (startManagerConf debounce) stopManager+withManagerConf conf = bracket (startManagerConf conf) stopManager +-- | Like 'startManager', but configurable startManagerConf :: WatchConfig -> IO WatchManager-startManagerConf debounce = initSession >>= createManager+startManagerConf conf+  | confUsePolling conf = pollingManager+  | otherwise = initSession >>= createManager   where     createManager :: Maybe NativeManager -> IO WatchManager-    createManager (Just nativeManager) = return (WatchManager debounce (Right nativeManager))-    createManager Nothing = return . (WatchManager debounce) . Left =<< createPollManager+    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 ()-watchDirChan (WatchManager db wm) = either (listen db) (listen db) wm+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 ()-watchTreeChan (WatchManager db wm) = either (listenRecursive db) (listenRecursive db) wm+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 ()-watchDir (WatchManager db wm) = either runFallback runNative wm-  where-    runFallback = threadChanFallback $ listen db-    runNative   = threadChanNative   $ listen db--threadChanNative :: (NativeManager -> FilePath -> ActionPredicate -> Chan Event -> IO b) -> NativeManager -> FilePath -> ActionPredicate -> Action -> IO b-threadChanNative listener iface path actPred action =-      threadChan action $ listener iface path actPred--threadChanFallback :: (PollManager -> FilePath -> ActionPredicate -> Chan Event -> IO b) -> PollManager -> FilePath -> ActionPredicate -> Action -> IO b-threadChanFallback listener iface path actPred action =-      threadChan action $ listener iface path actPred--threadChan :: Action -> (Chan Event -> IO b) -> IO b-threadChan action runListener = do-      chan <- newChan-      _    <- forkIO $ readEvents chan action Map.empty-      runListener chan-+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 ()-watchTree (WatchManager db wm) = either runFallback runNative wm-  where-    runFallback = threadChanFallback $ listenRecursive db-    runNative   = threadChanNative   $ listenRecursive db+watchTree :: WatchManager -> FilePath -> ActionPredicate -> Action -> IO StopListening+watchTree wm = threadChan listenRecursive wm -type ThreadLock = MVar ()-type PathLockMap = Map FilePath ThreadLock+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 -> PathLockMap -> IO ()-readEvents chan action  pathMap = do+readEvents :: EventChannel -> Action -> IO ()+readEvents chan action = forever $ do   event <- readChan chan-  let path = eventPath event-  mVar <- getMVar $ Map.lookup path pathMap-  _ <- takeMVar mVar >> (forkIO $ action event `finally` putMVar mVar ())-  readEvents chan action  $ Map.insert path mVar pathMap-  where-    getMVar :: Maybe ThreadLock -> IO ThreadLock-    getMVar (Just tl) = return tl-    getMVar Nothing   = newMVar ()+  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 ())
src/System/FSNotify/Devel.hs view
@@ -4,7 +4,7 @@     allEvents, existsEvents   ) where -import Prelude hiding (FilePath, catch)+import Prelude hiding (FilePath)  import Data.Text import Filesystem.Path.CurrentOS@@ -32,7 +32,7 @@          -> FilePath -- ^ Directory to watch          -> Text -- ^ extension          -> (FilePath -> IO ()) -- ^ action to run on file-         -> IO ()+         -> IO StopListening treeExtExists man dir ext action =   watchTree man dir (existsEvents $ flip hasExtension ext) (doAllEvents action) @@ -43,7 +43,7 @@          -> FilePath -- ^ Directory to watch          -> Text -- ^ extension          -> (FilePath -> IO ()) -- ^ action to run on file-         -> IO ()+         -> IO StopListening treeExtAny man dir ext action =   watchTree man dir (existsEvents $ flip hasExtension ext) (doAllEvents action) 
src/System/FSNotify/Linux.hs view
@@ -13,6 +13,7 @@ import Prelude hiding (FilePath)  import Control.Concurrent.Chan+import Control.Concurrent.MVar import Control.Exception import Control.Monad (when) import Data.IORef (atomicModifyIORef, readIORef)@@ -30,9 +31,6 @@ data EventVarietyMismatchException = EventVarietyMismatchException deriving (Show, Typeable) instance Exception EventVarietyMismatchException -void :: IO ()-void = return ()- -- Note that INo.Closed in this context is "modified" because we listen to -- CloseWrite events. fsnEvent :: FilePath -> UTCTime -> INo.Event -> Maybe Event@@ -62,7 +60,7 @@   where     writeToChan = writeChan chan event -- handleEvent _ _ _ Nothing | trace ("Linux handleEvent Nothing") False = undefined-handleEvent _ _ _ Nothing = void+handleEvent _ _ _ Nothing = return ()  varieties :: [INo.EventVariety] varieties = [INo.Create, INo.Delete, INo.MoveIn, INo.MoveOut, INo.CloseWrite]@@ -72,28 +70,59 @@    killSession = INo.killINotify -  listen db iNotify path actPred chan = do+  listen conf iNotify path actPred chan = do     path' <- canonicalizeDirPath path-    dbp <- newDebouncePayload db-    _ <- INo.addWatch iNotify varieties (encodeString path') (handler path' dbp)-    void+    dbp <- newDebouncePayload $ confDebounce conf+    wd <- INo.addWatch iNotify varieties (encodeString path') (handler path' dbp)+    return $ INo.removeWatch wd     where       handler :: FilePath -> DebouncePayload -> INo.Event -> IO ()       handler = handleInoEvent actPred chan -  listenRecursive db iNotify path actPred chan = do-    path' <- canonicalizeDirPath path-    paths <- findDirs True path'-    mapM_ pathHandler (path':paths)+  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-      pathHandler :: FilePath -> IO ()-      pathHandler filePath = do-        dbp <- newDebouncePayload db-        _ <- INo.addWatch iNotify varieties (fp filePath) (handler filePath dbp)-        void+      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 (fp filePath) (handler filePath dbp)+              return $ Just (wd:wds)         where           handler :: FilePath -> DebouncePayload -> INo.Event -> IO ()-          handler baseDir _   (INo.Created True dirPath) =-            listenRecursive db iNotify (baseDir </> (fp dirPath)) actPred chan+          handler baseDir _   (INo.Created True dirPath) = do+            listenRec (baseDir </> fp dirPath) wdVar           handler baseDir dbp event                      =             handleInoEvent actPred chan baseDir dbp event++  usesPolling = const False
src/System/FSNotify/Listener.hs view
@@ -7,6 +7,7 @@        ( debounce        , epsilonDefault        , FileListener(..)+       , StopListening        , newDebouncePayload        ) where @@ -19,6 +20,9 @@ import System.FSNotify.Path (fp) 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@@ -36,14 +40,17 @@   -- 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 ()+  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 ()+  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@@ -65,7 +72,7 @@  -- | Produces a fresh data payload used for debouncing events in a -- handler.-newDebouncePayload :: WatchConfig -> IO DebouncePayload+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
src/System/FSNotify/OSX.hs view
@@ -8,17 +8,17 @@        , NativeManager        ) where -import Prelude hiding (FilePath, catch)+import Prelude hiding (FilePath)  import Control.Concurrent.Chan import Control.Concurrent.MVar-import Control.Monad hiding (void)+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 Debug.Trace (trace)+import Data.Unique import Filesystem (isFile) import Filesystem.Path hiding (concat) import System.FSNotify.Listener@@ -30,15 +30,10 @@ data ListenType = NonRecursive | Recursive data WatchData = WatchData FSE.EventStream ListenType EventChannel --- TODO: We really should use something other than FilePath as a key to allow--- for more than one listener per FilePath.-type WatchMap = Map FilePath WatchData+type WatchMap = Map Unique WatchData data OSXManager = OSXManager (MVar WatchMap) type NativeManager = OSXManager -void :: IO ()-void = return ()- nil :: Word64 nil = 0x00 @@ -72,17 +67,11 @@ -- hfsevents package doesn't support non-recursive event reporting.  handleNonRecursiveFSEEvent :: ActionPredicate -> EventChannel -> FilePath -> DebouncePayload -> FSE.Event -> IO ()--- handleNonRecursiveFSEEvent _       _    dirPath _   fseEvent | trace ("OSX: handleNonRecursiveFSEEvent " ++ show dirPath ++ " " ++ show fseEvent) False = undefined 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 _    dirPath _   (event:_     ) | trace (   "OSX: handleNonRecursiveEvents "---                                                                       ++ show dirPath ++ " " ++ show event---                                                                       ++ "\n  " ++ fp (directory dirPath)---                                                                       ++ "\n  " ++ fp (directory (eventPath event))---                                                                       ++ "\n  " ++ show (actPred event)) False = undefined handleNonRecursiveEvents actPred chan dirPath dbp (event:events)   | directory dirPath == directory (eventPath event) && actPred event = do     case dbp of@@ -93,17 +82,15 @@       Nothing                           -> writeChan chan event     handleNonRecursiveEvents actPred chan dirPath dbp events   | otherwise                                                         = handleNonRecursiveEvents actPred chan dirPath dbp events-handleNonRecursiveEvents _ _ _ _ []                                   = void+handleNonRecursiveEvents _ _ _ _ []                                   = return ()  handleFSEEvent :: ActionPredicate -> EventChannel -> DebouncePayload -> FSE.Event -> IO ()--- handleFSEEvent _       _    _   fseEvent | trace ("OSX: handleFSEEvent " ++ show fseEvent) False = undefined 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 _    _   (event:_     ) | trace ("OSX: handleEvents " ++ show event ++ " " ++ show (actPred event)) False = undefined handleEvents actPred chan dbp (event:events) = do   when (actPred event) $ case dbp of       (Just (DebounceData epsilon ior)) -> do@@ -112,8 +99,26 @@         atomicModifyIORef ior (\_ -> (event, ()))       Nothing                           -> writeChan chan event   handleEvents actPred chan dbp events-handleEvents _ _ _ [] = void+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 [fp 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@@ -127,20 +132,8 @@       eventStreamDestroy' :: WatchData -> IO ()       eventStreamDestroy' (WatchData eventStream _ _) = FSE.eventStreamDestroy eventStream -  listen db (OSXManager mvarMap) path actPred chan = do-    path' <- canonicalizeDirPath path-    dbp <- newDebouncePayload db-    eventStream <- FSE.eventStreamCreate [fp path'] 0.0 True False True (handler path' dbp)-    modifyMVar_ mvarMap $ \watchMap -> return (Map.insert path' (WatchData eventStream NonRecursive chan) watchMap)-    where-      handler :: FilePath -> DebouncePayload -> FSE.Event -> IO ()-      handler = handleNonRecursiveFSEEvent actPred chan+  listen = listenFn handleNonRecursiveFSEEvent -  listenRecursive db (OSXManager mvarMap) path actPred chan = do-    path' <- canonicalizeDirPath path-    dbp <- newDebouncePayload db-    eventStream <- FSE.eventStreamCreate [fp path'] 0.0 True False True $ handler dbp-    modifyMVar_ mvarMap $ \watchMap -> return (Map.insert path' (WatchData eventStream Recursive chan) watchMap)-    where-      handler :: DebouncePayload -> FSE.Event -> IO ()-      handler = handleFSEEvent actPred chan+  listenRecursive = listenFn $ \actPred chan path -> handleFSEEvent actPred chan++  usesPolling = const False
src/System/FSNotify/Polling.hs view
@@ -59,9 +59,9 @@       modTime <- getModified path       return (path, modTime) -pollPath :: Bool -> EventChannel -> FilePath -> ActionPredicate -> Map FilePath UTCTime -> IO ()-pollPath recursive chan filePath actPred oldPathMap = do-  threadDelay 1000000+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@@ -83,7 +83,7 @@     handleEvents = mapM_ (handleEvent chan actPred)      pollPath' :: Map FilePath UTCTime -> IO ()-    pollPath' = pollPath recursive chan filePath actPred+    pollPath' = pollPath interval recursive chan filePath actPred   -- Additional init funciton exported to allow startManager to unconditionally@@ -91,24 +91,37 @@ 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) killThread'-    where-      killThread' :: WatchKey -> IO ()-      killThread' (WatchKey threadId) = killThread threadId+    forM_ (Map.keys watchMap) killWatchingThread -  listen _ (PollManager mvarMap) path actPred chan  = do+  listen conf (PollManager mvarMap) path actPred chan  = do     path' <- canonicalizeDirPath path     pmMap <- pathModMap False path'-    threadId <- forkIO $ pollPath False chan path' actPred pmMap-    modifyMVar_ mvarMap $ return . Map.insert (WatchKey threadId) (WatchData path' chan)+    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 _ (PollManager mvarMap) path actPred chan = do+  listenRecursive conf (PollManager mvarMap) path actPred chan = do     path' <- canonicalizeDirPath path     pmMap <- pathModMap True  path'-    threadId <- forkIO $ pollPath True chan path' actPred pmMap-    modifyMVar_ mvarMap $ return . Map.insert (WatchKey threadId) (WatchData path' chan)+    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
src/System/FSNotify/Types.hs view
@@ -8,6 +8,7 @@        , ActionPredicate        , Action        , WatchConfig(..)+       , Debounce(..)        , DebounceData(..)        , DebouncePayload        , Event(..)@@ -49,8 +50,26 @@  type EventChannel = Chan Event --- | Config object, currently used just for debouncing events.-data WatchConfig = DebounceDefault | Debounce NominalDiffTime | NoDebounce+-- | 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 events close to each other should be collapsed+-- together, and how close is close enough.+data Debounce+  = DebounceDefault+    -- ^ perform debouncing based on the default time interval+  | Debounce NominalDiffTime+    -- ^ perform debouncing based on the specified time interval+  | NoDebounce+    -- ^ do not perform debouncing  type IOEvent = IORef Event 
src/System/FSNotify/Win32.hs view
@@ -18,35 +18,39 @@ import System.FSNotify.Listener import System.FSNotify.Path (fp, canonicalizeDirPath) import System.FSNotify.Types+import Filesystem.Path 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. -void :: IO ()-void = return ()- -- Win32-notify has (temporarily?) dropped support for Renamed events.-fsnEvent :: UTCTime -> WNo.Event -> Maybe Event-fsnEvent timestamp (WNo.Created  False name) = Just $ Added    (fp name) timestamp-fsnEvent timestamp (WNo.Modified False name) = Just $ Modified (fp name) timestamp-fsnEvent timestamp (WNo.Deleted  False name) = Just $ Removed  (fp name) timestamp-fsnEvent _         _                         = Nothing+fsnEvent :: BaseDir -> UTCTime -> WNo.Event -> Maybe Event+fsnEvent basedir timestamp ev =+  case ev of+    WNo.Created  False name -> Just $ Added    (basedir </> fp name) timestamp+    WNo.Modified False name -> Just $ Modified (basedir </> fp name) timestamp+    WNo.Deleted  False name -> Just $ Removed  (basedir </> fp 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 :: ActionPredicate -> EventChannel -> DebouncePayload -> WNo.Event -> IO ()-handleWNoEvent actPred chan dbp inoEvent = do+handleWNoEvent :: BaseDir -> ActionPredicate -> EventChannel -> DebouncePayload -> WNo.Event -> IO ()+handleWNoEvent basedir actPred chan dbp inoEvent = do   currentTime <- getCurrentTime-  let maybeEvent = fsnEvent currentTime inoEvent+  let maybeEvent = fsnEvent basedir currentTime inoEvent   case maybeEvent of     Just evt -> handleEvent actPred chan dbp evt-    Nothing  -> void+    Nothing  -> return () handleEvent :: ActionPredicate -> EventChannel -> DebouncePayload -> Event -> IO () handleEvent actPred chan dbp event =   when (actPred event) $ case dbp of@@ -66,20 +70,19 @@    killSession = WNo.killWatchManager -  listen db watchManager path actPred chan = do+  listen conf watchManager path actPred chan = do     path' <- canonicalizeDirPath path-    dbp <- newDebouncePayload db-    _ <- WNo.watchDirectory watchManager (fp path') False varieties (handler actPred chan dbp)-    void+    dbp <- newDebouncePayload $ confDebounce conf+    wid <- WNo.watchDirectory watchManager (fp path') False varieties (handleWNoEvent path' actPred chan dbp)+    return $ WNo.killWatch wid -  listenRecursive db watchManager path actPred chan = do+  listenRecursive conf watchManager path actPred chan = do     path' <- canonicalizeDirPath path-    dbp <- newDebouncePayload db-    _ <- WNo.watchDirectory watchManager (fp path') True varieties (handler actPred chan dbp)-    void+    dbp <- newDebouncePayload $ confDebounce conf+    wid <- WNo.watchDirectory watchManager (fp path') True varieties (handleWNoEvent path' actPred chan dbp)+    return $ WNo.killWatch wid -handler :: ActionPredicate -> EventChannel -> DebouncePayload -> WNo.Event -> IO ()-handler = handleWNoEvent+  usesPolling = const False  varieties :: [WNo.EventVariety] varieties = [WNo.Create, WNo.Delete, WNo.Move, WNo.Modify]
+ test/EventUtils.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE OverloadedStrings, ImplicitParams #-}+module EventUtils where++import Prelude hiding (FilePath)+import Test.Tasty.HUnit+import Control.Concurrent+import Control.Concurrent.Async+import Control.Applicative+import Control.Monad+import Data.IORef+import Filesystem.Path+import Filesystem.Path.CurrentOS+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+    }+  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 evs++testDirPath :: FilePath+testDirPath = decodeString (unsafePerformIO getCurrentDirectory) </> "testdir"++expectEventsHere poll = expectEvents poll watchDir testDirPath+expectEventsHereRec poll = expectEvents poll watchTree testDirPath
− test/FSNotify.hs
@@ -1,198 +0,0 @@------ Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org--- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org-----module FSNotify (spec) where--import Prelude hiding (appendFile, FilePath, writeFile)--import Control.Concurrent.Chan (newChan, writeChan)-import Data.ByteString (empty)-import Data.Text (pack)-import Data.Time.Clock.POSIX (posixSecondsToUTCTime)-import Filesystem (removeFile, rename, writeFile, writeTextFile)-import Filesystem.Path.CurrentOS ((</>), FilePath)-import System.FilePath.Glob (compile, match, Pattern)-import System.FSNotify.Path (fp)-import System.FSNotify.Types-import Test.Hspec (describe, it, Spec)-import Util-type Assertion = IO ()--spec :: Spec-spec = do-  describe "watchDir" $ do-    it "Create file" $ testFileName "txt" >>= createFileSpec ActionEnv-    it "Modify file" $ testFileName "txt" >>= modifyFileSpec ActionEnv-    it "Remove file" $ testFileName "txt" >>= removeFileSpec ActionEnv-    it "Rename file" $ renameInput        >>= renameFileSpec ActionEnv-    it "Debounce"    $ testFileName "txt" >>= dbFileSpec     ActionEnv-  describe "watchDirChan" $ do-    it "Create file" $ testFileName "txt" >>= createFileSpec ChanEnv-    it "Modify file" $ testFileName "txt" >>= modifyFileSpec ChanEnv-    it "Remove file" $ testFileName "txt" >>= removeFileSpec ChanEnv-    it "Rename file" $ renameInput        >>= renameFileSpec ChanEnv-  describe "watchTree" $ do-    it "Create file (pre-existing directory)" $ testFileName "txt" >>= createFileSpecR1 ActionEnv-    it "Create file (create directory)"       $ testFileName "txt" >>= createFileSpecR2 ActionEnv-    it "Modify file" $ testFileName "txt" >>= modifyFileSpecR ActionEnv-    it "Remove file" $ testFileName "txt" >>= removeFileSpecR ActionEnv-    it "Rename file" $ renameInput        >>= renameFileSpecR ActionEnv-  describe "watchTreeChan" $ do-    it "Create file (pre-existing directory)" $ testFileName "txt" >>= createFileSpecR1 ChanEnv-    it "Create file (create directory)"       $ testFileName "txt" >>= createFileSpecR2 ChanEnv-    it "Modify file" $ testFileName "txt" >>= modifyFileSpecR ChanEnv-    it "Remove file" $ testFileName "txt" >>= removeFileSpecR ChanEnv-    it "Rename file" $ renameInput        >>= renameFileSpecR ChanEnv--createFileSpec :: ChanActionEnv -> FilePath -> Assertion-createFileSpec envType fileName = do-  inEnv envType DirEnv act action $ matchEvents matchers-  where-    action :: FilePath -> IO ()-    action envDir = writeFile (envDir </> fileName) empty-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File creation" (matchCreate fileName)]--modifyFileSpec :: ChanActionEnv -> FilePath -> Assertion-modifyFileSpec envType fileName = do-  withTempDir $ \envDir -> do-    writeFile (envDir </> fileName) empty-    inTempDirEnv envType DirEnv act action (matchEvents matchers) envDir-  where-    action :: FilePath -> IO ()-    action envDir = do-      writeTextFile  (envDir </> fileName) $ pack "Hello world"-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File modification" (matchModify fileName)]--removeFileSpec :: ChanActionEnv -> FilePath -> Assertion-removeFileSpec envType fileName = do-  withTempDir $ \envDir -> do-    writeFile (envDir </> fileName) empty-    inTempDirEnv envType DirEnv act action (matchEvents matchers) envDir-  where-    action :: FilePath -> IO ()-    action envDir = removeFile (envDir </> fileName)-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File deletion" (matchRemove fileName)]--renameFileSpec :: ChanActionEnv -> (FilePath, FilePath) -> Assertion-renameFileSpec envType (oldFileName, newFileName) = do-  withTempDir $ \envDir -> do-    writeFile (envDir </> oldFileName) empty-    inTempDirEnv envType DirEnv act action (matchEvents matchers) envDir-  where-    action :: FilePath -> IO ()-    action envDir = rename (envDir </> oldFileName) (envDir </> newFileName)-    matchers :: [EventPredicate]-    matchers = [ EventPredicate "Rename: File deletion" (matchRemove oldFileName)-               , EventPredicate "Rename: File creation" (matchCreate newFileName) ]---- TODO: This is a weak test. What we actually need is an interface for--- "anti-matchers" to ensure that certain events do NOT get reported.-dbFileSpec :: ChanActionEnv -> FilePath -> Assertion-dbFileSpec envType _ = do-  chan <- newChan-  inChanEnv envType DirEnv act (action chan) (matchEvents matchers) chan-  where-    action :: EventChannel -> FilePath -> IO ()-    action chan _ = do-      writeChan chan e1-      writeChan chan e2-    matchers :: [EventPredicate]-    matchers = [EventPredicate "First debounced event" (\e -> e == e1)]-    e1 :: Event-    e1 = Added (fp "") (posixSecondsToUTCTime 0)-    e2 :: Event-    e2 = Modified (fp "") (posixSecondsToUTCTime 0)--createFileSpecR1 :: ChanActionEnv -> FilePath -> Assertion-createFileSpecR1 envType fileName = do-  withTempDir $ \envDir -> do-    withNestedTempDir envDir $ \envPath -> do-      inTempDirEnv envType TreeEnv act (action envPath) (matchEvents matchers) envDir-  where-    action :: FilePath -> FilePath -> IO ()-    action envPath _ = do-      writeFile (envPath </> fileName) empty-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File creation" (matchCreate fileName)]--createFileSpecR2 :: ChanActionEnv -> FilePath -> Assertion-createFileSpecR2 envType fileName = do-  withTempDir $ \envDir -> do-    inTempDirEnv envType TreeEnv act (action envDir) (matchEvents matchers) envDir-  where-    action :: FilePath -> FilePath -> IO ()-    action envDir _ = do-      withNestedTempDir envDir $ \envPath -> writeFile (envPath </> fileName) empty-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File creation" (matchCreate fileName)]--modifyFileSpecR :: ChanActionEnv -> FilePath -> Assertion-modifyFileSpecR envType fileName = do-  withTempDir $ \envDir -> do-    withNestedTempDir envDir $ \envPath -> do-      writeFile (envPath </> fileName) empty-      inTempDirEnv envType TreeEnv act (action envPath) (matchEvents matchers) envDir-  where-    action :: FilePath -> FilePath -> IO ()-    action envPath _ = do-      writeTextFile  (envPath </> fileName) $ pack "Hello world"-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File deletion" (matchModify fileName)]--removeFileSpecR :: ChanActionEnv -> FilePath -> Assertion-removeFileSpecR envType fileName = do-  withTempDir $ \envDir -> do-    withNestedTempDir envDir $ \envPath -> do-      writeFile (envPath </> fileName) empty-      inTempDirEnv envType TreeEnv act (action envPath) (matchEvents matchers) envDir-  where-    action :: FilePath -> FilePath -> IO ()-    action envPath _ = do-      removeFile (envPath </> fileName)-    matchers :: [EventPredicate]-    matchers = [EventPredicate "File deletion" (matchRemove fileName)]--renameFileSpecR :: ChanActionEnv -> (FilePath, FilePath) -> Assertion-renameFileSpecR envType (oldFileName, newFileName) = do-  withTempDir $ \envDir -> do-    withNestedTempDir envDir $ \envPath -> do-      writeFile (envPath </> oldFileName) empty-      inTempDirEnv envType TreeEnv act (action envPath) (matchEvents matchers) envDir-  where-    action :: FilePath -> FilePath -> IO ()-    action envPath _ = rename (envPath </> oldFileName) (envPath </> newFileName)-    matchers :: [EventPredicate]-    matchers = [ EventPredicate "Rename: File deletion" (matchRemove oldFileName)-               , EventPredicate "Rename: File creation" (matchCreate newFileName) ]--renameInput :: IO (FilePath, FilePath)-renameInput = do-  oldName <- testFileName "txt"-  newName <- testFileName "txt"-  return (oldName, newName)--matchCreate :: FilePath -> Event -> Bool-matchCreate fileName (Added path _) = matchFP pattern path-  where-    pattern = compile $  "**/*" ++ fp fileName-matchCreate _ _ = False--matchModify :: FilePath -> Event -> Bool-matchModify fileName (Modified path _) = matchFP pattern path-  where-    pattern = compile $  "**/*" ++ fp fileName-matchModify _ _ = False--matchRemove :: FilePath -> Event -> Bool-matchRemove fileName (Removed path _) = matchFP pattern path-  where-    pattern = compile $  "**/*" ++ fp fileName-matchRemove _ _ = False--matchFP :: Pattern -> FilePath -> Bool-matchFP pattern path = match pattern $ fp path
− test/Path.hs
@@ -1,139 +0,0 @@------ Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org--- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org----{-# LANGUAGE CPP #-}--module Path (spec) where--import Prelude hiding (FilePath, writeFile)--import Control.Applicative ((<*>))-import Filesystem (writeFile)-import Filesystem.Path.CurrentOS (FilePath)-import Filesystem.Path ((</>), empty)-import System.FilePath.Glob (compile, match)-import System.FSNotify.Path (canonicalizeDirPath, canonicalizePath, findDirs, findFiles, fp)-import Test.Hspec (describe, it, Spec, shouldBe)-import Util-import qualified Data.ByteString as BS-type Assertion = IO ()---- Boolean XOR-(.^.) :: Bool -> Bool -> Bool-(.^.) True  True  = False-(.^.) True  False = True-(.^.) False True  = True-(.^.) False False = False--hasTrailingSlash :: FilePath -> (FilePath -> IO FilePath) -> Assertion-hasTrailingSlash path canonicalizeFn = do-  let expectedTail = last $ fp (fp "dir" </> empty) -- Get OS/filesystem's idea of a separator-  actualPath <- canonicalizeFn path-  let actualTail = last (fp actualPath) :: Char-  actualTail `shouldBe` expectedTail--relPath      :: FilePath-relPathSlash :: FilePath-absPath      :: FilePath-absPathSlash :: FilePath--relPath      = fp "."-#ifdef OS_Linux-absPath      = fp "/home"-absPathSlash = fp "/home/"-relPathSlash = fp "./" </> empty-#else-#  ifdef OS_Win32-absPath      = fp "C:" </> fp "Windows"-absPathSlash = fp "C:" </> fp "Windows" </> empty-relPathSlash = fp ".\\" </> empty-#  else-#    ifdef OS_Mac-absPath      = fp "/Users"-absPathSlash = fp "/Users/"-relPathSlash = fp "./"-#    else--- Assume UNIX-like for anything non-Linux/Windows/Mac-absPath      = fp "/home"-absPathSlash = fp "/home/"-relPathSlash = fp "./" </> empty-#    endif-#  endif-#endif---spec :: Spec-spec = do-  describe "canonicalizeDirPath" $ do-    it "Absolute path keeps trailing slash" $ do-      hasTrailingSlash absPathSlash canonicalizeDirPath-    it "Absolute path gains trailing slash" $ do-      hasTrailingSlash absPath canonicalizeDirPath-    it "Relative path keeps trailing slash" $ do-      hasTrailingSlash relPathSlash canonicalizeDirPath-    it "Relative path gains trailing slash" $ do-      hasTrailingSlash relPath canonicalizeDirPath-  describe "canonicalizePath" $ do-    it "Absolute path keeps trailing slash" $ do-      hasTrailingSlash absPathSlash canonicalizePath-    it "Relative path keeps trailing slash" $ do-      hasTrailingSlash relPathSlash canonicalizePath-  describe "findFiles" $ do-    it "Non-recursive" $ do-      withTempDir $ \tmpDir -> do-        fileName <- testFileName "txt"-        writeFile (tmpDir </> fileName) BS.empty-        files <- findFiles False tmpDir-        1 `shouldBe` length files-        let (resultFP:_) = files-            pattern = "**/*" ++ fp fileName-            result = fp resultFP-        if match (compile pattern) result then-          True `shouldBe` True-          else-          result `shouldBe` pattern-    it "Recursive" $ do-      withTempDir $ \tmpDir -> do-        withNestedTempDir tmpDir $ \tmpPath -> do-          fileName <- testFileName "txt"-          writeFile (tmpPath </> fileName) BS.empty-          files <- findFiles True tmpDir-          1 `shouldBe` length files-          let (resultFP:_) = files-              pattern = "**/*" ++ fp fileName-              result = fp resultFP-          if match (compile pattern) result then-            True `shouldBe` True-            else-            result `shouldBe` pattern-  describe "findDirs" $ do-    it "Non-recursive" $-      withTempDir $ \tmpDir -> do-        withNestedTempDir tmpDir $ \dirName -> do-          dirs <- findDirs False tmpDir-          1 `shouldBe` length dirs-          let (resultFP:_) = dirs-              pattern = "**/*" ++ fp dirName-              result = fp resultFP-          if match (compile pattern) result then-            True `shouldBe` True-            else-            result `shouldBe` pattern-    it "Recursive" $-      withTempDir $ \tmpDir -> do-        withNestedTempDir tmpDir $ \dirName1 -> do-          withNestedTempDir tmpDir $ \dirName2 -> do-            dirs <- findDirs False tmpDir-            2 `shouldBe` length dirs-            let pats = ["**/*" ++ fp dirName1, "**/*" ++ fp dirName2]-                patFns = map (match . compile) pats-                dirStrings = map fp dirs-                (r1:r2:r3:r4:_) = patFns <*> dirStrings-            -- The two patterns should succeed once and fail once on-            -- opposite tests.-            if (r1 .^. r2) && (r3 .^. r4) && (r1 .^. r3) && (r2 .^. r4) then-              True `shouldBe` True-              else-              dirStrings `shouldBe` pats-            return ()
− test/Util.hs
@@ -1,186 +0,0 @@------ Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org--- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org-----module Util where--import Prelude hiding (FilePath, catch, pred)--import Control.Concurrent (threadDelay)-import Control.Concurrent.Chan-import Control.Concurrent.MVar (MVar, newMVar, readMVar, swapMVar)-import Control.Exception-import Control.Monad (when)-import Data.Unique.Id-import Filesystem.Path.CurrentOS hiding (concat)-import Filesystem (createTree, removeTree)-import System.IO.Error (isPermissionError)-import System.FSNotify-import System.FSNotify.Path-import System.Random-import System.Timeout (timeout)--data ChanActionEnv =-    ChanEnv-  | ActionEnv-data DirTreeEnv =-    DirEnv-  | TreeEnv-data TestContext = TestContext ChanActionEnv DirTreeEnv ActionPredicate--data TestReport = TestReport FilePath [Event] deriving (Show)-data TestResult = TestResult Bool String TestReport deriving (Show)-type TestAction = FilePath -> IO ()-type MTestResult = MVar TestResult-type TestCase = MTestResult -> IO ()-type CurriedEventProcessor = TestReport -> IO (TestResult)-type EventProcessor = MTestResult -> CurriedEventProcessor-data EventPredicate = EventPredicate String (Event -> Bool)--void :: IO ()-void = return ()--predicateName :: EventPredicate -> String-predicateName (EventPredicate name _) = name--matchEvents :: [EventPredicate] -> EventProcessor-matchEvents preds mVar report@(TestReport _ events) =-  swapMVar mVar result >> return result-  where-    matchMatrix :: [[Bool]]-    matchMatrix = map (\(EventPredicate _ pred) -> map (\event -> pred event) events) preds-    matchList :: [Bool]-    matchList = map (\lst -> any id lst) matchMatrix-    errorList :: [(Bool, String)]-    errorList = zip matchList (map (\(EventPredicate errStr _) -> errStr) preds)-    errorString :: String-    errorString = foldl foldError "" errorList-    foldError :: String -> (Bool, String) -> String-    foldError accStr (success, errStr) = if not success then accStr ++ " " ++ errStr else accStr-    status :: Bool-    status = all id matchList-    result =   if status then-                 TestResult status "" report-               else-                 TestResult status ("Failed to match events: " ++ errorString) report--newId :: IO String-newId = randomIO >>= initIdSupply >>= return . show . hashedId . idFromSupply--testFileName :: String -> IO FilePath-testFileName ext = do-  uId <- newId-  return $ fp ("test-" ++ uId ++ "." ++ ext)--testName :: IO FilePath-testName = do-  uId <- newId-  return $ fp ("sandbox-" ++ uId) </> empty--dirPreAction :: Int-dirPreAction = 500000---- Delay to keep temporary directories around long enough for events to be--- picked up by OS (in microseconds)-dirPostAction :: Int-dirPostAction = 500000--withTempDir :: (FilePath -> IO ()) -> IO ()-withTempDir fn = withNestedTempDir empty fn--withNestedTempDir :: FilePath -> (FilePath -> IO ()) -> IO ()-withNestedTempDir firstPath fn = do-  secondPath <- testName-  let path = if firstPath /= empty then-               firstPath </> secondPath-             else-               secondPath-  bracket (createTree path >> threadDelay dirPreAction >> return path) attemptDirectoryRemoval fn--attemptDirectoryRemoval :: FilePath -> IO ()-attemptDirectoryRemoval path = do-  threadDelay dirPostAction-  catch-    (removeTree path)-    (\e -> when-           (not $ isPermissionError e)-           (throw e))--performAction :: TestAction -> FilePath -> IO ()-performAction action path = action path--reportOnAction :: FilePath -> EventChannel -> CurriedEventProcessor -> IO TestResult-reportOnAction = reportOnAction' []--reportOnAction' :: [Event] -> FilePath -> EventChannel -> CurriedEventProcessor -> IO TestResult-reportOnAction' events path chan processor = do-  result@(TestResult status _ _) <- processor (TestReport path events)-  if not status then do-    event <- readChan chan-    reportOnAction' (event:events) path chan processor-    else-    return result--actAndReport :: TestAction -> FilePath -> EventChannel -> CurriedEventProcessor -> IO TestResult-actAndReport action path chan processor = do-  performAction action path-  reportOnAction path chan processor--testTimeout :: Int-testTimeout = 3000000--timeoutTest :: MTestResult -> Maybe () -> IO ()-timeoutTest mResult Nothing = do-  result <- readMVar mResult-  error $ "TIMEOUT: Last test result: " ++ show result-timeoutTest mResult (Just _) = do-  result <- readMVar mResult-  case result of-    (TestResult False _ _) -> error $ show result-    (TestResult True  _ _) -> void--runTest :: TestCase -> IO ()-runTest test = do-  mVar <- newMVar $ TestResult False "Timeout with no test result" (TestReport empty [])-  timeout testTimeout (test mVar) >>= timeoutTest mVar--inEnv :: ChanActionEnv -> DirTreeEnv -> ActionPredicate -> TestAction -> EventProcessor -> IO ()-inEnv caEnv dtEnv reportPred action eventProcessor =-  withTempDir $ inTempDirEnv caEnv dtEnv reportPred action eventProcessor--inTempDirEnv :: ChanActionEnv -> DirTreeEnv -> ActionPredicate -> TestAction -> EventProcessor-> FilePath -> IO ()-inTempDirEnv caEnv dtEnv reportPred action eventProcessor path =-  withManagerConf NoDebounce $ \manager -> do-    chan <- newChan-    inTempDirChanEnv caEnv dtEnv reportPred action eventProcessor path manager chan--inChanEnv :: ChanActionEnv -> DirTreeEnv -> ActionPredicate -> TestAction -> EventProcessor -> EventChannel -> IO ()-inChanEnv caEnv dtEnv reportPred action eventProcessor chan =-  withTempDir $ \path -> do-    withManagerConf NoDebounce $ \manager -> do-      inTempDirChanEnv caEnv dtEnv reportPred action eventProcessor path manager chan--inTempDirChanEnv :: ChanActionEnv -> DirTreeEnv -> ActionPredicate -> TestAction -> EventProcessor-> FilePath -> WatchManager -> EventChannel -> IO ()-inTempDirChanEnv caEnv dtEnv reportPred action eventProcessor path manager chan = do-    watchInEnv caEnv dtEnv manager path reportPred chan-    runTest $ \mVar -> do-      _ <- actAndReport action path chan $ eventProcessor mVar-      void-    void--actionAsChan :: (WatchManager -> FilePath -> ActionPredicate -> Action       -> IO ()) ->-                 WatchManager -> FilePath -> ActionPredicate -> EventChannel -> IO ()-actionAsChan actionFunction wm path ap ec = actionFunction wm path ap (writeChan ec)--watchInEnv :: ChanActionEnv-           -> DirTreeEnv-           -> WatchManager-           -> FilePath-           -> ActionPredicate-           -> EventChannel-           -> IO ()-watchInEnv ChanEnv   DirEnv  = watchDirChan-watchInEnv ChanEnv   TreeEnv = watchTreeChan-watchInEnv ActionEnv DirEnv  = actionAsChan watchDir-watchInEnv ActionEnv TreeEnv = actionAsChan watchTree
− test/main.hs
@@ -1,11 +0,0 @@-module Main where--import Test.Hspec (hspec, Spec)-import qualified Path as P-import qualified FSNotify as FSN--main :: IO ()-main = hspec spec--spec :: Spec-spec = P.spec >> FSN.spec
+ test/test.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE OverloadedStrings, ImplicitParams, ViewPatterns #-}+import Prelude hiding+  ( FilePath, writeFile, writeFile, removeFile+  , createDirectory, removeDirectory)+import Test.Tasty+import Test.Tasty.HUnit+import Filesystem+import Filesystem.Path+import Filesystem.Path.CurrentOS+import System.FSNotify+import System.IO.Temp+import Text.Printf+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 = do+  hasNative <- nativeMgrSupported+  unless hasNative $+    putStrLn "WARNING: native manager cannot be used or tested on this platform"+  defaultMain $+    withResource+      (createTree testDirPath)+      (const $ removeTree testDirPath) $+      const $ tests hasNative++tests hasNative = testGroup "Tests" $ do+  poll <-+    if hasNative+      then [False, True]+      else [True]+  let ?timeInterval =+        if poll+          then 2*10^6+          else 5*10^5+  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) >> writeFile f "foo")+    , mkTest "delete file" [evRemoved] (\f -> writeFile f "") (\f -> removeFile f)+    , mkTest "directories are ignored" [] (const $ return ())+        (\f -> createDirectory False f >> removeDirectory f)+    ]+  return $ t nested recursive poll+  where+    mkTest title evs prepare action nested recursive poll =+      testCase title $+        withTempDirectory (encodeString testDirPath) "test." $ \(decodeString -> watchedDir) -> do+        let baseDir = if nested then watchedDir </> "subdir" else watchedDir+            f = baseDir </> filename+            expect =+              expectEvents poll+                (if recursive then watchTree else watchDir)+                watchedDir+        createDirectory 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"
− test/watch-here.hs
@@ -1,20 +0,0 @@-{--{-# LANGUAGE OverloadedStrings #-}-import Filesystem.Path.CurrentOS--}-import System.FSNotify-import Filesystem--main :: IO ()-main = do-  -- let wd = "."-  wd <- getWorkingDirectory-  print wd-  man <- startManager-  watchTree man wd (const True) print-  print "press retrun to stop"-  getLine-  print "watching stopped, press retrun to exit"-  stopManager man-  getLine-  return ()