on ef2ea11 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.fromCString\# "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.Functor.Identity (runIdentity)
96import Data.List.NonEmpty (NonEmpty)
97import Data.Word (Word8)
98import Foreign.C.String (peekCWStringLen)
99import Foreign.Marshal.Alloc (alloca, allocaBytes)
100import Foreign.Storable (peekByteOff)
101import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr, nullFunPtr, plusPtr)
102import Streamly.Prelude (SerialT, parallel)
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.Internal.Data.Stream.IsStream as S
123import qualified Streamly.Internal.Unicode.Stream as U
124import qualified Streamly.Internal.Unicode.Utf8 as UTF8
125import qualified Streamly.Internal.Data.Array.Foreign as A
126import Streamly.Internal.Data.Array.Foreign (Array)
128-- | Watch configuration, used to specify the events of interest and the
129-- behavior of the watch.
130--
131-- /Pre-release/
132--
133data Config = Config
134 { watchRec :: BOOL
135 , createFlags :: DWORD
136 }
138-------------------------------------------------------------------------------
139-- Boolean settings
140-------------------------------------------------------------------------------
142-- | Whether a setting is 'On' or 'Off'.
143--
144-- /Pre-release/
145--
146data Switch = On | Off deriving (Show, Eq)
148setFlag :: DWORD -> Switch -> Config -> Config
149setFlag mask status cfg@Config{..} =
150 let flags =
151 case status of
152 On -> createFlags .|. mask
153 Off -> createFlags .&. complement mask
154 in cfg {createFlags = flags}
156-- | Set watch event on directory recursively.
157--
158-- /default: Off/
159--
160-- /Pre-release/
161--
162setRecursiveMode :: Switch -> Config -> Config
163setRecursiveMode rec cfg@Config{} = cfg {watchRec = rec == On}
165-- | Generate notify events on file create, rename or delete.
166--
167-- From Windows API documentation: Any file name change in the watched
168-- directory or subtree causes a change notification wait operation to return.
169-- Changes include renaming, creating, or deleting a file.
170--
171-- /default: On/
172--
173-- /Pre-release/
174--
175setFileNameEvents :: Switch -> Config -> Config
176setFileNameEvents = setFlag fILE_NOTIFY_CHANGE_FILE_NAME
178-- | Generate notify events on directory create, rename or delete.
179--
180-- From Windows API documentaiton: Any directory-name change in the watched
181-- directory or subtree causes a change notification wait operation to return.
182-- Changes include creating or deleting a directory.
183--
184-- /default: On/
185--
186-- /Pre-release/
187--
188setDirNameEvents :: Switch -> Config -> Config
189setDirNameEvents = setFlag fILE_NOTIFY_CHANGE_DIR_NAME
191-- | Generate an 'isModified' event on any attribute change in the watched
192-- directory or subtree.
193--
194-- /default: Off/
195--
196-- /Pre-release/
197--
198setAttrsModified :: Switch -> Config -> Config
199setAttrsModified = setFlag fILE_NOTIFY_CHANGE_ATTRIBUTES
201-- | Generate an 'isModified' event when the file size is changed.
202--
203-- From Windows API documentation: Any file-size change in the watched
204-- directory or subtree causes a change notification wait operation to return.
205-- The operating system detects a change in file size only when the file is
206-- written to the disk. For operating systems that use extensive caching,
207-- detection occurs only when the cache is sufficiently flushed.
208--
209-- /default: Off/
210--
211-- /Pre-release/
212--
213setSizeModified :: Switch -> Config -> Config
214setSizeModified = setFlag fILE_NOTIFY_CHANGE_SIZE
216-- | Generate an 'isModified' event when the last write timestamp of the file
217-- inode is changed.
218--
219-- From Windows API documentation: Any change to the last write-time of files
220-- in the watched directory or subtree causes a change notification wait
221-- operation to return. The operating system detects a change to the last
222-- write-time only when the file is written to the disk. For operating systems
223-- that use extensive caching, detection occurs only when the cache is
224-- sufficiently flushed.
225--
226-- /default: Off/
227--
228-- /Pre-release/
229--
230setLastWriteTimeModified :: Switch -> Config -> Config
231setLastWriteTimeModified = setFlag fILE_NOTIFY_CHANGE_LAST_WRITE
233-- | Generate an 'isModified' event when any security-descriptor change occurs
234-- in the watched directory or subtree.
235--
236-- /default: Off/
237--
238-- /Pre-release/
239--
240setSecurityModified :: Switch -> Config -> Config
241setSecurityModified = setFlag fILE_NOTIFY_CHANGE_SECURITY
243-- | Set all tunable events 'On' or 'Off'. Equivalent to setting:
244--
245-- * setFileNameEvents
246-- * setDirNameEvents
247-- * setAttrsModified
248-- * setSizeModified
249-- * setLastWriteTimeModified
250-- * setSecurityModified
251--
252-- /Pre-release/
253--
254setAllEvents :: Switch -> Config -> Config
255setAllEvents s =
256 setFileNameEvents s
257 . setDirNameEvents s
258 . setAttrsModified s
259 . setSizeModified s
260 . setLastWriteTimeModified s
261 . setSecurityModified s
263-- | The tunable events that are enabled by default are:
264--
265-- * setFileNameEvents On
266-- * setDirNameEvents On
267-- * setSizeModified On
268-- * setLastWriteTimeModified On
269--
270-- /Pre-release/
271--
272defaultConfig :: Config
273defaultConfig =
274 setFileNameEvents On
275 $ setDirNameEvents On
276 $ setSizeModified On
277 $ setLastWriteTimeModified On
278 $ Config {watchRec = False, createFlags = 0}
280getConfigFlag :: Config -> DWORD
281getConfigFlag Config{..} = createFlags
283getConfigRecMode :: Config -> BOOL
284getConfigRecMode Config{..} = watchRec
286data Event = Event
287 { eventFlags :: DWORD
288 , eventRelPath :: String
289 , eventRootPath :: String
290 , totalBytes :: DWORD
291 } deriving (Show, Ord, Eq)
293-- For reference documentation see:
294--
295-- See https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-file_notify_information
296data FILE_NOTIFY_INFORMATION = FILE_NOTIFY_INFORMATION
297 { fniNextEntryOffset :: DWORD
298 , fniAction :: DWORD
299 , fniFileName :: String
300 } deriving Show
302type LPOVERLAPPED_COMPLETION_ROUTINE =
303 FunPtr ((DWORD, DWORD, LPOVERLAPPED) -> IO ())
305-- | A handle for a watch.
306getWatchHandle :: FilePath -> IO (HANDLE, FilePath)
307getWatchHandle dir = do
308 h <- createFile dir
309 -- Access mode
310 fILE_LIST_DIRECTORY
311 -- Share mode
312 (fILE_SHARE_READ .|. fILE_SHARE_WRITE)
313 -- Security attributes
314 Nothing
315 -- Create mode, we want to look at an existing directory
316 oPEN_EXISTING
317 -- File attribute, NOT using OVERLAPPED since we work synchronously
318 fILE_FLAG_BACKUP_SEMANTICS
319 -- No template file
320 Nothing
321 return (h, dir)
323-- For reference documentation see:
324--
325-- See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
326-- Note that this API uses UTF-16 for file system paths:
327-- 1. https://docs.microsoft.com/en-us/windows/win32/intl/unicode-in-the-windows-api
328-- 2. https://docs.microsoft.com/en-us/windows/win32/intl/unicode
329foreign import ccall safe
330 "windows.h ReadDirectoryChangesW" c_ReadDirectoryChangesW ::
331 HANDLE
332 -> LPVOID
333 -> DWORD
334 -> BOOL
335 -> DWORD
336 -> LPDWORD
337 -> LPOVERLAPPED
338 -> LPOVERLAPPED_COMPLETION_ROUTINE
339 -> IO BOOL
341readDirectoryChangesW ::
342 HANDLE
343 -> Ptr FILE_NOTIFY_INFORMATION
344 -> DWORD
345 -> BOOL
346 -> FileNotificationFlag
347 -> LPDWORD
348 -> IO ()
349readDirectoryChangesW h buf bufSize wst f br =
350 let res = c_ReadDirectoryChangesW
351 h (castPtr buf) bufSize wst f br nullPtr nullFunPtr
352 in failIfFalse_ "ReadDirectoryChangesW" res
354peekFNI :: Ptr FILE_NOTIFY_INFORMATION -> IO FILE_NOTIFY_INFORMATION
355peekFNI buf = do
356 neof <- peekByteOff buf 0
357 acti <- peekByteOff buf 4
358 fnle <- peekByteOff buf 8
359 -- Note: The path is UTF-16 encoded C WChars, peekCWStringLen converts
360 -- UTF-16 to UTF-32 Char String
361 fnam <- peekCWStringLen
362 -- start of array
363 (buf `plusPtr` 12,
364 -- fnle is the length in *bytes*, and a WCHAR is 2 bytes
365 fromEnum (fnle :: DWORD) `div` 2)
366 return $ FILE_NOTIFY_INFORMATION neof acti fnam
368readChangeEvents ::
369 Ptr FILE_NOTIFY_INFORMATION -> String -> DWORD -> IO [Event]
370readChangeEvents pfni root bytesRet = do
371 fni <- peekFNI pfni
372 let entry = Event
373 { eventFlags = fniAction fni
374 , eventRelPath = fniFileName fni
375 , eventRootPath = root
376 , totalBytes = bytesRet
377 }
378 nioff = fromEnum $ fniNextEntryOffset fni
379 entries <-
380 if nioff == 0
381 then return []
382 else readChangeEvents (pfni `plusPtr` nioff) root bytesRet
383 return $ entry : entries
385readDirectoryChanges ::
386 String -> HANDLE -> Bool -> FileNotificationFlag -> IO [Event]
387readDirectoryChanges root h wst mask = do
388 let maxBuf = 63 * 1024
389 allocaBytes maxBuf $ \buffer -> do
390 alloca $ \bret -> do
391 readDirectoryChangesW h buffer (toEnum maxBuf) wst mask bret
392 bytesRet <- peekByteOff bret 0
393 readChangeEvents buffer root bytesRet
395-- XXX Try to get these from windows header files
396type FileAction = DWORD
398fILE_ACTION_ADDED :: FileAction
399fILE_ACTION_ADDED = 1
401fILE_ACTION_REMOVED :: FileAction
402fILE_ACTION_REMOVED = 2
404fILE_ACTION_MODIFIED :: FileAction
405fILE_ACTION_MODIFIED = 3
407fILE_ACTION_RENAMED_OLD_NAME :: FileAction
408fILE_ACTION_RENAMED_OLD_NAME = 4
410fILE_ACTION_RENAMED_NEW_NAME :: FileAction
411fILE_ACTION_RENAMED_NEW_NAME = 5
413eventStreamAggr :: (HANDLE, FilePath, Config) -> SerialT IO Event
414eventStreamAggr (handle, rootPath, cfg) = do
415 let recMode = getConfigRecMode cfg
416 flagMasks = getConfigFlag cfg
417 S.concatMap S.fromList $ S.repeatM
418 $ readDirectoryChanges rootPath handle recMode flagMasks
420pathsToHandles ::
421 NonEmpty FilePath -> Config -> SerialT IO (HANDLE, FilePath, Config)
422pathsToHandles paths cfg = do
423 let pathStream = S.fromList (NonEmpty.toList paths)
424 st2 = S.mapM getWatchHandle pathStream
425 S.map (\(h, f) -> (h, f, cfg)) st2
427-------------------------------------------------------------------------------
428-- Utilities
429-------------------------------------------------------------------------------
431utf8ToString :: Array Word8 -> FilePath
432utf8ToString = runIdentity . S.toList . U.decodeUtf8 . A.toStream
434utf8ToStringList :: NonEmpty (Array Word8) -> NonEmpty FilePath
435utf8ToStringList = NonEmpty.map utf8ToString
437-- | Close a Directory handle.
438--
439closePathHandleStream :: SerialT IO (HANDLE, FilePath, Config) -> IO ()
440closePathHandleStream = S.mapM_ (\(h, _, _) -> closeHandle h)
442-- XXX
443-- Document the path treatment for Linux/Windows/macOS modules.
444-- Remove the utf-8 encoding requirement of paths? It can be encoding agnostic
445-- "\" separated bytes, the application can decide what encoding to use.
446-- Instead of always using widechar (-W) APIs can we call the underlying APIs
447-- based on the configured code page?
448-- https://docs.microsoft.com/en-us/windows/uwp/design/globalizing/use-utf8-code-page
449--
450-- | Start monitoring a list of directory paths for file system events with the
451-- supplied configuration modifier operation over the' defaultConfig'. The
452-- paths could be directories or symbolic links to directories.
453--
454-- When recursive mode is True, the whole directory tree under the path is
455-- watched recursively. When recursive mode is False, only the files and
456-- directories directly under the watched directory are monitored, contents of
457-- subdirectories are not monitored. Monitoring starts from the current time
458-- onwards. The paths are specified as UTF-8 encoded 'Array' of 'Word8'.
459--
460-- /Symbolic Links:/ If the pathname to be watched is a symbolic link then
461-- watch the target of the symbolic link instead of the symbolic link itself.
462-- Note that the path location in the events is through the original symbolic
463-- link path rather than the resolved path.
464--
465-- @
466-- watchWith
467-- ('setAttrsModified' On . 'setLastWriteTimeModified' Off)
468-- [Array.fromCString\# "dir"#]
469-- @
470--
471-- /Pre-release/
472--
473watchWith :: (Config -> Config) -> NonEmpty (Array Word8) -> SerialT IO Event
474watchWith f paths =
475 S.bracket before after (S.concatMapWith parallel eventStreamAggr)
477 where
479 before = return $ pathsToHandles (utf8ToStringList paths) $ f defaultConfig
480 after = liftIO . closePathHandleStream
482-- | Same as 'watchWith' using 'defaultConfig' and recursive mode.
483--
484-- >>> watchRecursive = watchWith (setRecursiveMode On)
485--
486-- /Pre-release/
487--
488watchRecursive :: NonEmpty (Array Word8) -> SerialT IO Event
489watchRecursive = watchWith (setRecursiveMode On)
491-- | Same as 'watchWith' using defaultConfig and non-recursive mode.
492--
493-- >>> watch = watchWith id
494--
495-- /Pre-release/
496--
497watch :: NonEmpty (Array Word8) -> SerialT IO Event
498watch = watchWith id
500getFlag :: DWORD -> Event -> Bool
501getFlag mask Event{..} = eventFlags == mask
503-- XXX Change the type to Array Word8 to make it compatible with other APIs.
504--
505-- | Get the file system object path for which the event is generated, relative
506-- to the watched root. The path is a UTF-8 encoded array of bytes.
507--
508-- /Pre-release/
509--
510getRelPath :: Event -> Array Word8
511getRelPath Event{..} = (UTF8.toArray . UTF8.pack) eventRelPath
513-- XXX Change the type to Array Word8 to make it compatible with other APIs.
514--
515-- | Get the watch root directory to which this event belongs.
516--
517-- /Pre-release/
518--
519getRoot :: Event -> Array Word8
520getRoot Event{..} = (UTF8.toArray . UTF8.pack) eventRootPath
522-- | Get the absolute file system object path for which the event is generated.
523--
524-- When the watch root is a symlink, the absolute path returned is via the
525-- original symlink and not through the resolved path.
526--
527-- /Pre-release/
528--
529getAbsPath :: Event -> Array Word8
530getAbsPath ev = getRoot ev <> A.fromCString# "\\"# <> getRelPath ev
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 ""