on fe28607 help sign in
637 lines 20 kB history raw
1-- Some code snippets are adapted from the fsnotify package.
2-- http://hackage.haskell.org/package/fsnotify-0.3.0.1/
3--
4-- |
5-- Module : Streamly.Internal.FileSystem.Event.Windows
6-- Copyright : (c) 2020 Composewell Technologies
7-- (c) 2012, Mark Dittmer
8-- License : BSD-3-Clause
9-- Maintainer : streamly@composewell.com
10-- Stability : pre-release
11-- Portability : GHC
12--
13-- =Overview
14--
15-- Use 'watchRecursive'or 'watch' with a list of file system dir paths you
16-- want to watch as argument. It returns a stream of 'Event' representing the
17-- file system events occurring under the watched paths.
18--
19-- @
20-- {-\# LANGUAGE MagicHash #-}
21-- Stream.mapM_ (putStrLn . 'showEvent') $ 'watchRecursive' [Array.fromList "path"]
22-- @
23--
24-- 'Event' is an opaque type. Accessor functions (e.g. 'showEvent' above)
25-- provided in this module are used to determine the attributes of the event.
26--
27-- =Design notes
28--
29-- For Windows reference documentation see:
30--
31-- * <https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information file notify information>
32-- * <https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw read directory changes>
33--
34-- We try to keep the macOS\/Linux/Windows event handling APIs and defaults
35-- semantically and syntactically as close as possible.
36--
37-- =Availability
38--
39-- As per the Windows reference docs, the fs event notification API is
40-- available in:
41--
42-- * Minimum supported client: Windows XP [desktop apps | UWP apps]
43-- * Minimum supported server: Windows Server 2003 [desktop apps | UWP apps
45module Streamly.Internal.FileSystem.Event.Windows
46 (
47 -- * Subscribing to events
49 -- ** Default configuration
50 Config
51 , defaultConfig
53 -- ** Watch Behavior
54 , setRecursiveMode
56 -- ** Events of Interest
57 , setFileNameEvents
58 , setDirNameEvents
59 , setAttrsModified
60 , setSecurityModified
61 , setSizeModified
62 , setLastWriteTimeModified
63 , setAllEvents
65 -- ** Watch APIs
66 , watch
67 , watchRecursive
68 , watchWith
70 -- * Handling Events
71 , Event(..)
72 , getRoot
73 , getRelPath
74 , getAbsPath
76 -- ** Item CRUD events
77 , isCreated
78 , isDeleted
79 , isModified
80 , isMoved
81 , isMovedFrom
82 , isMovedTo
84 -- ** Exception Conditions
85 , isEventsLost
87 -- * Debugging
88 , showEvent
89 )
90where
92import Data.Bits ((.|.), (.&.), complement)
93import Data.Char (ord)
94import Data.Functor.Identity (runIdentity)
95import Data.List.NonEmpty (NonEmpty)
96import Data.Word (Word8)
97import Foreign.C.String (peekCWStringLen)
98import Foreign.Marshal.Alloc (alloca, allocaBytes)
99import Foreign.Storable (peekByteOff)
100import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr, nullFunPtr, plusPtr)
101import Streamly.Internal.Data.Stream (Stream)
102import Streamly.Internal.Data.Stream.Concurrent (eager)
103import System.Win32.File
104 ( FileNotificationFlag
105 , LPOVERLAPPED
106 , closeHandle
107 , createFile
108 , fILE_FLAG_BACKUP_SEMANTICS
109 , fILE_LIST_DIRECTORY
110 , fILE_NOTIFY_CHANGE_FILE_NAME
111 , fILE_NOTIFY_CHANGE_DIR_NAME
112 , fILE_NOTIFY_CHANGE_ATTRIBUTES
113 , fILE_NOTIFY_CHANGE_SIZE
114 , fILE_NOTIFY_CHANGE_LAST_WRITE
115 , fILE_NOTIFY_CHANGE_SECURITY
116 , fILE_SHARE_READ
117 , fILE_SHARE_WRITE
118 , oPEN_EXISTING
119 )
120import System.Win32.Types (BOOL, DWORD, HANDLE, LPVOID, LPDWORD, failIfFalse_)
121import qualified Data.List.NonEmpty as NonEmpty
122import qualified Streamly.Data.Fold as Fold
123import qualified Streamly.Internal.Data.Stream as S
124import qualified Streamly.Internal.Data.Stream.Concurrent as S (parConcatMap)
125import qualified Streamly.Internal.Unicode.Stream as U
126import qualified Streamly.Internal.Unicode.Utf8 as UTF8
127import qualified Streamly.Internal.Data.Array as A
128import Streamly.Internal.Data.Array (Array)
130-- | Watch configuration, used to specify the events of interest and the
131-- behavior of the watch.
132--
133-- /Pre-release/
134--
135data Config = Config
136 { watchRec :: BOOL
137 , createFlags :: DWORD
138 }
140-------------------------------------------------------------------------------
141-- Boolean settings
142-------------------------------------------------------------------------------
144setFlag :: DWORD -> Bool -> Config -> Config
145setFlag mask status cfg@Config{..} =
146 let flags =
147 if status
148 then createFlags .|. mask
149 else createFlags .&. complement mask
150 in cfg {createFlags = flags}
152-- | Set watch event on directory recursively.
153--
154-- /default: False/
155--
156-- /Pre-release/
157--
158setRecursiveMode :: Bool -> Config -> Config
159setRecursiveMode recursive cfg@Config{} = cfg {watchRec = recursive}
161-- | Generate notify events on file create, rename or delete.
162--
163-- From Windows API documentation: Any file name change in the watched
164-- directory or subtree causes a change notification wait operation to return.
165-- Changes include renaming, creating, or deleting a file.
166--
167-- /default: True/
168--
169-- /Pre-release/
170--
171setFileNameEvents :: Bool -> Config -> Config
172setFileNameEvents = setFlag fILE_NOTIFY_CHANGE_FILE_NAME
174-- | Generate notify events on directory create, rename or delete.
175--
176-- From Windows API documentaiton: Any directory-name change in the watched
177-- directory or subtree causes a change notification wait operation to return.
178-- Changes include creating or deleting a directory.
179--
180-- /default: True/
181--
182-- /Pre-release/
183--
184setDirNameEvents :: Bool -> Config -> Config
185setDirNameEvents = setFlag fILE_NOTIFY_CHANGE_DIR_NAME
187-- | Generate an 'isModified' event on any attribute change in the watched
188-- directory or subtree.
189--
190-- /default: False/
191--
192-- /Pre-release/
193--
194setAttrsModified :: Bool -> Config -> Config
195setAttrsModified = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
197-- | Generate an 'isModified' event when the file size is changed.
198--
199-- From Windows API documentation: Any file-size change in the watched
200-- directory or subtree causes a change notification wait operation to return.
201-- The operating system detects a change in file size only when the file is
202-- written to the disk. For operating systems that use extensive caching,
203-- detection occurs only when the cache is sufficiently flushed.
204--
205-- /default: False/
206--
207-- /Pre-release/
208--
209setSizeModified :: Bool -> Config -> Config
210setSizeModified = setFlag fILE_NOTIFY_CHANGE_SIZE
212-- | Generate an 'isModified' event when the last write timestamp of the file
213-- inode is changed.
214--
215-- From Windows API documentation: Any change to the last write-time of files
216-- in the watched directory or subtree causes a change notification wait
217-- operation to return. The operating system detects a change to the last
218-- write-time only when the file is written to the disk. For operating systems
219-- that use extensive caching, detection occurs only when the cache is
220-- sufficiently flushed.
221--
222-- /default: False/
223--
224-- /Pre-release/
225--
226setLastWriteTimeModified :: Bool -> Config -> Config
227setLastWriteTimeModified = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
229-- | Generate an 'isModified' event when any security-descriptor change occurs
230-- in the watched directory or subtree.
231--
232-- /default: False/
233--
234-- /Pre-release/
235--
236setSecurityModified :: Bool -> Config -> Config
237setSecurityModified = setFlag fILE_NOTIFY_CHANGE_SECURITY
239-- | Set all tunable events 'True' or 'False'. Equivalent to setting:
240--
241-- * setFileNameEvents
242-- * setDirNameEvents
243-- * setAttrsModified
244-- * setSizeModified
245-- * setLastWriteTimeModified
246-- * setSecurityModified
247--
248-- /Pre-release/
249--
250setAllEvents :: Bool -> Config -> Config
251setAllEvents s =
252 setFileNameEvents s
253 . setDirNameEvents s
254 . setAttrsModified s
255 . setSizeModified s
256 . setLastWriteTimeModified s
257 . setSecurityModified s
259-- | The tunable events that are enabled by default are:
260--
261-- * setFileNameEvents True
262-- * setDirNameEvents True
263-- * setSizeModified True
264-- * setLastWriteTimeModified True
265--
266-- /Pre-release/
267--
268defaultConfig :: Config
269defaultConfig =
270 setFileNameEvents True
271 $ setDirNameEvents True
272 $ setSizeModified True
273 $ setLastWriteTimeModified True
274 $ Config {watchRec = False, createFlags = 0}
276getConfigFlag :: Config -> DWORD
277getConfigFlag Config{..} = createFlags
279getConfigRecMode :: Config -> BOOL
280getConfigRecMode Config{..} = watchRec
282data Event = Event
283 { eventFlags :: DWORD
284 , eventRelPath :: String
285 , eventRootPath :: String
286 , totalBytes :: DWORD
287 } deriving (Show, Ord, Eq)
289-- For reference documentation see:
290--
291-- See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
292data FILE_NOTIFY_INFORMATION = FILE_NOTIFY_INFORMATION
293 { fniNextEntryOffset :: DWORD
294 , fniAction :: DWORD
295 , fniFileName :: String
296 } deriving Show
298type LPOVERLAPPED_COMPLETION_ROUTINE =
299 FunPtr ((DWORD, DWORD, LPOVERLAPPED) -> IO ())
301-- | A handle for a watch.
302getWatchHandle :: FilePath -> IO (HANDLE, FilePath)
303getWatchHandle dir = do
304 h <- createFile dir
305 -- Access mode
306 fILE_LIST_DIRECTORY
307 -- Share mode
308 (fILE_SHARE_READ .|. fILE_SHARE_WRITE)
309 -- Security attributes
310 Nothing
311 -- Create mode, we want to look at an existing directory
312 oPEN_EXISTING
313 -- File attribute, NOT using OVERLAPPED since we work synchronously
314 fILE_FLAG_BACKUP_SEMANTICS
315 -- No template file
316 Nothing
317 return (h, dir)
319-- For reference documentation see:
320--
321-- See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
322-- Note that this API uses UTF-16 for file system paths:
323-- 1. https://docs.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api
324-- 2. https://docs.microsoft.com/en-us/windows/win32/intl/unicode
325foreign import ccall safe
326 "windows.h ReadDirectoryChangesW" c_ReadDirectoryChangesW ::
327 HANDLE
328 -> LPVOID
329 -> DWORD
330 -> BOOL
331 -> DWORD
332 -> LPDWORD
333 -> LPOVERLAPPED
334 -> LPOVERLAPPED_COMPLETION_ROUTINE
335 -> IO BOOL
337readDirectoryChangesW ::
338 HANDLE
339 -> Ptr FILE_NOTIFY_INFORMATION
340 -> DWORD
341 -> BOOL
342 -> FileNotificationFlag
343 -> LPDWORD
344 -> IO ()
345readDirectoryChangesW h buf bufSize wst f br =
346 let res = c_ReadDirectoryChangesW
347 h (castPtr buf) bufSize wst f br nullPtr nullFunPtr
348 in failIfFalse_ "ReadDirectoryChangesW" res
350peekFNI :: Ptr FILE_NOTIFY_INFORMATION -> IO FILE_NOTIFY_INFORMATION
351peekFNI buf = do
352 neof <- peekByteOff buf 0
353 acti <- peekByteOff buf 4
354 fnle <- peekByteOff buf 8
355 -- Note: The path is UTF-16 encoded C WChars, peekCWStringLen converts
356 -- UTF-16 to UTF-32 Char String
357 fnam <- peekCWStringLen
358 -- start of array
359 (buf `plusPtr` 12,
360 -- fnle is the length in *bytes*, and a WCHAR is 2 bytes
361 fromEnum (fnle :: DWORD) `div` 2)
362 return $ FILE_NOTIFY_INFORMATION neof acti fnam
364readChangeEvents ::
365 Ptr FILE_NOTIFY_INFORMATION -> String -> DWORD -> IO [Event]
366readChangeEvents pfni root bytesRet = do
367 fni <- peekFNI pfni
368 let entry = Event
369 { eventFlags = fniAction fni
370 , eventRelPath = fniFileName fni
371 , eventRootPath = root
372 , totalBytes = bytesRet
373 }
374 nioff = fromEnum $ fniNextEntryOffset fni
375 entries <-
376 if nioff == 0
377 then return []
378 else readChangeEvents (pfni `plusPtr` nioff) root bytesRet
379 return $ entry : entries
381readDirectoryChanges ::
382 String -> HANDLE -> Bool -> FileNotificationFlag -> IO [Event]
383readDirectoryChanges root h wst mask = do
384 let maxBuf = 63 * 1024
385 allocaBytes maxBuf $ \buffer -> do
386 alloca $ \bret -> do
387 readDirectoryChangesW h buffer (toEnum maxBuf) wst mask bret
388 bytesRet <- peekByteOff bret 0
389 readChangeEvents buffer root bytesRet
391-- XXX Try to get these from windows header files
392type FileAction = DWORD
394fILE_ACTION_ADDED :: FileAction
395fILE_ACTION_ADDED = 1
397fILE_ACTION_REMOVED :: FileAction
398fILE_ACTION_REMOVED = 2
400fILE_ACTION_MODIFIED :: FileAction
401fILE_ACTION_MODIFIED = 3
403fILE_ACTION_RENAMED_OLD_NAME :: FileAction
404fILE_ACTION_RENAMED_OLD_NAME = 4
406fILE_ACTION_RENAMED_NEW_NAME :: FileAction
407fILE_ACTION_RENAMED_NEW_NAME = 5
409eventStreamAggr :: (HANDLE, FilePath, Config) -> Stream IO Event
410eventStreamAggr (handle, rootPath, cfg) = do
411 let recMode = getConfigRecMode cfg
412 flagMasks = getConfigFlag cfg
413 repeatM = S.sequence . S.repeat
414 S.concatMap S.fromList $ repeatM
415 $ readDirectoryChanges rootPath handle recMode flagMasks
417pathsToHandles ::
418 NonEmpty FilePath -> Config -> Stream IO (HANDLE, FilePath, Config)
419pathsToHandles paths cfg = do
420 let pathStream = S.fromList (NonEmpty.toList paths)
421 st2 = S.mapM getWatchHandle pathStream
422 fmap (\(h, f) -> (h, f, cfg)) st2
424-------------------------------------------------------------------------------
425-- Utilities
426-------------------------------------------------------------------------------
428utf8ToString :: Array Word8 -> FilePath
429utf8ToString = runIdentity . S.fold Fold.toList . U.decodeUtf8 . A.read
431utf8ToStringList :: NonEmpty (Array Word8) -> NonEmpty FilePath
432utf8ToStringList = NonEmpty.map utf8ToString
434-- | Close a Directory handle.
435--
436closePathHandleStream :: Stream IO (HANDLE, FilePath, Config) -> IO ()
437closePathHandleStream =
438 let f (h, _, _) = closeHandle h
439 in S.fold (Fold.drainMapM f)
441-- XXX
442-- Document the path treatment for Linux/Windows/macOS modules.
443-- Remove the utf-8 encoding requirement of paths? It can be encoding agnostic
444-- "\" separated bytes, the application can decide what encoding to use.
445-- Instead of always using widechar (-W) APIs can we call the underlying APIs
446-- based on the configured code page?
447-- https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page
448--
449-- | Start monitoring a list of directory paths for file system events with the
450-- supplied configuration modifier operation over the' defaultConfig'. The
451-- paths could be directories or symbolic links to directories.
452--
453-- When recursive mode is True, the whole directory tree under the path is
454-- watched recursively. When recursive mode is False, only the files and
455-- directories directly under the watched directory are monitored, contents of
456-- subdirectories are not monitored. Monitoring starts from the current time
457-- onwards. The paths are specified as UTF-8 encoded 'Array' of 'Word8'.
458--
459-- /Symbolic Links:/ If the pathname to be watched is a symbolic link then
460-- watch the target of the symbolic link instead of the symbolic link itself.
461-- Note that the path location in the events is through the original symbolic
462-- link path rather than the resolved path.
463--
464-- @
465-- watchWith
466-- ('setAttrsModified' True . 'setLastWriteTimeModified' False)
467-- [Array.fromList "dir"]
468-- @
469--
470-- /Pre-release/
471--
472watchWith :: (Config -> Config) -> NonEmpty (Array Word8) -> Stream IO Event
473watchWith f paths =
474 S.bracketIO before after (S.parConcatMap (eager True) eventStreamAggr)
476 where
478 before = return $ pathsToHandles (utf8ToStringList paths) $ f defaultConfig
479 after = closePathHandleStream
481-- | Same as 'watchWith' using 'defaultConfig' and recursive mode.
482--
483-- >>> watchRecursive = watchWith (setRecursiveMode True)
484--
485-- /Pre-release/
486--
487watchRecursive :: NonEmpty (Array Word8) -> Stream IO Event
488watchRecursive = watchWith (setRecursiveMode True)
490-- | Same as 'watchWith' using defaultConfig and non-recursive mode.
491--
492-- >>> watch = watchWith id
493--
494-- /Pre-release/
495--
496watch :: NonEmpty (Array Word8) -> Stream IO Event
497watch = watchWith id
499getFlag :: DWORD -> Event -> Bool
500getFlag mask Event{..} = eventFlags == mask
502-- XXX Change the type to Array Word8 to make it compatible with other APIs.
503--
504-- | Get the file system object path for which the event is generated, relative
505-- to the watched root. The path is a UTF-8 encoded array of bytes.
506--
507-- /Pre-release/
508--
509getRelPath :: Event -> Array Word8
510getRelPath Event{..} = (UTF8.toArray . UTF8.pack) eventRelPath
512-- XXX Change the type to Array Word8 to make it compatible with other APIs.
513--
514-- | Get the watch root directory to which this event belongs.
515--
516-- /Pre-release/
517--
518getRoot :: Event -> Array Word8
519getRoot Event{..} = (UTF8.toArray . UTF8.pack) eventRootPath
521-- | Get the absolute file system object path for which the event is generated.
522--
523-- When the watch root is a symlink, the absolute path returned is via the
524-- original symlink and not through the resolved path.
525--
526-- /Pre-release/
527--
528getAbsPath :: Event -> Array Word8
529getAbsPath ev = getRoot ev <> backSlash <> getRelPath ev
530 where backSlash = A.fromList [ fromIntegral (ord '\\') ]
532-- XXX need to document the exact semantics of these.
533--
534-- | This event is generated when a file or directory is created in a watched
535-- directory or directory tree when in recursive watch mode. Creating a hard
536-- link also generates this event.
537--
538-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
539--
540-- /Pre-release/
541--
542isCreated :: Event -> Bool
543isCreated = getFlag fILE_ACTION_ADDED
545-- | This event is generated when a file or directory is deleted from the
546-- watched directory or directory tree when in recursive mode. This event is
547-- generated even when a hard link is deleted.
548--
549-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
550--
551-- /Pre-release/
552--
553isDeleted :: Event -> Bool
554isDeleted = getFlag fILE_ACTION_REMOVED
556-- | Generated for the original path when an object is moved from under a
557-- monitored directory.
558--
559-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
560--
561-- /Pre-release/
562--
563isMovedFrom :: Event -> Bool
564isMovedFrom = getFlag fILE_ACTION_RENAMED_OLD_NAME
566-- | Generated for the new path when an object is moved under a monitored
567-- directory.
568--
569-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
570--
571-- /Pre-release/
572--
573isMovedTo :: Event -> Bool
574isMovedTo = getFlag fILE_ACTION_RENAMED_NEW_NAME
576-- | Generated for a path that is moved from or moved to the monitored
577-- directory.
578--
579-- >>> isMoved ev = isMovedFrom ev || isMovedTo ev
580--
581-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
582-- /Occurs only for an object inside the watched directory/
583--
584-- /Pre-release/
585--
586isMoved :: Event -> Bool
587isMoved ev = isMovedFrom ev || isMovedTo ev
589-- XXX This event is generated only for files and not directories?
590--
591-- | This event occurs when a file or directory contents, timestamps or
592-- attributes are modified. Since it can occur on multiple changes, you may
593-- have to check the attributes to know what exactly changed when multiple type
594-- of modified events are enabled.
595--
596-- In non-recursive mode this event does not occur for directories. In
597-- recursive mode this event occurs for the parent directory if a file or
598-- directory inside it is created or renamed.
599--
600-- /Occurs when one of the @set*Modified@ events is enabled/
601--
602-- /Pre-release/
603--
604isModified :: Event -> Bool
605isModified = getFlag fILE_ACTION_MODIFIED
607-- | If the kernel event buffer overflows, entire contents of the buffer are
608-- discarded, therefore, events are lost. The user application must scan
609-- everything under the watched paths to know the current state of the file
610-- system tree.
611--
612-- /Pre-release/
613--
614isEventsLost :: Event -> Bool
615isEventsLost Event{..} = totalBytes == 0
617-------------------------------------------------------------------------------
618-- Debugging
619-------------------------------------------------------------------------------
621-- | Convert an 'Event' record to a String representation.
622showEvent :: Event -> String
623showEvent ev@Event{..} =
624 "--------------------------"
625 ++ "\nRoot = " ++ utf8ToString (getRoot ev)
626 ++ "\nPath = " ++ utf8ToString (getRelPath ev)
627 ++ "\ngetAbsPath = " ++ utf8ToString (getAbsPath ev)
628 ++ "\nFlags " ++ show eventFlags
629 ++ showev isEventsLost "Overflow"
630 ++ showev isCreated "Created"
631 ++ showev isDeleted "Deleted"
632 ++ showev isModified "Modified"
633 ++ showev isMovedFrom "MovedFrom"
634 ++ showev isMovedTo "MovedTo"
635 ++ "\n"
637 where showev f str = if f ev then "\n" ++ str else ""