on master help sign in
Implement FS event handling for MacOS
commitc10061b390d822e29e257e1bc7ed6569d217cdae authorHarendra Kumar <harendra@composewell.com> dateFri 7 Aug 2020, 16:35 (6 years ago) parentadc550b
5 files changed+1216
credits/COPYRIGHTS.md+5 src/Streamly/Internal/FileSystem/Event/Darwin.c+194 src/Streamly/Internal/FileSystem/Event/Darwin.h+18 src/Streamly/Internal/FileSystem/Event/Darwin.hs+991 streamly.cabal+8
credits/COPYRIGHTS.md+5
@@ -3,6 +3,11 @@ currently shipped with the package. To find parts of the code where the
33original or modified code has been included please search for the copyright
44notices in the individual files.
55
6+## Unreleased
7+
8+* macOS FS Event handling C code adapted from hfsevents package:
9+ * Copyright (c) 2012, Luite Stegeman
10+
611## 0.7.1
712
813* For compatibility with older GHC versions portions of PrimArray and
src/Streamly/Internal/FileSystem/Event/Darwin.cnew file+194
@@ -0,0 +1,194 @@
1+/*
2+ * Code adapted from the Haskell "hfsevents" package.
3+ *
4+ * Copyright (c) 2012, Luite Stegeman
5+ *
6+ */
7+
8+#include <CoreServices/CoreServices.h>
9+#include <pthread.h>
10+#include <unistd.h>
11+#include <FileSystem/Event/Darwin.h>
12+
13+/*
14+ * For reference documentaion see:
15+ * https://developer.apple.com/documentation/coreservices/file_system_events?language=objc
16+ * https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html
17+ *
18+ * An OS thread is started which runs the event loop. A pipe is created
19+ * and the events are sent to the pipes. The receiver can read the pipe
20+ * output end to get the events.
21+ */
22+
23+/******************************************************************************
24+ * Event watch
25+ *****************************************************************************/
26+
27+/* Write an event to the pipe input fd */
28+static void writeEvent(int fd, UInt64 eventId, UInt64 eventFlags, char* path)
29+{
30+ UInt64 buf[3];
31+ buf[0] = eventId;
32+ buf[1] = eventFlags;
33+ /* XXX Is the path string in UTF-8? */
34+ buf[2] = (UInt64)strlen(path);
35+ write(fd, buf, 3 * sizeof(UInt64));
36+ write(fd, path, strlen(path));
37+}
38+
39+/* thread state */
40+struct watch
41+{
42+ FSEventStreamRef eventStream;
43+ CFRunLoopRef runLoop;
44+ int writefd;
45+ pthread_mutex_t mut;
46+};
47+
48+/* Just writes the event to the pipe input fd */
49+static void watchCallback
50+ ( ConstFSEventStreamRef streamRef
51+ , void *clientCallBackInfo
52+ , size_t n
53+ , void *eventPaths
54+ , const FSEventStreamEventFlags eventFlags[]
55+ , const FSEventStreamEventId eventIds[]
56+ )
57+{
58+ int i;
59+ struct watch *w = clientCallBackInfo;
60+ char **paths = eventPaths;
61+
62+ for (i = 0; i < n; i++) {
63+ writeEvent(w->writefd, eventIds[i], eventFlags[i], paths[i]);
64+ }
65+}
66+
67+/******************************************************************************
68+ * Start a watch event loop
69+ *****************************************************************************/
70+
71+/* Event loop run in a pthread */
72+static void *watchRunLoop(void *vw)
73+{
74+ struct watch* w = (struct watch*) vw;
75+ CFRunLoopRef rl = CFRunLoopGetCurrent();
76+ CFRetain(rl);
77+ w->runLoop = rl;
78+ FSEventStreamScheduleWithRunLoop(w->eventStream, rl, kCFRunLoopDefaultMode);
79+ FSEventStreamStart(w->eventStream);
80+ pthread_mutex_unlock(&w->mut);
81+ CFRunLoopRun();
82+ pthread_exit(NULL);
83+}
84+
85+#define MAX_WATCH_PATHS 4096
86+
87+int createWatch
88+ ( struct pathName* folders
89+ , int n /* number of entries in folders */
90+ , UInt32 createFlags
91+ , UInt64 since
92+ , double latency
93+ , int* fd
94+ , void** wp
95+ )
96+{
97+ if (n > MAX_WATCH_PATHS) {
98+ return -1;
99+ }
100+
101+ int pfds[2];
102+ if (pipe (pfds)) {
103+ return -1;
104+ }
105+
106+ /*
107+ * XXX We can possibly use since == 0 to get all events since
108+ * beginning of time
109+ */
110+ if (!since) {
111+ since = kFSEventStreamEventIdSinceNow;
112+ }
113+
114+ /* Setup paths array */
115+ CFStringRef *cffolders = malloc(n * sizeof(CFStringRef));
116+ int i;
117+ for(i = 0; i < n; i++) {
118+ cffolders[i] = CFStringCreateWithBytes
119+ ( NULL
120+ , folders[i].pathBytes
121+ , folders[i].pathLen
122+ , kCFStringEncodingUTF8
123+ , false
124+ );
125+ }
126+ CFArrayRef paths = CFArrayCreate(NULL, (const void **)cffolders, n, NULL);
127+
128+ /* Setup context */
129+ struct watch *w = malloc(sizeof(struct watch));
130+ FSEventStreamContext ctx;
131+ ctx.version = 0;
132+ ctx.info = (void*)w;
133+ ctx.retain = NULL;
134+ ctx.release = NULL;
135+ ctx.copyDescription = NULL;
136+
137+ /* Create watch using paths and context*/
138+ FSEventStreamRef es = FSEventStreamCreate
139+ (NULL, &watchCallback, &ctx, paths, since, latency, createFlags);
140+
141+ /* Run the event loop in a pthread */
142+ int retval;
143+ if(es != NULL) {
144+ /* Success */
145+ w->writefd = pfds[1];
146+ w->eventStream = es;
147+ w->runLoop = NULL;
148+
149+ /* Lock to prevent race against watch destroy */
150+ pthread_mutex_init(&w->mut, NULL);
151+ pthread_mutex_lock(&w->mut);
152+ pthread_t t;
153+ pthread_create(&t, NULL, &watchRunLoop, (void*)w);
154+
155+ /* return the out fd and the watch struct */
156+ *fd = pfds[0];
157+ *wp = w;
158+ retval = 0;
159+ } else {
160+ /* Failure */
161+ close(pfds[0]);
162+ close(pfds[1]);
163+ free(w);
164+ retval = -1;
165+ }
166+
167+ /* Cleanup */
168+ for (i = 0; i < n; i++) {
169+ CFRelease (cffolders[i]);
170+ }
171+ free(cffolders);
172+ CFRelease(paths);
173+ return retval;
174+}
175+
176+/******************************************************************************
177+ * Stop a watch event loop
178+ *****************************************************************************/
179+
180+void destroyWatch(struct watch* w) {
181+ /* Stop the loop so the thread will exit */
182+ pthread_mutex_lock(&w->mut);
183+ FSEventStreamStop(w->eventStream);
184+ FSEventStreamInvalidate(w->eventStream);
185+ CFRunLoopStop(w->runLoop);
186+ CFRelease(w->runLoop);
187+ FSEventStreamRelease(w->eventStream);
188+ close(w->writefd);
189+ pthread_mutex_unlock(&w->mut);
190+
191+ /* Cleanup */
192+ pthread_mutex_destroy(&w->mut);
193+ free(w);
194+}
src/Streamly/Internal/FileSystem/Event/Darwin.hnew file+18
@@ -0,0 +1,18 @@
1+struct watch;
2+
3+struct pathName {
4+ const UInt8 *pathBytes;
5+ size_t pathLen;
6+};
7+
8+int createWatch
9+ ( struct pathName* folders
10+ , int n /* number of entries in "folders" */
11+ , UInt32 createFlags
12+ , UInt64 since
13+ , double latency
14+ , int* fd
15+ , void** wp
16+ );
17+
18+void destroyWatch(struct watch* w);
src/Streamly/Internal/FileSystem/Event/Darwin.hsnew file+991
@@ -0,0 +1,991 @@
1+-- |
2+-- Module : Streamly.Internal.FileSystem.Event.Darwin
3+-- Copyright : (c) 2020 Composewell Technologies
4+-- License : BSD-3-Clause
5+-- Maintainer : streamly@composewell.com
6+-- Stability : experimental
7+-- Portability : GHC
8+--
9+-- =Overview
10+--
11+-- Use 'watchTrees' 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.
14+--
15+-- @
16+-- {-\# LANGUAGE MagicHash #-}
17+-- Stream.mapM_ (putStrLn . 'showEvent') $ 'watchTrees' [Array.fromString\# "path"#]
18+-- @
19+--
20+-- 'Event' is an opaque type. Accessor functions (e.g. 'showEvent above)
21+-- provided in this module are used to determine the attributes of the event.
22+--
23+--
24+-- =Design notes
25+--
26+-- For reference documentation see:
27+--
28+-- *
29+-- "<https://developer.apple.com/documentation/coreservices/file_system_events?language=objc Apple FS Events Reference>"
30+-- * "<https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/UsingtheFSEventsFramework/UsingtheFSEventsFramework.html Apple FS Events Programming Guide>"
31+--
32+-- We try to keep the macOS\/Linux/Windows event handling APIs and defaults
33+-- semantically and syntactically as close as possible.
34+--
35+-- =TODO
36+--
37+-- APIs for persistent event-id related functionality, to get events from
38+-- specific event-id etc are not implemented.
39+
40+-- Need a real cpp to process Availability.h
41+{-# OPTIONS_GHC -pgmP cpp -optP -Wno-unused-command-line-argument #-}
42+
43+#include <Availability.h>
44+
45+#if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) \
46+ && __MAC_OS_X_VERSION_MIN_REQUIRED >= 10700
47+
48+module Streamly.Internal.FileSystem.Event.Darwin
49+ (
50+ -- * Creating a Watch
51+
52+ -- ** Default configuration
53+ Config
54+ , Toggle (..)
55+ , defaultConfig
56+
57+ -- ** Watch Behavior
58+ , BatchInfo (..)
59+ , setEventBatching
60+
61+ -- ** Events of Interest
62+ , setRootChanged
63+ , setFileEvents
64+ , setIgnoreSelf
65+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
66+ , setFullHistory
67+#endif
68+
69+ -- ** Watch APIs
70+ , watchTrees
71+ , watchTreesWith
72+
73+ -- * Handling Events
74+ , Event
75+ , getEventId
76+ , getAbsPath
77+
78+ -- ** Exception Conditions
79+ , isEventIdWrapped
80+ , isOverflow
81+ , isOverflowKernel
82+ , isOverflowUser
83+
84+ -- ** Root Level Events
85+ -- | Events that belong to the root path as a whole and not to specific
86+ -- itmes contained in it.
87+ , isMount
88+ , isUnmount
89+ , isHistoryDone
90+ , isRootChanged
91+
92+ -- ** Item Level Metadata change
93+ , isOwnerGroupModeChanged
94+ , isInodeAttrsChanged
95+ , isFinderInfoChanged
96+ , isXAttrsChanged
97+
98+ -- ** Item Level CRUD events
99+ -- | Note: Sometimes the "Deleted", "Renamed", "Modified" events have
100+ -- "Created" flag also present, this may perhaps be due to temporal
101+ -- coalescing of events. Though it is not clear why different types of
102+ -- events should be coalesced together. Therefore, it may be a good idea to
103+ -- check the presence of other flags before "Created" and to give
104+ -- precedence to those flags in such case. For example, on macOS 10.15.1,
105+ -- try "touch x; rm x" or "rm x; touch x" or "touch x; mv x y". Sometimes
106+ -- even just an "rm x" produces both flags.
107+ --
108+ -- When @rm -rf@ is used on the watched directory, only one 'isDeleted'
109+ -- event occurs and that is for the watch root. However, @rm -rf@ on a
110+ -- directory inside the watch root produces 'isDeleted' events for all
111+ -- files.
112+
113+ , isCreated
114+ , isDeleted
115+ , isMoved
116+ , isModified
117+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101300
118+ , isCloned
119+#endif
120+
121+ -- ** Item Path info
122+ , isDir
123+ , isFile
124+ , isSymLink
125+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
126+ , isHardLink
127+ , isLastHardLink
128+#endif
129+
130+ -- * Debugging
131+ , showEvent
132+ )
133+where
134+
135+import Control.Concurrent (MVar, newMVar, takeMVar, putMVar)
136+import Control.Monad (when)
137+import Control.Monad.IO.Class (liftIO)
138+import Data.Bits ((.|.), (.&.), complement)
139+import Data.Functor.Identity (runIdentity)
140+import Data.Word (Word8, Word32, Word64)
141+import Foreign.C.Types (CInt(..), CDouble(..), CSize(..), CUChar(..))
142+import Foreign.Marshal.Alloc (alloca)
143+import Foreign.Marshal.Array (withArray)
144+import Foreign.Ptr (Ptr, castPtr)
145+import Foreign.Storable (Storable(..))
146+import GHC.IO.Handle.FD (fdToHandle)
147+import Streamly (SerialT)
148+import Streamly.Internal.Data.Cont (contListMap)
149+import Streamly.Internal.Data.Parser (Parser)
150+import Streamly.Internal.Memory.Array.Types (Array(..))
151+import System.IO (Handle, hClose)
152+
153+import qualified Streamly.Internal.Data.Parser as PR
154+import qualified Streamly.Internal.Data.Stream.IsStream as S
155+import qualified Streamly.Internal.Data.Unicode.Stream as U
156+import qualified Streamly.Internal.FileSystem.Handle as FH
157+import qualified Streamly.Internal.Memory.Array as A
158+
159+-------------------------------------------------------------------------------
160+-- Subscription to events
161+-------------------------------------------------------------------------------
162+
163+-- | Watch configuration, used to specify the events of interest and the
164+-- behavior of the watch.
165+--
166+-- /Internal/
167+--
168+data Config = Config
169+ { latency :: Double
170+ , createFlags :: Word32
171+ }
172+
173+-------------------------------------------------------------------------------
174+-- Batching Events
175+-------------------------------------------------------------------------------
176+
177+foreign import capi
178+ "CoreServices/CoreServices.h value kFSEventStreamCreateFlagNoDefer"
179+ kFSEventStreamCreateFlagNoDefer :: Word32
180+
181+data BatchInfo =
182+ Throttle Double -- ^ Deliver an event immediately but suppress the
183+ -- following events upto the specified time.
184+ | Batch Double -- ^ Collapse all events that occurred in the specified
185+ -- time window (in seconds) and deliver them as a single
186+ -- batch.
187+
188+-- | Set how the events should be batched. See 'BatchInfo' for details. A
189+-- negative value for time is treated as 0.
190+--
191+-- /default: Batch 0.1/
192+--
193+-- /macOS 10.5+/
194+--
195+-- /Internal/
196+--
197+setEventBatching :: BatchInfo -> Config -> Config
198+setEventBatching batchInfo cfg@Config{..} =
199+ let (t, status) =
200+ case batchInfo of
201+ Throttle sec | sec < 0 -> (0, On)
202+ Throttle sec -> (sec, On)
203+ Batch sec | sec < 0 -> (0, Off)
204+ Batch sec -> (sec, Off)
205+ in setFlag kFSEventStreamCreateFlagNoDefer status $ cfg {latency = t}
206+
207+-------------------------------------------------------------------------------
208+-- Boolean settings
209+-------------------------------------------------------------------------------
210+
211+-- | Whether a setting is 'On' or 'Off'.
212+--
213+-- /Internal/
214+--
215+data Toggle = On | Off
216+
217+setFlag :: Word32 -> Toggle -> Config -> Config
218+setFlag mask status cfg@Config{..} =
219+ let flags =
220+ case status of
221+ On -> createFlags .|. mask
222+ Off -> createFlags .&. complement mask
223+ in cfg {createFlags = flags}
224+
225+foreign import capi
226+ "CoreServices/CoreServices.h value kFSEventStreamCreateFlagWatchRoot"
227+ kFSEventStreamCreateFlagWatchRoot :: Word32
228+
229+-- | Watch the changes to the path of the top level file system objects being
230+-- watched.
231+--
232+-- /default: Off/
233+--
234+-- /macOS 10.5+/
235+--
236+-- /Internal/
237+--
238+setRootChanged :: Toggle -> Config -> Config
239+setRootChanged = setFlag kFSEventStreamCreateFlagWatchRoot
240+
241+foreign import capi
242+ "CoreServices/CoreServices.h value kFSEventStreamCreateFlagFileEvents"
243+ kFSEventStreamCreateFlagFileEvents :: Word32
244+
245+-- | When this is 'Off' only events for the watched directories are reported.
246+-- For example, when a file is created inside a directory it is reported as an
247+-- event, the path name in the event is the path of the directory being
248+-- watched. Events cannot be distinguished based on the type of event.
249+--
250+-- When it is 'On' the path reported in the event is the path of the item and
251+-- not the of the directory in which it is contained. We can use the event
252+-- accessor functions to determine the event type and the path type etc.
253+--
254+-- /default: On/
255+--
256+-- /macOS 10.7+/
257+--
258+-- /Internal/
259+--
260+setFileEvents :: Toggle -> Config -> Config
261+setFileEvents = setFlag kFSEventStreamCreateFlagFileEvents
262+
263+foreign import capi
264+ "CoreServices/CoreServices.h value kFSEventStreamCreateFlagIgnoreSelf"
265+ kFSEventStreamCreateFlagIgnoreSelf :: Word32
266+
267+-- | When this is 'On' events generated by the current process are not
268+-- reported.
269+--
270+-- /default: Off/
271+--
272+-- /macOS 10.6+/
273+--
274+-- /Internal/
275+--
276+setIgnoreSelf :: Toggle -> Config -> Config
277+setIgnoreSelf = setFlag kFSEventStreamCreateFlagIgnoreSelf
278+
279+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
280+foreign import capi
281+ "CoreServices/CoreServices.h value kFSEventStreamCreateFlagFullHistory"
282+ kFSEventStreamCreateFlagFullHistory :: Word32
283+
284+-- | When this is 'On' all events since the beginning of time are reported.
285+--
286+-- /default: Off/
287+--
288+-- /macOS 10.15+/
289+--
290+-- /Internal/
291+--
292+setFullHistory :: Toggle -> Config -> Config
293+setFullHistory = setFlag kFSEventStreamCreateFlagFullHistory
294+#endif
295+
296+-------------------------------------------------------------------------------
297+-- Default config
298+-------------------------------------------------------------------------------
299+
300+-- | The default settings are:
301+--
302+-- * 'setEventBatching' ('Batch' 0.1)
303+-- * 'setFileEvents' 'On'
304+-- * 'setRootChanged' 'Off'
305+-- * 'setIgnoreSelf' 'Off'
306+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500
307+-- * 'setFullHistory' 'Off'
308+#endif
309+--
310+-- /Internal/
311+--
312+defaultConfig :: Config
313+defaultConfig = setFileEvents On $ Config
314+ { latency = 0.1
315+ , createFlags = 0
316+ }
317+
318+-------------------------------------------------------------------------------
319+-- Open an event stream (C FFI)
320+-------------------------------------------------------------------------------
321+
322+-- See FileSystem/Event/Darwin.h
323+
324+data CWatch
325+
326+-- Representation of "struct pathName" in "FileSystem/Event/Darwin.h"
327+data PathName = PathName
328+ { pathBytes :: Ptr CUChar
329+ , pathLen :: CSize
330+ }
331+
332+instance Storable PathName where
333+ alignment _ = sizeOf (undefined :: Ptr a)
334+ sizeOf _ = sizeOf (undefined :: Ptr CUChar) + sizeOf (undefined :: CSize)
335+ peek ptr = do
336+ bptr <- peekByteOff ptr 0
337+ len <- peekByteOff ptr (sizeOf (undefined :: Ptr CUChar))
338+ return $ PathName bptr len
339+ poke ptr PathName{..} = do
340+ pokeByteOff ptr 0 pathBytes
341+ pokeByteOff ptr (sizeOf (undefined :: Ptr CUChar)) pathLen
342+
343+foreign import ccall safe "FileSystem/Event/Darwin.h createWatch" createWatch
344+ :: Ptr PathName
345+ -> CInt
346+ -> Word32
347+ -> Word64
348+ -> CDouble
349+ -> Ptr (CInt)
350+ -> Ptr (Ptr CWatch)
351+ -> IO CInt
352+
353+foreign import ccall safe "FileSystem/Event/Darwin.h destroyWatch" destroyWatch
354+ :: Ptr CWatch -> IO ()
355+
356+-------------------------------------------------------------------------------
357+-- Open an event stream
358+-------------------------------------------------------------------------------
359+
360+-- | A handle for a watch.
361+data Watch = Watch Handle (Ptr CWatch) (MVar Bool)
362+
363+-- | Given a 'Config' and @paths@ start monitoring the paths for file system
364+-- events. Returns a 'Watch' handle which can then be used to read the event
365+-- stream or to close the watch.
366+--
367+-- Implementation Note: This API creates an OS level thread where an event loop
368+-- runs and writes the events to the write side fd of a POSIX pipe. Event
369+-- stream APIs read the events from the read end of this pipe. Since an OS
370+-- level thread is forked, creating a new watch stream is expensive. It is
371+-- advised to minimize the number of watches.
372+--
373+-- /Internal/
374+--
375+openWatch :: Config -> [Array Word8] -> IO Watch
376+openWatch Config{..} paths = do
377+ withPathNames paths $ \arraysPtr ->
378+ alloca $ \fdPtr -> do
379+ alloca $ \watchPPtr -> do
380+ let nArrays = fromIntegral (length paths)
381+ seconds = (realToFrac latency)
382+ r <- createWatch
383+ arraysPtr nArrays createFlags 0 seconds fdPtr watchPPtr
384+ when (r /= 0) $
385+ ioError (userError "openWatch: failed to create watch.")
386+ fd <- peek fdPtr
387+ h <- fdToHandle fd
388+ watchPtr <- peek watchPPtr
389+ closeLock <- newMVar False
390+ return $ Watch h watchPtr closeLock
391+
392+ where
393+
394+ withPathName :: Array Word8 -> (PathName -> IO a) -> IO a
395+ withPathName arr act = do
396+ A.withPtr arr $ \ptr ->
397+ let pname = PathName (castPtr ptr) (fromIntegral (A.length arr))
398+ in act pname
399+
400+ withPathNames = contListMap withPathName withArray
401+
402+-- | Close a 'Watch' handle.
403+--
404+-- /Internal/
405+--
406+closeWatch :: Watch -> IO ()
407+closeWatch (Watch h watchPtr mvar) = do
408+ hClose h
409+ closed <- takeMVar mvar
410+ when (not closed) $ destroyWatch watchPtr
411+ putMVar mvar True
412+
413+-------------------------------------------------------------------------------
414+-- Raw events read from the watch file handle
415+-------------------------------------------------------------------------------
416+
417+-- | An Event generated by the file system. Use the accessor functions to
418+-- examine the event.
419+--
420+-- /Internal/
421+--
422+data Event = Event
423+ { eventId :: Word64
424+ , eventFlags :: Word64
425+ , eventAbsPath :: Array Word8
426+ } deriving (Show, Ord, Eq)
427+
428+-- XXX We can perhaps use parseD monad instance for fusing with parseMany? Need
429+-- to measure the perf.
430+--
431+-- XXX should we use a magic in the header to avoid any issues due to
432+-- misalignment of records? Can that ever happen?
433+--
434+readOneEvent :: Parser IO Word8 Event
435+readOneEvent = do
436+ arr <- PR.takeEQ 24 (A.writeN 24)
437+ let arr1 = A.unsafeCast arr :: Array Word64
438+ eid = A.unsafeIndex arr1 0
439+ eflags = A.unsafeIndex arr1 1
440+ pathLen = fromIntegral $ A.unsafeIndex arr1 2
441+ path <- PR.takeEQ pathLen (A.writeN pathLen)
442+ return $ Event
443+ { eventId = eid
444+ , eventFlags = eflags
445+ , eventAbsPath = path
446+ }
447+
448+watchToStream :: Watch -> SerialT IO Event
449+watchToStream (Watch handle _ _) =
450+ S.parseMany readOneEvent $ S.unfold FH.read handle
451+
452+-- | Start monitoring a list of file system paths for file system events with
453+-- the supplied configuration operation over the 'defaultConfig'. The
454+-- paths could be files or directories. When the path is a directory, the
455+-- whole directory tree under it is watched recursively. Monitoring starts from
456+-- the current time onwards. The paths are specified as UTF-8 encoded 'Array'
457+-- of 'Word8'.
458+--
459+-- If the path name to be watched is a symbolic link then the target of the
460+-- link is watched instead of the symbolic link itself. If the symbolic link is
461+-- removed later the target is still watched as usual.
462+--
463+-- When 'setFileEvents' is 'Off' then events occurring inside the watch root
464+-- cannot be distinguished from each other. For example, we cannot determine
465+-- if an event is a "create" or "delete", we only know that some event ocurred
466+-- under the watched directory hierarchy. Also, no path information for the
467+-- target item of the event is generated.
468+--
469+-- If the watched path is deleted and created again the events start coming
470+-- again for that path. A file type path being watched could be deleted and
471+-- recreated as a directory. If the watched path is moved to another path (or
472+-- is deleted and recreated as a symbolic link to another path) then no events
473+-- are reported for any changes under the new path.
474+--
475+-- If a watch is started on a non-existing path then the path is not watched
476+-- even if it is created later. The macOS API does not fail for a non-existing
477+-- path. If a non-existing path is watched with 'setRootChanged' then an
478+-- 'isRootChanged' event is reported if the path is created later and the
479+-- "path" field in the event is set to the dirname of the path rather than the
480+-- full absolute path. This is the observed behavior on macOS 10.15.1.
481+--
482+-- @
483+-- {-\# LANGUAGE MagicHash #-}
484+-- watchTreesWith ('setIgnoreSelf' 'On' . 'setRootChanged' 'On') [Array.fromString\# "path"#]
485+-- @
486+--
487+-- /Internal/
488+--
489+watchTreesWith :: (Config -> Config) -> [Array Word8] -> SerialT IO Event
490+watchTreesWith f paths = S.bracket before after watchToStream
491+
492+ where
493+
494+ before = liftIO $ openWatch (f defaultConfig) paths
495+ after = (liftIO . closeWatch)
496+
497+-- | Like 'watchTreesWith' but uses the 'defaultConfig' options.
498+--
499+-- @
500+-- watchTrees = watchTreesWith id
501+-- @
502+--
503+watchTrees :: [Array Word8] -> SerialT IO Event
504+watchTrees = watchTreesWith id
505+
506+-------------------------------------------------------------------------------
507+-- Examine the event stream
508+-------------------------------------------------------------------------------
509+
510+-- | Get the event id of an 'Event'. Event-id is a monotonically increasing
511+-- 64-bit integer identifying an event uniquely.
512+--
513+-- /Internal/
514+--
515+getEventId :: Event -> Word64
516+getEventId Event{..} = eventId
517+
518+foreign import capi
519+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagEventIdsWrapped"
520+ kFSEventStreamEventFlagEventIdsWrapped :: Word64
521+
522+-- | Determine whether the event id has wrapped. This is impossible on any
523+-- traditional hardware in any reasonable amount of time unless the event-id
524+-- starts from a very high value, because the event-id is a 64-bit integer.
525+--
526+-- /macOS 10.5+/
527+--
528+-- /Internal/
529+--
530+isEventIdWrapped :: Event -> Bool
531+isEventIdWrapped = getFlag kFSEventStreamEventFlagEventIdsWrapped
532+
533+-- | Get the absolute path of the file system object for which the event is
534+-- generated. The path is a UTF-8 encoded array of bytes.
535+--
536+-- /Internal/
537+--
538+getAbsPath :: Event -> Array Word8
539+getAbsPath Event{..} = eventAbsPath
540+
541+-------------------------------------------------------------------------------
542+-- Event types
543+-------------------------------------------------------------------------------
544+
545+getFlag :: Word64 -> Event -> Bool
546+getFlag mask Event{..} = eventFlags .&. mask /= 0
547+
548+-------------------------------------------------------------------------------
549+-- Error events
550+-------------------------------------------------------------------------------
551+
552+foreign import capi
553+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagMustScanSubDirs"
554+ kFSEventStreamEventFlagMustScanSubDirs :: Word64
555+
556+-- | Events may be dropped if the kernel or the user process is not able to
557+-- cope up with the speed, in that case multiple events from child nodes may be
558+-- coalesced into an event for the parent node thus losing some information
559+-- about where the event came from. The user application must then scan
560+-- everything under the parent node if it wants to know the current state.
561+--
562+-- The event path in such case points to the highest common path under which
563+-- the events were coaleseced together.
564+--
565+-- /macOS 10.5+/
566+--
567+-- /Internal/
568+--
569+isOverflow :: Event -> Bool
570+isOverflow = getFlag kFSEventStreamEventFlagMustScanSubDirs
571+
572+foreign import capi
573+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagKernelDropped"
574+ kFSEventStreamEventFlagKernelDropped :: Word64
575+
576+-- | Did an overflow occur due to a kernel processing issue? Set only if an
577+-- overflow occurred.
578+--
579+-- /macOS 10.5+/
580+--
581+-- /Internal/
582+--
583+isOverflowKernel :: Event -> Bool
584+isOverflowKernel = getFlag kFSEventStreamEventFlagKernelDropped
585+
586+foreign import capi
587+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagUserDropped"
588+ kFSEventStreamEventFlagUserDropped :: Word64
589+
590+-- | Did an overflow occur due to a user process issue? Set only if an overflow
591+-- occurred.
592+--
593+-- /macOS 10.5+/
594+--
595+-- /Internal/
596+--
597+isOverflowUser :: Event -> Bool
598+isOverflowUser = getFlag kFSEventStreamEventFlagUserDropped
599+
600+-------------------------------------------------------------------------------
601+-- Global Sentinel Events
602+-------------------------------------------------------------------------------
603+
604+foreign import capi
605+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagHistoryDone"
606+ kFSEventStreamEventFlagHistoryDone :: Word64
607+
608+-- | Determine whether the event is a history done marker event. A history done
609+-- event is generated when the historical events from before the current time
610+-- are done. Historical events are generated when the "since" parameter in the
611+-- watch config is set to before the current time.
612+--
613+-- /macOS 10.5+/
614+--
615+-- /Internal/
616+--
617+isHistoryDone :: Event -> Bool
618+isHistoryDone = getFlag kFSEventStreamEventFlagHistoryDone
619+
620+-------------------------------------------------------------------------------
621+-- Events affecting the watched path only
622+-------------------------------------------------------------------------------
623+
624+foreign import capi
625+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagRootChanged"
626+ kFSEventStreamEventFlagRootChanged :: Word64
627+
628+-- | Determine whether the event indicates a change of path of the monitored
629+-- object itself. Note that the object may become unreachable or deleted after
630+-- a change of path.
631+--
632+-- /Applicable only when 'setRootChanged' is 'On'/
633+--
634+-- /Occurs only for the watched path/
635+--
636+-- /macOS 10.5+/
637+--
638+-- /Internal/
639+--
640+isRootChanged :: Event -> Bool
641+isRootChanged = getFlag kFSEventStreamEventFlagRootChanged
642+
643+-------------------------------------------------------------------------------
644+-- Global events under the watched path
645+-------------------------------------------------------------------------------
646+
647+foreign import capi
648+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagMount"
649+ kFSEventStreamEventFlagMount :: Word64
650+
651+-- | Determine whether the event is a mount event. A mount event is generated
652+-- if a volume is mounted under the path being watched.
653+--
654+-- /macOS 10.5+/
655+--
656+-- /Internal/
657+--
658+isMount :: Event -> Bool
659+isMount = getFlag kFSEventStreamEventFlagMount
660+
661+foreign import capi
662+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagUnmount"
663+ kFSEventStreamEventFlagUnmount :: Word64
664+
665+-- | Determine whether the event is an unmount event. An unmount event is
666+-- generated if a volume mounted under the path being watched is unmounted.
667+--
668+-- /macOS 10.5+/
669+--
670+-- /Internal/
671+--
672+isUnmount :: Event -> Bool
673+isUnmount = getFlag kFSEventStreamEventFlagUnmount
674+
675+-------------------------------------------------------------------------------
676+-- Metadata change Events (applicable only when 'setFileEvents' is 'On')
677+-------------------------------------------------------------------------------
678+
679+foreign import capi
680+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemChangeOwner"
681+ kFSEventStreamEventFlagItemChangeOwner :: Word64
682+
683+-- | Determine whether the event is ownership, group, permissions or ACL change
684+-- event of an object contained within the monitored path.
685+--
686+-- Note that this event may be generated even if the metadata is changed to the
687+-- same value again.
688+--
689+-- /Applicable only when 'setFileEvents' is 'On'/
690+--
691+-- /Can occur for watched path or a file inside it/
692+--
693+-- /macOS 10.7+/
694+--
695+-- /Internal/
696+--
697+isOwnerGroupModeChanged :: Event -> Bool
698+isOwnerGroupModeChanged = getFlag kFSEventStreamEventFlagItemChangeOwner
699+
700+foreign import capi
701+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemInodeMetaMod"
702+ kFSEventStreamEventFlagItemInodeMetaMod :: Word64
703+
704+-- | Determine whether the event indicates inode metadata change for an object
705+-- contained within the monitored path. This event is generated when inode
706+-- attributes other than the owner, group or permissions are changed e.g. file
707+-- modification time. It does not occur when the link count of a file changes
708+-- (as of macOS 10.15.1).
709+--
710+-- /Applicable only when 'setFileEvents' is 'On'/
711+--
712+-- /Can occur for watched path or a file inside it/
713+--
714+-- /macOS 10.7+/
715+--
716+-- /Internal/
717+--
718+isInodeAttrsChanged :: Event -> Bool
719+isInodeAttrsChanged = getFlag kFSEventStreamEventFlagItemInodeMetaMod
720+
721+foreign import capi
722+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemFinderInfoMod"
723+ kFSEventStreamEventFlagItemFinderInfoMod :: Word64
724+
725+-- | Determine whether the event indicates finder information metadata change
726+-- for an object contained within the monitored path.
727+--
728+-- /Applicable only when 'setFileEvents' is 'On'/
729+--
730+-- /macOS 10.7+/
731+--
732+-- /Internal/
733+--
734+isFinderInfoChanged :: Event -> Bool
735+isFinderInfoChanged = getFlag kFSEventStreamEventFlagItemFinderInfoMod
736+
737+foreign import capi
738+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemXattrMod"
739+ kFSEventStreamEventFlagItemXattrMod :: Word64
740+
741+-- | Determine whether the event indicates extended attributes metadata change
742+-- for an object contained within the monitored path.
743+--
744+-- /Applicable only when 'setFileEvents' is 'On'/
745+--
746+-- /Can occur for watched path or a file inside it/
747+--
748+-- /macOS 10.7+/
749+--
750+-- /Internal/
751+--
752+isXAttrsChanged :: Event -> Bool
753+isXAttrsChanged = getFlag kFSEventStreamEventFlagItemXattrMod
754+
755+-------------------------------------------------------------------------------
756+-- CRUD Events (applicable only when 'setFileEvents' is 'On')
757+-------------------------------------------------------------------------------
758+
759+foreign import capi
760+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemCreated"
761+ kFSEventStreamEventFlagItemCreated :: Word64
762+
763+-- | Determine whether the event indicates creation of an object within the
764+-- monitored path. This event is generated when any file system object other
765+-- than a hard link is created. On hard linking only an 'isInodeAttrsChanged'
766+-- event on the directory is generated, it is not a create event. However, when
767+-- a hard link is deleted 'isDeleted' and 'isHardLink' both are true.
768+--
769+-- This event can occur for the watched path if the path was deleted/moved and
770+-- created again (tested on macOS 10.15.1). However, if a watch is started on a
771+-- non-existing path and the path is created later, then the this event is not
772+-- generated, the path is not watched.
773+--
774+-- /Applicable only when 'setFileEvents' is 'On'/
775+--
776+-- /Can occur for watched path or a file inside it/
777+--
778+-- /macOS 10.7+/
779+--
780+-- /Internal/
781+--
782+isCreated :: Event -> Bool
783+isCreated = getFlag kFSEventStreamEventFlagItemCreated
784+
785+foreign import capi
786+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemRemoved"
787+ kFSEventStreamEventFlagItemRemoved :: Word64
788+
789+-- | Determine whether the event indicates deletion of an object within the
790+-- monitored path. This is true when a file or a hardlink is deleted.
791+--
792+-- Applicable only when 'setFileEvents' is 'On'.
793+--
794+-- /Can occur for watched path or a file inside it/
795+--
796+-- /macOS 10.7+/
797+--
798+-- /Internal/
799+--
800+isDeleted :: Event -> Bool
801+isDeleted = getFlag kFSEventStreamEventFlagItemRemoved
802+
803+foreign import capi
804+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemRenamed"
805+ kFSEventStreamEventFlagItemRenamed :: Word64
806+
807+-- | Determine whether the event indicates rename of an object within the
808+-- monitored path. This event is generated when a file is renamed within the
809+-- watched directory or if it is moved out of or in the watched directory.
810+--
811+-- /Applicable only when 'setFileEvents' is 'On'/
812+--
813+-- /Can occur for watched path or a file inside it/
814+--
815+-- /macOS 10.7+/
816+--
817+-- /Internal/
818+--
819+isMoved :: Event -> Bool
820+isMoved = getFlag kFSEventStreamEventFlagItemRenamed
821+
822+foreign import capi
823+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemModified"
824+ kFSEventStreamEventFlagItemModified :: Word64
825+
826+-- | Determine whether the event indicates modification of an object within the
827+-- monitored path. This event is generated only for files and not directories.
828+--
829+-- Applicable only when 'setFileEvents' is 'On'.
830+--
831+-- /macOS 10.7+/
832+--
833+-- /Internal/
834+--
835+isModified :: Event -> Bool
836+isModified = getFlag kFSEventStreamEventFlagItemModified
837+
838+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101300
839+foreign import capi
840+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemCloned"
841+ kFSEventStreamEventFlagItemCloned :: Word64
842+
843+-- | Determine whether the event indicates cloning of an object within the
844+-- monitored path.
845+--
846+-- Applicable only when 'setFileEvents' is 'On'.
847+--
848+-- /macOS 10.13+/
849+--
850+-- /Internal/
851+--
852+isCloned :: Event -> Bool
853+isCloned = getFlag kFSEventStreamEventFlagItemCloned
854+#endif
855+
856+-------------------------------------------------------------------------------
857+-- Information about path type (applicable only when 'setFileEvents' is 'On')
858+-------------------------------------------------------------------------------
859+
860+foreign import capi
861+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemIsDir"
862+ kFSEventStreamEventFlagItemIsDir :: Word64
863+
864+-- | Determine whether the event is for a directory path.
865+--
866+-- Applicable only when 'setFileEvents' is 'On'.
867+--
868+-- /macOS 10.7+/
869+--
870+-- /Internal/
871+--
872+isDir :: Event -> Bool
873+isDir = getFlag kFSEventStreamEventFlagItemIsDir
874+
875+foreign import capi
876+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemIsFile"
877+ kFSEventStreamEventFlagItemIsFile :: Word64
878+
879+-- | Determine whether the event is for a file path.
880+--
881+-- Applicable only when 'setFileEvents' is 'On'.
882+--
883+-- /macOS 10.7+/
884+--
885+-- /Internal/
886+--
887+isFile :: Event -> Bool
888+isFile = getFlag kFSEventStreamEventFlagItemIsFile
889+
890+foreign import capi
891+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemIsSymlink"
892+ kFSEventStreamEventFlagItemIsSymlink :: Word64
893+
894+-- | Determine whether the event is for a symbolic link path.
895+--
896+-- Applicable only when 'setFileEvents' is 'On'.
897+--
898+-- /macOS 10.7+/
899+--
900+-- /Internal/
901+--
902+isSymLink :: Event -> Bool
903+isSymLink = getFlag kFSEventStreamEventFlagItemIsSymlink
904+
905+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
906+foreign import capi
907+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemIsHardlink"
908+ kFSEventStreamEventFlagItemIsHardlink :: Word64
909+
910+-- | Determine whether the event is for a file with more than one hard link.
911+-- When 'isFile' is true we can check for 'isHardLink'. Note that 'isHardLink'
912+-- is not true when a hard link is created, however, it is true when a file
913+-- which has or had more than one link in the past is removed. This is not true
914+-- if a file never had more than one link.
915+--
916+-- Applicable only when 'setFileEvents' is 'On'.
917+--
918+-- /macOS 10.10+/
919+--
920+-- /Internal/
921+--
922+isHardLink :: Event -> Bool
923+isHardLink = getFlag kFSEventStreamEventFlagItemIsHardlink
924+
925+foreign import capi
926+ "CoreServices/CoreServices.h value kFSEventStreamEventFlagItemIsLastHardlink"
927+ kFSEventStreamEventFlagItemIsLastHardlink :: Word64
928+
929+-- | Determine whether the event is for a hard link path with only one hard
930+-- link. If 'isHardLink' is true then we can check for 'isLastHardLink'. This
931+-- is true when a file has had more than one link and now the last link is
932+-- removed. In that case both 'isHardLink' and 'isLastHardLink" would be true.
933+--
934+-- Applicable only when 'setFileEvents' is 'On'.
935+--
936+-- /macOS 10.10+/
937+--
938+-- /Internal/
939+--
940+isLastHardLink :: Event -> Bool
941+isLastHardLink = getFlag kFSEventStreamEventFlagItemIsLastHardlink
942+#endif
943+
944+-------------------------------------------------------------------------------
945+-- Debugging
946+-------------------------------------------------------------------------------
947+
948+-- | Convert an 'Event' record to a String representation.
949+showEvent :: Event -> String
950+showEvent ev@Event{..} =
951+ let path = runIdentity $ S.toList $ U.decodeUtf8 $ A.toStream eventAbsPath
952+ in "--------------------------"
953+ ++ "\nId = " ++ show eventId
954+ ++ "\nPath = " ++ show path
955+ ++ "\nFlags " ++ show eventFlags
956+
957+ ++ showev isOverflow "Overflow"
958+ ++ showev isOverflowKernel "OverflowKernel"
959+ ++ showev isOverflowUser "OverflowUser"
960+
961+ ++ showev isRootChanged "RootChanged"
962+ ++ showev isMount "Mount"
963+ ++ showev isUnmount "Unmount"
964+ ++ showev isHistoryDone "HistoryDone"
965+
966+ ++ showev isOwnerGroupModeChanged "OwnerGroupModeChanged"
967+ ++ showev isInodeAttrsChanged "InodeAttrsChanged"
968+ ++ showev isFinderInfoChanged "FinderInfoChanged"
969+ ++ showev isXAttrsChanged "XAttrsChanged"
970+
971+ ++ showev isCreated "Created"
972+ ++ showev isDeleted "Deleted"
973+ ++ showev isModified "Modified"
974+ ++ showev isMoved "Moved"
975+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101300
976+ ++ showev isCloned "Cloned"
977+#endif
978+
979+ ++ showev isDir "Dir"
980+ ++ showev isFile "File"
981+ ++ showev isSymLink "SymLink"
982+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000
983+ ++ showev isHardLink "HardLink"
984+ ++ showev isLastHardLink "LastHardLink"
985+#endif
986+ ++ "\n"
987+
988+ where showev f str = if f ev then "\n" ++ str else ""
989+#else
990+module Streamly.Internal.FileSystem.Event.Darwin () where
991+#endif
streamly.cabal+8
@@ -145,6 +145,7 @@ extra-source-files:
145145 src/Streamly/Internal/Data/Array/PrimInclude.hs
146146 src/Streamly/Internal/Data/Array/Prim/TypesInclude.hs
147147 src/Streamly/Internal/Data/Array/Prim/MutTypesInclude.hs
148+ src/Streamly/Internal/FileSystem/Event/Darwin.h
148149 src/config.h.in
149150 src/inline.hs
150151 stack.yaml
@@ -350,8 +351,15 @@ library
350351 include-dirs: src
351352 if os(windows)
352353 c-sources: src/Streamly/Internal/Data/Time/Windows.c
354+
353355 if os(darwin)
356+ frameworks: Cocoa
357+ include-dirs: src/Streamly/Internal
354358 c-sources: src/Streamly/Internal/Data/Time/Darwin.c
359+ , src/Streamly/Internal/FileSystem/Event/Darwin.c
360+ exposed-modules:
361+ Streamly.Internal.FileSystem.Event.Darwin
362+
355363 if os(linux)
356364 exposed-modules: Streamly.Internal.FileSystem.Event.Linux
357365