fix: catch IOExceptions in reReadRecipe and wrap watchDir callback for resilience

This commit is contained in:
2026-05-19 23:40:13 -04:00
parent 956a4bc7d7
commit ce9062a4b3
+30 -17
View File
@@ -1,4 +1,5 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- | WAI application for the Roux recipe server. {- | WAI application for the Roux recipe server.
@@ -10,7 +11,7 @@ module Roux.Server (
) where ) where
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Exception (SomeException, catch) import Control.Exception (IOException, SomeException, catch, try)
import Control.Monad (forever, when) import Control.Monad (forever, when)
import Data.ByteString.Lazy (ByteString) import Data.ByteString.Lazy (ByteString)
import Data.Char (toLower) import Data.Char (toLower)
@@ -76,9 +77,16 @@ watchRecipes dir state = do
let cfg = defaultConfig{confWatchMode = WatchModeOS} let cfg = defaultConfig{confWatchMode = WatchModeOS}
withManagerConf cfg $ \mgr -> do withManagerConf cfg $ \mgr -> do
_stopListening <- watchDir mgr dir (const True) $ \ev -> _stopListening <- watchDir mgr dir (const True) $ \ev ->
when (eventIsDirectory ev == IsFile && ".cook" `isSuffixOf` eventPath ev) $ do catch
let path = makeRelative dir (eventPath ev) ( do
reReadRecipe dir path state when (eventIsDirectory ev == IsFile && ".cook" `isSuffixOf` eventPath ev) $ do
let path = makeRelative dir (eventPath ev)
reReadRecipe dir path state
)
( \e ->
putStrLn $
"[roux] watchDir callback error: " <> show (e :: SomeException)
)
-- Keep the thread alive (watchDir runs the callback on the manager thread) -- Keep the thread alive (watchDir runs the callback on the manager thread)
forever $ threadDelay 1000000 forever $ threadDelay 1000000
@@ -89,22 +97,27 @@ reReadRecipe :: FilePath -> FilePath -> IORef (Map FilePath Idx.RecipeInfo) -> I
reReadRecipe dir path state = do reReadRecipe dir path state = do
let key = map toLower (stripCookExt path) let key = map toLower (stripCookExt path)
let fullPath = dir </> path let fullPath = dir </> path
exists <- doesFileExist fullPath result <- try (doesFileExist fullPath)
if not exists case result of
then do Left (_ :: IOException) -> do
putStrLn $ "[roux] IO error checking " <> path <> ", skipping"
Right False -> do
atomicModifyIORef' state $ \m -> atomicModifyIORef' state $ \m ->
(Map.delete key m, ()) (Map.delete key m, ())
putStrLn $ "[roux] removed " <> path putStrLn $ "[roux] removed " <> path
else do Right True -> do
info <- Idx.loadRecipe dir path content <- try (Idx.loadRecipe dir path)
case Idx.riRecipe info of case content of
Left err -> do Left (_ :: IOException) -> do
putStrLn $ "[roux] parse error in " <> path <> ": " <> err putStrLn $ "[roux] IO error reading " <> path <> ", keeping previous version"
putStrLn $ "[roux] keeping previous version" Right info -> case Idx.riRecipe info of
Right _ -> do Left err -> do
atomicModifyIORef' state $ \m -> putStrLn $ "[roux] parse error in " <> path <> ": " <> err
(Map.insert key info m, ()) putStrLn $ "[roux] keeping previous version"
putStrLn $ "[roux] updated " <> path Right _ -> do
atomicModifyIORef' state $ \m ->
(Map.insert key info m, ())
putStrLn $ "[roux] updated " <> path
-- | Route an incoming request to the appropriate handler. -- | Route an incoming request to the appropriate handler.
handleRequest :: IORef (Map FilePath Idx.RecipeInfo) -> Wai.Application handleRequest :: IORef (Map FilePath Idx.RecipeInfo) -> Wai.Application