on bc0da07 help sign in
639 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 , Switch (..)
52 , defaultConfig
54 -- ** Watch Behavior
55 , setRecursiveMode
57 -- ** Events of Interest
58 , setFileNameEvents
59 , setDirNameEvents
60 , setAttrsModified
61 , setSecurityModified
62 , setSizeModified
63 , setLastWriteTimeModified
64 , setAllEvents
66 -- ** Watch APIs
67 , watch
68 , watchRecursive
69 , watchWith
71 -- * Handling Events
72 , Event(..)
73 , getRoot
74 , getRelPath
75 , getAbsPath
77 -- ** Item CRUD events
78 , isCreated
79 , isDeleted
80 , isModified
81 , isMoved
82 , isMovedFrom
83 , isMovedTo
85 -- ** Exception Conditions
86 , isEventsLost
88 -- * Debugging
89 , showEvent
90 )
91where
93import Control.Monad.IO.Class (MonadIO(liftIO))
94import Data.Bits ((.|.), (.&.), complement)
95import Data.Char (ord)
96import Data.Functor.Identity (runIdentity)
97import Data.List.NonEmpty (NonEmpty)
98import Data.Word (Word8)
99import Foreign.C.String (peekCWStringLen)
100import Foreign.Marshal.Alloc (alloca, allocaBytes)
101import Foreign.Storable (peekByteOff)
102import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr, nullFunPtr, plusPtr)
103import Streamly.Prelude (SerialT, parallel)
104import System.Win32.File
105 ( FileNotificationFlag
106 , LPOVERLAPPED
107 , closeHandle
108 , createFile
109 , fILE_FLAG_BACKUP_SEMANTICS
110 , fILE_LIST_DIRECTORY
111 , fILE_NOTIFY_CHANGE_FILE_NAME
112 , fILE_NOTIFY_CHANGE_DIR_NAME
113 , fILE_NOTIFY_CHANGE_ATTRIBUTES
114 , fILE_NOTIFY_CHANGE_SIZE
115 , fILE_NOTIFY_CHANGE_LAST_WRITE
116 , fILE_NOTIFY_CHANGE_SECURITY
117 , fILE_SHARE_READ
118 , fILE_SHARE_WRITE
119 , oPEN_EXISTING
120 )
121import System.Win32.Types (BOOL, DWORD, HANDLE, LPVOID, LPDWORD, failIfFalse_)
122import qualified Data.List.NonEmpty as NonEmpty
123import qualified Streamly.Internal.Data.Stream.IsStream as S
124import qualified Streamly.Internal.Unicode.Stream as U
125import qualified Streamly.Internal.Unicode.Utf8 as UTF8
126import qualified Streamly.Internal.Data.Array.Unboxed as A
127import Streamly.Internal.Data.Array.Unboxed (Array)
129-- | Watch configuration, used to specify the events of interest and the
130-- behavior of the watch.
131--
132-- /Pre-release/
133--
134data Config = Config
135 { watchRec :: BOOL
136 , createFlags :: DWORD
137 }
139-------------------------------------------------------------------------------
140-- Boolean settings
141-------------------------------------------------------------------------------
143-- | Whether a setting is 'On' or 'Off'.
144--
145-- /Pre-release/
146--
147data Switch = On | Off deriving (Show, Eq)
149setFlag :: DWORD -> Switch -> Config -> Config
150setFlag mask status cfg@Config{..} =
151 let flags =
152 case status of
153 On -> createFlags .|. mask
154 Off -> createFlags .&. complement mask
155 in cfg {createFlags = flags}
157-- | Set watch event on directory recursively.
158--
159-- /default: Off/
160--
161-- /Pre-release/
162--
163setRecursiveMode :: Switch -> Config -> Config
164setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec == On}
166-- | Generate notify events on file create, rename or delete.
167--
168-- From Windows API documentation: Any file name change in the watched
169-- directory or subtree causes a change notification wait operation to return.
170-- Changes include renaming, creating, or deleting a file.
171--
172-- /default: On/
173--
174-- /Pre-release/
175--
176setFileNameEvents :: Switch -> Config -> Config
177setFileNameEvents = setFlag fILE_NOTIFY_CHANGE_FILE_NAME
179-- | Generate notify events on directory create, rename or delete.
180--
181-- From Windows API documentaiton: Any directory-name change in the watched
182-- directory or subtree causes a change notification wait operation to return.
183-- Changes include creating or deleting a directory.
184--
185-- /default: On/
186--
187-- /Pre-release/
188--
189setDirNameEvents :: Switch -> Config -> Config
190setDirNameEvents = setFlag fILE_NOTIFY_CHANGE_DIR_NAME
192-- | Generate an 'isModified' event on any attribute change in the watched
193-- directory or subtree.
194--
195-- /default: Off/
196--
197-- /Pre-release/
198--
199setAttrsModified :: Switch -> Config -> Config
200setAttrsModified = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
202-- | Generate an 'isModified' event when the file size is changed.
203--
204-- From Windows API documentation: Any file-size change in the watched
205-- directory or subtree causes a change notification wait operation to return.
206-- The operating system detects a change in file size only when the file is
207-- written to the disk. For operating systems that use extensive caching,
208-- detection occurs only when the cache is sufficiently flushed.
209--
210-- /default: Off/
211--
212-- /Pre-release/
213--
214setSizeModified :: Switch -> Config -> Config
215setSizeModified = setFlag fILE_NOTIFY_CHANGE_SIZE
217-- | Generate an 'isModified' event when the last write timestamp of the file
218-- inode is changed.
219--
220-- From Windows API documentation: Any change to the last write-time of files
221-- in the watched directory or subtree causes a change notification wait
222-- operation to return. The operating system detects a change to the last
223-- write-time only when the file is written to the disk. For operating systems
224-- that use extensive caching, detection occurs only when the cache is
225-- sufficiently flushed.
226--
227-- /default: Off/
228--
229-- /Pre-release/
230--
231setLastWriteTimeModified :: Switch -> Config -> Config
232setLastWriteTimeModified = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
234-- | Generate an 'isModified' event when any security-descriptor change occurs
235-- in the watched directory or subtree.
236--
237-- /default: Off/
238--
239-- /Pre-release/
240--
241setSecurityModified :: Switch -> Config -> Config
242setSecurityModified = setFlag fILE_NOTIFY_CHANGE_SECURITY
244-- | Set all tunable events 'On' or 'Off'. Equivalent to setting:
245--
246-- * setFileNameEvents
247-- * setDirNameEvents
248-- * setAttrsModified
249-- * setSizeModified
250-- * setLastWriteTimeModified
251-- * setSecurityModified
252--
253-- /Pre-release/
254--
255setAllEvents :: Switch -> Config -> Config
256setAllEvents s =
257 setFileNameEvents s
258 . setDirNameEvents s
259 . setAttrsModified s
260 . setSizeModified s
261 . setLastWriteTimeModified s
262 . setSecurityModified s
264-- | The tunable events that are enabled by default are:
265--
266-- * setFileNameEvents On
267-- * setDirNameEvents On
268-- * setSizeModified On
269-- * setLastWriteTimeModified On
270--
271-- /Pre-release/
272--
273defaultConfig :: Config
274defaultConfig =
275 setFileNameEvents On
276 $ setDirNameEvents On
277 $ setSizeModified On
278 $ setLastWriteTimeModified On
279 $ Config {watchRec = False, createFlags = 0}
281getConfigFlag :: Config -> DWORD
282getConfigFlag Config{..} = createFlags
284getConfigRecMode :: Config -> BOOL
285getConfigRecMode Config{..} = watchRec
287data Event = Event
288 { eventFlags :: DWORD
289 , eventRelPath :: String
290 , eventRootPath :: String
291 , totalBytes :: DWORD
292 } deriving (Show, Ord, Eq)
294-- For reference documentation see:
295--
296-- See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
297data FILE_NOTIFY_INFORMATION = FILE_NOTIFY_INFORMATION
298 { fniNextEntryOffset :: DWORD
299 , fniAction :: DWORD
300 , fniFileName :: String
301 } deriving Show
303type LPOVERLAPPED_COMPLETION_ROUTINE =
304 FunPtr ((DWORD, DWORD, LPOVERLAPPED) -> IO ())
306-- | A handle for a watch.
307getWatchHandle :: FilePath -> IO (HANDLE, FilePath)
308getWatchHandle dir = do
309 h <- createFile dir
310 -- Access mode
311 fILE_LIST_DIRECTORY
312 -- Share mode
313 (fILE_SHARE_READ .|. fILE_SHARE_WRITE)
314 -- Security attributes
315 Nothing
316 -- Create mode, we want to look at an existing directory
317 oPEN_EXISTING
318 -- File attribute, NOT using OVERLAPPED since we work synchronously
319 fILE_FLAG_BACKUP_SEMANTICS
320 -- No template file
321 Nothing
322 return (h, dir)
324-- For reference documentation see:
325--
326-- See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
327-- Note that this API uses UTF-16 for file system paths:
328-- 1. https://docs.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api
329-- 2. https://docs.microsoft.com/en-us/windows/win32/intl/unicode
330foreign import ccall safe
331 "windows.h ReadDirectoryChangesW" c_ReadDirectoryChangesW ::
332 HANDLE
333 -> LPVOID
334 -> DWORD
335 -> BOOL
336 -> DWORD
337 -> LPDWORD
338 -> LPOVERLAPPED
339 -> LPOVERLAPPED_COMPLETION_ROUTINE
340 -> IO BOOL
342readDirectoryChangesW ::
343 HANDLE
344 -> Ptr FILE_NOTIFY_INFORMATION
345 -> DWORD
346 -> BOOL
347 -> FileNotificationFlag
348 -> LPDWORD
349 -> IO ()
350readDirectoryChangesW h buf bufSize wst f br =
351 let res = c_ReadDirectoryChangesW
352 h (castPtr buf) bufSize wst f br nullPtr nullFunPtr
353 in failIfFalse_ "ReadDirectoryChangesW" res
355peekFNI :: Ptr FILE_NOTIFY_INFORMATION -> IO FILE_NOTIFY_INFORMATION
356peekFNI buf = do
357 neof <- peekByteOff buf 0
358 acti <- peekByteOff buf 4
359 fnle <- peekByteOff buf 8
360 -- Note: The path is UTF-16 encoded C WChars, peekCWStringLen converts
361 -- UTF-16 to UTF-32 Char String
362 fnam <- peekCWStringLen
363 -- start of array
364 (buf `plusPtr` 12,
365 -- fnle is the length in *bytes*, and a WCHAR is 2 bytes
366 fromEnum (fnle :: DWORD) `div` 2)
367 return $ FILE_NOTIFY_INFORMATION neof acti fnam
369readChangeEvents ::
370 Ptr FILE_NOTIFY_INFORMATION -> String -> DWORD -> IO [Event]
371readChangeEvents pfni root bytesRet = do
372 fni <- peekFNI pfni
373 let entry = Event
374 { eventFlags = fniAction fni
375 , eventRelPath = fniFileName fni
376 , eventRootPath = root
377 , totalBytes = bytesRet
378 }
379 nioff = fromEnum $ fniNextEntryOffset fni
380 entries <-
381 if nioff == 0
382 then return []
383 else readChangeEvents (pfni `plusPtr` nioff) root bytesRet
384 return $ entry : entries
386readDirectoryChanges ::
387 String -> HANDLE -> Bool -> FileNotificationFlag -> IO [Event]
388readDirectoryChanges root h wst mask = do
389 let maxBuf = 63 * 1024
390 allocaBytes maxBuf $ \buffer -> do
391 alloca $ \bret -> do
392 readDirectoryChangesW h buffer (toEnum maxBuf) wst mask bret
393 bytesRet <- peekByteOff bret 0
394 readChangeEvents buffer root bytesRet
396-- XXX Try to get these from windows header files
397type FileAction = DWORD
399fILE_ACTION_ADDED :: FileAction
400fILE_ACTION_ADDED = 1
402fILE_ACTION_REMOVED :: FileAction
403fILE_ACTION_REMOVED = 2
405fILE_ACTION_MODIFIED :: FileAction
406fILE_ACTION_MODIFIED = 3
408fILE_ACTION_RENAMED_OLD_NAME :: FileAction
409fILE_ACTION_RENAMED_OLD_NAME = 4
411fILE_ACTION_RENAMED_NEW_NAME :: FileAction
412fILE_ACTION_RENAMED_NEW_NAME = 5
414eventStreamAggr :: (HANDLE, FilePath, Config) -> SerialT IO Event
415eventStreamAggr (handle, rootPath, cfg) = do
416 let recMode = getConfigRecMode cfg
417 flagMasks = getConfigFlag cfg
418 S.concatMap S.fromList $ S.repeatM
419 $ readDirectoryChanges rootPath handle recMode flagMasks
421pathsToHandles ::
422 NonEmpty FilePath -> Config -> SerialT IO (HANDLE, FilePath, Config)
423pathsToHandles paths cfg = do
424 let pathStream = S.fromList (NonEmpty.toList paths)
425 st2 = S.mapM getWatchHandle pathStream
426 S.map (\(h, f) -> (h, f, cfg)) st2
428-------------------------------------------------------------------------------
429-- Utilities
430-------------------------------------------------------------------------------
432utf8ToString :: Array Word8 -> FilePath
433utf8ToString = runIdentity . S.toList . U.decodeUtf8 . A.toStream
435utf8ToStringList :: NonEmpty (Array Word8) -> NonEmpty FilePath
436utf8ToStringList = NonEmpty.map utf8ToString
438-- | Close a Directory handle.
439--
440closePathHandleStream :: SerialT IO (HANDLE, FilePath, Config) -> IO ()
441closePathHandleStream = S.mapM_ (\(h, _, _) -> closeHandle h)
443-- XXX
444-- Document the path treatment for Linux/Windows/macOS modules.
445-- Remove the utf-8 encoding requirement of paths? It can be encoding agnostic
446-- "\" separated bytes, the application can decide what encoding to use.
447-- Instead of always using widechar (-W) APIs can we call the underlying APIs
448-- based on the configured code page?
449-- https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page
450--
451-- | Start monitoring a list of directory paths for file system events with the
452-- supplied configuration modifier operation over the' defaultConfig'. The
453-- paths could be directories or symbolic links to directories.
454--
455-- When recursive mode is True, the whole directory tree under the path is
456-- watched recursively. When recursive mode is False, only the files and
457-- directories directly under the watched directory are monitored, contents of
458-- subdirectories are not monitored. Monitoring starts from the current time
459-- onwards. The paths are specified as UTF-8 encoded 'Array' of 'Word8'.
460--
461-- /Symbolic Links:/ If the pathname to be watched is a symbolic link then
462-- watch the target of the symbolic link instead of the symbolic link itself.
463-- Note that the path location in the events is through the original symbolic
464-- link path rather than the resolved path.
465--
466-- @
467-- watchWith
468-- ('setAttrsModified' On . 'setLastWriteTimeModified' Off)
469-- [Array.fromList "dir"]
470-- @
471--
472-- /Pre-release/
473--
474watchWith :: (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
475watchWith f paths =
476 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
478 where
480 before = return $ pathsToHandles (utf8ToStringList paths) $ f defaultConfig
481 after = liftIO . closePathHandleStream
483-- | Same as 'watchWith' using 'defaultConfig' and recursive mode.
484--
485-- >>> watchRecursive = watchWith (setRecursiveMode On)
486--
487-- /Pre-release/
488--
489watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
490watchRecursive = watchWith (setRecursiveMode On)
492-- | Same as 'watchWith' using defaultConfig and non-recursive mode.
493--
494-- >>> watch = watchWith id
495--
496-- /Pre-release/
497--
498watch :: NonEmpty (Array Word8) -> SerialT IO Event
499watch = watchWith id
501getFlag :: DWORD -> Event -> Bool
502getFlag mask Event{..} = eventFlags == mask
504-- XXX Change the type to Array Word8 to make it compatible with other APIs.
505--
506-- | Get the file system object path for which the event is generated, relative
507-- to the watched root. The path is a UTF-8 encoded array of bytes.
508--
509-- /Pre-release/
510--
511getRelPath :: Event -> Array Word8
512getRelPath Event{..} = (UTF8.toArray . UTF8.pack) eventRelPath
514-- XXX Change the type to Array Word8 to make it compatible with other APIs.
515--
516-- | Get the watch root directory to which this event belongs.
517--
518-- /Pre-release/
519--
520getRoot :: Event -> Array Word8
521getRoot Event{..} = (UTF8.toArray . UTF8.pack) eventRootPath
523-- | Get the absolute file system object path for which the event is generated.
524--
525-- When the watch root is a symlink, the absolute path returned is via the
526-- original symlink and not through the resolved path.
527--
528-- /Pre-release/
529--
530getAbsPath :: Event -> Array Word8
531getAbsPath ev = getRoot ev <> backSlash <> getRelPath ev
532 where backSlash = A.fromList [ fromIntegral (ord '\\') ]
534-- XXX need to document the exact semantics of these.
535--
536-- | This event is generated when a file or directory is created in a watched
537-- directory or directory tree when in recursive watch mode. Creating a hard
538-- link also generates this event.
539--
540-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
541--
542-- /Pre-release/
543--
544isCreated :: Event -> Bool
545isCreated = getFlag fILE_ACTION_ADDED
547-- | This event is generated when a file or directory is deleted from the
548-- watched directory or directory tree when in recursive mode. This event is
549-- generated even when a hard link is deleted.
550--
551-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
552--
553-- /Pre-release/
554--
555isDeleted :: Event -> Bool
556isDeleted = getFlag fILE_ACTION_REMOVED
558-- | Generated for the original path when an object is moved from under a
559-- monitored directory.
560--
561-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
562--
563-- /Pre-release/
564--
565isMovedFrom :: Event -> Bool
566isMovedFrom = getFlag fILE_ACTION_RENAMED_OLD_NAME
568-- | Generated for the new path when an object is moved under a monitored
569-- directory.
570--
571-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
572--
573-- /Pre-release/
574--
575isMovedTo :: Event -> Bool
576isMovedTo = getFlag fILE_ACTION_RENAMED_NEW_NAME
578-- | Generated for a path that is moved from or moved to the monitored
579-- directory.
580--
581-- >>> isMoved ev = isMovedFrom ev || isMovedTo ev
582--
583-- /Occurs when either 'setFileNameEvents' or 'setDirNameEvents' is enabled/
584-- /Occurs only for an object inside the watched directory/
585--
586-- /Pre-release/
587--
588isMoved :: Event -> Bool
589isMoved ev = isMovedFrom ev || isMovedTo ev
591-- XXX This event is generated only for files and not directories?
592--
593-- | This event occurs when a file or directory contents, timestamps or
594-- attributes are modified. Since it can occur on multiple changes, you may
595-- have to check the attributes to know what exactly changed when multiple type
596-- of modified events are enabled.
597--
598-- In non-recursive mode this event does not occur for directories. In
599-- recursive mode this event occurs for the parent directory if a file or
600-- directory inside it is created or renamed.
601--
602-- /Occurs when one of the @set*Modified@ events is enabled/
603--
604-- /Pre-release/
605--
606isModified :: Event -> Bool
607isModified = getFlag fILE_ACTION_MODIFIED
609-- | If the kernel event buffer overflows, entire contents of the buffer are
610-- discarded, therefore, events are lost. The user application must scan
611-- everything under the watched paths to know the current state of the file
612-- system tree.
613--
614-- /Pre-release/
615--
616isEventsLost :: Event -> Bool
617isEventsLost Event{..} = totalBytes == 0
619-------------------------------------------------------------------------------
620-- Debugging
621-------------------------------------------------------------------------------
623-- | Convert an 'Event' record to a String representation.
624showEvent :: Event -> String
625showEvent ev@Event{..} =
626 "--------------------------"
627 ++ "\nRoot = " ++ utf8ToString (getRoot ev)
628 ++ "\nPath = " ++ utf8ToString (getRelPath ev)
629 ++ "\ngetAbsPath = " ++ utf8ToString (getAbsPath ev)
630 ++ "\nFlags " ++ show eventFlags
631 ++ showev isEventsLost "Overflow"
632 ++ showev isCreated "Created"
633 ++ showev isDeleted "Deleted"
634 ++ showev isModified "Modified"
635 ++ showev isMovedFrom "MovedFrom"
636 ++ showev isMovedTo "MovedTo"
637 ++ "\n"
639 where showev f str = if f ev then "\n" ++ str else ""