kqueue: add directory content tracking, and change determination

This *may* now return Add or Delete Changes as appropriate. All I know
for sure is that it compiles.

I had hoped to avoid maintaining my own state about the content of the
directory tree, and rely on git to check what was changed. But I can't;
I need to know about new and deleted subdirectories to add them to the
watch list, and git doesn't deal with (empty) directories.

So, wrote all the code to scan directories, remember their past contents,
compare with current contents, generate appropriate Change events, and
update bookkeeping info appropriately.
This commit is contained in:
Joey Hess 2012-06-18 21:29:30 -04:00
parent ae7d07ddcb
commit 2bfcc0b09c
2 changed files with 119 additions and 66 deletions

View file

@ -56,33 +56,6 @@ dirContentsRecursive' topdir (dir:dirs) = unsafeInterleaveIO $ do
, collect (dirEntry:files) dirs' entries
)
{- Gets the subdirectories in a directory, and their subdirectories,
- recursively, and lazily. Prunes sections of the tree matching a
- condition. -}
dirTree :: FilePath -> (FilePath -> Bool) -> IO [FilePath]
dirTree topdir prune
| prune topdir = return []
| otherwise = (:) topdir <$> dirTree' topdir prune [""]
dirTree' :: FilePath -> (FilePath -> Bool) -> [FilePath] -> IO [FilePath]
dirTree' _ _ [] = return []
dirTree' topdir prune (dir:dirs)
| prune dir = dirTree' topdir prune dirs
| otherwise = unsafeInterleaveIO $ do
subdirs <- collect [] =<< dirContents (topdir </> dir)
subdirs' <- dirTree' topdir prune (subdirs ++ dirs)
return $ subdirs ++ subdirs'
where
collect dirs' [] = return dirs'
collect dirs' (entry:entries)
| dirCruft entry || prune entry = collect dirs' entries
| otherwise = do
let dirEntry = dir </> entry
ifM (doesDirectoryExist $ topdir </> dirEntry)
( collect (dirEntry:dirs') entries
, collect dirs' entries
)
{- Moves one filename to another.
- First tries a rename, but falls back to moving across devices if needed. -}
moveFile :: FilePath -> FilePath -> IO ()

View file

@ -8,14 +8,10 @@
{-# LANGUAGE ForeignFunctionInterface #-}
module Utility.Kqueue (
scanRecursive,
addSubDir,
removeSubDir,
initKqueue,
stopKqueue,
waitChange,
Change(..),
) where
import Common
@ -25,60 +21,144 @@ import Foreign.C.Types
import Foreign.Ptr
import Foreign.Marshal
import qualified Data.Map as M
import qualified Data.Set as S
type DirMap = M.Map Fd FilePath
data Change
= Deleted FilePath
| Added FilePath
deriving (Show)
data Kqueue = Kqueue Fd DirMap
isAdd :: Change -> Bool
isAdd (Added _) = True
isAdd (Deleted _) = False
isDelete :: Change -> Bool
isDelete = not . isAdd
changedFile :: Change -> FilePath
changedFile (Added f) = f
changedFile (Deleted f) = f
data Kqueue = Kqueue Fd DirMap Pruner
type Pruner = FilePath -> Bool
type DirMap = M.Map Fd DirInfo
{- A directory, and its last known contents (with filenames relative to it) -}
data DirInfo = DirInfo
{ dirName :: FilePath
, dirCache :: S.Set FilePath
}
deriving (Show)
getDirInfo :: FilePath -> IO DirInfo
getDirInfo dir = do
contents <- S.fromList . filter (not . dirCruft)
<$> getDirectoryContents dir
return $ DirInfo dir contents
{- Difference between the dirCaches of two DirInfos. -}
(//) :: DirInfo -> DirInfo -> [Change]
old // new = deleted ++ added
where
deleted = calc Deleted old new
added = calc Added new old
calc a x y = map a . map (dirName x </>) $
S.toList $ S.difference (dirCache x) (dirCache y)
{- Builds a map of directories in a tree, possibly pruning some.
- Opens each directory in the tree. -}
scanRecursive :: FilePath -> (FilePath -> Bool) -> IO DirMap
scanRecursive dir prune = M.fromList <$> (mapM opendir =<< dirTree dir prune)
- Opens each directory in the tree, and records its current contents. -}
scanRecursive :: FilePath -> Pruner -> IO DirMap
scanRecursive topdir prune = M.fromList <$> walk [] [topdir]
where
opendir d = (,)
<$> openFd d ReadOnly Nothing defaultFileFlags
<*> pure d
walk c [] = return c
walk c (dir:rest)
| prune dir = walk c rest
| otherwise = do
info <- getDirInfo dir
fd <- openFd dir ReadOnly Nothing defaultFileFlags
dirs <- filterM (\d -> doesDirectoryExist $ dir </> d)
(S.toList $ dirCache info)
walk ((fd, info):c) (dirs++rest)
{- Adds a subdirectory (and all its subdirectories, unless pruned) to a
- directory map. -}
addSubDir :: DirMap -> FilePath -> (FilePath -> Bool) -> IO DirMap
addSubDir dirmap dir prune = M.union dirmap <$> scanRecursive dir prune
{- Adds a list of subdirectories (and all their children), unless pruned to a
- directory map. Adding a subdirectory that's already in the map will
- cause its contents to be refreshed. -}
addSubDirs :: DirMap -> Pruner -> [FilePath] -> IO DirMap
addSubDirs dirmap prune dirs = do
newmap <- foldr M.union M.empty <$>
mapM (\d -> scanRecursive d prune) dirs
return $ M.union newmap dirmap -- prefer newmap
{- Removes a subdirectory (and all its subdirectories) from a directory map. -}
removeSubDir :: FilePath -> DirMap -> IO DirMap
removeSubDir dir dirmap = do
mapM_ closeFd $ M.keys toremove) $ closeFd
{- Removes a subdirectory (and all its children) from a directory map. -}
removeSubDir :: DirMap -> FilePath -> IO DirMap
removeSubDir dirmap dir = do
mapM_ closeFd $ M.keys toremove
return rest
where
(toremove, rest) = M.partition (dirContains dir) dirmap
(toremove, rest) = M.partition (dirContains dir . dirName) dirmap
foreign import ccall unsafe "libkqueue.h init_kqueue" c_init_kqueue
:: CInt -> Ptr Fd -> IO Fd
foreign import ccall unsafe "libkqueue.h waitchange_kqueue" c_waitchange_kqueue
:: Fd -> IO Fd
{- Initializes a Kqueue to watch a map of directories. -}
initKqueue :: DirMap -> IO Kqueue
initKqueue dirmap = withArrayLen (M.keys dirmap) $ \fdcnt c_fds -> do
h <- c_init_kqueue (fromIntegral fdcnt) c_fds
return $ Kqueue h dirmap
{- Initializes a Kqueue to watch a directory, and all its subdirectories. -}
initKqueue :: FilePath -> Pruner -> IO Kqueue
initKqueue dir pruned = do
dirmap <- scanRecursive dir pruned
withArrayLen (M.keys dirmap) $ \fdcnt c_fds -> do
h <- c_init_kqueue (fromIntegral fdcnt) c_fds
return $ Kqueue h dirmap pruned
{- Stops a Kqueue. Note: Does not directly close the Fds in the dirmap,
- so it can be reused. -}
stopKqueue :: Kqueue -> IO ()
stopKqueue (Kqueue h _) = closeFd h
stopKqueue (Kqueue h _ _) = closeFd h
{- Waits for a change on a Kqueue, and returns the directory
- where a change took place.
-
- The kqueue interface does not tell what type of change took place in
{- Waits for a change on a Kqueue.
- May update the Kqueue.
-}
waitChange :: Kqueue -> IO (Kqueue, [Change])
waitChange kq@(Kqueue h dirmap _) = do
changedfd <- c_waitchange_kqueue h
case M.lookup changedfd dirmap of
Nothing -> return (kq, [])
Just info -> handleChange kq changedfd info
{- The kqueue interface does not tell what type of change took place in
- the directory; it could be an added file, a deleted file, a renamed
- file, a new subdirectory, or a deleted subdirectory, or a moved
- subdirectory.
- subdirectory.
-
- Note that if subdirectories have changed, the caller should re-run
- initKqueue to get them watched. -}
waitChange :: Kqueue -> IO (Maybe FilePath)
waitChange (Kqueue h dirmap) = do
changed <- c_waitchange_kqueue h
return $ M.lookup changed dirmap
- So to determine this, the contents of the directory are compared
- with its last cached contents. The Kqueue is updated to watch new
- directories as necessary.
-}
handleChange :: Kqueue -> Fd -> DirInfo -> IO (Kqueue, [Change])
handleChange kq@(Kqueue h dirmap pruner) fd olddirinfo =
go =<< catchMaybeIO (getDirInfo $ dirName olddirinfo)
where
go (Just newdirinfo) = do
let changes = olddirinfo // newdirinfo
let (added, deleted) = partition isAdd changes
-- Scan newly added directories to add to the map.
-- (Newly added files will fail getDirInfo.)
newdirinfos <- catMaybes <$>
mapM (catchMaybeIO . getDirInfo . changedFile) added
newmap <- addSubDirs dirmap pruner $ map dirName newdirinfos
-- Remove deleted directories from the map.
newmap' <- foldM removeSubDir newmap (map changedFile deleted)
-- Update the cached dirinfo just looked up.
let newmap'' = M.insertWith' const fd newdirinfo newmap'
ret (newmap'', changes)
go Nothing = do
-- The directory has been moved or deleted, so
-- remove it from our map.
newmap <- removeSubDir dirmap (dirName olddirinfo)
ret (newmap, [])
ret (newmap, changes) = return $ (Kqueue h newmap pruner, changes)