summaryrefslogtreecommitdiffstats
path: root/powerline/lib/watcher/inotify.py
diff options
context:
space:
mode:
Diffstat (limited to 'powerline/lib/watcher/inotify.py')
-rw-r--r--powerline/lib/watcher/inotify.py268
1 files changed, 268 insertions, 0 deletions
diff --git a/powerline/lib/watcher/inotify.py b/powerline/lib/watcher/inotify.py
new file mode 100644
index 0000000..c4f1200
--- /dev/null
+++ b/powerline/lib/watcher/inotify.py
@@ -0,0 +1,268 @@
+# vim:fileencoding=utf-8:noet
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+import errno
+import os
+import ctypes
+
+from threading import RLock
+
+from powerline.lib.inotify import INotify
+from powerline.lib.monotonic import monotonic
+from powerline.lib.path import realpath
+
+
+class INotifyFileWatcher(INotify):
+ def __init__(self, expire_time=10):
+ super(INotifyFileWatcher, self).__init__()
+ self.watches = {}
+ self.modified = {}
+ self.last_query = {}
+ self.lock = RLock()
+ self.expire_time = expire_time * 60
+
+ def expire_watches(self):
+ now = monotonic()
+ for path, last_query in tuple(self.last_query.items()):
+ if last_query - now > self.expire_time:
+ self.unwatch(path)
+
+ def process_event(self, wd, mask, cookie, name):
+ if wd == -1 and (mask & self.Q_OVERFLOW):
+ # We missed some INOTIFY events, so we don't
+ # know the state of any tracked files.
+ for path in tuple(self.modified):
+ if os.path.exists(path):
+ self.modified[path] = True
+ else:
+ self.watches.pop(path, None)
+ self.modified.pop(path, None)
+ self.last_query.pop(path, None)
+ return
+
+ for path, num in tuple(self.watches.items()):
+ if num == wd:
+ if mask & self.IGNORED:
+ self.watches.pop(path, None)
+ self.modified.pop(path, None)
+ self.last_query.pop(path, None)
+ else:
+ if mask & self.ATTRIB:
+ # The watched file could have had its inode changed, in
+ # which case we will not get any more events for this
+ # file, so re-register the watch. For example by some
+ # other file being renamed as this file.
+ try:
+ self.unwatch(path)
+ except OSError:
+ pass
+ try:
+ self.watch(path)
+ except OSError as e:
+ if getattr(e, 'errno', None) != errno.ENOENT:
+ raise
+ else:
+ self.modified[path] = True
+ else:
+ self.modified[path] = True
+
+ def unwatch(self, path):
+ ''' Remove the watch for path. Raises an OSError if removing the watch
+ fails for some reason. '''
+ path = realpath(path)
+ with self.lock:
+ self.modified.pop(path, None)
+ self.last_query.pop(path, None)
+ wd = self.watches.pop(path, None)
+ if wd is not None:
+ if self._rm_watch(self._inotify_fd, wd) != 0:
+ self.handle_error()
+
+ def watch(self, path):
+ ''' Register a watch for the file/directory named path. Raises an OSError if path
+ does not exist. '''
+ path = realpath(path)
+ with self.lock:
+ if path not in self.watches:
+ bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
+ flags = self.MOVE_SELF | self.DELETE_SELF
+ buf = ctypes.c_char_p(bpath)
+ # Try watching path as a directory
+ wd = self._add_watch(self._inotify_fd, buf, flags | self.ONLYDIR)
+ if wd == -1:
+ eno = ctypes.get_errno()
+ if eno != errno.ENOTDIR:
+ self.handle_error()
+ # Try watching path as a file
+ flags |= (self.MODIFY | self.ATTRIB)
+ wd = self._add_watch(self._inotify_fd, buf, flags)
+ if wd == -1:
+ self.handle_error()
+ self.watches[path] = wd
+ self.modified[path] = False
+
+ def is_watching(self, path):
+ with self.lock:
+ return realpath(path) in self.watches
+
+ def __call__(self, path):
+ ''' Return True if path has been modified since the last call. Can
+ raise OSError if the path does not exist. '''
+ path = realpath(path)
+ with self.lock:
+ self.last_query[path] = monotonic()
+ self.expire_watches()
+ if path not in self.watches:
+ # Try to re-add the watch, it will fail if the file does not
+ # exist/you don't have permission
+ self.watch(path)
+ return True
+ self.read(get_name=False)
+ if path not in self.modified:
+ # An ignored event was received which means the path has been
+ # automatically unwatched
+ return True
+ ans = self.modified[path]
+ if ans:
+ self.modified[path] = False
+ return ans
+
+ def close(self):
+ with self.lock:
+ for path in tuple(self.watches):
+ try:
+ self.unwatch(path)
+ except OSError:
+ pass
+ super(INotifyFileWatcher, self).close()
+
+
+class NoSuchDir(ValueError):
+ pass
+
+
+class BaseDirChanged(ValueError):
+ pass
+
+
+class DirTooLarge(ValueError):
+ def __init__(self, bdir):
+ ValueError.__init__(self, 'The directory {0} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir))
+
+
+class INotifyTreeWatcher(INotify):
+ is_dummy = False
+
+ def __init__(self, basedir, ignore_event=None):
+ super(INotifyTreeWatcher, self).__init__()
+ self.basedir = realpath(basedir)
+ self.watch_tree()
+ self.modified = True
+ self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event
+
+ def watch_tree(self):
+ self.watched_dirs = {}
+ self.watched_rmap = {}
+ try:
+ self.add_watches(self.basedir)
+ except OSError as e:
+ if e.errno == errno.ENOSPC:
+ raise DirTooLarge(self.basedir)
+
+ def add_watches(self, base, top_level=True):
+ ''' Add watches for this directory and all its descendant directories,
+ recursively. '''
+ base = realpath(base)
+ # There may exist a link which leads to an endless
+ # add_watches loop or to maximum recursion depth exceeded
+ if not top_level and base in self.watched_dirs:
+ return
+ try:
+ is_dir = self.add_watch(base)
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ # The entry could have been deleted between listdir() and
+ # add_watch().
+ if top_level:
+ raise NoSuchDir('The dir {0} does not exist'.format(base))
+ return
+ if e.errno == errno.EACCES:
+ # We silently ignore entries for which we don't have permission,
+ # unless they are the top level dir
+ if top_level:
+ raise NoSuchDir('You do not have permission to monitor {0}'.format(base))
+ return
+ raise
+ else:
+ if is_dir:
+ try:
+ files = os.listdir(base)
+ except OSError as e:
+ if e.errno in (errno.ENOTDIR, errno.ENOENT):
+ # The dir was deleted/replaced between the add_watch()
+ # and listdir()
+ if top_level:
+ raise NoSuchDir('The dir {0} does not exist'.format(base))
+ return
+ raise
+ for x in files:
+ self.add_watches(os.path.join(base, x), top_level=False)
+ elif top_level:
+ # The top level dir is a file, not good.
+ raise NoSuchDir('The dir {0} does not exist'.format(base))
+
+ def add_watch(self, path):
+ bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
+ wd = self._add_watch(
+ self._inotify_fd,
+ ctypes.c_char_p(bpath),
+
+ # Ignore symlinks and watch only directories
+ self.DONT_FOLLOW | self.ONLYDIR |
+
+ self.MODIFY | self.CREATE | self.DELETE |
+ self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO |
+ self.ATTRIB | self.DELETE_SELF
+ )
+ if wd == -1:
+ eno = ctypes.get_errno()
+ if eno == errno.ENOTDIR:
+ return False
+ raise OSError(eno, 'Failed to add watch for: {0}: {1}'.format(path, self.os.strerror(eno)))
+ self.watched_dirs[path] = wd
+ self.watched_rmap[wd] = path
+ return True
+
+ def process_event(self, wd, mask, cookie, name):
+ if wd == -1 and (mask & self.Q_OVERFLOW):
+ # We missed some INOTIFY events, so we don't
+ # know the state of any tracked dirs.
+ self.watch_tree()
+ self.modified = True
+ return
+ path = self.watched_rmap.get(wd, None)
+ if path is not None:
+ if not self.ignore_event(path, name):
+ self.modified = True
+ if mask & self.CREATE:
+ # A new sub-directory might have been created, monitor it.
+ try:
+ if not isinstance(path, bytes):
+ name = name.decode(self.fenc)
+ self.add_watch(os.path.join(path, name))
+ except OSError as e:
+ if e.errno == errno.ENOENT:
+ # Deleted before add_watch()
+ pass
+ elif e.errno == errno.ENOSPC:
+ raise DirTooLarge(self.basedir)
+ else:
+ raise
+ if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir:
+ raise BaseDirChanged('The directory %s was moved/deleted' % path)
+
+ def __call__(self):
+ self.read()
+ ret = self.modified
+ self.modified = False
+ return ret