@@ -101,6 +101,8 @@ module Streamly.Internal.FileSystem.Event.Linux
101 -- ** Watch APIs
101 -- ** Watch APIs
102 , watchPathsWith
102 , watchPathsWith
103 , watchPaths
103 , watchPaths
104 , addToWatch
106 , addToWatch
105 , removeFromWatch
107 , removeFromWatch
@@ -140,15 +142,19 @@ module Streamly.Internal.FileSystem.Event.Linux
141 -- * Debugging
143 -- * Debugging
142 , showEvent
144 , showEvent
146import Control.Monad (void, when)
149import Control.Monad (void, when)
147−import Control.Monad.IO.Class (liftIO)
150+import Control.Monad.IO.Class (MonadIO, liftIO)
151+import Data.Bifunctor (bimap)
148import Data.Bits ((.|.), (.&.), complement)
152import Data.Bits ((.|.), (.&.), complement)
149import Data.Foldable (foldlM)
153import Data.Foldable (foldlM)
150import Data.Functor.Identity (runIdentity)
154import Data.Functor.Identity (runIdentity)
151import Data.IntMap.Lazy (IntMap)
155import Data.IntMap.Lazy (IntMap)
156+import Data.IORef (IORef, newIORef, readIORef, writeIORef)
157+import Data.Function ((&))
152import Data.List.NonEmpty (NonEmpty)
158import Data.List.NonEmpty (NonEmpty)
153import Data.Word (Word8, Word32)
159import Data.Word (Word8, Word32)
154import Foreign.C.Error (throwErrnoIfMinus1)
160import Foreign.C.Error (throwErrnoIfMinus1)
@@ -162,6 +168,7 @@ import GHC.IO.Handle.FD (mkHandleFromFD)
162import Streamly.Prelude (SerialT)
168import Streamly.Prelude (SerialT)
163import Streamly.Internal.Data.Parser (Parser)
169import Streamly.Internal.Data.Parser (Parser)
164import Streamly.Internal.Data.Array.Storable.Foreign.Types (Array(..))
170import Streamly.Internal.Data.Array.Storable.Foreign.Types (Array(..))
171+import System.FilePath ((</>))
165import System.IO (Handle, hClose, IOMode(ReadMode))
172import System.IO (Handle, hClose, IOMode(ReadMode))
166#if !MIN_VERSION_base(4,10,0)
173#if !MIN_VERSION_base(4,10,0)
167import Control.Concurrent.MVar (readMVar)
174import Control.Concurrent.MVar (readMVar)
@@ -175,12 +182,13 @@ import GHC.IO.Handle.FD (handleToFd)
176import qualified Data.IntMap.Lazy as Map
183import qualified Data.IntMap.Lazy as Map
177import qualified Data.List.NonEmpty as NonEmpty
184import qualified Data.List.NonEmpty as NonEmpty
185+import qualified Streamly.Internal.Data.Array.Storable.Foreign as A
178import qualified Streamly.Internal.Data.Fold as FL
186import qualified Streamly.Internal.Data.Fold as FL
179import qualified Streamly.Internal.Data.Parser as PR
187import qualified Streamly.Internal.Data.Parser as PR
180import qualified Streamly.Internal.Data.Stream.IsStream as S
188import qualified Streamly.Internal.Data.Stream.IsStream as S
181−import qualified Streamly.Internal.Unicode.Stream as U
189+import qualified Streamly.Internal.FileSystem.Dir as Dir
182import qualified Streamly.Internal.FileSystem.Handle as FH
190import qualified Streamly.Internal.FileSystem.Handle as FH
183−import qualified Streamly.Internal.Data.Array.Storable.Foreign as A
191+import qualified Streamly.Internal.Unicode.Stream as U
185-------------------------------------------------------------------------------
193-------------------------------------------------------------------------------
186-- Subscription to events
194-- Subscription to events
@@ -192,7 +200,9 @@ import qualified Streamly.Internal.Data.Array.Storable.Foreign as A
192-- /Internal/
200-- /Internal/
194data Config = Config
202data Config = Config
195− { createFlags :: Word32 }
204+ , createFlags :: Word32
197-------------------------------------------------------------------------------
207-------------------------------------------------------------------------------
198-- Boolean settings
208-- Boolean settings
@@ -217,9 +227,69 @@ setFlag mask status cfg@Config{..} =
217 in cfg {createFlags = flags}
227 in cfg {createFlags = flags}
219-------------------------------------------------------------------------------
229-------------------------------------------------------------------------------
220−-- Settings
230+-- Utilities
221-------------------------------------------------------------------------------
231-------------------------------------------------------------------------------
232+splitPath :: (MonadIO m) => FilePath -> FilePath -> m [FilePath]
233+splitPath pat xs = S.toList
234+ $ S.splitOnSeq (A.fromList pat) (FL.toList) (S.fromList xs)
236+pathDiff :: (MonadIO m) => FilePath -> FilePath -> m FilePath
238+ splits <- splitPath pat xs
239+ let j = case length splits of
242+ t = case length j of
247+dirListing :: String -> SerialT IO (Either String String)
250+ $ S.iterateMapLeftsWith S.ahead listDir (S.yield $ (Left s))
260+ Dir.toEither dir -- SerialT IO (Either String String)
261+ & S.map (bimap prefix prefix) -- SerialT IO (Either String String)
265+ prefix x = dir ++ "/" ++ x
267+toPathList :: String -> IO [String]
269+ S.toList $ S.map toStr $ dirListing root
278+toUtf8 :: MonadIO m => String -> m (Array Word8)
279+toUtf8 = A.fromStream . U.encodeUtf8 . S.fromList
281+-- | Set watch event on directory recursively.
287+setRecursiveMode :: Bool -> Config -> Config
288+setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec}
290+-------------------------------------------------------------------------------
292+-------------------------------------------------------------------------------
224foreign import capi
294foreign import capi
225 "sys/inotify.h value IN_DONT_FOLLOW" iN_DONT_FOLLOW :: Word32
295 "sys/inotify.h value IN_DONT_FOLLOW" iN_DONT_FOLLOW :: Word32
@@ -501,14 +571,14 @@ defaultConfig :: Config
501defaultConfig =
571defaultConfig =
502 setWhenExists AddIfExists
572 setWhenExists AddIfExists
503 $ setAllEvents On
573 $ setAllEvents On
504− $ Config { createFlags = 0 }
574+ $ Config {watchRec = False, createFlags = 0}
506-------------------------------------------------------------------------------
576-------------------------------------------------------------------------------
507-- Open an event stream
577-- Open an event stream
508-------------------------------------------------------------------------------
578-------------------------------------------------------------------------------
510-- | A handle for a watch.
580-- | A handle for a watch.
511−data Watch = Watch Handle (IntMap (Array Word8))
581+data Watch = Watch Handle (IORef (IntMap (Array Word8, Array Word8)))
513-- Instead of using the watch descriptor we can provide APIs that use the path
583-- Instead of using the watch descriptor we can provide APIs that use the path
514-- itself to identify the watch. That will require us to maintain a map from wd
584-- itself to identify the watch. That will require us to maintain a map from wd
@@ -545,7 +615,8 @@ createWatch = do
546 True -- use non-blocking IO
616 True -- use non-blocking IO
547 Nothing -- TextEncoding (binary)
617 Nothing -- TextEncoding (binary)
618+ emptyMapRef <- newIORef Map.empty
548− return $ Watch h Map.empty
619+ return $ Watch h emptyMapRef
550foreign import ccall unsafe
621foreign import ccall unsafe
551 "sys/inotify.h inotify_add_watch" c_inotify_add_watch
622 "sys/inotify.h inotify_add_watch" c_inotify_add_watch
@@ -580,13 +651,40 @@ handleToFd h = case h of
581-- /Internal/
652-- /Internal/
654+addToWatchExecutor ::
659+ -> IO (Config, Watch)
660+addToWatchExecutor cfg@Config{..} (Watch handle wdMap) pathArr root = do
661+ fd <- handleToFd handle
662+ wd <- A.asCString pathArr $ \pathPtr ->
663+ throwErrnoIfMinus1 ("addToWatch " ++ utf8ToString pathArr) $
664+ c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
665+ km <- readIORef wdMap
666+ let k = (Map.insert (fromIntegral wd) (root, pathArr) km)
668+ return $ (cfg, Watch handle wdMap)
583−addToWatch :: Config -> Watch -> Array Word8 -> IO Watch
670+addToWatch :: Config -> Watch -> Array Word8 -> IO (Config, Watch)
584−addToWatch Config{..} (Watch handle wdMap) path = do
671+addToWatch cfg@Config{..} (Watch handle wdMap) path = do
585 fd <- handleToFd handle
672 fd <- handleToFd handle
586 wd <- A.asCString path $ \pathPtr ->
673 wd <- A.asCString path $ \pathPtr ->
587 throwErrnoIfMinus1 ("addToWatch " ++ utf8ToString path) $
674 throwErrnoIfMinus1 ("addToWatch " ++ utf8ToString path) $
588 c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
675 c_inotify_add_watch (fdFD fd) pathPtr (CUInt createFlags)
676+ km <- readIORef wdMap
589− return $ Watch handle (Map.insert (fromIntegral wd) path wdMap)
677+ let k = (Map.insert (fromIntegral wd) (path, path) km)
679+ return $ (cfg, Watch handle wdMap)
681+addToWatchRec :: Config -> Watch -> FilePath -> FilePath -> IO (Config, Watch)
682+addToWatchRec cfg watch path origRoot= do
683+ subdirs <- toPathList path
684+ pathArrList <- mapM toUtf8 subdirs
685+ rootArr <- toUtf8 origRoot
686+ foldlM (\(cfg1, w1) pathArr -> addToWatchExecutor cfg1 w1 pathArr rootArr)
687+ (cfg, watch) pathArrList
591foreign import ccall unsafe
689foreign import ccall unsafe
592 "sys/inotify.h inotify_rm_watch" c_inotify_rm_watch
690 "sys/inotify.h inotify_rm_watch" c_inotify_rm_watch
@@ -600,13 +698,15 @@ foreign import ccall unsafe
600removeFromWatch :: Watch -> Array Word8 -> IO Watch
698removeFromWatch :: Watch -> Array Word8 -> IO Watch
601removeFromWatch (Watch handle wdMap) path = do
699removeFromWatch (Watch handle wdMap) path = do
602 fd <- handleToFd handle
700 fd <- handleToFd handle
701+ km <- readIORef wdMap
603− wdMap1 <- foldlM (step fd) Map.empty (Map.toList wdMap)
702+ wdMap1 <- foldlM (step fd) Map.empty (Map.toList km)
703+ writeIORef wdMap wdMap1
604− return $ Watch handle wdMap1
704+ return $ Watch handle wdMap
608 step fd newMap (wd, v) = do
708 step fd newMap (wd, v) = do
609− if v == path
709+ if (fst v) == path
611 let err = "removeFromWatch " ++ show (utf8ToString path)
711 let err = "removeFromWatch " ++ show (utf8ToString path)
612 rm = c_inotify_rm_watch (fdFD fd) (fromIntegral wd)
712 rm = c_inotify_rm_watch (fdFD fd) (fromIntegral wd)
@@ -620,17 +720,32 @@ removeFromWatch (Watch handle wdMap) path = do
621-- /Internal/
721-- /Internal/
623−openWatch :: Config -> NonEmpty (Array Word8) -> IO Watch
725+ -> NonEmpty (Array Word8)
726+ -> IO (Config, Watch)
624openWatch cfg paths = do
727openWatch cfg paths = do
728+ let pathList = NonEmpty.toList paths
730+ foldlM (\(cfg1, w1) path -> addToWatch cfg1 w1 path)
735+ -> NonEmpty (Array Word8)
736+ -> IO (Config, Watch)
737+openWatchRec cfg paths = do
738+ let pathList = map utf8ToString $ NonEmpty.toList paths
625 w <- createWatch
739 w <- createWatch
626− foldlM (\w1 pth -> addToWatch cfg w1 pth) w (NonEmpty.toList paths)
740+ foldlM (\(cfg1, w1) path -> addToWatchRec cfg1 w1 path path)
628-- | Close a 'Watch' handle.
743-- | Close a 'Watch' handle.
630-- /Internal/
745-- /Internal/
632−closeWatch :: Watch -> IO ()
747+closeWatch :: (Config, Watch) -> IO ()
633−closeWatch (Watch h _) = hClose h
748+closeWatch (_, Watch h _) = hClose h
635-------------------------------------------------------------------------------
750-------------------------------------------------------------------------------
636-- Raw events read from the watch file handle
751-- Raw events read from the watch file handle
@@ -648,7 +763,7 @@ data Event = Event
648 , eventFlags :: Word32
763 , eventFlags :: Word32
649 , eventCookie :: Word32
764 , eventCookie :: Word32
650 , eventRelPath :: Array Word8
765 , eventRelPath :: Array Word8
651− , eventMap :: IntMap (Array Word8)
766+ , eventMap :: IntMap (Array Word8, Array Word8)
652 } deriving (Show, Ord, Eq)
767 } deriving (Show, Ord, Eq)
654-- The inotify event struct from the man page/header file:
769-- The inotify event struct from the man page/header file:
@@ -665,14 +780,16 @@ data Event = Event
665-- XXX We can perhaps use parseD monad instance for fusing with parseMany? Need
780-- XXX We can perhaps use parseD monad instance for fusing with parseMany? Need
666-- to measure the perf.
781-- to measure the perf.
668−readOneEvent :: IntMap (Array Word8) -> Parser IO Word8 Event
783+readOneEvent :: Config -> Watch -> Parser IO Word8 Event
669−readOneEvent wdMap = do
784+readOneEvent cfg wt@(Watch _ wdMap) = do
670 let headerLen = (sizeOf (undefined :: CInt)) + 12
785 let headerLen = (sizeOf (undefined :: CInt)) + 12
671 arr <- PR.takeEQ headerLen (A.writeN headerLen)
786 arr <- PR.takeEQ headerLen (A.writeN headerLen)
672 (ewd, eflags, cookie, pathLen) <- PR.yieldM $ A.asPtr arr readHeader
787 (ewd, eflags, cookie, pathLen) <- PR.yieldM $ A.asPtr arr readHeader
673 -- XXX need the "initial" in parsers to return a step type so that "take 0"
788 -- XXX need the "initial" in parsers to return a step type so that "take 0"
674 -- can return without an input. otherwise if pathLen is 0 we will keep
789 -- can return without an input. otherwise if pathLen is 0 we will keep
675 -- waiting to read one more char before we return this event.
790 -- waiting to read one more char before we return this event.
791+ -- PR.yieldM $ print ("Flag1 = " ++ show eflags)
792+ -- PR.yieldM $ print ("pathLen = " ++ show pathLen)
677 if pathLen /= 0
794 if pathLen /= 0
@@ -681,15 +798,30 @@ readOneEvent wdMap = do
682 pth <- PR.sliceSepByMax (== 0) pathLen (A.writeN pathLen)
799 pth <- PR.sliceSepByMax (== 0) pathLen (A.writeN pathLen)
683 let remaining = pathLen - A.length pth - 1
800 let remaining = pathLen - A.length pth - 1
801+ -- PR.yieldM $ print ("remaining = " ++ show remaining)
684 when (remaining /= 0) $ PR.takeEQ remaining FL.drain
802 when (remaining /= 0) $ PR.takeEQ remaining FL.drain
803+ -- PR.yieldM $ print ("takeEQ = " ++ show remaining)
685 return pth
804 return pth
686 else return $ A.fromList []
805 else return $ A.fromList []
806+ -- PR.yieldM $ print ("Flag2 = " ++ show eflags)
807+ xm <- PR.yieldM $ readIORef wdMap
808+ let base = case Map.lookup (fromIntegral ewd) xm of
810+ Nothing -> (path, path)
811+ let xpath = utf8ToString (snd base) </> utf8ToString path
812+ _ <- if eflags .&. iN_CREATE /= 0 && eflags .&. iN_ISDIR /= 0
814+ processEvent cfg wt xpath (utf8ToString $ fst base)
815+ else return (cfg, wt)
816+ rm2 <- PR.yieldM $ readIORef wdMap
817+ pdiff <- PR.yieldM $ pathDiff (utf8ToString $ fst base) xpath
818+ relPath <- PR.yieldM $ toUtf8 (pdiff)
687 return $ Event
819 return $ Event
688 { eventWd = (fromIntegral ewd)
820 { eventWd = (fromIntegral ewd)
689 , eventFlags = eflags
821 , eventFlags = eflags
690 , eventCookie = cookie
822 , eventCookie = cookie
691− , eventRelPath = path
823+ , eventRelPath = relPath
692− , eventMap = wdMap
824+ , eventMap = rm2
@@ -702,8 +834,21 @@ readOneEvent wdMap = do
702 pathLen :: Word32 <- peekByteOff ptr (len + 8)
834 pathLen :: Word32 <- peekByteOff ptr (len + 8)
703 return (ewd, eflags, cookie, fromIntegral pathLen)
835 return (ewd, eflags, cookie, fromIntegral pathLen)
842+ -> Parser IO Word8 (Config, Watch)
843+ processEvent cfg1@Config{..} wt1 xpath root = do
846+ PR.yieldM $ addToWatchRec cfg1 wt1 xpath root
848+ PR.yieldM $ return (cfg1, wt1)
705−watchToStream :: Watch -> SerialT IO Event
850+watchToStream :: (Config, Watch) -> SerialT IO Event
706−watchToStream (Watch handle wdMap) =
851+watchToStream (cfg, wt@(Watch handle _)) = do
707 -- Do not use too small a buffer. As per inotify man page:
852 -- Do not use too small a buffer. As per inotify man page:
709 -- The behavior when the buffer given to read(2) is too small to return
854 -- The behavior when the buffer given to read(2) is too small to return
@@ -714,7 +859,7 @@ watchToStream (Watch handle wdMap) =
714 -- sizeof(struct inotify_event) + NAME_MAX + 1
859 -- sizeof(struct inotify_event) + NAME_MAX + 1
716 -- will be sufficient to read at least one event.
861 -- will be sufficient to read at least one event.
717− S.parseMany (readOneEvent wdMap) $ S.unfold FH.read handle
862+ S.parseMany (readOneEvent cfg wt) $ S.unfold FH.read handle
719-- | Start monitoring a list of file system paths for file system events with
864-- | Start monitoring a list of file system paths for file system events with
720-- the supplied configuration operation over the 'defaultConfig'. The
865-- the supplied configuration operation over the 'defaultConfig'. The
@@ -752,6 +897,33 @@ watchPathsWith f paths = S.bracket before after watchToStream
752watchPaths :: NonEmpty (Array Word8) -> SerialT IO Event
897watchPaths :: NonEmpty (Array Word8) -> SerialT IO Event
753watchPaths = watchPathsWith id
898watchPaths = watchPathsWith id
900+-- | Start monitoring a list of file system paths for file system events with
901+-- the supplied configuration operation over the 'defaultConfig'. The
902+-- paths could be files or directories. When the path is a directory, the
903+-- whole directory tree under it is watched recursively. Monitoring starts from
904+-- the current time onwards.
909+ (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
910+watchTreesWith f paths = S.bracket before after watchToStream
915+ $ openWatchRec (f $ setRecursiveMode True defaultConfig) paths
916+ after = liftIO . closeWatch
918+-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
921+-- watchTrees = watchTreesWith id
924+watchTrees :: NonEmpty (Array Word8) -> SerialT IO Event
925+watchTrees = watchTreesWith id
755-------------------------------------------------------------------------------
927-------------------------------------------------------------------------------
756-- Examine event stream
928-- Examine event stream
757-------------------------------------------------------------------------------
929-------------------------------------------------------------------------------
@@ -769,13 +941,12 @@ getRoot Event{..} =
769 if (eventWd >= 1)
941 if (eventWd >= 1)
771 case Map.lookup (fromIntegral eventWd) eventMap of
943 case Map.lookup (fromIntegral eventWd) eventMap of
772− Just path -> path
944+ Just path -> fst path
773 Nothing ->
945 Nothing ->
774 error $ "Bug: getRoot: No path found corresponding to the "
946 error $ "Bug: getRoot: No path found corresponding to the "
775 ++ "watch descriptor " ++ show eventWd
947 ++ "watch descriptor " ++ show eventWd
776 else A.fromList []
948 else A.fromList []
779-- XXX should we use a Maybe here?
950-- XXX should we use a Maybe here?
780-- | Get the file system object path for which the event is generated, relative
951-- | Get the file system object path for which the event is generated, relative
781-- to the watched root. The path is a "/" separated array of bytes.
952-- to the watched root. The path is a "/" separated array of bytes.
@@ -1017,6 +1188,12 @@ isDir = getFlag iN_ISDIR
1017-------------------------------------------------------------------------------
1188-------------------------------------------------------------------------------
1018-- Debugging
1189-- Debugging
1019-------------------------------------------------------------------------------
1190-------------------------------------------------------------------------------
1191+showEventShort :: Event -> String
1192+showEventShort ev@Event{..} = (utf8ToString $ getRelPath ev)
1193+ ++ "_" ++ show eventFlags
1194+ ++ showev isDir "Dir"
1196+ where showev f str = if f ev then "_" ++ str else ""
1021-- | Convert an 'Event' record to a String representation.
1198-- | Convert an 'Event' record to a String representation.
1022showEvent :: Event -> String
1199showEvent :: Event -> String
@@ -1055,4 +1232,4 @@ showEvent ev@Event{..} =
1056#warning "Disabling module Streamly.Internal.FileSystem.Event.Linux. Does not support kernels older than 2.6.36."
1233#warning "Disabling module Streamly.Internal.FileSystem.Event.Linux. Does not support kernels older than 2.6.36."
1057module Streamly.Internal.FileSystem.Event.Linux () where
1234module Streamly.Internal.FileSystem.Event.Linux () where