-------------------------------------------------------------------------------- {-# LANGUAGE CPP #-} module Hakyll.Preview.Poll ( watchUpdates ) where -------------------------------------------------------------------------------- import Control.Concurrent (forkIO) import Control.Concurrent.MVar (newEmptyMVar, takeMVar, tryPutMVar) import Control.Exception (AsyncException, fromException, handle, throw) import Control.Monad (forever, void, when) import System.Directory (canonicalizePath) import System.FilePath (pathSeparators) import qualified System.FSNotify as FSNotify #ifdef mingw32_HOST_OS import Control.Concurrent (threadDelay) import Control.Exception (IOException, throw, try) import System.Directory (doesFileExist) import System.Exit (exitFailure) import System.FilePath (()) import System.IO (Handle, IOMode (ReadMode), hClose, openFile) import System.IO.Error (isPermissionError) #endif -------------------------------------------------------------------------------- import Hakyll.Core.Configuration import Hakyll.Core.Identifier import Hakyll.Core.Identifier.Pattern -------------------------------------------------------------------------------- -- | A thread that watches for updates in a 'providerDirectory' and recompiles -- a site as soon as any changes occur watchUpdates :: Configuration -> IO Pattern -> IO () watchUpdates conf update = do let providerDir = providerDirectory conf shouldBuild <- newEmptyMVar pattern <- update fullProviderDir <- canonicalizePath $ providerDirectory conf manager <- FSNotify.startManager let allowed event = do -- Absolute path of the changed file. This must be inside provider -- dir, since that's the only dir we're watching. let path = FSNotify.eventPath event relative = dropWhile (`elem` pathSeparators) $ drop (length fullProviderDir) path identifier = fromFilePath relative shouldIgnore <- shouldIgnoreFile conf path return $ not shouldIgnore && matches pattern identifier -- This thread continually watches the `shouldBuild` MVar and builds -- whenever a value is present. _ <- forkIO $ forever $ do event <- takeMVar shouldBuild handle (\e -> case fromException e of Nothing -> putStrLn (show e) Just async -> throw (async :: AsyncException)) (update' event providerDir) -- Send an event whenever something occurs so that the thread described -- above will do a build. void $ FSNotify.watchTree manager providerDir (not . isRemove) $ \event -> do allowed' <- allowed event when allowed' $ void $ tryPutMVar shouldBuild event where #ifndef mingw32_HOST_OS update' _ _ = void update #else update' event provider = do let path = provider FSNotify.eventPath event -- on windows, a 'Modified' event is also sent on file deletion fileExists <- doesFileExist path when fileExists . void $ waitOpen path ReadMode (\_ -> update) 10 -- continuously attempts to open the file in between sleep intervals -- handler is run only once it is able to open the file waitOpen :: FilePath -> IOMode -> (Handle -> IO r) -> Integer -> IO r waitOpen _ _ _ 0 = do putStrLn "[ERROR] Failed to retrieve modified file for regeneration" exitFailure waitOpen path mode handler retries = do res <- try $ openFile path mode :: IO (Either IOException Handle) case res of Left ex -> if isPermissionError ex then do threadDelay 100000 waitOpen path mode handler (retries - 1) else throw ex Right h -> do handled <- handler h hClose h return handled #endif -------------------------------------------------------------------------------- isRemove :: FSNotify.Event -> Bool isRemove (FSNotify.Removed {}) = True isRemove _ = False