269 lines
7.7 KiB
Python
269 lines
7.7 KiB
Python
|
# 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 dont
|
||
|
# 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 dont 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 dont 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 dont
|
||
|
# 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
|