Skip to content

Commit 1e5fc92

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 1e5fc92

File tree

2 files changed

+60
-33
lines changed

2 files changed

+60
-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.4.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: 41 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,21 @@ 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
3536

3637
import Glean.Util.ConfigProvider
3738

3839
data ConfigAPI = ConfigAPI
39-
{ opts :: LocalConfigOptions
40-
, inotify :: INotify
40+
{ canonConfigDir :: FilePath
41+
, opts :: LocalConfigOptions
42+
-- ^ Canonicalized configuration directory (since the file watcher will
43+
-- likely canonicalize paths).
44+
, watchManager :: FSNotify.WatchManager
4145
, subscriptions ::
42-
MVar (HashMap ConfigPath (WatchDescriptor, [ByteString -> IO ()]))
46+
MVar (HashMap ConfigPath [ByteString -> IO ()])
4347
}
4448

4549
newtype LocalConfigOptions = LocalConfigOptions
@@ -57,6 +61,23 @@ newtype ConfigProviderException = ConfigProviderException Text
5761

5862
instance Exception ConfigProviderException
5963

64+
-- | Whether to accept a FS event for a given path
65+
acceptEvent :: FSNotify.Event -> Bool
66+
acceptEvent (FSNotify.Added _path _time FSNotify.IsFile) = True
67+
acceptEvent (FSNotify.Modified _path _time FSNotify.IsFile) = True
68+
-- Included for documentation of intent
69+
acceptEvent (FSNotify.Removed _path _time _isDir) = False
70+
acceptEvent (FSNotify.ModifiedAttributes _path _time _isDir) = False
71+
acceptEvent _ = False
72+
73+
onEvent :: ConfigAPI -> FilePath -> IO ()
74+
onEvent ConfigAPI{..} path = do
75+
-- Maybe weird symlink things going on? We don't know what to do, in any case
76+
callbacks <- fromMaybe [] . HashMap.lookup (Text.pack path) <$> readMVar subscriptions
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,37 @@ 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 paths back; we would like to
97+
-- reason based on prefixes of them, so we need to canonicalize here.
98+
canonConfigDir <- canonicalizePath =<< getDir opts
99+
let cfg = ConfigAPI canonConfigDir opts watchManager subs
100+
_stopListening <-
101+
FSNotify.watchTree watchManager canonConfigDir acceptEvent (\ev -> onEvent cfg (FSNotify.eventPath ev))
102+
f cfg
76103

77104
type Subscription ConfigAPI = LocalSubscription
78105

79106
subscribe cfg@ConfigAPI{..} path updated deserializer = do
80107
a <- get cfg path deserializer
81108
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
109+
let absPath = Text.pack $ canonConfigDir </> (Text.unpack path)
110+
modifyMVar_ subscriptions $ \hm ->
111+
let changed contents =
112+
deserialize path deserializer contents >>= updated
113+
in pure $ HashMap.insertWith (<>) absPath [changed] hm
101114
return LocalSubscription
102115

103116
cancel _ _ = return () -- unimplemented for now
104117

105118
get ConfigAPI{..} path deserializer = do
106-
dir <- getDir opts
107-
contents <- ByteString.readFile (dir </> Text.unpack path)
119+
contents <- ByteString.readFile (canonConfigDir </> Text.unpack path)
108120
`catch` \e ->
109121
if isDoesNotExistError e
110122
then throwIO $ ConfigProviderException $
111123
"no config for " <> path <> " at " <>
112-
Text.pack (dir </> Text.unpack path)
124+
Text.pack (canonConfigDir </> Text.unpack path)
113125
else
114126
throwIO e
115127
deserialize path deserializer contents

0 commit comments

Comments
 (0)