2-- Module : Streamly.Internal.FileSystem.Event.Linux
3-- Copyright : (c) 2020 Composewell Technologies
4-- License : BSD-3-Clause
5-- Maintainer : streamly@composewell.com
6-- Stability : experimental
11-- Use 'watchPaths' with a list of file system paths you want to watch as
12-- argument. It returns a stream of 'Event' representing the file system events
13-- occurring under the watched paths.
16-- Stream.mapM_ (putStrLn . 'showEvent') $ 'watchPaths' [Array.fromCString\# "dir"#]
19-- 'Event' is an opaque type. Accessor functions (e.g. 'showEvent' above)
20-- provided in this module are used to determine the attributes of the event.
22-- Identical successive events may be coalesced into a single event.
26-- For reference documentation see:
28-- * <https://man7.org/linux/man-pages/man7/inotify.7.html inotify man page>
30-- We try to keep the macOS\/Linux/Windows event handling APIs and defaults
31-- semantically and syntactically as close as possible.
35-- When testing on Linux Kernel version @5.3.0-53-generic #47-Ubuntu@, the last
36-- event for the root path seems to be delayed until one more event occurs.
38-- = Differences between macOS and Linux APIs:
40-- 1. macOS watch is based on the path provided to it, if the path is
41-- deleted and recreated it will still be watched, if the path moves to another
42-- path it won't be watched anymore. Whereas Linux watch is based on a handle
43-- to the path, if the path is deleted and recreated it won't be watched, if
44-- the path moves to another it can still be watched (though this is
47-- 2. macOS watches the directory hierarchy recursively, Linux watches only one
48-- level of dir, recursive watch has to be built in user space by watching for
49-- create events and adding the new directories to the watch. Not sure how this
50-- will scale for too many paths.
52-- 3. In macOS the path of the subject of the event is absolute, in Linux the
53-- path is the name of the object inside the dir being watched.
55-- 4. On Linux 'watchPaths' fails if a path does not exist, on macOS it does
60#if HAVE_DECL_IN_EXCL_UNLINK
61module Streamly.Internal.FileSystem.Event.Linux
63 -- * Subscribing to events
65 -- ** Default configuration
78 -- ** Events of Interest
79 -- *** Root Level Events
83 -- *** Item Level Metadata change
86 -- *** Item Level Access
92 -- *** Item CRUD events
102 -- XXX watchPaths is redundant now because we can use watchTrees with
103 -- setRecursiveMode False. Perhaps we can use a common "watch" API.
118 -- ** Exception Conditions
121 -- ** Root Level Events
127 -- ** Item Level Metadata change
130 -- ** Item Level Access
136 -- ** Item CRUD events
151import Control.Monad (void, when)
152import Control.Monad.IO.Class (MonadIO, liftIO)
153import Data.Bits ((.|.), (.&.), complement)
154import Data.Char (ord)
155import Data.Foldable (foldlM)
156import Data.Functor.Identity (runIdentity)
157import Data.IntMap.Lazy (IntMap)
158import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef)
159import Data.List.NonEmpty (NonEmpty)
160#if !(MIN_VERSION_base(4,11,0))
161import Data.Semigroup (Semigroup(..))
163import Data.Word (Word8, Word32)
164import Foreign.C.Error (throwErrnoIfMinus1)
165import Foreign.C.String (CString)
166import Foreign.C.Types (CInt(..), CUInt(..))
167import Foreign.Ptr (Ptr)
168import Foreign.Storable (peek, peekByteOff, sizeOf)
169import GHC.IO.Device (IODeviceType(Stream))
170import GHC.IO.FD (fdFD, mkFD)
171import GHC.IO.Handle.FD (mkHandleFromFD)
172import Streamly.Prelude (SerialT)
173import Streamly.Internal.Data.Parser (Parser)
174import Streamly.Internal.Data.Array.Foreign.Types (Array(..))
175import System.IO (Handle, hClose, IOMode(ReadMode))
176#if !MIN_VERSION_base(4,10,0)
177import Control.Concurrent.MVar (readMVar)
178import Data.Typeable (cast)
179import GHC.IO.Exception (IOException(..), IOErrorType(..), ioException)
181import GHC.IO.Handle.Types (Handle__(..), Handle(FileHandle, DuplexHandle))
183import GHC.IO.Handle.FD (handleToFd)
186import qualified Data.IntMap.Lazy as Map
187import qualified Data.List.NonEmpty as NonEmpty
188import qualified Streamly.Internal.Data.Array.Foreign as A
189import qualified Streamly.Internal.Data.Fold as FL
190import qualified Streamly.Internal.Data.Parser as PR
191import qualified Streamly.Internal.Data.Stream.IsStream as S
192import qualified Streamly.Internal.FileSystem.Dir as Dir
193import qualified Streamly.Internal.FileSystem.Handle as FH
194import qualified Streamly.Internal.Unicode.Stream as U
196-------------------------------------------------------------------------------
197-- Subscription to events
198-------------------------------------------------------------------------------
200-- | Watch configuration, used to specify the events of interest and the
201-- behavior of the watch.
207 , createFlags :: Word32
210-------------------------------------------------------------------------------
212-------------------------------------------------------------------------------
214-- | Whether a setting is 'On' or 'Off'.
218data Toggle = On | Off
220toggle :: Toggle -> Toggle
224setFlag :: Word32 -> Toggle -> Config -> Config
225setFlag mask status cfg@Config{..} =
228 On -> createFlags .|. mask
229 Off -> createFlags .&. complement mask
230 in cfg {createFlags = flags}
232-------------------------------------------------------------------------------
234-------------------------------------------------------------------------------
236-- | Watch the whole directory tree recursively instead of watching just one
237-- level of directory.
243setRecursiveMode :: Bool -> Config -> Config
244setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec}
247 "sys/inotify.h value IN_DONT_FOLLOW" iN_DONT_FOLLOW :: Word32
249-- | If the pathname to be watched is a symbolic link then watch the target of
250-- the symbolic link instead of the symbolic link itself.
256setFollowSymLinks :: Toggle -> Config -> Config
257setFollowSymLinks s = setFlag iN_DONT_FOLLOW (toggle s)
260 "sys/inotify.h value IN_EXCL_UNLINK" iN_EXCL_UNLINK :: Word32
262-- | If an object moves out of the directory being watched then stop watching
269setUnwatchMoved :: Toggle -> Config -> Config
270setUnwatchMoved = setFlag iN_EXCL_UNLINK
272#if HAVE_DECL_IN_MASK_CREATE
274 "sys/inotify.h value IN_MASK_CREATE" iN_MASK_CREATE :: Word32
278 "sys/inotify.h value IN_MASK_ADD" iN_MASK_ADD :: Word32
280-- | What to do if a watch already exists when 'openWatch' or 'addToWatch' is
286 AddIfExists -- ^ Do not set an existing setting to 'Off' only set to 'On'
287 | ReplaceIfExists -- ^ Replace the existing settings with new settings
288#if HAVE_DECL_IN_MASK_CREATE
289 | FailIfExists -- ^ Fail the API
292-- | When adding a new path to the watch, specify what to do if a watch already
293-- exists on that path.
295-- /default: FailIfExists/
299setWhenExists :: WhenExists -> Config -> Config
300setWhenExists val cfg =
302 AddIfExists -> setFlag iN_MASK_ADD On cfg
303 ReplaceIfExists -> setFlag iN_MASK_ADD Off cfg
304#if HAVE_DECL_IN_MASK_CREATE
305 FailIfExists -> setFlag iN_MASK_CREATE On cfg
309 "sys/inotify.h value IN_ONESHOT" iN_ONESHOT :: Word32
311-- | Watch the object only for one event and then remove it from the watch.
317setOneShot :: Toggle -> Config -> Config
318setOneShot = setFlag iN_ONESHOT
321 "sys/inotify.h value IN_ONLYDIR" iN_ONLYDIR :: Word32
323-- | Watch the object only if it is a directory. This provides a race-free way
324-- to ensure that the watched object is a directory.
330setOnlyDir :: Toggle -> Config -> Config
331setOnlyDir = setFlag iN_ONLYDIR
333-------------------------------------------------------------------------------
334-- Event types that can occur
335-------------------------------------------------------------------------------
338 "sys/inotify.h value IN_DELETE_SELF" iN_DELETE_SELF :: Word32
340-- | Report when the watched path itself gets deleted.
346setRootDeleted :: Toggle -> Config -> Config
347setRootDeleted = setFlag iN_DELETE_SELF
350 "sys/inotify.h value IN_MOVE_SELF" iN_MOVE_SELF :: Word32
352-- | Report when the watched root path itself gets renamed.
358setRootMoved :: Toggle -> Config -> Config
359setRootMoved = setFlag iN_MOVE_SELF
362 "sys/inotify.h value IN_ATTRIB" iN_ATTRIB :: Word32
364-- | Report when the metadata e.g. owner, permission modes, modifications times
365-- of an object changes.
371setMetadataChanged :: Toggle -> Config -> Config
372setMetadataChanged = setFlag iN_ATTRIB
375 "sys/inotify.h value IN_ACCESS" iN_ACCESS :: Word32
377-- | Report when a file is accessed.
383setAccessed :: Toggle -> Config -> Config
384setAccessed = setFlag iN_ACCESS
387 "sys/inotify.h value IN_OPEN" iN_OPEN :: Word32
389-- | Report when a file is opened.
395setOpened :: Toggle -> Config -> Config
396setOpened = setFlag iN_OPEN
399 "sys/inotify.h value IN_CLOSE_WRITE" iN_CLOSE_WRITE :: Word32
401-- | Report when a file that was opened for writes is closed.
407setWriteClosed :: Toggle -> Config -> Config
408setWriteClosed = setFlag iN_CLOSE_WRITE
411 "sys/inotify.h value IN_CLOSE_NOWRITE" iN_CLOSE_NOWRITE :: Word32
413-- | Report when a file that was opened for not writing is closed.
419setNonWriteClosed :: Toggle -> Config -> Config
420setNonWriteClosed = setFlag iN_CLOSE_NOWRITE
423 "sys/inotify.h value IN_CREATE" iN_CREATE :: Word32
425-- | Report when a file is created.
431setCreated :: Toggle -> Config -> Config
432setCreated = setFlag iN_CREATE
435 "sys/inotify.h value IN_DELETE" iN_DELETE :: Word32
437-- | Report when a file is deleted.
443setDeleted :: Toggle -> Config -> Config
444setDeleted = setFlag iN_DELETE
447 "sys/inotify.h value IN_MOVED_FROM" iN_MOVED_FROM :: Word32
449-- | Report the source of a move.
455setMovedFrom :: Toggle -> Config -> Config
456setMovedFrom = setFlag iN_MOVED_FROM
459 "sys/inotify.h value IN_MOVED_TO" iN_MOVED_TO :: Word32
461-- | Report the target of a move.
467setMovedTo :: Toggle -> Config -> Config
468setMovedTo = setFlag iN_MOVED_TO
471 "sys/inotify.h value IN_MODIFY" iN_MODIFY :: Word32
473-- | Report when a file is modified.
479setModified :: Toggle -> Config -> Config
480setModified = setFlag iN_MODIFY
482-- | Set all events 'On' or 'Off'.
488setAllEvents :: Toggle -> Config -> Config
492 . setMetadataChanged s
496 . setNonWriteClosed s
504-------------------------------------------------------------------------------
506-------------------------------------------------------------------------------
508-- The defaults are set in such a way that the behavior on macOS and Linux is
509-- as much compatible as possible.
513-- * 'setFollowSymLinks' 'On'
514-- * 'setUnwatchMoved' 'On'
515-- * 'setOneShot' 'Off'
516-- * 'setOnlyDir' 'Off'
517-- * 'setWhenExists' 'AddIfExists'
518-- * 'setAllEvents' 'On'
522defaultConfig :: Config
524 setWhenExists AddIfExists
531-------------------------------------------------------------------------------
532-- Open an event stream
533-------------------------------------------------------------------------------
535-- | A handle for a watch.
538 Handle -- File handle for the watch
540 (IntMap -- Key is the watch descriptor
541 ( Array Word8 -- Absolute path of the watch root
542 , Array Word8 -- Path of subdir relative to watch root
547-- Instead of using the watch descriptor we can provide APIs that use the path
548-- itself to identify the watch. That will require us to maintain a map from wd
549-- to path in the Watch handle.
551newtype WD = WD CInt deriving Show
553foreign import ccall unsafe
554 "sys/inotify.h inotify_init" c_inotify_init :: IO CInt
556-- | Create a 'Watch' handle. 'addToWatch' can be used to add paths being
557-- monitored by this watch.
561createWatch :: IO Watch
563 rawfd <- throwErrnoIfMinus1 "createWatch" c_inotify_init
564 -- we could use fdToHandle but it cannot determine the fd type
565 -- automatically for the inotify fd
570 (Just (Stream, 0, 0)) -- (IODeviceType, CDev, CIno)
571 False -- not a socket
572 False -- non-blocking is false
573 let fdString = "<createWatch file descriptor: " ++ show fd ++ ">"
580 True -- use non-blocking IO
581 Nothing -- TextEncoding (binary)
582 emptyMapRef <- newIORef Map.empty
583 return $ Watch h emptyMapRef
585foreign import ccall unsafe
586 "sys/inotify.h inotify_add_watch" c_inotify_add_watch
587 :: CInt -> CString -> CUInt -> IO CInt
589-- XXX we really do not know the path encoding, all we know is that it is "/"
590-- separated bytes. So these may fail or convert the path in an unexpected
591-- manner. We should ultimately remove all usage of these.
593toUtf8 :: MonadIO m => String -> m (Array Word8)
594toUtf8 = A.fromStream . U.encodeUtf8 . S.fromList
596utf8ToString :: Array Word8 -> String
597utf8ToString = runIdentity . S.toList . U.decodeUtf8' . A.toStream
599#if !MIN_VERSION_base(4,10,0)
600-- | Turn an existing Handle into a file descriptor. This function throws an
601-- IOError if the Handle does not reference a file descriptor.
602handleToFd :: Handle -> IO FD
603handleToFd h = case h of
604 FileHandle _ mv -> do
605 Handle__{haDevice = dev} <- readMVar mv
608 Nothing -> throwErr "not a file descriptor"
609 DuplexHandle{} -> throwErr "not a file handle"
613 throwErr msg = ioException $ IOError (Just h)
614 InappropriateType "handleToFd" msg Nothing Nothing
617-- | Add a trailing "/" at the end of the path if there is none. Do not add a
618-- "/" if the path is empty.
620ensureTrailingSlash :: Array Word8 -> Array Word8
621ensureTrailingSlash path =
622 if A.length path /= 0
624 let mx = A.readIndex path (A.length path - 1)
626 Nothing -> error "ensureTrailingSlash: Bug: Invalid index"
628 if x /= fromIntegral (ord '/')
629 then path <> A.fromCString# "/"#
633-- | @addToWatch cfg watch root subpath@ adds @subpath@ to the list of paths
634-- being monitored under @root@ via the watch handle @watch@. @root@ must be
635-- an absolute path and @subpath@ must be relative to @root@.
639addToWatch :: Config -> Watch -> Array Word8 -> Array Word8 -> IO ()
640addToWatch cfg@Config{..} watch@(Watch handle wdMap) root0 path0 = do
641 -- XXX do not add if the path is already added
642 -- XXX if the watch is added by the scan and not via an event we can
643 -- generate a create event assuming that the create may have been lost. We
644 -- can also mark in the map that this entry was added by the scan. So if an
645 -- actual create event later comes and tries to add this again then we can
646 -- ignore that and drop the create event to avoid duplicate create, because
647 -- we have already emitted it.
649 -- When a directory is added by the scan we should also emit create events
650 -- for files that may have got added to the dir. However, such create
651 -- events may get duplicated because of a race between the scan generated
652 -- versus real events.
654 -- Or we may distinguish between scan generated events and real events so
655 -- that the application can assume that other events may been lost and
656 -- handle it. For example, if it is a dir create the application can read
657 -- the dir to scan the files in it.
659 let root = ensureTrailingSlash root0
660 path = ensureTrailingSlash path0
661 absPath = root <> path
662 fd <- handleToFd handle
664 -- XXX we need to tolerate an error where we are adding a watch for a
665 -- non-existing file because the file may have got deleted by the time we
666 -- added the watch. Perhaps we can have a flag in config for this and keep
667 -- the default value to tolerate the error.
669 -- XXX The file may have even got deleted and then recreated which we will
670 -- never get to know, document this.
671 wd <- A.asCString absPath $ \pathPtr ->
672 throwErrnoIfMinus1 ("addToWatch: " ++ utf8ToString absPath) $
673 c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
675 -- We add the parent first so that we start getting events for any new
676 -- creates and add the new subdirectories on creates while we are adding
678 modifyIORef wdMap (Map.insert (fromIntegral wd) (root, path))
680 -- Now add the children. If we missed any creates while we were adding the
681 -- parent, this will make sure they are added too.
683 -- XXX Ensure that we generate events that we may have missed while we were
686 -- XXX toDirs currently uses paths as String, we need to convert it
687 -- to "/" separated by byte arrays.
689 S.mapM_ (\p -> addToWatch cfg watch root (path <> p))
691 $ Dir.toDirs $ utf8ToString absPath
693foreign import ccall unsafe
694 "sys/inotify.h inotify_rm_watch" c_inotify_rm_watch
695 :: CInt -> CInt -> IO CInt
697-- | Remove an absolute root path from a 'Watch', if a path was moved after
698-- adding you need to provide the original path which was used to add the
703removeFromWatch :: Watch -> Array Word8 -> IO ()
704removeFromWatch (Watch handle wdMap) path = do
705 fd <- handleToFd handle
706 km <- readIORef wdMap
707 wdMap1 <- foldlM (step fd) Map.empty (Map.toList km)
708 writeIORef wdMap wdMap1
712 step fd newMap (wd, v) = do
715 let err = "removeFromWatch: " ++ show (utf8ToString path)
716 rm = c_inotify_rm_watch (fdFD fd) (fromIntegral wd)
717 void $ throwErrnoIfMinus1 err rm
719 else return $ Map.insert wd v newMap
721-- | Given a 'Config' and list of @paths@ ("/" separated byte arrays) start
722-- monitoring the paths for file system events. Returns a 'Watch' handle which
723-- can then be used to read the event stream or to close the watch.
727openWatch :: Config -> NonEmpty (Array Word8) -> IO Watch
728openWatch cfg paths = do
730 mapM_ (\p -> addToWatch cfg w p (A.fromList [])) $ NonEmpty.toList paths
733-- | Close a 'Watch' handle.
737closeWatch :: Watch -> IO ()
738closeWatch (Watch h _) = hClose h
740-------------------------------------------------------------------------------
741-- Raw events read from the watch file handle
742-------------------------------------------------------------------------------
744newtype Cookie = Cookie Word32 deriving (Show, Eq)
746-- | An Event generated by the file system. Use the accessor functions to
753 , eventFlags :: Word32
754 , eventCookie :: Word32
755 , eventRelPath :: Array Word8
756 , eventMap :: IntMap (Array Word8, Array Word8)
757 } deriving (Show, Ord, Eq)
759-- The inotify event struct from the man page/header file:
761-- struct inotify_event {
762-- int wd; /* Watch descriptor */
763-- uint32_t mask; /* Mask describing event */
764-- uint32_t cookie; /* Unique cookie associating related
765-- events (for rename(2)) */
766-- uint32_t len; /* Size of name field */
767-- char name[]; /* Optional null-terminated name */
770-- XXX We can perhaps use parseD monad instance for fusing with parseMany? Need
771-- to measure the perf.
773readOneEvent :: Config -> Watch -> Parser IO Word8 Event
774readOneEvent cfg wt@(Watch _ wdMap) = do
775 let headerLen = (sizeOf (undefined :: CInt)) + 12
776 arr <- PR.takeEQ headerLen (A.writeN headerLen)
777 (ewd, eflags, cookie, pathLen) <- PR.yieldM $ A.asPtr arr readHeader
778 -- XXX need the "initial" in parsers to return a step type so that "take 0"
779 -- can return without an input. otherwise if pathLen is 0 we will keep
780 -- waiting to read one more char before we return this event.
784 -- XXX sliceSepByMax drops the separator so assumes a null
785 -- terminated path, we should use a takeWhile nested inside a
789 $ FL.sliceSepByMax (== 0) pathLen (A.writeN pathLen)
790 let remaining = pathLen - A.length pth - 1
791 when (remaining /= 0) $ PR.takeEQ remaining FL.drain
793 else return $ A.fromList []
794 wdm <- PR.yieldM $ readIORef wdMap
796 case Map.lookup (fromIntegral ewd) wdm of
799 error $ "readOneEvent: "
800 <> "Unknown watch descriptor: "
802 let -- "sub" is guaranteed to have a trailing "/"
804 -- Check for "ISDIR" first because it is less likely
805 isDirCreate = eflags .&. iN_ISDIR /= 0 && eflags .&. iN_CREATE /= 0
806 when (watchRec cfg && isDirCreate)
807 $ PR.yieldM $ addToWatch cfg wt root sub1
808 -- XXX Handle IN_DELETE, IN_DELETE_SELF, IN_MOVE_SELF, IN_MOVED_FROM,
810 -- What if a large dir tree gets moved in to our hierarchy? Do we get a
811 -- single event for the top level dir in this case?
813 { eventWd = (fromIntegral ewd)
814 , eventFlags = eflags
815 , eventCookie = cookie
816 , eventRelPath = sub1
822 readHeader (ptr :: Ptr Word8) = do
823 let len = sizeOf (undefined :: CInt)
825 eflags <- peekByteOff ptr len
826 cookie <- peekByteOff ptr (len + 4)
827 pathLen :: Word32 <- peekByteOff ptr (len + 8)
828 return (ewd, eflags, cookie, fromIntegral pathLen)
830watchToStream :: Config -> Watch -> SerialT IO Event
831watchToStream cfg wt@(Watch handle _) = do
832 -- Do not use too small a buffer. As per inotify man page:
834 -- The behavior when the buffer given to read(2) is too small to return
835 -- information about the next event depends on the kernel version: in
836 -- kernels before 2.6.21, read(2) returns 0; since kernel 2.6.21, read(2)
837 -- fails with the error EINVAL. Specifying a buffer of size
839 -- sizeof(struct inotify_event) + NAME_MAX + 1
841 -- will be sufficient to read at least one event.
842 S.parseMany (readOneEvent cfg wt) $ S.unfold FH.read handle
844-- | Start monitoring a list of file system paths for file system events with
845-- the supplied configuration operation over the 'defaultConfig'. The
846-- paths could be files or directories. When the path is a directory, only the
847-- files and directories directly under the watched directory are monitored,
848-- contents of subdirectories are not monitored. Monitoring starts from the
849-- current time onwards. The paths are specified as "/" separated 'Array' of
854-- ('setFollowSymLinks' On . 'setUnwatchMoved' Off)
855-- [Array.fromCString\# "dir"#]
861 (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
862watchPathsWith f = watchTreesWith (f . setRecursiveMode False)
864-- | Like 'watchPathsWith' but uses the 'defaultConfig' options.
867-- watchPaths = watchPathsWith id
872watchPaths :: NonEmpty (Array Word8) -> SerialT IO Event
873watchPaths = watchPathsWith id
875-- XXX We should not go across the mount points of network file systems or file
876-- systems that are known to not generate any events.
878-- | Start monitoring a list of file system paths for file system events with
879-- the supplied configuration operation over the 'defaultConfig'. The
880-- paths could be files or directories. When the path is a directory, the
881-- whole directory tree under it is watched recursively. Monitoring starts from
882-- the current time onwards.
884-- Note that recrusive watch on a large directory tree could be expensive. When
885-- starting a watch, the whole tree must be read and watches are started on
886-- each directory in the tree. The initial time to start the watch as well as
887-- the memory required is proportional to the number of directories in the
890-- When new directories are created under the tree they are added to the watch
891-- on receiving the directory create event. However, the creation of a dir and
892-- adding a watch for it is not atomic. The implementation takes care of this
893-- and makes sure that watches are added for all directories. However, In the
894-- mean time, the directory may have received more events which may get lost.
895-- Handling of any such lost events is yet to be implemented.
897-- See the Linux __inotify__ man page for more details.
902 (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
903watchTreesWith f paths = S.bracket before after (watchToStream cfg)
907 cfg = f defaultConfig
908 before = liftIO $ openWatch cfg paths
909 after = liftIO . closeWatch
911-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
914-- watchTrees = watchTreesWith id
917watchTrees :: NonEmpty (Array Word8) -> SerialT IO Event
918watchTrees = watchTreesWith id
920-------------------------------------------------------------------------------
921-- Examine event stream
922-------------------------------------------------------------------------------
924-- | Get the watch root corresponding to the 'Event'.
926-- Note that if a path was moved after adding to the watch, this will give the
927-- original path and not the new path after moving.
929-- TBD: we can possibly update the watch root on a move self event.
933getRoot :: Event -> Array Word8
937 case Map.lookup (fromIntegral eventWd) eventMap of
938 Just path -> fst path
940 error $ "Bug: getRoot: No path found corresponding to the "
941 ++ "watch descriptor " ++ show eventWd
944-- XXX should we use a Maybe here?
945-- | Get the file system object path for which the event is generated, relative
946-- to the watched root. The path is a "/" separated array of bytes.
950getRelPath :: Event -> Array Word8
951getRelPath Event{..} = eventRelPath
954-- | Get the absolute file system object path for which the event is generated.
955-- The path is a "/" separated array of bytes.
959getAbsPath :: Event -> Array Word8
960getAbsPath ev = getRoot ev <> getRelPath ev
962-- XXX should we use a Maybe?
963-- | Cookie is set when a rename occurs. The cookie value can be used to
964-- connect the 'isMovedFrom' and 'isMovedTo' events, if both the events belong
965-- to the same move operation then they will have the same cookie value.
969getCookie :: Event -> Cookie
970getCookie Event{..} = Cookie eventCookie
972-------------------------------------------------------------------------------
974-------------------------------------------------------------------------------
976getFlag :: Word32 -> Event -> Bool
977getFlag mask Event{..} = eventFlags .&. mask /= 0
979-------------------------------------------------------------------------------
981-------------------------------------------------------------------------------
984 "sys/inotify.h value IN_Q_OVERFLOW" iN_Q_OVERFLOW :: Word32
986-- XXX rename to isQOverflowed or hasOverflowed?
988-- macOS overflow API is more specific, it tells which paths have lost the
989-- events due to overflow.
991-- | Event queue overflowed (WD is invalid for this event) and we may have lost
992-- some events.. The user application must scan everything under the watched
993-- paths to know the current state.
997isOverflow :: Event -> Bool
998isOverflow = getFlag iN_Q_OVERFLOW
1000-------------------------------------------------------------------------------
1001-- Events affecting the watched path only
1002-------------------------------------------------------------------------------
1005 "sys/inotify.h value IN_IGNORED" iN_IGNORED :: Word32
1007-- Compare with isRootChanged on macOS. isRootChanged includes all these cases.
1009-- | A path was removed from the watch explicitly using 'removeFromWatch' or
1010-- automatically (file was deleted, or filesystem was unmounted).
1012-- /Occurs only for a watched path/
1016isRootUnwatched :: Event -> Bool
1017isRootUnwatched = getFlag iN_IGNORED
1019-- | Watched file/directory was itself deleted. (This event also occurs if an
1020-- object is moved to another filesystem, since mv(1) in effect copies the file
1021-- to the other filesystem and then deletes it from the original filesystem.)
1022-- In addition, an 'isRootUnwatched' event will subsequently be generated
1023-- for the watch descriptor.
1025-- /Occurs only for a watched path/
1029isRootDeleted :: Event -> Bool
1030isRootDeleted = getFlag iN_DELETE_SELF
1032-- | Watched file/directory was itself moved within the file system.
1034-- /Occurs only for a watched path/
1038isRootMoved :: Event -> Bool
1039isRootMoved = getFlag iN_MOVE_SELF
1042 "sys/inotify.h value IN_UNMOUNT" iN_UNMOUNT :: Word32
1044-- | Filesystem containing watched object was unmounted. In addition, an
1045-- 'isRootUnwatched' event will subsequently be generated for the watch
1048-- /Occurs only for a watched path/
1052isRootUnmounted :: Event -> Bool
1053isRootUnmounted = getFlag iN_UNMOUNT
1055-------------------------------------------------------------------------------
1056-- Metadata change Events
1057-------------------------------------------------------------------------------
1059-- macOS has multiple APIs for metadata change for different metadata.
1061-- | Determine whether the event indicates inode metadata change for an object
1062-- contained within the monitored path.
1064-- Metadata change may include, permissions (e.g., chmod(2)), timestamps
1065-- (e.g., utimensat(2)), extended attributes (setxattr(2)), link count (since
1066-- Linux 2.6.25; e.g., for the target of link(2) and for unlink(2)), and
1067-- user/group ID (e.g., chown(2)).
1069-- /Can occur for watched path or a file inside it/
1073isMetadataChanged :: Event -> Bool
1074isMetadataChanged = getFlag iN_ATTRIB
1076-------------------------------------------------------------------------------
1078-------------------------------------------------------------------------------
1080-- | File was accessed (e.g. read, execve).
1082-- /Occurs only for a file inside the watched directory/
1086isAccessed :: Event -> Bool
1087isAccessed = getFlag iN_ACCESS
1089-- | File or directory was opened.
1091-- /Occurs only for a file inside the watched directory/
1095isOpened :: Event -> Bool
1096isOpened = getFlag iN_OPEN
1098-- | File opened for writing was closed.
1100-- /Occurs only for a file inside the watched directory/
1104isWriteClosed :: Event -> Bool
1105isWriteClosed = getFlag iN_CLOSE_WRITE
1107-- XXX what if it was opened for append? Does NOWRITE mean all cases where the
1108-- mode was not write? A dir open comes in this category?
1110-- | File or directory opened for read but not write was closed.
1112-- /Can occur for watched path or a file inside it/
1116isNonWriteClosed :: Event -> Bool
1117isNonWriteClosed = getFlag iN_CLOSE_NOWRITE
1119-------------------------------------------------------------------------------
1121-------------------------------------------------------------------------------
1123-- On macOS this is not generated on hard linking but on Linux it is.
1125-- | File/directory created in watched directory (e.g., open(2) O_CREAT,
1126-- mkdir(2), link(2), symlink(2), bind(2) on a UNIX domain socket).
1128-- /Occurs only for an object inside the watched directory/
1132isCreated :: Event -> Bool
1133isCreated = getFlag iN_CREATE
1135-- | File/directory deleted from watched directory.
1137-- /Occurs only for an object inside the watched directory/
1141isDeleted :: Event -> Bool
1142isDeleted = getFlag iN_DELETE
1144-- XXX what if an object is moved in from outside or moved out of the monitored
1147-- | Generated for the original path when an object is moved from under a
1148-- monitored directory.
1150-- /Occurs only for an object inside the watched directory/
1154isMovedFrom :: Event -> Bool
1155isMovedFrom = getFlag iN_MOVED_FROM
1157-- | Generated for the new path when an object is moved under a monitored
1160-- /Occurs only for an object inside the watched directory/
1164isMovedTo :: Event -> Bool
1165isMovedTo = getFlag iN_MOVED_TO
1167-- | Determine whether the event indicates modification of an object within the
1168-- monitored path. This event is generated only for files and not directories.
1170-- /Occurs only for an object inside the watched directory/
1174isModified :: Event -> Bool
1175isModified = getFlag iN_MODIFY
1177-------------------------------------------------------------------------------
1178-- Information about path type (applicable only when 'setFileEvents' is 'On')
1179-------------------------------------------------------------------------------
1182 "sys/inotify.h value IN_ISDIR" iN_ISDIR :: Word32
1184-- | Determine whether the event is for a directory path.
1188isDir :: Event -> Bool
1189isDir = getFlag iN_ISDIR
1191-------------------------------------------------------------------------------
1193-------------------------------------------------------------------------------
1195-- | Convert an 'Event' record to a String representation.
1196showEvent :: Event -> String
1197showEvent ev@Event{..} =
1198 "--------------------------"
1199 ++ "\nWd = " ++ show eventWd
1200 ++ "\nRoot = " ++ show (utf8ToString $ getRoot ev)
1201 ++ "\nRelative Path = " ++ show (utf8ToString $ getRelPath ev)
1202 ++ "\nAbsolute Path = " ++ show (utf8ToString $ getAbsPath ev)
1203 ++ "\nCookie = " ++ show (getCookie ev)
1204 ++ "\nFlags " ++ show eventFlags
1206 ++ showev isOverflow "Overflow"
1208 ++ showev isRootUnwatched "RootUnwatched"
1209 ++ showev isRootDeleted "RootDeleted"
1210 ++ showev isRootMoved "RootMoved"
1211 ++ showev isRootUnmounted "RootUnmounted"
1213 ++ showev isMetadataChanged "MetadataChanged"
1215 ++ showev isAccessed "Accessed"
1216 ++ showev isOpened "Opened"
1217 ++ showev isWriteClosed "WriteClosed"
1218 ++ showev isNonWriteClosed "NonWriteClosed"
1220 ++ showev isCreated "Created"
1221 ++ showev isDeleted "Deleted"
1222 ++ showev isModified "Modified"
1223 ++ showev isMovedFrom "MovedFrom"
1224 ++ showev isMovedTo "MovedTo"
1226 ++ showev isDir "Dir"
1229 where showev f str = if f ev then "\n" ++ str else ""
1231#warning "Disabling module Streamly.Internal.FileSystem.Event.Linux. Does not support kernels older than 2.6.36."
1232module Streamly.Internal.FileSystem.Event.Linux () where
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list