reorg
This commit is contained in:
parent
c08a4d3d93
commit
c077cee44a
5 changed files with 8 additions and 8 deletions
185
Utility/DirWatcher/INotify.hs
Normal file
185
Utility/DirWatcher/INotify.hs
Normal file
|
@ -0,0 +1,185 @@
|
|||
{- higher-level inotify interface
|
||||
-
|
||||
- Copyright 2012 Joey Hess <joey@kitenet.net>
|
||||
-
|
||||
- Licensed under the GNU GPL version 3 or higher.
|
||||
-}
|
||||
|
||||
module Utility.DirWatcher.INotify where
|
||||
|
||||
import Common hiding (isDirectory)
|
||||
import Utility.ThreadLock
|
||||
import Utility.DirWatcher.Types
|
||||
|
||||
import System.INotify
|
||||
import qualified System.Posix.Files as Files
|
||||
import System.IO.Error
|
||||
import Control.Exception (throw)
|
||||
|
||||
{- Watches for changes to files in a directory, and all its subdirectories
|
||||
- that are not ignored, using inotify. This function returns after
|
||||
- its initial scan is complete, leaving a thread running. Callbacks are
|
||||
- made for different events.
|
||||
-
|
||||
- Inotify is weak at recursive directory watching; the whole directory
|
||||
- tree must be scanned and watches set explicitly for each subdirectory.
|
||||
-
|
||||
- To notice newly created subdirectories, inotify is used, and
|
||||
- watches are registered for those directories. There is a race there;
|
||||
- things can be added to a directory before the watch gets registered.
|
||||
-
|
||||
- To close the inotify race, each time a new directory is found, it also
|
||||
- recursively scans it, assuming all files in it were just added,
|
||||
- and registering each subdirectory.
|
||||
-
|
||||
- Note: Due to the race amelioration, multiple add events may occur
|
||||
- for the same file.
|
||||
-
|
||||
- Note: Moving a file will cause events deleting it from its old location
|
||||
- and adding it to the new location.
|
||||
-
|
||||
- Note: It's assumed that when a file that was open for write is closed,
|
||||
- it's finished being written to, and can be added.
|
||||
-
|
||||
- Note: inotify has a limit to the number of watches allowed,
|
||||
- /proc/sys/fs/inotify/max_user_watches (default 8192).
|
||||
- So this will fail if there are too many subdirectories. The
|
||||
- errHook is called when this happens.
|
||||
-}
|
||||
watchDir :: INotify -> FilePath -> (FilePath -> Bool) -> WatchHooks -> IO ()
|
||||
watchDir i dir ignored hooks
|
||||
| ignored dir = noop
|
||||
| otherwise = do
|
||||
-- Use a lock to make sure events generated during initial
|
||||
-- scan come before real inotify events.
|
||||
lock <- newLock
|
||||
let handler event = withLock lock (void $ go event)
|
||||
flip catchNonAsync failedwatch $ do
|
||||
void (addWatch i watchevents dir handler)
|
||||
`catchIO` failedaddwatch
|
||||
withLock lock $
|
||||
mapM_ scan =<< filter (not . dirCruft) <$>
|
||||
getDirectoryContents dir
|
||||
where
|
||||
recurse d = watchDir i d ignored hooks
|
||||
|
||||
-- Select only inotify events required by the enabled
|
||||
-- hooks, but always include Create so new directories can
|
||||
-- be scanned.
|
||||
watchevents = Create : addevents ++ delevents ++ modifyevents
|
||||
addevents
|
||||
| hashook addHook || hashook addSymlinkHook = [MoveIn, CloseWrite]
|
||||
| otherwise = []
|
||||
delevents
|
||||
| hashook delHook || hashook delDirHook = [MoveOut, Delete]
|
||||
| otherwise = []
|
||||
modifyevents
|
||||
| hashook modifyHook = [Modify]
|
||||
| otherwise = []
|
||||
|
||||
scan f = unless (ignored f) $ do
|
||||
ms <- getstatus f
|
||||
case ms of
|
||||
Nothing -> return ()
|
||||
Just s
|
||||
| Files.isDirectory s ->
|
||||
recurse $ indir f
|
||||
| Files.isSymbolicLink s ->
|
||||
runhook addSymlinkHook f ms
|
||||
| Files.isRegularFile s ->
|
||||
runhook addHook f ms
|
||||
| otherwise ->
|
||||
noop
|
||||
|
||||
go (Created { isDirectory = isd, filePath = f })
|
||||
| isd = recurse $ indir f
|
||||
| otherwise = do
|
||||
ms <- getstatus f
|
||||
case ms of
|
||||
Just s
|
||||
| Files.isSymbolicLink s ->
|
||||
when (hashook addSymlinkHook) $
|
||||
runhook addSymlinkHook f ms
|
||||
| Files.isRegularFile s ->
|
||||
when (hashook addHook) $
|
||||
runhook addHook f ms
|
||||
_ -> noop
|
||||
-- Closing a file is assumed to mean it's done being written,
|
||||
-- so a new add event is sent.
|
||||
go (Closed { isDirectory = False, maybeFilePath = Just f }) =
|
||||
checkfiletype Files.isRegularFile addHook f
|
||||
-- When a file or directory is moved in, scan it to add new
|
||||
-- stuff.
|
||||
go (MovedIn { filePath = f }) = scan f
|
||||
go (MovedOut { isDirectory = isd, filePath = f })
|
||||
| isd = runhook delDirHook f Nothing
|
||||
| otherwise = runhook delHook f Nothing
|
||||
-- Verify that the deleted item really doesn't exist,
|
||||
-- since there can be spurious deletion events for items
|
||||
-- in a directory that has been moved out, but is still
|
||||
-- being watched.
|
||||
go (Deleted { isDirectory = isd, filePath = f })
|
||||
| isd = guarded $ runhook delDirHook f Nothing
|
||||
| otherwise = guarded $ runhook delHook f Nothing
|
||||
where
|
||||
guarded = unlessM (filetype (const True) f)
|
||||
go (Modified { isDirectory = isd, maybeFilePath = Just f })
|
||||
| isd = noop
|
||||
| otherwise = runhook modifyHook f Nothing
|
||||
go _ = noop
|
||||
|
||||
hashook h = isJust $ h hooks
|
||||
|
||||
runhook h f s
|
||||
| ignored f = noop
|
||||
| otherwise = maybe noop (\a -> a (indir f) s) (h hooks)
|
||||
|
||||
indir f = dir </> f
|
||||
|
||||
getstatus f = catchMaybeIO $ getSymbolicLinkStatus $ indir f
|
||||
checkfiletype check h f = do
|
||||
ms <- getstatus f
|
||||
case ms of
|
||||
Just s
|
||||
| check s -> runhook h f ms
|
||||
_ -> noop
|
||||
filetype t f = catchBoolIO $ t <$> getSymbolicLinkStatus (indir f)
|
||||
|
||||
failedaddwatch e
|
||||
-- Inotify fails when there are too many watches with a
|
||||
-- disk full error.
|
||||
| isFullError e =
|
||||
case errHook hooks of
|
||||
Nothing -> error $ "failed to add inotify watch on directory " ++ dir ++ " (" ++ show e ++ ")"
|
||||
Just hook -> tooManyWatches hook dir
|
||||
-- The directory could have been deleted.
|
||||
| isDoesNotExistError e = return ()
|
||||
| otherwise = throw e
|
||||
|
||||
failedwatch e = hPutStrLn stderr $ "failed to add watch on directory " ++ dir ++ " (" ++ show e ++ ")"
|
||||
|
||||
tooManyWatches :: (String -> Maybe FileStatus -> IO ()) -> FilePath -> IO ()
|
||||
tooManyWatches hook dir = do
|
||||
sysctlval <- querySysctl [Param maxwatches] :: IO (Maybe Integer)
|
||||
hook (unlines $ basewarning : maybe withoutsysctl withsysctl sysctlval) Nothing
|
||||
where
|
||||
maxwatches = "fs.inotify.max_user_watches"
|
||||
basewarning = "Too many directories to watch! (Not watching " ++ dir ++")"
|
||||
withoutsysctl = ["Increase the value in /proc/sys/fs/inotify/max_user_watches"]
|
||||
withsysctl n = let new = n * 10 in
|
||||
[ "Increase the limit permanently by running:"
|
||||
, " echo " ++ maxwatches ++ "=" ++ show new ++
|
||||
" | sudo tee -a /etc/sysctl.conf; sudo sysctl -p"
|
||||
, "Or temporarily by running:"
|
||||
, " sudo sysctl -w " ++ maxwatches ++ "=" ++ show new
|
||||
]
|
||||
|
||||
querySysctl :: Read a => [CommandParam] -> IO (Maybe a)
|
||||
querySysctl ps = getM go ["sysctl", "/sbin/sysctl", "/usr/sbin/sysctl"]
|
||||
where
|
||||
go p = do
|
||||
v <- catchMaybeIO $ readProcess p (toCommand ps)
|
||||
case v of
|
||||
Nothing -> return Nothing
|
||||
Just s -> return $ parsesysctl s
|
||||
parsesysctl s = readish =<< lastMaybe (words s)
|
Loading…
Add table
Add a link
Reference in a new issue