packages feed

hs-rs-notify-0.1.0.10: src/Notify.hs

{-|
Module      : Notify
Description : Filesystem notifications. This is a wrapper around rust's notify crate.

-}
module Notify
  ( Config(..)
  , State
  , watch
  , force
  , end
  ) where

import Control.Concurrent
import Control.Monad (when)
import Data.Foldable (null, traverse_)
import Data.Int
import qualified Data.Text as T
import Foreign.C.String
import Foreign.C.Types
import Foreign.ForeignPtr ()
import Foreign.Ptr
import Protolude hiding (State, force, state)
import System.FilePath
import System.Posix.Process
import System.Posix.Signals
import System.Posix.Types (ProcessID)
import System.Process ()

foreign import ccall "watch_for_changes" watchForChanges ::
               CString ->
                 CInt ->
                   FunPtr (CString -> IO ()) -> FunPtr (CString -> IO ()) -> IO ()

foreign import ccall "wrapper" mkCallback ::
               (CString -> IO ()) -> IO (FunPtr (CString -> IO ()))

{-| Internal state of the watcher.
We keep track of running processes and the config.
You might need this if you want to use `end` or `force`.
-}
data State = State
  { onChange :: IO ()
  , mVar :: MVar (Maybe ProcessID)
  }

{-| Configuration for a watcher.
-}
data Config = Config
  { pathToWatch :: FilePath -- Watch files recursivelly under this path.
  , relevantExtensions :: [T.Text] -- Which extensions do we care about? Empty list will accept all.
  , debounceInSecs :: Int -- Debounce next run by x seconds.
  }

watch :: Config -> IO () -> (T.Text -> IO ()) -> IO State
watch config onChange onError = do
  mVar <- newMVar Nothing
  let state = State {onChange = onChange, mVar = mVar}
  _ <- forkIO (start mVar config onChange onError)
  pure state

force :: State -> IO ()
force State {mVar, onChange} = startProcess mVar onChange

end :: State -> IO ()
end State {mVar} = stopProcess mVar

start :: MVar (Maybe ProcessID) -> Config -> IO () -> (T.Text -> IO ()) -> IO ()
start mVar Config {pathToWatch, debounceInSecs, relevantExtensions} onChange onError = do
  onChangeCb <- mkCallback $ callbackInProcess mVar onChange relevantExtensions
  onErrorCb <- mkCallback $ onErrorCallback onError
  pathCStr <- newCString pathToWatch
  watchForChanges pathCStr (mkCInt debounceInSecs) onChangeCb onErrorCb

mkCInt :: Int -> CInt
mkCInt = fromIntegral

onErrorCallback :: (T.Text -> IO ()) -> CString -> IO ()
onErrorCallback cb msgC = do
  msg <- peekCString msgC
  cb (T.pack msg)

callbackInProcess ::
     MVar (Maybe ProcessID) -> IO () -> [T.Text] -> CString -> IO ()
callbackInProcess mVar cb relevantExtensions pathC = do
  eventForPath <- peekCString pathC
  when (isRelevant eventForPath relevantExtensions || null relevantExtensions) $
    startProcess mVar cb

startProcess :: MVar (Maybe ProcessID) -> IO () -> IO ()
startProcess mVar cb = do
  stopProcess mVar
  processId <- forkProcess cb
  putMVar mVar (Just processId)

stopProcess :: MVar (Maybe ProcessID) -> IO ()
stopProcess mVar = do
  runningProcess <- takeMVar mVar
  traverse_ (signalProcess softwareTermination) runningProcess
  traverse_ (getProcessStatus True False) runningProcess -- here be dragons, potentially

isRelevant :: FilePath -> [T.Text] -> Bool
isRelevant path = elem (T.pack (takeExtension path))