Add inotify extension
authorBryan O'Sullivan <bos@serpentine.com>
Wed, 12 Mar 2008 15:30:11 -0700
changeset 6239 39cfcef4f463
parent 6236 ad6b123de1c7
child 6240 6679405e95da
Add inotify extension
hgext/inotify/__init__.py
hgext/inotify/client.py
hgext/inotify/common.py
hgext/inotify/linux/__init__.py
hgext/inotify/linux/_inotify.c
hgext/inotify/linux/watcher.py
hgext/inotify/server.py
setup.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/__init__.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,104 @@
+# __init__.py - inotify-based status acceleration for Linux
+#
+# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
+# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+'''inotify-based status acceleration for Linux systems
+'''
+
+# todo: socket permissions
+
+from mercurial.i18n import gettext as _
+from mercurial import cmdutil, util
+import client, errno, os, server, socket
+from weakref import proxy
+
+def serve(ui, repo, **opts):
+    '''start an inotify server for this repository'''
+    timeout = opts.get('timeout')
+    if timeout:
+        timeout = float(timeout) * 1e3
+
+    class service:
+        def init(self):
+            self.master = server.Master(ui, repo, timeout)
+
+        def run(self):
+            try:
+                self.master.run()
+            finally:
+                self.master.shutdown()
+
+    service = service()
+    cmdutil.service(opts, initfn=service.init, runfn=service.run)
+
+def reposetup(ui, repo):
+    if not repo.local():
+        return
+
+    # XXX: weakref until hg stops relying on __del__
+    repo = proxy(repo)
+
+    class inotifydirstate(repo.dirstate.__class__):
+        # Set to True if we're the inotify server, so we don't attempt
+        # to recurse.
+        inotifyserver = False
+
+        def status(self, files, match, list_ignored, list_clean,
+                   list_unknown=True):
+            try:
+                if not list_ignored and not self.inotifyserver:
+                    result = client.query(ui, repo, files, match, False,
+                                          list_clean, list_unknown)
+                    if result is not None:
+                        return result
+            except socket.error, err:
+                if err[0] == errno.ECONNREFUSED:
+                    ui.warn(_('(found dead inotify server socket; '
+                                   'removing it)\n'))
+                    os.unlink(repo.join('inotify.sock'))
+                elif err[0] != errno.ENOENT:
+                    raise
+                if ui.configbool('inotify', 'autostart'):
+                    query = None
+                    ui.debug(_('(starting inotify server)\n'))
+                    try:
+                        server.start(ui, repo)
+                        query = client.query
+                    except server.AlreadyStartedException, inst:
+                        # another process may have started its own
+                        # inotify server while this one was starting.
+                        ui.debug(str(inst))
+                        query = client.query
+                    except Exception, inst:
+                        ui.warn(_('could not start inotify server: '
+                                       '%s\n') % inst)
+                        ui.print_exc()
+
+                    if query:
+                        try:
+                            return query(ui, repo, files or [], match,
+                                         list_ignored, list_clean, list_unknown)
+                        except socket.error, err:
+                            ui.warn(_('could not talk to new inotify '
+                                           'server: %s\n') % err[1])
+                            ui.print_exc()
+
+            return super(inotifydirstate, self).status(
+                files, match or util.always, list_ignored, list_clean,
+                list_unknown)
+
+    repo.dirstate.__class__ = inotifydirstate
+
+cmdtable = {
+    '^inserve':
+    (serve,
+     [('d', 'daemon', None, _('run server in background')),
+      ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
+      ('t', 'idle-timeout', '', _('minutes to sit idle before exiting')),
+      ('', 'pid-file', '', _('name of file to write process ID to'))],
+     _('hg inserve [OPT]...')),
+    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/client.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,55 @@
+# client.py - inotify status client
+#
+# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
+# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from mercurial.i18n import gettext as _
+from mercurial import ui
+import common
+import os, select, socket, stat, struct, sys
+
+def query(ui, repo, names, match, list_ignored, list_clean, list_unknown=True):
+    sock = socket.socket(socket.AF_UNIX)
+    sockpath = repo.join('inotify.sock')
+    sock.connect(sockpath)
+
+    def genquery():
+        for n in names or []:
+            yield n
+        states = 'almrx!'
+        if list_ignored:
+            raise ValueError('this is insanity')
+        if list_clean: states += 'n'
+        if list_unknown: states += '?'
+        yield states
+
+    req = '\0'.join(genquery())
+
+    sock.sendall(chr(common.version))
+    sock.sendall(req)
+    sock.shutdown(socket.SHUT_WR)
+
+    cs = common.recvcs(sock)
+    version = ord(cs.read(1))
+
+    if version != common.version:
+        ui.warn(_('(inotify: received response from incompatible server '
+                  'version %d)\n') % version)
+        return None
+
+    try:
+        resphdr = struct.unpack(common.resphdrfmt, cs.read(common.resphdrsize))
+    except struct.error:
+        return None
+
+    def readnames(nbytes):
+        if nbytes:
+            names = cs.read(nbytes)
+            if names:
+                return filter(match, names.split('\0'))
+        return []
+
+    return map(readnames, resphdr)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/common.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,26 @@
+# server.py - inotify common protocol code
+#
+# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
+# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+import cStringIO, socket, struct
+
+version = 1
+
+resphdrfmt = '>llllllll'
+resphdrsize = struct.calcsize(resphdrfmt)
+
+def recvcs(sock):
+    cs = cStringIO.StringIO()
+    s = True
+    try:
+        while s:
+            s = sock.recv(65536)
+            cs.write(s)
+    finally:
+        sock.shutdown(socket.SHUT_RD)
+    cs.seek(0)
+    return cs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/linux/__init__.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,41 @@
+# __init__.py - low-level interfaces to the Linux inotify subsystem
+
+# Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
+
+# This library is free software; you can redistribute it and/or modify
+# it under the terms of version 2.1 of the GNU Lesser General Public
+# License, incorporated herein by reference.
+
+'''Low-level interface to the Linux inotify subsystem.
+
+The inotify subsystem provides an efficient mechanism for file status
+monitoring and change notification.
+
+This package provides the low-level inotify system call interface and
+associated constants and helper functions.
+
+For a higher-level interface that remains highly efficient, use the
+inotify.watcher package.'''
+
+__author__ = "Bryan O'Sullivan <bos@serpentine.com>"
+
+from _inotify import *
+
+procfs_path = '/proc/sys/fs/inotify'
+
+def _read_procfs_value(name):
+    def read_value():
+        try:
+            return int(open(procfs_path + '/' + name).read())
+        except OSError, err:
+            return None
+
+    read_value.__doc__ = '''Return the value of the %s setting from /proc.
+
+    If inotify is not enabled on this system, return None.''' % name
+
+    return read_value
+
+max_queued_events = _read_procfs_value('max_queued_events')
+max_user_instances = _read_procfs_value('max_user_instances')
+max_user_watches = _read_procfs_value('max_user_watches')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/linux/_inotify.c	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,608 @@
+/*
+ * _inotify.c - Python extension interfacing to the Linux inotify subsystem
+ *
+ * Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of version 2.1 of the GNU Lesser General
+ * Public License, incorporated herein by reference.
+ */
+
+#include <Python.h>
+#include <alloca.h>
+#include <sys/inotify.h>
+#include <stdint.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+static PyObject *init(PyObject *self, PyObject *args)
+{
+    PyObject *ret = NULL;
+    int fd = -1;
+    
+     if (!PyArg_ParseTuple(args, ":init"))
+	goto bail;
+    
+    Py_BEGIN_ALLOW_THREADS
+    fd = inotify_init();
+    Py_END_ALLOW_THREADS
+
+    if (fd == -1) {
+	PyErr_SetFromErrno(PyExc_OSError);
+	goto bail;
+    }
+	
+    ret = PyInt_FromLong(fd);
+    if (ret == NULL)
+	goto bail;
+
+    goto done;
+    
+bail:
+    if (fd != -1)
+	close(fd);
+
+    Py_CLEAR(ret);
+    
+done:
+    return ret;
+}
+
+PyDoc_STRVAR(
+    init_doc,
+    "init() -> fd\n"
+    "\n"
+    "Initialise an inotify instance.\n"
+    "Return a file descriptor associated with a new inotify event queue.");
+
+static PyObject *add_watch(PyObject *self, PyObject *args)
+{
+    PyObject *ret = NULL;
+    uint32_t mask;
+    int wd = -1;
+    char *path;
+    int fd;
+
+    if (!PyArg_ParseTuple(args, "isI:add_watch", &fd, &path, &mask))
+	goto bail;
+
+    Py_BEGIN_ALLOW_THREADS
+    wd = inotify_add_watch(fd, path, mask);
+    Py_END_ALLOW_THREADS
+
+    if (wd == -1) {
+	PyErr_SetFromErrnoWithFilename(PyExc_OSError, path);
+	goto bail;
+    }
+    
+    ret = PyInt_FromLong(wd);
+    if (ret == NULL)
+	goto bail;
+    
+    goto done;
+    
+bail:
+    if (wd != -1)
+	inotify_rm_watch(fd, wd);
+    
+    Py_CLEAR(ret);
+
+done:
+    return ret;
+}
+
+PyDoc_STRVAR(
+    add_watch_doc,
+    "add_watch(fd, path, mask) -> wd\n"
+    "\n"
+    "Add a watch to an inotify instance, or modify an existing watch.\n"
+    "\n"
+    "        fd: file descriptor returned by init()\n"
+    "        path: path to watch\n"
+    "        mask: mask of events to watch for\n"
+    "\n"
+    "Return a unique numeric watch descriptor for the inotify instance\n"
+    "mapped by the file descriptor.");
+
+static PyObject *remove_watch(PyObject *self, PyObject *args)
+{
+    PyObject *ret = NULL;
+    uint32_t wd;
+    int fd;
+    int r;
+    
+    if (!PyArg_ParseTuple(args, "iI:remove_watch", &fd, &wd))
+	goto bail;
+
+    Py_BEGIN_ALLOW_THREADS
+    r = inotify_rm_watch(fd, wd);
+    Py_END_ALLOW_THREADS
+
+    if (r == -1) {
+	PyErr_SetFromErrno(PyExc_OSError);
+	goto bail;
+    }
+    
+    Py_INCREF(Py_None);
+    
+    goto done;
+    
+bail:
+    Py_CLEAR(ret);
+    
+done:
+    return ret;
+}
+
+PyDoc_STRVAR(
+    remove_watch_doc,
+    "remove_watch(fd, wd)\n"
+    "\n"
+    "        fd: file descriptor returned by init()\n"
+    "        wd: watch descriptor returned by add_watch()\n"
+    "\n"
+    "Remove a watch associated with the watch descriptor wd from the\n"
+    "inotify instance associated with the file descriptor fd.\n"
+    "\n"
+    "Removing a watch causes an IN_IGNORED event to be generated for this\n"
+    "watch descriptor.");
+
+#define bit_name(x) {x, #x}
+
+static struct {
+    int bit;
+    const char *name;
+    PyObject *pyname;
+} bit_names[] = {
+    bit_name(IN_ACCESS),
+    bit_name(IN_MODIFY),
+    bit_name(IN_ATTRIB),
+    bit_name(IN_CLOSE_WRITE),
+    bit_name(IN_CLOSE_NOWRITE),
+    bit_name(IN_OPEN),
+    bit_name(IN_MOVED_FROM),
+    bit_name(IN_MOVED_TO),
+    bit_name(IN_CREATE),
+    bit_name(IN_DELETE),
+    bit_name(IN_DELETE_SELF),
+    bit_name(IN_MOVE_SELF),
+    bit_name(IN_UNMOUNT),
+    bit_name(IN_Q_OVERFLOW),
+    bit_name(IN_IGNORED),
+    bit_name(IN_ONLYDIR),
+    bit_name(IN_DONT_FOLLOW),
+    bit_name(IN_MASK_ADD),
+    bit_name(IN_ISDIR),
+    bit_name(IN_ONESHOT),
+    {0}
+};
+
+static PyObject *decode_mask(int mask)
+{
+    PyObject *ret = PyList_New(0);
+    int i;
+
+    if (ret == NULL)
+	goto bail;
+    
+    for (i = 0; bit_names[i].bit; i++) {
+	if (mask & bit_names[i].bit) {
+	    if (bit_names[i].pyname == NULL) {
+		bit_names[i].pyname = PyString_FromString(bit_names[i].name);
+		if (bit_names[i].pyname == NULL)
+		    goto bail;
+	    }
+	    Py_INCREF(bit_names[i].pyname);
+	    if (PyList_Append(ret, bit_names[i].pyname) == -1)
+		goto bail;
+	}
+    }
+    
+    goto done;
+    
+bail:
+    Py_CLEAR(ret);
+
+done:
+    return ret;
+}
+    
+static PyObject *pydecode_mask(PyObject *self, PyObject *args)
+{
+    int mask;
+    
+    if (!PyArg_ParseTuple(args, "i:decode_mask", &mask))
+	return NULL;
+
+    return decode_mask(mask);
+}
+    
+PyDoc_STRVAR(
+    decode_mask_doc,
+    "decode_mask(mask) -> list_of_strings\n"
+    "\n"
+    "Decode an inotify mask value into a list of strings that give the\n"
+    "name of each bit set in the mask.");
+
+static char doc[] = "Low-level inotify interface wrappers.";
+
+static void define_const(PyObject *dict, const char *name, uint32_t val)
+{
+    PyObject *pyval = PyInt_FromLong(val);
+    PyObject *pyname = PyString_FromString(name);
+
+    if (!pyname || !pyval)
+	goto bail;
+    
+    PyDict_SetItem(dict, pyname, pyval);
+
+bail:
+    Py_XDECREF(pyname);
+    Py_XDECREF(pyval);
+}
+
+static void define_consts(PyObject *dict)
+{
+    define_const(dict, "IN_ACCESS", IN_ACCESS);
+    define_const(dict, "IN_MODIFY", IN_MODIFY);
+    define_const(dict, "IN_ATTRIB", IN_ATTRIB);
+    define_const(dict, "IN_CLOSE_WRITE", IN_CLOSE_WRITE);
+    define_const(dict, "IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE);
+    define_const(dict, "IN_OPEN", IN_OPEN);
+    define_const(dict, "IN_MOVED_FROM", IN_MOVED_FROM);
+    define_const(dict, "IN_MOVED_TO", IN_MOVED_TO);
+
+    define_const(dict, "IN_CLOSE", IN_CLOSE);
+    define_const(dict, "IN_MOVE", IN_MOVE);
+
+    define_const(dict, "IN_CREATE", IN_CREATE);
+    define_const(dict, "IN_DELETE", IN_DELETE);
+    define_const(dict, "IN_DELETE_SELF", IN_DELETE_SELF);
+    define_const(dict, "IN_MOVE_SELF", IN_MOVE_SELF);
+    define_const(dict, "IN_UNMOUNT", IN_UNMOUNT);
+    define_const(dict, "IN_Q_OVERFLOW", IN_Q_OVERFLOW);
+    define_const(dict, "IN_IGNORED", IN_IGNORED);
+
+    define_const(dict, "IN_ONLYDIR", IN_ONLYDIR);
+    define_const(dict, "IN_DONT_FOLLOW", IN_DONT_FOLLOW);
+    define_const(dict, "IN_MASK_ADD", IN_MASK_ADD);
+    define_const(dict, "IN_ISDIR", IN_ISDIR);
+    define_const(dict, "IN_ONESHOT", IN_ONESHOT);
+    define_const(dict, "IN_ALL_EVENTS", IN_ALL_EVENTS);
+}
+
+struct event {
+    PyObject_HEAD
+    PyObject *wd;
+    PyObject *mask;
+    PyObject *cookie;
+    PyObject *name;
+};
+    
+static PyObject *event_wd(PyObject *self, void *x)
+{
+    struct event *evt = (struct event *) self;
+    Py_INCREF(evt->wd);
+    return evt->wd;
+}
+    
+static PyObject *event_mask(PyObject *self, void *x)
+{
+    struct event *evt = (struct event *) self;
+    Py_INCREF(evt->mask);
+    return evt->mask;
+}
+    
+static PyObject *event_cookie(PyObject *self, void *x)
+{
+    struct event *evt = (struct event *) self;
+    Py_INCREF(evt->cookie);
+    return evt->cookie;
+}
+    
+static PyObject *event_name(PyObject *self, void *x)
+{
+    struct event *evt = (struct event *) self;
+    Py_INCREF(evt->name);
+    return evt->name;
+}
+
+static struct PyGetSetDef event_getsets[] = {
+    {"wd", event_wd, NULL,
+     "watch descriptor"},
+    {"mask", event_mask, NULL,
+     "event mask"},
+    {"cookie", event_cookie, NULL,
+     "rename cookie, if rename-related event"},
+    {"name", event_name, NULL,
+     "file name"},
+    {NULL}
+};
+
+PyDoc_STRVAR(
+    event_doc,
+    "event: Structure describing an inotify event.");
+
+static PyObject *event_new(PyTypeObject *t, PyObject *a, PyObject *k)
+{
+    return (*t->tp_alloc)(t, 0);
+}
+
+static void event_dealloc(struct event *evt)
+{
+    Py_XDECREF(evt->wd);
+    Py_XDECREF(evt->mask);
+    Py_XDECREF(evt->cookie);
+    Py_XDECREF(evt->name);
+    
+    (*evt->ob_type->tp_free)(evt);
+}
+
+static PyObject *event_repr(struct event *evt)
+{
+    int wd = PyInt_AsLong(evt->wd);
+    int cookie = evt->cookie == Py_None ? -1 : PyInt_AsLong(evt->cookie);
+    PyObject *ret = NULL, *pymasks = NULL, *pymask = NULL;
+    PyObject *join = NULL;
+    char *maskstr;
+
+    join = PyString_FromString("|");
+    if (join == NULL)
+	goto bail;
+
+    pymasks = decode_mask(PyInt_AsLong(evt->mask));
+    if (pymasks == NULL)
+	goto bail;
+    
+    pymask = _PyString_Join(join, pymasks);
+    if (pymask == NULL)
+	goto bail;
+    
+    maskstr = PyString_AsString(pymask);
+    
+    if (evt->name != Py_None) {
+	PyObject *pyname = PyString_Repr(evt->name, 1);
+	char *name = pyname ? PyString_AsString(pyname) : "???";
+	
+	if (cookie == -1)
+	    ret = PyString_FromFormat("event(wd=%d, mask=%s, name=%s)",
+				      wd, maskstr, name);
+	else
+	    ret = PyString_FromFormat("event(wd=%d, mask=%s, "
+				      "cookie=0x%x, name=%s)",
+				      wd, maskstr, cookie, name);
+
+	Py_XDECREF(pyname);
+    } else {
+	if (cookie == -1)
+	    ret = PyString_FromFormat("event(wd=%d, mask=%s)",
+				      wd, maskstr);
+	else {
+	    ret = PyString_FromFormat("event(wd=%d, mask=%s, cookie=0x%x)",
+				      wd, maskstr, cookie);
+	}
+    }
+
+    goto done;
+bail:
+    Py_CLEAR(ret);
+    
+done:
+    Py_XDECREF(pymask);
+    Py_XDECREF(pymasks);
+    Py_XDECREF(join);
+
+    return ret;
+}
+
+static PyTypeObject event_type = {
+    PyObject_HEAD_INIT(NULL)
+    0,                         /*ob_size*/
+    "_inotify.event",             /*tp_name*/
+    sizeof(struct event), /*tp_basicsize*/
+    0,                         /*tp_itemsize*/
+    (destructor)event_dealloc, /*tp_dealloc*/
+    0,                         /*tp_print*/
+    0,                         /*tp_getattr*/
+    0,                         /*tp_setattr*/
+    0,                         /*tp_compare*/
+    (reprfunc)event_repr,      /*tp_repr*/
+    0,                         /*tp_as_number*/
+    0,                         /*tp_as_sequence*/
+    0,                         /*tp_as_mapping*/
+    0,                         /*tp_hash */
+    0,                         /*tp_call*/
+    0,                         /*tp_str*/
+    0,                         /*tp_getattro*/
+    0,                         /*tp_setattro*/
+    0,                         /*tp_as_buffer*/
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/
+    event_doc,           /* tp_doc */
+    0,                         /* tp_traverse */
+    0,                         /* tp_clear */
+    0,                         /* tp_richcompare */
+    0,                         /* tp_weaklistoffset */
+    0,                         /* tp_iter */
+    0,                         /* tp_iternext */
+    0,                         /* tp_methods */
+    0,                         /* tp_members */
+    event_getsets,      /* tp_getset */
+    0,                         /* tp_base */
+    0,                         /* tp_dict */
+    0,                         /* tp_descr_get */
+    0,                         /* tp_descr_set */
+    0,                         /* tp_dictoffset */
+    0,                         /* tp_init */
+    0,                         /* tp_alloc */
+    event_new,          /* tp_new */
+};
+    
+PyObject *read_events(PyObject *self, PyObject *args)
+{
+    PyObject *ctor_args = NULL;
+    PyObject *pybufsize = NULL;
+    PyObject *ret = NULL;
+    int bufsize = 65536;
+    char *buf = NULL;
+    int nread, pos;
+    int fd;
+
+    if (!PyArg_ParseTuple(args, "i|O:read", &fd, &pybufsize))
+        goto bail;
+
+    if (pybufsize && pybufsize != Py_None)
+	bufsize = PyInt_AsLong(pybufsize);
+    
+    ret = PyList_New(0);
+    if (ret == NULL)
+	goto bail;
+    
+    if (bufsize <= 0) {
+	int r;
+	
+	Py_BEGIN_ALLOW_THREADS
+	r = ioctl(fd, FIONREAD, &bufsize);
+	Py_END_ALLOW_THREADS
+	
+	if (r == -1) {
+	    PyErr_SetFromErrno(PyExc_OSError);
+	    goto bail;
+	}
+	if (bufsize == 0)
+	    goto done;
+    }
+    else {
+	static long name_max;
+	static long name_fd = -1;
+	long min;
+	
+	if (name_fd != fd) {
+	    name_fd = fd;
+	    Py_BEGIN_ALLOW_THREADS
+	    name_max = fpathconf(fd, _PC_NAME_MAX);
+	    Py_END_ALLOW_THREADS
+	}
+	
+	min = sizeof(struct inotify_event) + name_max + 1;
+	
+	if (bufsize < min) {
+	    PyErr_Format(PyExc_ValueError, "bufsize must be at least %d",
+			 (int) min);
+	    goto bail;
+	}
+    }
+
+    buf = alloca(bufsize);
+    
+    Py_BEGIN_ALLOW_THREADS
+    nread = read(fd, buf, bufsize);
+    Py_END_ALLOW_THREADS
+
+    if (nread == -1) {
+	PyErr_SetFromErrno(PyExc_OSError);
+	goto bail;
+    }
+
+    ctor_args = PyTuple_New(0);
+
+    if (ctor_args == NULL)
+	goto bail;
+    
+    pos = 0;
+    
+    while (pos < nread) {
+	struct inotify_event *in = (struct inotify_event *) (buf + pos);
+	struct event *evt;
+	PyObject *obj;
+
+	obj = PyObject_CallObject((PyObject *) &event_type, ctor_args);
+
+	if (obj == NULL)
+	    goto bail;
+
+	evt = (struct event *) obj;
+
+	evt->wd = PyInt_FromLong(in->wd);
+	evt->mask = PyInt_FromLong(in->mask);
+	if (in->mask & IN_MOVE)
+	    evt->cookie = PyInt_FromLong(in->cookie);
+	else {
+	    Py_INCREF(Py_None);
+	    evt->cookie = Py_None;
+	}
+	if (in->len)
+	    evt->name = PyString_FromString(in->name);
+	else {
+	    Py_INCREF(Py_None);
+	    evt->name = Py_None;
+	}
+
+	if (!evt->wd || !evt->mask || !evt->cookie || !evt->name)
+	    goto mybail;
+
+	if (PyList_Append(ret, obj) == -1)
+	    goto mybail;
+
+	pos += sizeof(struct inotify_event) + in->len;
+	continue;
+
+    mybail:
+	Py_CLEAR(evt->wd);
+	Py_CLEAR(evt->mask);
+	Py_CLEAR(evt->cookie);
+	Py_CLEAR(evt->name);
+	Py_DECREF(obj);
+
+	goto bail;
+    }
+    
+    goto done;
+
+bail:
+    Py_CLEAR(ret);
+    
+done:
+    Py_XDECREF(ctor_args);
+
+    return ret;
+}
+
+PyDoc_STRVAR(
+    read_doc,
+    "read(fd, bufsize[=65536]) -> list_of_events\n"
+    "\n"
+    "\nRead inotify events from a file descriptor.\n"
+    "\n"
+    "        fd: file descriptor returned by init()\n"
+    "        bufsize: size of buffer to read into, in bytes\n"
+    "\n"
+    "Return a list of event objects.\n"
+    "\n"
+    "If bufsize is > 0, block until events are available to be read.\n"
+    "Otherwise, immediately return all events that can be read without\n"
+    "blocking.");
+
+
+static PyMethodDef methods[] = {
+    {"init", init, METH_VARARGS, init_doc},
+    {"add_watch", add_watch, METH_VARARGS, add_watch_doc},
+    {"remove_watch", remove_watch, METH_VARARGS, remove_watch_doc},
+    {"read", read_events, METH_VARARGS, read_doc},
+    {"decode_mask", pydecode_mask, METH_VARARGS, decode_mask_doc},
+    {NULL},
+};
+
+void init_inotify(void)
+{
+    PyObject *mod, *dict;
+
+    if (PyType_Ready(&event_type) == -1)
+        return;
+
+    mod = Py_InitModule3("_inotify", methods, doc);
+
+    dict = PyModule_GetDict(mod);
+    
+    if (dict)
+	define_consts(dict);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/linux/watcher.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,335 @@
+# watcher.py - high-level interfaces to the Linux inotify subsystem
+
+# Copyright 2006 Bryan O'Sullivan <bos@serpentine.com>
+
+# This library is free software; you can redistribute it and/or modify
+# it under the terms of version 2.1 of the GNU Lesser General Public
+# License, incorporated herein by reference.
+
+'''High-level interfaces to the Linux inotify subsystem.
+
+The inotify subsystem provides an efficient mechanism for file status
+monitoring and change notification.
+
+The Watcher class hides the low-level details of the inotify
+interface, and provides a Pythonic wrapper around it.  It generates
+events that provide somewhat more information than raw inotify makes
+available.
+
+The AutoWatcher class is more useful, as it automatically watches
+newly-created directories on your behalf.'''
+
+__author__ = "Bryan O'Sullivan <bos@serpentine.com>"
+
+import _inotify as inotify
+import array
+import errno
+import fcntl
+import os
+import termios
+
+
+class Event(object):
+    '''Derived inotify event class.
+
+    The following fields are available:
+
+        mask: event mask, indicating what kind of event this is
+
+        cookie: rename cookie, if a rename-related event
+
+        path: path of the directory in which the event occurred
+
+        name: name of the directory entry to which the event occurred
+        (may be None if the event happened to a watched directory)
+
+        fullpath: complete path at which the event occurred
+
+        wd: watch descriptor that triggered this event'''
+
+    __slots__ = (
+        'cookie',
+        'fullpath',
+        'mask',
+        'name',
+        'path',
+        'raw',
+        'wd',
+        )
+
+    def __init__(self, raw, path):
+        self.path = path
+        self.raw = raw
+        if raw.name:
+            self.fullpath = path + '/' + raw.name
+        else:
+            self.fullpath = path
+
+        self.wd = raw.wd
+        self.mask = raw.mask
+        self.cookie = raw.cookie
+        self.name = raw.name
+    
+    def __repr__(self):
+        r = repr(self.raw)
+        return 'Event(path=' + repr(self.path) + ', ' + r[r.find('(')+1:]
+
+
+_event_props = {
+    'access': 'File was accessed',
+    'modify': 'File was modified',
+    'attrib': 'Attribute of a directory entry was changed',
+    'close_write': 'File was closed after being written to',
+    'close_nowrite': 'File was closed without being written to',
+    'open': 'File was opened',
+    'moved_from': 'Directory entry was renamed from this name',
+    'moved_to': 'Directory entry was renamed to this name',
+    'create': 'Directory entry was created',
+    'delete': 'Directory entry was deleted',
+    'delete_self': 'The watched directory entry was deleted',
+    'move_self': 'The watched directory entry was renamed',
+    'unmount': 'Directory was unmounted, and can no longer be watched',
+    'q_overflow': 'Kernel dropped events due to queue overflow',
+    'ignored': 'Directory entry is no longer being watched',
+    'isdir': 'Event occurred on a directory',
+    }
+
+for k, v in _event_props.iteritems():
+    mask = getattr(inotify, 'IN_' + k.upper())
+    def getter(self):
+        return self.mask & mask
+    getter.__name__ = k
+    getter.__doc__ = v
+    setattr(Event, k, property(getter, doc=v))
+
+del _event_props
+
+
+class Watcher(object):
+    '''Provide a Pythonic interface to the low-level inotify API.
+
+    Also adds derived information to each event that is not available
+    through the normal inotify API, such as directory name.'''
+
+    __slots__ = (
+        'fd',
+        '_paths',
+        '_wds',
+        )
+
+    def __init__(self):
+        '''Create a new inotify instance.'''
+
+        self.fd = inotify.init()
+        self._paths = {}
+        self._wds = {}
+
+    def fileno(self):
+        '''Return the file descriptor this watcher uses.
+
+        Useful for passing to select and poll.'''
+
+        return self.fd
+
+    def add(self, path, mask):
+        '''Add or modify a watch.
+
+        Return the watch descriptor added or modified.'''
+
+        path = os.path.normpath(path)
+        wd = inotify.add_watch(self.fd, path, mask)
+        self._paths[path] = wd, mask
+        self._wds[wd] = path, mask
+        return wd
+
+    def remove(self, wd):
+        '''Remove the given watch.'''
+
+        inotify.remove_watch(self.fd, wd)
+        self._remove(wd)
+
+    def _remove(self, wd):
+        path_mask = self._wds.pop(wd, None)
+        if path_mask is not None:
+            self._paths.pop(path_mask[0])
+
+    def path(self, path):
+        '''Return a (watch descriptor, event mask) pair for the given path.
+        
+        If the path is not being watched, return None.'''
+
+        return self._paths.get(path)
+
+    def wd(self, wd):
+        '''Return a (path, event mask) pair for the given watch descriptor.
+
+        If the watch descriptor is not valid or not associated with
+        this watcher, return None.'''
+
+        return self._wds.get(wd)
+        
+    def read(self, bufsize=None):
+        '''Read a list of queued inotify events.
+
+        If bufsize is zero, only return those events that can be read
+        immediately without blocking.  Otherwise, block until events are
+        available.'''
+
+        events = []
+        for evt in inotify.read(self.fd, bufsize):
+            events.append(Event(evt, self._wds[evt.wd][0]))
+            if evt.mask & inotify.IN_IGNORED:
+                self._remove(evt.wd)
+            elif evt.mask & inotify.IN_UNMOUNT:
+                self.close()
+        return events
+
+    def close(self):
+        '''Shut down this watcher.
+
+        All subsequent method calls are likely to raise exceptions.'''
+
+        os.close(self.fd)
+        self.fd = None
+        self._paths = None
+        self._wds = None
+
+    def __len__(self):
+        '''Return the number of active watches.'''
+
+        return len(self._paths)
+
+    def __iter__(self):
+        '''Yield a (path, watch descriptor, event mask) tuple for each
+        entry being watched.'''
+
+        for path, (wd, mask) in self._paths.iteritems():
+            yield path, wd, mask
+
+    def __del__(self):
+        if self.fd is not None:
+            os.close(self.fd)
+
+    ignored_errors = [errno.ENOENT, errno.EPERM, errno.ENOTDIR]
+
+    def add_iter(self, path, mask, onerror=None):
+        '''Add or modify watches over path and its subdirectories.
+
+        Yield each added or modified watch descriptor.
+
+        To ensure that this method runs to completion, you must
+        iterate over all of its results, even if you do not care what
+        they are.  For example:
+
+            for wd in w.add_iter(path, mask):
+                pass
+
+        By default, errors are ignored.  If optional arg "onerror" is
+        specified, it should be a function; it will be called with one
+        argument, an OSError instance.  It can report the error to
+        continue with the walk, or raise the exception to abort the
+        walk.'''
+
+        # Add the IN_ONLYDIR flag to the event mask, to avoid a possible
+        # race when adding a subdirectory.  In the time between the
+        # event being queued by the kernel and us processing it, the
+        # directory may have been deleted, or replaced with a different
+        # kind of entry with the same name.
+
+        submask = mask | inotify.IN_ONLYDIR
+
+        try:
+            yield self.add(path, mask)
+        except OSError, err:
+            if onerror and err.errno not in self.ignored_errors:
+                onerror(err)
+        for root, dirs, names in os.walk(path, topdown=False, onerror=onerror):
+            for d in dirs:
+                try:
+                    yield self.add(root + '/' + d, submask)
+                except OSError, err:
+                    if onerror and err.errno not in self.ignored_errors:
+                        onerror(err)
+
+    def add_all(self, path, mask, onerror=None):
+        '''Add or modify watches over path and its subdirectories.
+
+        Return a list of added or modified watch descriptors.
+
+        By default, errors are ignored.  If optional arg "onerror" is
+        specified, it should be a function; it will be called with one
+        argument, an OSError instance.  It can report the error to
+        continue with the walk, or raise the exception to abort the
+        walk.'''
+
+        return [w for w in self.add_iter(path, mask, onerror)]
+
+
+class AutoWatcher(Watcher):
+    '''Watcher class that automatically watches newly created directories.'''
+
+    __slots__ = (
+        'addfilter',
+        )
+
+    def __init__(self, addfilter=None):
+        '''Create a new inotify instance.
+
+        This instance will automatically watch newly created
+        directories.
+
+        If the optional addfilter parameter is not None, it must be a
+        callable that takes one parameter.  It will be called each time
+        a directory is about to be automatically watched.  If it returns
+        True, the directory will be watched if it still exists,
+        otherwise, it will beb skipped.'''
+
+        super(AutoWatcher, self).__init__()
+        self.addfilter = addfilter
+
+    _dir_create_mask = inotify.IN_ISDIR | inotify.IN_CREATE
+
+    def read(self, bufsize=None):
+        events = super(AutoWatcher, self).read(bufsize)
+        for evt in events:
+            if evt.mask & self._dir_create_mask == self._dir_create_mask:
+                if self.addfilter is None or self.addfilter(evt):
+                    parentmask = self._wds[evt.wd][1]
+                    # See note about race avoidance via IN_ONLYDIR above.
+                    mask = parentmask | inotify.IN_ONLYDIR
+                    try:
+                        self.add_all(evt.fullpath, mask)
+                    except OSError, err:
+                        if err.errno not in self.ignored_errors:
+                            raise
+        return events
+
+
+class Threshold(object):
+    '''Class that indicates whether a file descriptor has reached a
+    threshold of readable bytes available.
+
+    This class is not thread-safe.'''
+
+    __slots__ = (
+        'fd',
+        'threshold',
+        '_iocbuf',
+        )
+
+    def __init__(self, fd, threshold=1024):
+        self.fd = fd
+        self.threshold = threshold
+        self._iocbuf = array.array('i', [0])
+
+    def readable(self):
+        '''Return the number of bytes readable on this file descriptor.'''
+
+        fcntl.ioctl(self.fd, termios.FIONREAD, self._iocbuf, True)
+        return self._iocbuf[0]
+
+    def __call__(self):
+        '''Indicate whether the number of readable bytes has met or
+        exceeded the threshold.'''
+
+        return self.readable() >= self.threshold
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/inotify/server.py	Wed Mar 12 15:30:11 2008 -0700
@@ -0,0 +1,717 @@
+# server.py - inotify status server
+#
+# Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
+# Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
+#
+# This software may be used and distributed according to the terms
+# of the GNU General Public License, incorporated herein by reference.
+
+from mercurial.i18n import gettext as _
+from mercurial import osutil, ui, util
+import common
+import errno, os, select, socket, stat, struct, sys, time
+
+try:
+    import hgext.inotify.linux as inotify
+    from hgext.inotify.linux import watcher
+except ImportError:
+    print >> sys.stderr, '*** native support is required for this extension'
+    raise
+
+class AlreadyStartedException(Exception): pass
+
+def join(a, b):
+    if a:
+        if a[-1] == '/':
+            return a + b
+        return a + '/' + b
+    return b
+
+walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
+
+def walkrepodirs(repo):
+    '''Iterate over all subdirectories of this repo.
+    Exclude the .hg directory, any nested repos, and ignored dirs.'''
+    rootslash = repo.root + os.sep
+    def walkit(dirname, top):
+        hginside = False
+        try:
+            for name, kind in osutil.listdir(rootslash + dirname):
+                if kind == stat.S_IFDIR:
+                    if name == '.hg':
+                        hginside = True
+                        if not top: break
+                    else:
+                        d = join(dirname, name)
+                        if repo.dirstate._ignore(d):
+                            continue
+                        for subdir, hginsub in walkit(d, False):
+                            if not hginsub:
+                                yield subdir, False
+        except OSError, err:
+            if err.errno not in walk_ignored_errors:
+                raise
+        yield rootslash + dirname, hginside
+    for dirname, hginside in walkit('', True):
+        yield dirname
+
+def walk(repo, root):
+    '''Like os.walk, but only yields regular files.'''
+
+    # This function is critical to performance during startup.
+
+    reporoot = root == ''
+    rootslash = repo.root + os.sep
+
+    def walkit(root, reporoot):
+        files, dirs = [], []
+        hginside = False
+
+        try:
+            fullpath = rootslash + root
+            for name, kind in osutil.listdir(fullpath):
+                if kind == stat.S_IFDIR:
+                    if name == '.hg':
+                        hginside = True
+                        if reporoot:
+                            continue
+                        else:
+                            break
+                    dirs.append(name)
+                elif kind in (stat.S_IFREG, stat.S_IFLNK):
+                    path = join(root, name)
+                    files.append((name, kind))
+
+            yield hginside, fullpath, dirs, files
+
+            for subdir in dirs:
+                path = join(root, subdir)
+                if repo.dirstate._ignore(path):
+                    continue
+                for result in walkit(path, False):
+                    if not result[0]:
+                        yield result
+        except OSError, err:
+            if err.errno not in walk_ignored_errors:
+                raise
+    for result in walkit(root, reporoot):
+        yield result[1:]
+
+def _explain_watch_limit(ui, repo, count):
+    path = '/proc/sys/fs/inotify/max_user_watches'
+    try:
+        limit = int(file(path).read())
+    except IOError, err:
+        if err.errno != errno.ENOENT:
+            raise
+        raise util.Abort(_('this system does not seem to '
+                           'support inotify'))
+    ui.warn(_('*** the current per-user limit on the number '
+              'of inotify watches is %s\n') % limit)
+    ui.warn(_('*** this limit is too low to watch every '
+              'directory in this repository\n'))
+    ui.warn(_('*** counting directories: '))
+    ndirs = len(list(walkrepodirs(repo)))
+    ui.warn(_('found %d\n') % ndirs)
+    newlimit = min(limit, 1024)
+    while newlimit < ((limit + ndirs) * 1.1):
+        newlimit *= 2
+    ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
+            (limit, newlimit))
+    ui.warn(_('***  echo %d > %s\n') % (newlimit, path))
+    raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
+                     % repo.root)
+
+class Watcher(object):
+    poll_events = select.POLLIN
+    statuskeys = 'almr!?'
+
+    def __init__(self, ui, repo, master):
+        self.ui = ui
+        self.repo = repo
+        self.wprefix = self.repo.wjoin('')
+        self.timeout = None
+        self.master = master
+        self.mask = (
+            inotify.IN_ATTRIB |
+            inotify.IN_CREATE |
+            inotify.IN_DELETE |
+            inotify.IN_DELETE_SELF |
+            inotify.IN_MODIFY |
+            inotify.IN_MOVED_FROM |
+            inotify.IN_MOVED_TO |
+            inotify.IN_MOVE_SELF |
+            inotify.IN_ONLYDIR |
+            inotify.IN_UNMOUNT |
+            0)
+        try:
+            self.watcher = watcher.Watcher()
+        except OSError, err:
+            raise util.Abort(_('inotify service not available: %s') %
+                             err.strerror)
+        self.threshold = watcher.Threshold(self.watcher)
+        self.registered = True
+        self.fileno = self.watcher.fileno
+
+        self.repo.dirstate.__class__.inotifyserver = True
+
+        self.tree = {}
+        self.statcache = {}
+        self.statustrees = dict([(s, {}) for s in self.statuskeys])
+
+        self.watches = 0
+        self.last_event = None
+
+        self.eventq = {}
+        self.deferred = 0
+
+        self.ds_info = self.dirstate_info()
+        self.scan()
+
+    def event_time(self):
+        last = self.last_event
+        now = time.time()
+        self.last_event = now
+
+        if last is None:
+            return 'start'
+        delta = now - last
+        if delta < 5:
+            return '+%.3f' % delta
+        if delta < 50:
+            return '+%.2f' % delta
+        return '+%.1f' % delta
+
+    def dirstate_info(self):
+        try:
+            st = os.lstat(self.repo.join('dirstate'))
+            return st.st_mtime, st.st_ino
+        except OSError, err:
+            if err.errno != errno.ENOENT:
+                raise
+            return 0, 0
+
+    def add_watch(self, path, mask):
+        if not path:
+            return
+        if self.watcher.path(path) is None:
+            if self.ui.debugflag:
+                self.ui.note(_('watching %r\n') % path[len(self.wprefix):])
+            try:
+                self.watcher.add(path, mask)
+                self.watches += 1
+            except OSError, err:
+                if err.errno in (errno.ENOENT, errno.ENOTDIR):
+                    return
+                if err.errno != errno.ENOSPC:
+                    raise
+                _explain_watch_limit(self.ui, self.repo, self.watches)
+
+    def setup(self):
+        self.ui.note(_('watching directories under %r\n') % self.repo.root)
+        self.add_watch(self.repo.path, inotify.IN_DELETE)
+        self.check_dirstate()
+
+    def wpath(self, evt):
+        path = evt.fullpath
+        if path == self.repo.root:
+            return ''
+        if path.startswith(self.wprefix):
+            return path[len(self.wprefix):]
+        raise 'wtf? ' + path
+
+    def dir(self, tree, path):
+        if path:
+            for name in path.split('/'):
+                tree.setdefault(name, {})
+                tree = tree[name]
+        return tree
+
+    def lookup(self, path, tree):
+        if path:
+            try:
+                for name in path.split('/'):
+                    tree = tree[name]
+            except KeyError:
+                return 'x'
+            except TypeError:
+                return 'd'
+        return tree
+
+    def split(self, path):
+        c = path.rfind('/')
+        if c == -1:
+            return '', path
+        return path[:c], path[c+1:]
+
+    def filestatus(self, fn, st):
+        try:
+            type_, mode, size, time = self.repo.dirstate._map[fn][:4]
+        except KeyError:
+            type_ = '?'
+        if type_ == 'n':
+            if not st:
+                return '!'
+            st_mode, st_size, st_mtime = st
+            if size and (size != st_size or (mode ^ st_mode) & 0100):
+                return 'm'
+            if time != int(st_mtime):
+                return 'l'
+            return 'n'
+        if type_ in 'ma' and not st:
+            return '!'
+        if type_ == '?' and self.repo.dirstate._ignore(fn):
+            return 'i'
+        return type_
+
+    def updatestatus(self, wfn, st=None, status=None, oldstatus=None):
+        if st:
+            status = self.filestatus(wfn, st)
+        else:
+            self.statcache.pop(wfn, None)
+        root, fn = self.split(wfn)
+        d = self.dir(self.tree, root)
+        if oldstatus is None:
+            oldstatus = d.get(fn)
+        isdir = False
+        if oldstatus:
+            try:
+                if not status:
+                    if oldstatus in 'almn':
+                        status = '!'
+                    elif oldstatus == 'r':
+                        status = 'r'
+            except TypeError:
+                # oldstatus may be a dict left behind by a deleted
+                # directory
+                isdir = True
+            else:
+                if oldstatus in self.statuskeys and oldstatus != status:
+                    del self.dir(self.statustrees[oldstatus], root)[fn]
+        if self.ui.debugflag and oldstatus != status:
+            if isdir:
+                self.ui.note('status: %r dir(%d) -> %s\n' %
+                             (wfn, len(oldstatus), status))
+            else:
+                self.ui.note('status: %r %s -> %s\n' %
+                             (wfn, oldstatus, status))
+        if not isdir:
+            if status and status != 'i':
+                d[fn] = status
+                if status in self.statuskeys:
+                    dd = self.dir(self.statustrees[status], root)
+                    if oldstatus != status or fn not in dd:
+                        dd[fn] = status
+            else:
+                d.pop(fn, None)
+
+    def check_deleted(self, key):
+        # Files that had been deleted but were present in the dirstate
+        # may have vanished from the dirstate; we must clean them up.
+        nuke = []
+        for wfn, ignore in self.walk(key, self.statustrees[key]):
+            if wfn not in self.repo.dirstate:
+                nuke.append(wfn)
+        for wfn in nuke:
+            root, fn = self.split(wfn)
+            del self.dir(self.statustrees[key], root)[fn]
+            del self.dir(self.tree, root)[fn]
+        
+    def scan(self, topdir=''):
+        self.handle_timeout()
+        ds = self.repo.dirstate._map.copy()
+        self.add_watch(join(self.repo.root, topdir), self.mask)
+        for root, dirs, entries in walk(self.repo, topdir):
+            for d in dirs:
+                self.add_watch(join(root, d), self.mask)
+            wroot = root[len(self.wprefix):]
+            d = self.dir(self.tree, wroot)
+            for fn, kind in entries:
+                wfn = join(wroot, fn)
+                self.updatestatus(wfn, self.getstat(wfn))
+                ds.pop(wfn, None)
+        wtopdir = topdir
+        if wtopdir and wtopdir[-1] != '/':
+            wtopdir += '/'
+        for wfn, state in ds.iteritems():
+            if not wfn.startswith(wtopdir):
+                continue
+            status = state[0]
+            st = self.getstat(wfn)
+            if status == 'r' and not st:
+                self.updatestatus(wfn, st, status=status)
+            else:
+                self.updatestatus(wfn, st, oldstatus=status)
+        self.check_deleted('!')
+        self.check_deleted('r')
+
+    def check_dirstate(self):
+        ds_info = self.dirstate_info()
+        if ds_info == self.ds_info:
+            return
+        self.ds_info = ds_info
+        if not self.ui.debugflag:
+            self.last_event = None
+        self.ui.note(_('%s dirstate reload\n') % self.event_time())
+        self.repo.dirstate.invalidate()
+        self.scan()
+        self.ui.note(_('%s end dirstate reload\n') % self.event_time())
+
+    def walk(self, states, tree, prefix=''):
+        # This is the "inner loop" when talking to the client.
+        
+        for name, val in tree.iteritems():
+            path = join(prefix, name)
+            try:
+                if val in states:
+                    yield path, val
+            except TypeError:
+                for p in self.walk(states, val, path):
+                    yield p
+
+    def update_hgignore(self):
+        # An update of the ignore file can potentially change the
+        # states of all unknown and ignored files.
+
+        # XXX If the user has other ignore files outside the repo, or
+        # changes their list of ignore files at run time, we'll
+        # potentially never see changes to them.  We could get the
+        # client to report to us what ignore data they're using.
+        # But it's easier to do nothing than to open that can of
+        # worms.
+
+        if self.repo.dirstate.ignorefunc is not None:
+            self.repo.dirstate.ignorefunc = None
+            self.ui.note('rescanning due to .hgignore change\n')
+            self.scan()
+        
+    def getstat(self, wpath):
+        try:
+            return self.statcache[wpath]
+        except KeyError:
+            try:
+                return self.stat(wpath)
+            except OSError, err:
+                if err.errno != errno.ENOENT:
+                    raise
+        
+    def stat(self, wpath):
+        try:
+            st = os.lstat(join(self.wprefix, wpath))
+            ret = st.st_mode, st.st_size, st.st_mtime
+            self.statcache[wpath] = ret
+            return ret
+        except OSError, err:
+            self.statcache.pop(wpath, None)
+            raise
+            
+    def created(self, wpath):
+        if wpath == '.hgignore':
+            self.update_hgignore()
+        try:
+            st = self.stat(wpath)
+            if stat.S_ISREG(st[0]):
+                self.updatestatus(wpath, st)
+        except OSError, err:
+            pass
+
+    def modified(self, wpath):
+        if wpath == '.hgignore':
+            self.update_hgignore()
+        try:
+            st = self.stat(wpath)
+            if stat.S_ISREG(st[0]):
+                if self.repo.dirstate[wpath] in 'lmn':
+                    self.updatestatus(wpath, st)
+        except OSError:
+            pass
+
+    def deleted(self, wpath):
+        if wpath == '.hgignore':
+            self.update_hgignore()
+        elif wpath.startswith('.hg/'):
+            if wpath == '.hg/wlock':
+                self.check_dirstate()
+            return
+
+        self.updatestatus(wpath, None)
+        
+    def schedule_work(self, wpath, evt):
+        self.eventq.setdefault(wpath, [])
+        prev = self.eventq[wpath]
+        try:
+            if prev and evt == 'm' and prev[-1] in 'cm':
+                return
+            self.eventq[wpath].append(evt)
+        finally:
+            self.deferred += 1
+            self.timeout = 250
+
+    def deferred_event(self, wpath, evt):
+        if evt == 'c':
+            self.created(wpath)
+        elif evt == 'm':
+            self.modified(wpath)
+        elif evt == 'd':
+            self.deleted(wpath)
+            
+    def process_create(self, wpath, evt):
+        if self.ui.debugflag:
+            self.ui.note(_('%s event: created %s\n') %
+                         (self.event_time(), wpath))
+
+        if evt.mask & inotify.IN_ISDIR:
+            self.scan(wpath)
+        else:
+            self.schedule_work(wpath, 'c')
+
+    def process_delete(self, wpath, evt):
+        if self.ui.debugflag:
+            self.ui.note(('%s event: deleted %s\n') %
+                         (self.event_time(), wpath))
+
+        if evt.mask & inotify.IN_ISDIR:
+            self.scan(wpath)
+        else:
+            self.schedule_work(wpath, 'd')
+
+    def process_modify(self, wpath, evt):
+        if self.ui.debugflag:
+            self.ui.note(_('%s event: modified %s\n') %
+                         (self.event_time(), wpath))
+
+        if not (evt.mask & inotify.IN_ISDIR):
+            self.schedule_work(wpath, 'm')
+
+    def process_unmount(self, evt):
+        self.ui.warn(_('filesystem containing %s was unmounted\n') %
+                     evt.fullpath)
+        sys.exit(0)
+
+    def handle_event(self, fd, event):
+        if self.ui.debugflag:
+            self.ui.note('%s readable: %d bytes\n' %
+                         (self.event_time(), self.threshold.readable()))
+        if not self.threshold():
+            if self.registered:
+                if self.ui.debugflag:
+                    self.ui.note('%s below threshold - unhooking\n' %
+                                 (self.event_time()))
+                self.master.poll.unregister(fd)
+                self.registered = False
+                self.timeout = 250
+        else:
+            self.read_events()
+
+    def read_events(self, bufsize=None):
+        events = self.watcher.read(bufsize)
+        if self.ui.debugflag:
+            self.ui.note('%s reading %d events\n' %
+                         (self.event_time(), len(events)))
+        for evt in events:
+            wpath = self.wpath(evt)
+            if evt.mask & inotify.IN_UNMOUNT:
+                self.process_unmount(wpath, evt)
+            elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
+                self.process_modify(wpath, evt)
+            elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
+                             inotify.IN_MOVED_FROM):
+                self.process_delete(wpath, evt)
+            elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
+                self.process_create(wpath, evt)
+
+    def handle_timeout(self):
+        if not self.registered:
+            if self.ui.debugflag:
+                self.ui.note('%s hooking back up with %d bytes readable\n' %
+                             (self.event_time(), self.threshold.readable()))
+            self.read_events(0)
+            self.master.poll.register(self, select.POLLIN)
+            self.registered = True
+
+        if self.eventq:
+            if self.ui.debugflag:
+                self.ui.note('%s processing %d deferred events as %d\n' %
+                             (self.event_time(), self.deferred,
+                              len(self.eventq)))
+            eventq = self.eventq.items()
+            eventq.sort()
+            for wpath, evts in eventq:
+                for evt in evts:
+                    self.deferred_event(wpath, evt)
+            self.eventq.clear()
+            self.deferred = 0
+        self.timeout = None
+
+    def shutdown(self):
+        self.watcher.close()
+
+class Server(object):
+    poll_events = select.POLLIN
+
+    def __init__(self, ui, repo, watcher, timeout):
+        self.ui = ui
+        self.repo = repo
+        self.watcher = watcher
+        self.timeout = timeout
+        self.sock = socket.socket(socket.AF_UNIX)
+        self.sockpath = self.repo.join('inotify.sock')
+        try:
+            self.sock.bind(self.sockpath)
+        except socket.error, err:
+            if err[0] == errno.EADDRINUSE:
+                raise AlreadyStartedException(_('could not start server: %s') \
+                                              % err[1])
+            raise
+        self.sock.listen(5)
+        self.fileno = self.sock.fileno
+
+    def handle_timeout(self):
+        pass
+
+    def handle_event(self, fd, event):
+        sock, addr = self.sock.accept()
+
+        cs = common.recvcs(sock)
+        version = ord(cs.read(1))
+
+        sock.sendall(chr(common.version))
+
+        if version != common.version:
+            self.ui.warn(_('received query from incompatible client '
+                           'version %d\n') % version)
+            return
+
+        names = cs.read().split('\0')
+        
+        states = names.pop()
+
+        self.ui.note(_('answering query for %r\n') % states)
+
+        if self.watcher.timeout:
+            # We got a query while a rescan is pending.  Make sure we
+            # rescan before responding, or we could give back a wrong
+            # answer.
+            self.watcher.handle_timeout()
+
+        if not names:
+            def genresult(states, tree):
+                for fn, state in self.watcher.walk(states, tree):
+                    yield fn
+        else:
+            def genresult(states, tree):
+                for fn in names:
+                    l = self.watcher.lookup(fn, tree)
+                    try:
+                        if l in states:
+                            yield fn
+                    except TypeError:
+                        for f, s in self.watcher.walk(states, l, fn):
+                            yield f
+
+        results = ['\0'.join(r) for r in [
+            genresult('l', self.watcher.statustrees['l']),
+            genresult('m', self.watcher.statustrees['m']),
+            genresult('a', self.watcher.statustrees['a']),
+            genresult('r', self.watcher.statustrees['r']),
+            genresult('!', self.watcher.statustrees['!']),
+            '?' in states and genresult('?', self.watcher.statustrees['?']) or [],
+            [],
+            'c' in states and genresult('n', self.watcher.tree) or [],
+            ]]
+
+        try:
+            try:
+                sock.sendall(struct.pack(common.resphdrfmt,
+                                         *map(len, results)))
+                sock.sendall(''.join(results))
+            finally:
+                sock.shutdown(socket.SHUT_WR)
+        except socket.error, err:
+            if err[0] != errno.EPIPE:
+                raise
+
+    def shutdown(self):
+        self.sock.close()
+        try:
+            os.unlink(self.sockpath)
+        except OSError, err:
+            if err.errno != errno.ENOENT:
+                raise
+
+class Master(object):
+    def __init__(self, ui, repo, timeout=None):
+        self.ui = ui
+        self.repo = repo
+        self.poll = select.poll()
+        self.watcher = Watcher(ui, repo, self)
+        self.server = Server(ui, repo, self.watcher, timeout)
+        self.table = {}
+        for obj in (self.watcher, self.server):
+            fd = obj.fileno()
+            self.table[fd] = obj
+            self.poll.register(fd, obj.poll_events)
+
+    def register(self, fd, mask):
+        self.poll.register(fd, mask)
+
+    def shutdown(self):
+        for obj in self.table.itervalues():
+            obj.shutdown()
+
+    def run(self):
+        self.watcher.setup()
+        self.ui.note(_('finished setup\n'))
+        if os.getenv('TIME_STARTUP'):
+            sys.exit(0)
+        while True:
+            timeout = None
+            timeobj = None
+            for obj in self.table.itervalues():
+                if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
+                    timeout, timeobj = obj.timeout, obj
+            try:
+                if self.ui.debugflag:
+                    if timeout is None:
+                        self.ui.note('polling: no timeout\n')
+                    else:
+                        self.ui.note('polling: %sms timeout\n' % timeout)
+                events = self.poll.poll(timeout)
+            except select.error, err:
+                if err[0] == errno.EINTR:
+                    continue
+                raise
+            if events:
+                for fd, event in events:
+                    self.table[fd].handle_event(fd, event)
+            elif timeobj:
+                timeobj.handle_timeout()
+
+def start(ui, repo):
+    m = Master(ui, repo)
+    sys.stdout.flush()
+    sys.stderr.flush()
+
+    pid = os.fork()
+    if pid:
+        return pid
+
+    os.setsid()
+
+    fd = os.open('/dev/null', os.O_RDONLY)
+    os.dup2(fd, 0)
+    if fd > 0:
+        os.close(fd)
+
+    fd = os.open(ui.config('inotify', 'log', '/dev/null'),
+                 os.O_RDWR | os.O_CREAT | os.O_TRUNC)
+    os.dup2(fd, 1)
+    os.dup2(fd, 2)
+    if fd > 2:
+        os.close(fd)
+
+    try:
+        m.run()
+    finally:
+        m.shutdown()
+        os._exit(0)
--- a/setup.py	Wed Mar 12 22:20:10 2008 +0100
+++ b/setup.py	Wed Mar 12 15:30:11 2008 -0700
@@ -59,6 +59,14 @@
     Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c'])
     ]
 
+packages = ['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert']
+
+if sys.platform == 'linux2' and os.uname()[2] > '2.6':
+    # the inotify extension is only usable with Linux 2.6 kernels
+    ext_modules.append(Extension('hgext.inotify.linux._inotify',
+                                 ['hgext/inotify/linux/_inotify.c']))
+    packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
+
 try:
     import posix
     ext_modules.append(Extension('mercurial.osutil', ['mercurial/osutil.c']))
@@ -73,7 +81,7 @@
       description='Scalable distributed SCM',
       license='GNU GPL',
       scripts=['hg'],
-      packages=['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert'],
+      packages=packages,
       ext_modules=ext_modules,
       data_files=[(os.path.join('mercurial', root),
                    [os.path.join(root, file_) for file_ in files])