54ad1b4cfb
Works around this bug in unix-compat: https://github.com/jacobstanley/unix-compat/issues/56 getFileStatus and other FilePath using functions in unix-compat do not do UNC conversion on Windows. Made Utility.RawFilePath use convertToWindowsNativeNamespace to do the necessary conversion on windows to support long filenames. Audited all imports of System.PosixCompat.Files to make sure that no functions that operate on FilePath were imported from it. Instead, use the equvilants from Utility.RawFilePath. In particular the re-export of that module in Common had to be removed, which led to lots of other changes throughout the code. The changes to Build.Configure, Build.DesktopFile, and Build.TestConfig make Utility.Directory not be needed to build setup. And so let it use Utility.RawFilePath, which depends on unix, which cannot be in setup-depends. Sponsored-by: Dartmouth College's Datalad project
205 lines
6.7 KiB
Haskell
205 lines
6.7 KiB
Haskell
{- higher-level inotify interface
|
|
-
|
|
- Copyright 2012 Joey Hess <id@joeyh.name>
|
|
-
|
|
- License: BSD-2-clause
|
|
-}
|
|
|
|
module Utility.DirWatcher.INotify (watchDir) where
|
|
|
|
import Common
|
|
import Utility.ThreadLock
|
|
import Utility.DirWatcher.Types
|
|
import qualified Utility.RawFilePath as R
|
|
|
|
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) -> Bool -> WatchHooks -> IO ()
|
|
watchDir i dir ignored scanevents 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 (toInternalFilePath dir) handler)
|
|
`catchIO` failedaddwatch
|
|
withLock lock $
|
|
mapM_ scan =<< filter (not . dirCruft) <$>
|
|
getDirectoryContents dir
|
|
where
|
|
recurse d = watchDir i d ignored scanevents 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 ->
|
|
when scanevents $
|
|
runhook addSymlinkHook f ms
|
|
| Files.isRegularFile s ->
|
|
when scanevents $
|
|
runhook addHook f ms
|
|
| otherwise ->
|
|
noop
|
|
|
|
go (Created { isDirectory = isd, filePath = fi })
|
|
| 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
|
|
where
|
|
f = fromInternalFilePath fi
|
|
|
|
-- 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 fi }) =
|
|
checkfiletype Files.isRegularFile addHook $
|
|
fromInternalFilePath fi
|
|
|
|
-- When a file or directory is moved in, scan it to add new
|
|
-- stuff.
|
|
go (MovedIn { filePath = fi }) = scan $ fromInternalFilePath fi
|
|
go (MovedOut { isDirectory = isd, filePath = fi })
|
|
| isd = runhook delDirHook f Nothing
|
|
| otherwise = runhook delHook f Nothing
|
|
where
|
|
f = fromInternalFilePath fi
|
|
|
|
-- 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 = fi })
|
|
| isd = guarded $ runhook delDirHook f Nothing
|
|
| otherwise = guarded $ runhook delHook f Nothing
|
|
where
|
|
guarded = unlessM (filetype (const True) f)
|
|
f = fromInternalFilePath fi
|
|
|
|
go (Modified { isDirectory = isd, maybeFilePath = Just fi })
|
|
| isd = noop
|
|
| otherwise = runhook modifyHook (fromInternalFilePath fi) 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 $ R.getSymbolicLinkStatus $ toRawFilePath $ 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 <$> R.getSymbolicLinkStatus (toRawFilePath (indir f))
|
|
|
|
failedaddwatch e
|
|
-- Inotify fails when there are too many watches with a
|
|
-- disk full error.
|
|
| isFullError e =
|
|
case errHook hooks of
|
|
Nothing -> giveup $ "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)
|
|
|
|
toInternalFilePath :: FilePath -> RawFilePath
|
|
toInternalFilePath = toRawFilePath
|
|
|
|
fromInternalFilePath :: RawFilePath -> FilePath
|
|
fromInternalFilePath = fromRawFilePath
|