This post describes how to get notifications on file modifications made through a text editor. Using python on linux (ubuntu).
I am working on adding some inotify goodness to doit. For that I want to receive one, and one only, notification every time a file is modified. Inotify makes the hard work of watching the file system and Pyinotify provides a python interface. But using it was not straight-forward as I expected. The problem is that editors manipulate files on its own ways…
I have started with a modified example from Pyinotify tutorial to watch for file modifications. The IN_MODIFY event looked like perfect to me, it is an event that is called when “file was modified”.
It worked fine when I used “echo”. But than when I tried with Emacs I got 3 notifications. With VIM it was even worst, I got no notifications and an error message!
[Pyinotify ERROR] The pathname ‘notify.py’ of this watch <Watch wd=1 mask=4095 auto_add=False proc_fun=None path=notify.py exclude_filter=<function at 0x257e398> dir=False > has probably changed and couldn’t be updated, so it cannot be trusted anymore. To fix this error move directories/files only between watched parents directories, in this case e.g. put a watch on ‘.’.
The error message is pretty clear, I should watch the folder containing the file… In order to understand what was going on I came up with the following code. It watch for all inotify events on file’s folder:
import os.path
import pyinotify
class EventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
print "==> ", event.maskname, ": ", event.pathname
wm = pyinotify.WatchManager() # Watch Manager
#mask = pyinotify.IN_MODIFY
mask = pyinotify.ALL_EVENTS
ev = EventHandler()
notifier = pyinotify.Notifier(wm, ev)
for watch_file in ['notify.py']:
watch_dir = os.path.dirname(os.path.abspath(watch_file))
wm.add_watch(watch_dir, mask)
notifier.loop()
I got the following results doing modifications on the file with echo, emacs and vim:
- echo a >> notify.py
==> IN_OPEN : /my_folder/notify.py
==> IN_MODIFY : /my_folder/notify.py
==> IN_CLOSE_WRITE : /my_folder/notify.py
Exactly what I expected :)
- emacs (C-x C-s)
==> IN_CREATE : /my_folder/.#notify.py
==> IN_MODIFY : /my_folder/notify.py
==> IN_OPEN : /my_folder/notify.py
==> IN_MODIFY : /my_folder/notify.py
==> IN_CLOSE_WRITE : /my_folder/notify.py
==> IN_DELETE : /my_folder/.#notify.py
I am not trying to understand emacs internals… but I can notice that IN_CLOSE_WRITE raised just one event.
- vim (:w)
==> IN_MODIFY : /my_folder/.notify.py.swp
==> IN_CREATE : /my_folder/4913
==> IN_OPEN : /my_folder/4913
==> IN_ATTRIB : /my_folder/4913
==> IN_CLOSE_WRITE : /my_folder/4913
==> IN_DELETE : /my_folder/4913
==> IN_MOVED_FROM : /my_folder/notify.py
==> IN_MOVED_TO : /my_folder/notify.py~
==> IN_CREATE : /my_folder/notify.py
==> IN_OPEN : /my_folder/notify.py
==> IN_MODIFY : /my_folder/notify.py
==> IN_CLOSE_WRITE : /my_folder/notify.py
==> IN_ATTRIB : /my_folder/notify.py
==> IN_MODIFY : /my_folder/.notify.py.swp
==> IN_DELETE : /my_folder/notify.py~
==> IN_CLOSE_WRITE : /my_folder/.notify.py.swp
==> IN_DELETE : /my_folder/.notify.py.swp
It seems Vim do not modify the file directly. It just move/replaces the original one with the edited through a “swap file”. IN_CLOSE_WRITE was called for the target file only once…
I played around a bit more and it is clear that IN_CLOSE_WRITE is what I was really looking for. In order to deal with VIM I should watch the folder and not the file itself. Putting all pieces together I got this code:
import os.path
import pyinotify
class FileModifyWatcher(object):
def __init__(self, file_list):
self.file_list = set([os.path.abspath(f) for f in file_list])
self.watch_dirs = set([os.path.dirname(f) for f in self.file_list])
def handle_event(self, event):
if event.pathname in self.file_list:
print "==> ", event.maskname, ": ", event.pathname
def loop(self):
handle_event = self.handle_event
class EventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
handle_event(event)
wm = pyinotify.WatchManager() # Watch Manager
mask = pyinotify.IN_CLOSE_WRITE
ev = EventHandler()
notifier = pyinotify.Notifier(wm, ev)
for watch_this in self.watch_dirs:
wm.add_watch(watch_this, mask)
notifier.loop()
if __name__ == "__main__":
fw = FileModifyWatcher(['notify.py'])
fw.loop()
To use this, subclass FileModifyWatcher, over-write handle_event. Just pass the list of files to watch to its constructor and thats it ;)