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