@@ -18,8 +18,8 @@ module Glean.Impl.ConfigProvider (
1818
1919import Control.Concurrent
2020import Control.Exception
21+ import Data.Maybe (fromMaybe )
2122import qualified Data.ByteString as ByteString
22- import qualified Data.ByteString.Char8 as BC
2323import Data.ByteString (ByteString )
2424import Data.HashMap.Strict (HashMap )
2525import qualified Data.HashMap.Strict as HashMap
@@ -29,17 +29,22 @@ import Options.Applicative
2929import System.Directory
3030import System.FilePath
3131import System.IO.Error
32- import System.INotify
32+
33+ import qualified System.FSNotify as FSNotify
3334
3435import Util.Control.Exception
36+ import Util.Concurrent
3537
3638import Glean.Util.ConfigProvider
3739
3840data ConfigAPI = ConfigAPI
39- { opts :: LocalConfigOptions
40- , inotify :: INotify
41+ { canonConfigDir :: FilePath
42+ , opts :: LocalConfigOptions
43+ -- ^ Canonicalized configuration directory (since the file watcher will
44+ -- likely canonicalize paths).
45+ , watchManager :: FSNotify. WatchManager
4146 , subscriptions ::
42- MVar (HashMap ConfigPath ( WatchDescriptor , [ByteString -> IO () ]))
47+ IO ( MVar (HashMap ConfigPath [ByteString -> IO () ]))
4348 }
4449
4550newtype LocalConfigOptions = LocalConfigOptions
@@ -57,6 +62,22 @@ newtype ConfigProviderException = ConfigProviderException Text
5762
5863instance Exception ConfigProviderException
5964
65+ -- | Whether to accept a FS event for a given path
66+ acceptEvent :: FSNotify. Event -> Bool
67+ acceptEvent (FSNotify. Added _path _time FSNotify. IsFile ) = True
68+ acceptEvent (FSNotify. Modified _path _time FSNotify. IsFile ) = True
69+ -- Included for documentation of intent
70+ acceptEvent (FSNotify. Removed _path _time _isDir) = False
71+ acceptEvent (FSNotify. ModifiedAttributes _path _time _isDir) = False
72+ acceptEvent _ = False
73+
74+ onEvent :: MVar (HashMap ConfigPath [ByteString -> IO () ]) -> FilePath -> IO ()
75+ onEvent subs path = do
76+ callbacks <- fromMaybe [] . HashMap. lookup (Text. pack path) <$> readMVar subs
77+ contents <- ByteString. readFile path
78+ mapM_ ($ contents) callbacks
79+ `catchAll` \ _ -> return ()
80+
6081instance ConfigProvider ConfigAPI where
6182 configOptions = do
6283 configDir <- optional $ strOption
@@ -70,46 +91,42 @@ instance ConfigProvider ConfigAPI where
7091 defaultConfigOptions = LocalConfigOptions { configDir = Nothing }
7192
7293 withConfigProvider opts f =
73- withINotify $ \ inotify -> do
94+ FSNotify. withManager $ \ watchManager -> do
7495 subs <- newMVar HashMap. empty
75- f (ConfigAPI opts inotify subs)
96+ -- fsnotify seems to give us canonicalized absolute paths back; we would
97+ -- like to look things up by the paths it gives us, so we need to have
98+ -- our own paths be canonicalized and absolute as well.
99+ canonConfigDir <- canonicalizePath =<< getDir opts
100+ -- Defer watcher startup until someone actually subscribes to an event
101+ -- (notably, proving that the config directory actually exists so that we
102+ -- can watch it, as watching a nonexistent directory on Linux is an
103+ -- error).
104+ subs' <- cacheSuccess
105+ (subs <$ FSNotify. watchTree watchManager canonConfigDir acceptEvent (\ ev -> onEvent subs (FSNotify. eventPath ev)))
106+ let cfg = ConfigAPI canonConfigDir opts watchManager subs'
107+ f cfg
76108
77109 type Subscription ConfigAPI = LocalSubscription
78110
79111 subscribe cfg@ ConfigAPI {.. } path updated deserializer = do
80112 a <- get cfg path deserializer
81113 updated a
82- dir <- getDir opts
83- modifyMVar_ subscriptions $ \ hm -> do
84- let
85- changed contents =
86- deserialize path deserializer contents >>= updated
87- case HashMap. lookup path hm of
88- Just (watch, others) ->
89- return $ HashMap. insert path (watch, changed: others) hm
90- Nothing -> do
91- let file = BC. pack $ dir </> Text. unpack path
92- watch <- addWatch inotify [Modify ,MoveIn ,Create ] file $ \ _events -> do
93- callbacks <- withMVar subscriptions $ \ hm -> do
94- case HashMap. lookup path hm of
95- Nothing -> return []
96- Just (_, callbacks) -> return callbacks
97- contents <- ByteString. readFile (dir </> Text. unpack path)
98- mapM_ ($ contents) callbacks
99- `catchAll` \ _ -> return ()
100- return $ HashMap. insert path (watch, [changed]) hm
114+ let absPath = Text. pack $ canonConfigDir </> Text. unpack path
115+ subscriptions >>= \ subs -> modifyMVar_ subs $ \ hm ->
116+ let changed contents =
117+ deserialize path deserializer contents >>= updated
118+ in pure $ HashMap. insertWith (<>) absPath [changed] hm
101119 return LocalSubscription
102120
103121 cancel _ _ = return () -- unimplemented for now
104122
105123 get ConfigAPI {.. } path deserializer = do
106- dir <- getDir opts
107- contents <- ByteString. readFile (dir </> Text. unpack path)
124+ contents <- ByteString. readFile (canonConfigDir </> Text. unpack path)
108125 `catch` \ e ->
109126 if isDoesNotExistError e
110127 then throwIO $ ConfigProviderException $
111128 " no config for " <> path <> " at " <>
112- Text. pack (dir </> Text. unpack path)
129+ Text. pack (canonConfigDir </> Text. unpack path)
113130 else
114131 throwIO e
115132 deserialize path deserializer contents
0 commit comments