0
0
.dotfiles/powerline-bin/powerline/lib/watcher/inotify.py

269 lines
7.7 KiB
Python
Raw Normal View History

2024-09-07 11:49:53 -04:00
# 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