{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -- | Controls the file system watching in the daemon. The file system watching must run in a -- separate process to prevent blocking because of file operations interfering with watch. module Language.Haskell.Tools.Daemon.Watch where import Control.Concurrent import Control.Exception (catches) import Control.Monad import Control.Monad.State.Strict import qualified Data.Aeson as A () import Data.Maybe (Maybe(..), catMaybes, isNothing) import Data.Tuple (swap) import GhcMonad (Session(..), reflectGhc) import System.Environment (getExecutablePath) import System.FSWatch.Repr (WatchProcess(..), PE(..)) import System.FSWatch.Slave (waitNotifies, createWatchProcess) import System.FilePath import System.IO (IO, FilePath) import Language.Haskell.Tools.Daemon.ErrorHandling (userExceptionHandlers, exceptionHandlers) import Language.Haskell.Tools.Daemon.Protocol import Language.Haskell.Tools.Daemon.State (DaemonSessionState) import Language.Haskell.Tools.Daemon.Update (updateForFileChanges) -- | Starts the watch process and a thread that receives notifications from it. The notification -- thread will invoke updates on the daemon state to re-load files. createWatchProcess' :: Maybe FilePath -> Session -> MVar DaemonSessionState -> MVar [Marker] -> (ResponseMsg -> IO ()) -> IO (Maybe WatchProcess, [ThreadId]) createWatchProcess' watchExePath ghcSess daemonSess warnMVars upClient = do exePath <- case watchExePath of Just exe -> return exe Nothing -> guessExePath process <- createWatchProcess exePath 500 initProcess process where initProcess process = do reloaderThread <- forkIO $ forever $ void $ do changes <- waitForChanges process putStrLn $ "changes: " ++ show changes let changedFiles = catMaybes $ map getModifiedFile changes addedFiles = catMaybes $ map getAddedFile changes removedFiles = catMaybes $ map getRemovedFile changes reloadAction = updateForFileChanges upClient warnMVars addedFiles changedFiles removedFiles handlers = userExceptionHandlers (upClient . ErrorMessage) (\err hint -> upClient (CompilationProblem err hint)) ++ exceptionHandlers (return ()) (upClient . ErrorMessage) when (length changedFiles + length addedFiles + length removedFiles > 0) (void (modifyMVar daemonSess (\st -> swap <$> reflectGhc (runStateT reloadAction st) ghcSess)) `catches` handlers) return $ (Just process, [reloaderThread]) waitForChanges process = do changes <- waitNotifies process refactoring <- isNothing <$> tryReadMVar daemonSess -- if a refactoring is in progress, we should wait for all the changes to appear if refactoring then (changes ++) <$> waitForChanges process else return changes getModifiedFile (Mod file) | takeExtension file `elem` sourceExtensions = Just file getModifiedFile _ = Nothing getAddedFile (Add file) | takeExtension file `elem` sourceExtensions = Just file getAddedFile _ = Nothing getRemovedFile (Rem file) | takeExtension file `elem` sourceExtensions = Just file getRemovedFile _ = Nothing sourceExtensions = [ ".hs", ".hs-boot", ".cabal" ] guessExePath = do exePath <- getExecutablePath return $ takeDirectory exePath "hfswatch" -- | Stops the watch process and all threads associated with it. stopWatch :: WatchProcess -> [ThreadId] -> IO () stopWatch WatchProcess{..} threads = do forM threads killThread wShutdown