summaryrefslogtreecommitdiff
path: root/hgext/inotify/linux
diff options
context:
space:
mode:
Diffstat (limited to 'hgext/inotify/linux')
-rw-r--r--hgext/inotify/linux/__init__.py44
-rw-r--r--hgext/inotify/linux/_inotify.c649
-rw-r--r--hgext/inotify/linux/watcher.py335
3 files changed, 1028 insertions, 0 deletions
diff --git a/hgext/inotify/linux/__init__.py b/hgext/inotify/linux/__init__.py
new file mode 100644
index 0000000..8a1bafd
--- /dev/null
+++ b/hgext/inotify/linux/__init__.py
@@ -0,0 +1,44 @@
+# __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, or any later version.
+
+'''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:
+ fp = open(procfs_path + '/' + name)
+ r = int(fp.read())
+ fp.close()
+ return r
+ except OSError:
+ 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')
diff --git a/hgext/inotify/linux/_inotify.c b/hgext/inotify/linux/_inotify.c
new file mode 100644
index 0000000..5e31b85
--- /dev/null
+++ b/hgext/inotify/linux/_inotify.c
@@ -0,0 +1,649 @@
+/*
+ * _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 or any later version.
+ */
+
+#include <Python.h>
+#include <alloca.h>
+#include <sys/inotify.h>
+#include <stdint.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include <util.h>
+
+/* Variables used in the event string representation */
+static PyObject *join;
+static PyObject *er_wm;
+static PyObject *er_wmc;
+static PyObject *er_wmn;
+static PyObject *er_wmcn;
+
+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)
+{
+ uint32_t wd;
+ int fd;
+ int r;
+
+ if (!PyArg_ParseTuple(args, "iI:remove_watch", &fd, &wd))
+ return NULL;
+
+ Py_BEGIN_ALLOW_THREADS;
+ r = inotify_rm_watch(fd, wd);
+ Py_END_ALLOW_THREADS;
+
+ if (r == -1) {
+ PyErr_SetFromErrno(PyExc_OSError);
+ return NULL;
+ }
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+
+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);
+
+ Py_TYPE(evt)->tp_free(evt);
+}
+
+static PyObject *event_repr(struct event *evt)
+{
+ int cookie = evt->cookie == Py_None ? -1 : PyInt_AsLong(evt->cookie);
+ PyObject *ret = NULL, *pymasks = NULL, *pymask = NULL;
+ PyObject *tuple = NULL, *formatstr = NULL;
+
+ pymasks = decode_mask(PyInt_AsLong(evt->mask));
+ if (pymasks == NULL)
+ goto bail;
+
+ pymask = _PyString_Join(join, pymasks);
+ if (pymask == NULL)
+ goto bail;
+
+ if (evt->name != Py_None) {
+ if (cookie == -1) {
+ formatstr = er_wmn;
+ tuple = PyTuple_Pack(3, evt->wd, pymask, evt->name);
+ }
+ else {
+ formatstr = er_wmcn;
+ tuple = PyTuple_Pack(4, evt->wd, pymask,
+ evt->cookie, evt->name);
+ }
+ } else {
+ if (cookie == -1) {
+ formatstr = er_wm;
+ tuple = PyTuple_Pack(2, evt->wd, pymask);
+ }
+ else {
+ formatstr = er_wmc;
+ tuple = PyTuple_Pack(3, evt->wd, pymask, evt->cookie);
+ }
+ }
+
+ if (tuple == NULL)
+ goto bail;
+
+ ret = PyNumber_Remainder(formatstr, tuple);
+
+ if (ret == NULL)
+ goto bail;
+
+ goto done;
+bail:
+ Py_CLEAR(ret);
+
+done:
+ Py_XDECREF(pymask);
+ Py_XDECREF(pymasks);
+ Py_XDECREF(tuple);
+
+ return ret;
+}
+
+static PyTypeObject event_type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "_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;
+}
+
+static int init_globals(void)
+{
+ join = PyString_FromString("|");
+ er_wm = PyString_FromString("event(wd=%d, mask=%s)");
+ er_wmn = PyString_FromString("event(wd=%d, mask=%s, name=%s)");
+ er_wmc = PyString_FromString("event(wd=%d, mask=%s, cookie=0x%x)");
+ er_wmcn = PyString_FromString("event(wd=%d, mask=%s, cookie=0x%x, name=%s)");
+
+ return join && er_wm && er_wmn && er_wmc && er_wmcn;
+}
+
+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},
+};
+
+#ifdef IS_PY3K
+static struct PyModuleDef _inotify_module = {
+ PyModuleDef_HEAD_INIT,
+ "_inotify",
+ doc,
+ -1,
+ methods
+};
+
+PyMODINIT_FUNC PyInit__inotify(void)
+{
+ PyObject *mod, *dict;
+
+ mod = PyModule_Create(&_inotify_module);
+
+ if (mod == NULL)
+ return NULL;
+
+ if (!init_globals())
+ return;
+
+ dict = PyModule_GetDict(mod);
+
+ if (dict)
+ define_consts(dict);
+
+ return mod;
+}
+#else
+void init_inotify(void)
+{
+ PyObject *mod, *dict;
+
+ if (PyType_Ready(&event_type) == -1)
+ return;
+
+ if (!init_globals())
+ return;
+
+ mod = Py_InitModule3("_inotify", methods, doc);
+
+ dict = PyModule_GetDict(mod);
+
+ if (dict)
+ define_consts(dict);
+}
+#endif
diff --git a/hgext/inotify/linux/watcher.py b/hgext/inotify/linux/watcher.py
new file mode 100644
index 0000000..cd62006
--- /dev/null
+++ b/hgext/inotify/linux/watcher.py
@@ -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, or any later version.
+
+'''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