2013/12/27: hinotify's addWatch and replaced files

Normally, at least in Haskell, the type gives enough information about a function to use it correctly. Recently, however, I stumbled about a function where it paid off reading the documentation. The types were as follows.


$ ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> import System.INotify
Prelude System.INotify> :t initINotify
initINotify :: IO INotify
Prelude System.INotify> :t addWatch
addWatch
  :: INotify
     -> [EventVariety]
     -> FilePath
     -> (Event -> IO ())
     -> IO WatchDescriptor
Prelude System.INotify> :q
Leaving GHCi.
$ 

The interesting thing here is that the object watched is a FilePath. To a naive user this might hint that it is the path that is watched. And, to make things worse, this is not completely wrong. Consider the following program hinotify.hs.
import Control.Concurrent
import System.INotify

fpath :: FilePath
fpath = "/home/aehlig/hinotify/foo"

watch :: IO ()
watch = do
  inotify <- initINotify
  _ <- addWatch inotify [Modify, Delete] fpath . const $ do
    putStrLn "Got notified."
    contents <-  readFile fpath
    putStrLn $ "Found " ++ contents
  return ()

tick :: IO ()
tick = do
  threadDelay 1000000
  putStrLn "tick"

main :: IO ()
main = do
  _ <- forkIO watch
  mapM_ (const tick) [1..10]
download

Compiling it with ghc hinotify.hs and running it, it really seems to watch the specified file.
aehlig@sandbox:~/hinotify$ echo foo > foo; (./hinotify &); sleep 2; echo foo2 > foo; sleep 2; echo  bar > foo; sleep 2; echo baz > foo; sleep 10
tick
tick
Got notified.
Found 
Got notified.
Found foo2

tick
tick
Got notified.
Found 
Got notified.
Found bar

tick
tick
Got notified.
Found 
Got notified.
Found baz

tick
tick
tick
tick
aehlig@sandbox:~/hinotify$

You also note that writing a file is not atomically. However, once you start updating (as you should) the file atomically you notice that it is not the path but the file you're watching.
aehlig@sandbox:~/hinotify$ echo foo > foo; (./hinotify &); sleep 2; echo foo2 > new; mv new foo; sleep 2; echo  bar > new; mv new foo; sleep 2; echo baz > new; mv new foo; sleep 10
tick
tick
Got notified.
Found foo2

tick
tick
tick
tick
tick
tick
tick
tick
aehlig@sandbox:~/hinotify$

Logging the event shows that it is indeed Ignored. So, if we really want to watch the path, we have to reinstantiate the watch, e.g., as follows.
import Control.Concurrent
import Control.Monad
import System.INotify

fpath :: FilePath
fpath = "/home/aehlig/hinotify/foo"

watch :: IO ()
watch = do
  inotify <- initINotify
  _ <- addWatch inotify [Modify, Delete] fpath $ \e -> do
    when (e == Ignored) watch
    putStrLn $ "Got notified: " ++ show e
    contents <-  readFile fpath
    putStrLn $ "Found " ++ contents
  return ()

tick :: IO ()
tick = do
  threadDelay 1000000
  putStrLn "tick"

main :: IO ()
main = do
  _ <- forkIO watch
  mapM_ (const tick) [1..10]
download

Then, everything works, even if we mix atomic and naive updates.
aehlig@sandbox:~/hinotify$ echo foo > foo; (./hinotify &); sleep 2; echo foo2 > new; mv new foo; sleep 2; echo  bar > new; mv new foo; sleep 2; echo naiveecho > foo; sleep 2; echo baz > new; mv new foo; sleep 10
tick
tick
Got notified: Ignored
Found foo2

tick
tick
Got notified: Ignored
Found bar

tick
tick
Got notified: Modified {isDirectory = False, maybeFilePath = Nothing}
Found 
Got notified: Modified {isDirectory = False, maybeFilePath = Nothing}
Found naiveecho

tick
tick
Got notified: Ignored
Found baz

tick
tick
aehlig@sandbox:~/hinotify$