inotify & text editors (emacs, vim, …)

7 03 2010

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 ;)

About these ads

Actions

Information

2 responses

7 09 2010
Ace

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?

27 11 2010
Caio Filippo Corro » Blog Archive » Inotify example - PHP, C/C++, Java, bla bla bla.

[...] 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. [...]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s




Follow

Get every new post delivered to your Inbox.

%d bloggers like this: