{-# 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, 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
watchUpdates :: Configuration -> IO Pattern -> IO ()
watchUpdates :: Configuration -> IO Pattern -> IO ()
watchUpdates Configuration
conf IO Pattern
update = do
let providerDir :: FilePath
providerDir = Configuration -> FilePath
providerDirectory Configuration
conf
shouldBuild <- IO (MVar Event)
forall a. IO (MVar a)
newEmptyMVar
pattern <- update
fullProviderDir <- canonicalizePath providerDir
manager <- FSNotify.startManager
checkIgnore <- shouldWatchIgnore conf
let allowed Event
event = do
let path :: FilePath
path = Event -> FilePath
FSNotify.eventPath Event
event
relative :: FilePath
relative = (Char -> Bool) -> FilePath -> FilePath
forall a. (a -> Bool) -> [a] -> [a]
dropWhile (Char -> FilePath -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` FilePath
pathSeparators) (FilePath -> FilePath) -> FilePath -> FilePath
forall a b. (a -> b) -> a -> b
$
Int -> FilePath -> FilePath
forall a. Int -> [a] -> [a]
drop (FilePath -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length FilePath
fullProviderDir) FilePath
path
identifier :: Identifier
identifier = FilePath -> Identifier
fromFilePath FilePath
relative
shouldIgnore <- FilePath -> IO Bool
checkIgnore FilePath
path
return $ not shouldIgnore && matches pattern identifier
_ <- forkIO $ forever $ do
event <- takeMVar shouldBuild
handle
(\SomeException
e -> case SomeException -> Maybe AsyncException
forall e. Exception e => SomeException -> Maybe e
fromException SomeException
e of
Maybe AsyncException
Nothing -> FilePath -> IO ()
putStrLn (SomeException -> FilePath
forall a. Show a => a -> FilePath
show SomeException
e)
Just AsyncException
async -> AsyncException -> IO ()
forall a e. (HasCallStack, Exception e) => e -> a
throw (AsyncException
async :: AsyncException))
(update' event providerDir)
void $ FSNotify.watchTree manager providerDir (not . isRemove) $ \Event
event -> do
allowed' <- Event -> IO Bool
allowed Event
event
when allowed' $ void $ tryPutMVar shouldBuild event
where
#ifndef mingw32_HOST_OS
update' :: p -> p -> IO ()
update' p
_ p
_ = IO Pattern -> IO ()
forall (f :: * -> *) a. Functor f => f a -> f ()
void IO Pattern
update
#else
update' event provider = do
let path = provider </> FSNotify.eventPath event
fileExists <- doesFileExist path
when fileExists . void $ waitOpen path ReadMode (\_ -> update) 10
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 :: ActionPredicate
isRemove (FSNotify.Removed {}) = Bool
True
isRemove Event
_ = Bool
False