module System.Directory.Watcher (
EventType(..), Event(..), eventType, eventPath, eventTime,
Watcher(..),
withWatcher,
watchDir, watchDir_, unwatchDir, isWatchingDir,
watchTree, watchTree_, unwatchTree, isWatchingTree,
readEvent, eventGroup, onEvents, onEvents_
) where
import Control.Lens (makeLenses)
import Control.Arrow
import Control.Concurrent
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Maybe (isJust)
import Data.Ratio ((%))
import Data.String (fromString)
import Data.Time.Clock (NominalDiffTime)
import Data.Time.Clock.POSIX
import System.FilePath (takeDirectory, isDrive)
import System.Directory
import qualified System.FSNotify as FS
import HsDev.Util (uniqueBy)
data EventType = Added | Modified | Removed deriving (Eq, Ord, Enum, Bounded, Read, Show)
data Event = Event {
_eventType :: EventType,
_eventPath :: FilePath,
_eventTime :: POSIXTime }
deriving (Eq, Ord, Show)
makeLenses ''Event
data Watcher a = Watcher {
watcherDirs :: MVar (Map FilePath (Bool, IO ())),
watcherMan :: FS.WatchManager,
watcherChan :: Chan (a, Event) }
withWatcher :: (Watcher a -> IO b) -> IO b
withWatcher act = FS.withManager $ \man -> do
ch <- newChan
dirs <- newMVar M.empty
act $ Watcher dirs man ch
watchDir :: Watcher a -> FilePath -> (Event -> Bool) -> a -> IO ()
watchDir w f p v = do
e <- doesDirectoryExist f
when e $ do
f' <- canonicalizePath f
watching <- isWatchingDir w f'
unless watching $ do
stop <- FS.watchDir
(watcherMan w)
(fromString f')
(p . fromEvent)
(writeChan (watcherChan w) . (,) v . fromEvent)
modifyMVar_ (watcherDirs w) $ return . M.insert f' (False, stop)
watchDir_ :: Watcher () -> FilePath -> (Event -> Bool) -> IO ()
watchDir_ w f p = watchDir w f p ()
unwatchDir :: Watcher a -> FilePath -> IO Bool
unwatchDir w f = do
f' <- canonicalizePath f
stop <- modifyMVar (watcherDirs w) $ return . (M.delete f' &&& M.lookup f')
maybe (return ()) snd stop
return $ isJust stop
isWatchingDir :: Watcher a -> FilePath -> IO Bool
isWatchingDir w f = do
f' <- canonicalizePath f
dirs <- readMVar (watcherDirs w)
return $ isWatchingDir' dirs f'
watchTree :: Watcher a -> FilePath -> (Event -> Bool) -> a -> IO ()
watchTree w f p v = do
e <- doesDirectoryExist f
when e $ do
f' <- canonicalizePath f
watching <- isWatchingTree w f'
unless watching $ do
stop <- FS.watchTree
(watcherMan w)
(fromString f')
(p . fromEvent)
(writeChan (watcherChan w) . (,) v . fromEvent)
modifyMVar_ (watcherDirs w) $ return . M.insert f' (True, stop)
watchTree_ :: Watcher () -> FilePath -> (Event -> Bool) -> IO ()
watchTree_ w f p = watchTree w f p ()
unwatchTree :: Watcher a -> FilePath -> IO Bool
unwatchTree w f = do
f' <- canonicalizePath f
stop <- modifyMVar (watcherDirs w) $ return . (M.delete f' &&& M.lookup f')
maybe (return ()) snd stop
return $ isJust stop
isWatchingTree :: Watcher a -> FilePath -> IO Bool
isWatchingTree w f = do
f' <- canonicalizePath f
dirs <- readMVar (watcherDirs w)
return $ isWatchingTree' dirs f'
readEvent :: Watcher a -> IO (a, Event)
readEvent = readChan . watcherChan
eventGroup :: Watcher a -> NominalDiffTime -> ([(a, Event)] -> IO ()) -> IO ()
eventGroup w tm onGroup = do
groupVar <- newTMVarIO []
syncVar <- newEmptyTMVarIO
_ <- async $ forever $ do
ev <- readChan (watcherChan w)
_ <- atomically $ tryPutTMVar syncVar ()
atomically $ do
evs <- takeTMVar groupVar
putTMVar groupVar (ev : evs)
forever $ do
_ <- atomically $ takeTMVar syncVar
threadDelay $ floor (tm * 1e6)
evs' <- atomically $ do
evs <- takeTMVar groupVar
putTMVar groupVar []
_ <- tryTakeTMVar syncVar
return $ reverse evs
onGroup $ uniqueBy (\(_, ev') -> (_eventType ev', _eventPath ev')) evs'
onEvents :: Watcher a -> NominalDiffTime -> ([(a, Event)] -> IO ()) -> IO ()
onEvents = eventGroup
onEvents_ :: Watcher a -> ([(a, Event)] -> IO ()) -> IO ()
onEvents_ w = onEvents w (fromRational (1 % 5))
fromEvent :: FS.Event -> Event
fromEvent e = Event t (FS.eventPath e) (utcTimeToPOSIXSeconds $ FS.eventTime e) where
t = case e of
FS.Added _ _ -> Added
FS.Modified _ _ -> Modified
FS.Removed _ _ -> Removed
isWatchingDir' :: Map FilePath (Bool, IO ()) -> FilePath -> Bool
isWatchingDir' m dir
| Just (_, _) <- M.lookup dir m = True
| isDrive dir = False
| otherwise = isWatchingTree' m (takeDirectory dir)
isWatchingTree' :: Map FilePath (Bool, IO ()) -> FilePath -> Bool
isWatchingTree' m dir
| Just (True, _) <- M.lookup dir m = True
| isDrive dir = False
| otherwise = isWatchingTree' m (takeDirectory dir)