plugins-auto-0.0.1: System/Plugins/Auto/FileSystemWatcher.hs
-- | Api for watching and notifying file changes.
-- Currently, this is based on inotify, but people may want to
-- provide implementations of this module for non-inotify-supported
-- platforms.
module System.Plugins.Auto.FileSystemWatcher
( FSWatcher
, FSWatchDescriptor
, initFSWatcher
, addWatch
, removeWatch
) where
import Control.Concurrent.MVar (MVar,readMVar,modifyMVar,modifyMVar_,newMVar)
import System.INotify (INotify, WatchDescriptor, Event(..), EventVariety(..),initINotify)
import qualified System.INotify as I(addWatch, removeWatch)
import System.FilePath (splitFileName)
import qualified Data.Map as Map
import Data.Map (Map)
-- | A FSWatcher watches several files for changes. If a file is deleted from the
-- containing folder, it keeps watching the folder in case the file is added
-- again.
data FSWatcher = FSWatcher
INotify -- INotify handle
(MVar
(Map FilePath -- Folder containing the file
( WatchDescriptor -- Watch descriptor of the folder
, Map String -- File being observed
(Event -> IO ()) -- Handler to run on file events
)
)
)
-- | Identifier used to stop watching files.
data FSWatchDescriptor = FSWatchDescriptor FSWatcher FilePath
-- | Initializes a watcher.
initFSWatcher :: IO FSWatcher
initFSWatcher = do
iN <- initINotify
fmvar <- newMVar Map.empty
return$ FSWatcher iN fmvar
-- Replacement for splitFileName which returns "." instead of an empty folder.
splitFileName' :: FilePath -> (FilePath,String)
splitFileName' fp =
let (d,f) = splitFileName fp
in (if null d then "." else d,f)
-- | Runs the callback IO action on modifications to the file at the given path.
--
-- Each file can have only one callback IO action. Registering a new IO action
-- discards any previously registered callback.
--
-- The returned FSWatchDescriptor value can be used to stop watching the file.
addWatch :: FSWatcher -> FilePath -> IO () -> IO FSWatchDescriptor
addWatch piN@(FSWatcher iN fmvar) fp hdl =
let (d,f) = splitFileName' fp
in modifyMVar fmvar$ \fm ->
case Map.lookup d fm of
Nothing -> do
wd <- I.addWatch iN [Modify, Move, Delete] d $ \e -> do
case e of
Ignored -> return ()
Deleted { filePath = f' } -> callHandler e d f'
MovedIn { filePath = f' } -> callHandler e d f'
Modified { maybeFilePath = Just f' } -> callHandler e d f'
_ -> return ()
return ( Map.insert d (wd,Map.singleton f (const hdl)) fm
, FSWatchDescriptor piN fp
)
Just (wd,ffm) -> return ( Map.insert d (wd,Map.insert f (const hdl) ffm) fm
, FSWatchDescriptor piN fp
)
where
callHandler e d f = do
fm <- readMVar fmvar
case Map.lookup d fm of
Nothing -> return ()
Just (_,ffm) -> case Map.lookup f ffm of
Nothing -> return ()
Just mhdl -> mhdl e
-- | Stops watching the file associated to the given file descriptor.
removeWatch :: FSWatchDescriptor -> IO ()
removeWatch (FSWatchDescriptor (FSWatcher _iN fmvar) fp) =
let (d,f) = splitFileName' fp
in modifyMVar_ fmvar$ \fm ->
case Map.lookup d fm of
Nothing -> error$ "removeWatchP: invalid handle for file "++fp
Just (wd,ffm) -> let ffm' = Map.delete f ffm
in if Map.null ffm' then I.removeWatch wd >> return (Map.delete d fm)
else return (Map.insert d (wd,ffm') fm)