2012-06-18 16:25:20 +00:00
|
|
|
{- BSD kqueue file modification notification interface
|
|
|
|
-
|
|
|
|
- Copyright 2012 Joey Hess <joey@kitenet.net>
|
|
|
|
-
|
|
|
|
- Licensed under the GNU GPL version 3 or higher.
|
|
|
|
-}
|
|
|
|
|
|
|
|
{-# LANGUAGE ForeignFunctionInterface #-}
|
|
|
|
|
2012-06-18 17:01:58 +00:00
|
|
|
module Utility.Kqueue (
|
2012-06-18 20:33:27 +00:00
|
|
|
initKqueue,
|
|
|
|
stopKqueue,
|
2012-06-18 17:01:58 +00:00
|
|
|
waitChange,
|
2012-06-19 01:29:30 +00:00
|
|
|
Change(..),
|
2012-06-19 02:13:26 +00:00
|
|
|
changedFile,
|
|
|
|
isAdd,
|
|
|
|
isDelete,
|
2012-06-19 03:47:48 +00:00
|
|
|
runHooks,
|
2012-06-18 17:01:58 +00:00
|
|
|
) where
|
2012-06-18 16:25:20 +00:00
|
|
|
|
|
|
|
import Common
|
2012-06-19 03:47:48 +00:00
|
|
|
import Utility.Types.DirWatcher
|
2012-06-18 16:25:20 +00:00
|
|
|
|
|
|
|
import System.Posix.Types
|
|
|
|
import Foreign.C.Types
|
|
|
|
import Foreign.Ptr
|
|
|
|
import Foreign.Marshal
|
2012-06-18 17:01:58 +00:00
|
|
|
import qualified Data.Map as M
|
2012-06-19 01:29:30 +00:00
|
|
|
import qualified Data.Set as S
|
|
|
|
|
|
|
|
data Change
|
|
|
|
= Deleted FilePath
|
|
|
|
| Added FilePath
|
|
|
|
deriving (Show)
|
|
|
|
|
|
|
|
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
|
2012-06-18 17:01:58 +00:00
|
|
|
|
2012-06-19 01:29:30 +00:00
|
|
|
data Kqueue = Kqueue Fd DirMap Pruner
|
2012-06-18 16:25:20 +00:00
|
|
|
|
2012-06-19 01:29:30 +00:00
|
|
|
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]
|
2012-06-19 02:13:26 +00:00
|
|
|
oldc // newc = deleted ++ added
|
2012-06-19 01:29:30 +00:00
|
|
|
where
|
2012-06-19 02:13:26 +00:00
|
|
|
deleted = calc Deleted oldc newc
|
|
|
|
added = calc Added newc oldc
|
2012-06-19 01:29:30 +00:00
|
|
|
calc a x y = map a . map (dirName x </>) $
|
|
|
|
S.toList $ S.difference (dirCache x) (dirCache y)
|
2012-06-18 20:18:59 +00:00
|
|
|
|
2012-06-18 17:19:40 +00:00
|
|
|
{- Builds a map of directories in a tree, possibly pruning some.
|
2012-06-19 01:29:30 +00:00
|
|
|
- Opens each directory in the tree, and records its current contents. -}
|
|
|
|
scanRecursive :: FilePath -> Pruner -> IO DirMap
|
|
|
|
scanRecursive topdir prune = M.fromList <$> walk [] [topdir]
|
2012-06-18 17:19:40 +00:00
|
|
|
where
|
2012-06-19 01:29:30 +00:00
|
|
|
walk c [] = return c
|
|
|
|
walk c (dir:rest)
|
|
|
|
| prune dir = walk c rest
|
|
|
|
| otherwise = do
|
2012-06-19 02:13:26 +00:00
|
|
|
minfo <- catchMaybeIO $ getDirInfo dir
|
|
|
|
case minfo of
|
|
|
|
Nothing -> walk c rest
|
|
|
|
Just info -> do
|
|
|
|
mfd <- catchMaybeIO $
|
|
|
|
openFd dir ReadOnly Nothing defaultFileFlags
|
|
|
|
case mfd of
|
|
|
|
Nothing -> walk c rest
|
|
|
|
Just fd -> do
|
|
|
|
let subdirs = map (dir </>) $
|
|
|
|
S.toList $ dirCache info
|
|
|
|
walk ((fd, info):c) (subdirs ++ rest)
|
2012-06-19 01:29:30 +00:00
|
|
|
|
|
|
|
{- 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 children) from a directory map. -}
|
|
|
|
removeSubDir :: DirMap -> FilePath -> IO DirMap
|
|
|
|
removeSubDir dirmap dir = do
|
|
|
|
mapM_ closeFd $ M.keys toremove
|
2012-06-18 23:14:58 +00:00
|
|
|
return rest
|
|
|
|
where
|
2012-06-19 01:29:30 +00:00
|
|
|
(toremove, rest) = M.partition (dirContains dir . dirName) dirmap
|
2012-06-18 17:19:40 +00:00
|
|
|
|
2012-06-18 20:18:59 +00:00
|
|
|
foreign import ccall unsafe "libkqueue.h init_kqueue" c_init_kqueue
|
2012-06-19 01:46:04 +00:00
|
|
|
:: IO Fd
|
|
|
|
foreign import ccall unsafe "libkqueue.h addfds_kqueue" c_addfds_kqueue
|
|
|
|
:: Fd -> CInt -> Ptr Fd -> IO ()
|
2012-06-18 20:18:59 +00:00
|
|
|
foreign import ccall unsafe "libkqueue.h waitchange_kqueue" c_waitchange_kqueue
|
|
|
|
:: Fd -> IO Fd
|
|
|
|
|
2012-06-19 01:29:30 +00:00
|
|
|
{- Initializes a Kqueue to watch a directory, and all its subdirectories. -}
|
|
|
|
initKqueue :: FilePath -> Pruner -> IO Kqueue
|
|
|
|
initKqueue dir pruned = do
|
|
|
|
dirmap <- scanRecursive dir pruned
|
2012-06-19 01:46:04 +00:00
|
|
|
h <- c_init_kqueue
|
|
|
|
let kq = Kqueue h dirmap pruned
|
|
|
|
updateKqueue kq
|
|
|
|
return kq
|
|
|
|
|
|
|
|
{- Updates a Kqueue, adding watches for its map. -}
|
|
|
|
updateKqueue :: Kqueue -> IO ()
|
|
|
|
updateKqueue (Kqueue h dirmap _) =
|
2012-06-19 01:29:30 +00:00
|
|
|
withArrayLen (M.keys dirmap) $ \fdcnt c_fds -> do
|
2012-06-19 01:46:04 +00:00
|
|
|
c_addfds_kqueue h (fromIntegral fdcnt) c_fds
|
2012-06-18 16:25:20 +00:00
|
|
|
|
2012-06-18 20:18:59 +00:00
|
|
|
{- Stops a Kqueue. Note: Does not directly close the Fds in the dirmap,
|
|
|
|
- so it can be reused. -}
|
2012-06-18 20:33:27 +00:00
|
|
|
stopKqueue :: Kqueue -> IO ()
|
2012-06-19 01:29:30 +00:00
|
|
|
stopKqueue (Kqueue h _ _) = closeFd h
|
2012-06-18 20:18:59 +00:00
|
|
|
|
2012-06-19 01:29:30 +00:00
|
|
|
{- 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
|
2012-06-18 17:19:40 +00:00
|
|
|
- the directory; it could be an added file, a deleted file, a renamed
|
|
|
|
- file, a new subdirectory, or a deleted subdirectory, or a moved
|
2012-06-19 01:29:30 +00:00
|
|
|
- subdirectory.
|
2012-06-18 17:19:40 +00:00
|
|
|
-
|
2012-06-19 01:29:30 +00:00
|
|
|
- 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])
|
2012-06-19 02:13:26 +00:00
|
|
|
handleChange (Kqueue h dirmap pruner) fd olddirinfo =
|
2012-06-19 01:29:30 +00:00
|
|
|
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'
|
2012-06-19 01:46:04 +00:00
|
|
|
|
|
|
|
-- When new directories were added, need to update
|
|
|
|
-- the kqueue to watch them.
|
|
|
|
let kq' = Kqueue h newmap'' pruner
|
|
|
|
unless (null newdirinfos) $
|
|
|
|
updateKqueue kq'
|
|
|
|
|
|
|
|
return (kq', changes)
|
2012-06-19 01:29:30 +00:00
|
|
|
go Nothing = do
|
|
|
|
-- The directory has been moved or deleted, so
|
|
|
|
-- remove it from our map.
|
|
|
|
newmap <- removeSubDir dirmap (dirName olddirinfo)
|
2012-06-19 01:46:04 +00:00
|
|
|
return (Kqueue h newmap pruner, [])
|
2012-06-19 03:47:48 +00:00
|
|
|
|
|
|
|
{- Processes changes on the Kqueue, calling the hooks as appropriate.
|
|
|
|
- Never returns. -}
|
|
|
|
runHooks :: Kqueue -> WatchHooks -> IO ()
|
|
|
|
runHooks kq hooks = do
|
|
|
|
(kq', changes) <- Kqueue.waitChange kq
|
|
|
|
forM_ changes $ dispatch kq'
|
|
|
|
runHooks kq' hooks
|
|
|
|
where
|
|
|
|
-- Kqueue returns changes for both whole directories
|
|
|
|
-- being added and deleted, and individual files being
|
|
|
|
-- added and deleted.
|
|
|
|
dispatch q change status
|
|
|
|
| isAdd change = withstatus s (dispatchadd q)
|
|
|
|
| isDelete change = callhook delDirHook change
|
|
|
|
dispatchadd q change s
|
|
|
|
| Files.isSymbolicLink = callhook addSymlinkHook change
|
|
|
|
| Files.isDirectory = print $ "TODO: recursive directory add: " ++ show change
|
|
|
|
| Files.isRegularFile = callhook addHook change
|
|
|
|
| otherwise = noop
|
|
|
|
callhook h change = hooks h $ changedFile change
|
|
|
|
withstatus change a = maybe noop (a change) =<<
|
|
|
|
(catchMaybeIO (getSymbolicLinkStatus (changedFile change)
|