| # 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 be 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 |