-
Notifications
You must be signed in to change notification settings - Fork 48
Expand file tree
/
Copy pathLinux.hs
More file actions
180 lines (151 loc) · 8.34 KB
/
Copy pathLinux.hs
File metadata and controls
180 lines (151 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
--
-- Copyright (c) 2012 Mark Dittmer - http://www.markdittmer.org
-- Developed for a Google Summer of Code project - http://gsoc2012.markdittmer.org
--
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module System.FSNotify.Linux (
FileListener(..)
, NativeManager
) where
import Control.Concurrent.MVar
import Control.Exception.Safe as E
import Control.Monad
import Data.Function
import Data.Monoid
import Data.String
import Data.Time.Clock (UTCTime)
import Data.Time.Clock.POSIX
import Prelude hiding (FilePath)
import System.FSNotify.Find
import System.FSNotify.Linux.Util
import System.FSNotify.Listener
import System.FSNotify.Types
import System.FilePath ((</>))
import qualified System.INotify as INo
import System.Posix.ByteString (RawFilePath)
import System.Posix.Files (getFileStatus, isDirectory, modificationTimeHiRes)
data INotifyListener = INotifyListener { listenerINotify :: INo.INotify }
type NativeManager = INotifyListener
data EventVarietyMismatchException = EventVarietyMismatchException deriving (Show, Typeable)
instance Exception EventVarietyMismatchException
fsnEvents :: RawFilePath -> UTCTime -> INo.Event -> IO [Event]
fsnEvents basePath' timestamp (INo.Attributes (boolToIsDirectory -> isDir) (Just raw)) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [ModifiedAttributes (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.Modified (boolToIsDirectory -> isDir) (Just raw)) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [Modified (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.Closed (boolToIsDirectory -> isDir) (Just raw) True) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [CloseWrite (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.Created (boolToIsDirectory -> isDir) raw) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [Added (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.MovedOut (boolToIsDirectory -> isDir) raw _cookie) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [Removed (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.MovedIn (boolToIsDirectory -> isDir) raw _cookie) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [Added (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp (INo.Deleted (boolToIsDirectory -> isDir) raw) = do
basePath <- fromRawFilePath basePath'
fromHinotifyPath raw >>= \name -> return [Removed (basePath </> name) timestamp isDir]
fsnEvents basePath' timestamp INo.DeletedSelf = do
basePath <- fromRawFilePath basePath'
return [WatchedDirectoryRemoved basePath timestamp IsDirectory]
fsnEvents _ _ INo.Ignored = return []
fsnEvents basePath' timestamp inoEvent = do
basePath <- fromRawFilePath basePath'
return [Unknown basePath timestamp IsFile (show inoEvent)]
handleInoEvent :: ActionPredicate -> EventCallback -> RawFilePath -> MVar Bool -> INo.Event -> IO ()
handleInoEvent actPred callback basePath watchStillExistsVar inoEvent = do
when (INo.DeletedSelf == inoEvent) $ modifyMVar_ watchStillExistsVar $ const $ return False
currentTime <- getCurrentTime
events <- fsnEvents basePath currentTime inoEvent
forM_ events $ \event -> when (actPred event) $ callback event
varieties :: [INo.EventVariety]
varieties = [INo.Create, INo.Delete, INo.MoveIn, INo.MoveOut, INo.Attrib, INo.Modify, INo.CloseWrite, INo.DeleteSelf]
instance FileListener INotifyListener () where
initSession _ = E.handle (\(e :: IOException) -> return $ Left $ fromString $ show e) $ do
inotify <- INo.initINotify
return $ Right $ INotifyListener inotify
killSession (INotifyListener {listenerINotify}) = INo.killINotify listenerINotify
listen _conf (INotifyListener {listenerINotify}) path actPred callback = do
rawPath <- toRawFilePath path
canonicalRawPath <- canonicalizeRawDirPath rawPath
watchStillExistsVar <- newMVar True
hinotifyPath <- rawToHinotifyPath canonicalRawPath
wd <- INo.addWatch listenerINotify varieties hinotifyPath (handleInoEvent actPred callback canonicalRawPath watchStillExistsVar)
return $
modifyMVar_ watchStillExistsVar $ \wse -> do
when wse $ INo.removeWatch wd
return False
listenRecursive _conf listener initialPath actPred callback = do
-- wdVar stores the list of created watch descriptors. We use it to
-- cancel the whole recursive listening task.
--
-- To avoid a race condition (when a new watch is added right after
-- we've stopped listening), we replace the MVar contents with Nothing
-- to signify that the listening task is cancelled, and no new watches
-- should be added.
wdVar <- newMVar (Just [])
let
removeWatches wds = forM_ wds $ \(wd, watchStillExistsVar) ->
modifyMVar_ watchStillExistsVar $ \wse -> do
when wse $
handle (\(e :: SomeException) -> putStrLn ("Error removing watch: " <> show wd <> " (" <> show e <> ")"))
(INo.removeWatch wd)
return False
stopListening = modifyMVar_ wdVar $ \x -> maybe (return ()) removeWatches x >> return Nothing
-- Add watches to this directory plus every sub-directory
rawInitialPath <- toRawFilePath initialPath
rawCanonicalInitialPath <- canonicalizeRawDirPath rawInitialPath
watchDirectoryRecursively listener wdVar actPred callback True rawCanonicalInitialPath
traverseAllDirs rawCanonicalInitialPath $ \subPath ->
watchDirectoryRecursively listener wdVar actPred callback False subPath
return stopListening
type RecursiveWatches = MVar (Maybe [(INo.WatchDescriptor, MVar Bool)])
watchDirectoryRecursively :: INotifyListener -> RecursiveWatches -> ActionPredicate -> EventCallback -> Bool -> RawFilePath -> IO ()
watchDirectoryRecursively listener@(INotifyListener {listenerINotify}) wdVar actPred callback isRootWatchedDir rawFilePath = do
modifyMVar_ wdVar $ \case
Nothing -> return Nothing
Just wds -> do
watchStillExistsVar <- newMVar True
hinotifyPath <- rawToHinotifyPath rawFilePath
wd <- INo.addWatch listenerINotify varieties hinotifyPath (handleRecursiveEvent rawFilePath actPred callback watchStillExistsVar isRootWatchedDir listener wdVar)
return $ Just ((wd, watchStillExistsVar):wds)
handleRecursiveEvent :: RawFilePath -> ActionPredicate -> EventCallback -> MVar Bool -> Bool -> INotifyListener -> RecursiveWatches -> INo.Event -> IO ()
handleRecursiveEvent baseDir actPred callback watchStillExistsVar isRootWatchedDir listener wdVar event = do
case event of
(INo.Created True hiNotifyPath) -> do
-- A new directory was created, so add recursive inotify watches to it
rawDirPath <- rawFromHinotifyPath hiNotifyPath
let newRawDir = baseDir <//> rawDirPath
timestampBeforeAddingWatch <- getPOSIXTime
watchDirectoryRecursively listener wdVar actPred callback False newRawDir
newDir <- fromRawFilePath newRawDir
-- Find all files/folders that might have been created *after* the timestamp, and hence might have been
-- missed by the watch
-- TODO: there's a chance of this generating double events, fix
files <- find False newDir -- TODO: expose the ability to set followSymlinks to True?
forM_ files $ \newPath -> do
fileStatus <- getFileStatus newPath
let modTime = modificationTimeHiRes fileStatus
when (modTime > timestampBeforeAddingWatch) $ do
let isDir = if isDirectory fileStatus then IsDirectory else IsFile
let addedEvent = (Added (newDir </> newPath) (posixSecondsToUTCTime timestampBeforeAddingWatch) isDir)
when (actPred addedEvent) $ callback addedEvent
_ -> return ()
-- If the watched directory was removed, mark the watch as already removed
case event of
INo.DeletedSelf -> modifyMVar_ watchStillExistsVar $ const $ return False
_ -> return ()
-- Forward the event. Ignore a DeletedSelf if we're not on the root directory,
-- since the watch above us will pick up the delete of that directory.
case event of
INo.DeletedSelf | not isRootWatchedDir -> return ()
_ -> handleInoEvent actPred callback baseDir watchStillExistsVar event