Skip to content

Commit 0b6568e

Browse files
committed
macOS: rewrite fs config provider using fsnotify
This means that we lose an hinotify dependency and replace it with fsnotify, which supports all platforms (and also can poll if needed!) and actually *uses* hinotify internally. With this change, Glean builds and has basic functionality working on my machine on macOS!!
1 parent 59789d8 commit 0b6568e

File tree

2 files changed

+65
-33
lines changed

2 files changed

+65
-33
lines changed

glean.cabal.in

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,11 @@ common deps
195195
ghc-prim >=0.5.2.0 && <0.11,
196196
parsec ^>=3.1.13.0,
197197
haxl >= 2.1.2.0 && < 2.6,
198-
hinotify ^>= 0.4.1
198+
fsnotify ^>= 0.4.2.0
199+
199200

200201
common hsc2hs-cpp
201-
hsc2hs-options: --cc=g++ --lflag=-lstdc++ --cflag=-D__HSC2HS__=1 --cflag=-std=c++17
202+
hsc2hs-options: --cc=c++ --lflag=-lstdc++ --cflag=-D__HSC2HS__=1 --cflag=-std=c++17
202203

203204
common thrift-server
204205
if flag(fbthrift)
@@ -394,8 +395,9 @@ library rts
394395
glean/rts/thrift.h
395396
glean/rts/timer.h
396397
glean/rts/validate.h
397-
-- __atomic_is_lock_free missing with clang
398-
extra-libraries: atomic
398+
if os(linux)
399+
-- __atomic_is_lock_free missing with clang
400+
extra-libraries: atomic
399401
pkgconfig-depends: libunwind, libglog, icu-uc, gflags, libxxhash
400402

401403
library storage
@@ -1950,6 +1952,10 @@ common angle-test
19501952
glean:schema,
19511953
glean:typed
19521954

1955+
common util-test
1956+
build-depends:
1957+
glean:stubs,
1958+
19531959
test-suite angle-test-angle
19541960
import: test, angle-test
19551961
type: exitcode-stdio-1.0
@@ -2175,6 +2181,15 @@ test-suite catalog
21752181
quickcheck-io,
21762182
glean:util
21772183

2184+
test-suite configprovider
2185+
import: test, util-test
2186+
type: exitcode-stdio-1.0
2187+
main-is: ConfigProviderTest.hs
2188+
ghc-options: -main-is ConfigProviderTest
2189+
hs-source-dirs: glean/util/tests
2190+
build-depends:
2191+
glean:util,
2192+
21782193
test-suite lifecycle
21792194
import: test
21802195
type: exitcode-stdio-1.0

glean/util/Glean/Impl/ConfigProvider.hs

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ module Glean.Impl.ConfigProvider (
1818

1919
import Control.Concurrent
2020
import Control.Exception
21+
import Data.Maybe (fromMaybe)
2122
import qualified Data.ByteString as ByteString
22-
import qualified Data.ByteString.Char8 as BC
2323
import Data.ByteString (ByteString)
2424
import Data.HashMap.Strict (HashMap)
2525
import qualified Data.HashMap.Strict as HashMap
@@ -29,17 +29,22 @@ import Options.Applicative
2929
import System.Directory
3030
import System.FilePath
3131
import System.IO.Error
32-
import System.INotify
32+
33+
import qualified System.FSNotify as FSNotify
3334

3435
import Util.Control.Exception
36+
import Util.Concurrent
3537

3638
import Glean.Util.ConfigProvider
3739

3840
data 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

4550
newtype LocalConfigOptions = LocalConfigOptions
@@ -57,6 +62,22 @@ newtype ConfigProviderException = ConfigProviderException Text
5762

5863
instance 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+
6081
instance 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

Comments
 (0)