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
- emacs (C-x C-s)
- vim (:w)
==> IN_OPEN : /my_folder/notify.py
==> IN_MODIFY : /my_folder/notify.py
==> IN_CLOSE_WRITE : /my_folder/notify.py
Exactly what I expected :)
==> 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.
==> 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 ;)
Hi, I’m a student from China , I had the same problem when using inotify.
My program can’t work correctly with gedit, vim ,etc.
It looks like that you have solved this problem.
I don’t really understand your solution since i can’t read python.
My test code is written in C, could you help me?
[…] If you're checking for changes made on a file, you better first check which events are sent using the IN_ALL_EVENTS mask, it's sometimes strange. […]