Add libuv-based watcher

Fixes #821
This commit is contained in:
ZyX 2014-06-29 00:52:23 +04:00
parent f5f85ab808
commit ea3cd2c1c7
3 changed files with 184 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import sys
from powerline.lib.watcher.stat import StatFileWatcher from powerline.lib.watcher.stat import StatFileWatcher
from powerline.lib.watcher.inotify import INotifyFileWatcher from powerline.lib.watcher.inotify import INotifyFileWatcher
from powerline.lib.watcher.tree import TreeWatcher from powerline.lib.watcher.tree import TreeWatcher
from powerline.lib.watcher.uv import UvFileWatcher, UvNotFound
from powerline.lib.inotify import INotifyError from powerline.lib.inotify import INotifyError
@ -39,6 +40,9 @@ def create_file_watcher(pl, watcher_type='auto', expire_time=10):
# Explicitly selected inotify watcher: do not catch INotifyError then. # Explicitly selected inotify watcher: do not catch INotifyError then.
pl.debug('Using requested inotify watcher', prefix='watcher') pl.debug('Using requested inotify watcher', prefix='watcher')
return INotifyFileWatcher(expire_time=expire_time) return INotifyFileWatcher(expire_time=expire_time)
elif watcher_type == 'uv':
pl.debug('Using requested uv watcher', prefix='watcher')
return UvFileWatcher()
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
try: try:
@ -47,6 +51,12 @@ def create_file_watcher(pl, watcher_type='auto', expire_time=10):
except INotifyError: except INotifyError:
pl.info('Failed to create inotify watcher', prefix='watcher') pl.info('Failed to create inotify watcher', prefix='watcher')
try:
pl.debug('Using libuv-based watcher')
return UvFileWatcher()
except UvNotFound:
pl.debug('Failed to import pyuv')
pl.debug('Using stat-based watcher') pl.debug('Using stat-based watcher')
return StatFileWatcher() return StatFileWatcher()

View File

@ -7,6 +7,7 @@ from powerline.lib.monotonic import monotonic
from powerline.lib.inotify import INotifyError from powerline.lib.inotify import INotifyError
from powerline.lib.path import realpath from powerline.lib.path import realpath
from powerline.lib.watcher.inotify import INotifyTreeWatcher, DirTooLarge, NoSuchDir, BaseDirChanged from powerline.lib.watcher.inotify import INotifyTreeWatcher, DirTooLarge, NoSuchDir, BaseDirChanged
from powerline.lib.watcher.uv import UvTreeWatcher, UvNotFound
class DummyTreeWatcher(object): class DummyTreeWatcher(object):
@ -30,6 +31,8 @@ class TreeWatcher(object):
def get_watcher(self, path, ignore_event): def get_watcher(self, path, ignore_event):
if self.watcher_type == 'inotify': if self.watcher_type == 'inotify':
return INotifyTreeWatcher(path, ignore_event=ignore_event) return INotifyTreeWatcher(path, ignore_event=ignore_event)
if self.watcher_type == 'uv':
return UvTreeWatcher(path, ignore_event=ignore_event)
if self.watcher_type == 'dummy': if self.watcher_type == 'dummy':
return DummyTreeWatcher(path) return DummyTreeWatcher(path)
# FIXME # FIXME
@ -42,6 +45,10 @@ class TreeWatcher(object):
except (INotifyError, DirTooLarge) as e: except (INotifyError, DirTooLarge) as e:
if not isinstance(e, INotifyError): if not isinstance(e, INotifyError):
self.pl.warn('Failed to watch path: {0} with error: {1}'.format(path, e)) self.pl.warn('Failed to watch path: {0} with error: {1}'.format(path, e))
try:
return UvTreeWatcher(path, ignore_event=ignore_event)
except UvNotFound:
pass
return DummyTreeWatcher(path) return DummyTreeWatcher(path)
else: else:
raise ValueError('Unknown watcher type: {0}'.format(self.watcher_type)) raise ValueError('Unknown watcher type: {0}'.format(self.watcher_type))

167
powerline/lib/watcher/uv.py Normal file
View File

@ -0,0 +1,167 @@
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, absolute_import, print_function)
from powerline.lib.path import realpath
from collections import defaultdict
from threading import RLock
from functools import partial
from threading import Thread
import os
class UvNotFound(NotImplementedError):
pass
pyuv = None
def import_pyuv():
global pyuv
if not pyuv:
try:
pyuv = __import__('pyuv')
except ImportError:
raise UvNotFound
class UvThread(Thread):
daemon = True
def __init__(self, loop):
self.uv_loop = loop
super(UvThread, self).__init__()
def run(self):
while True:
self.uv_loop.run()
def join(self):
self.uv_loop.stop()
return super(UvThread, self).join()
_uv_thread = None
def start_uv_thread():
global _uv_thread
if _uv_thread is None:
loop = pyuv.Loop()
_uv_thread = UvThread(loop)
_uv_thread.start()
return _uv_thread.uv_loop
class UvWatcher(object):
def __init__(self):
import_pyuv()
self.watches = {}
self.lock = RLock()
self.loop = start_uv_thread()
def watch(self, path):
with self.lock:
if path not in self.watches:
try:
self.watches[path] = pyuv.fs.FSEvent(
self.loop,
path,
partial(self._record_event, path),
pyuv.fs.UV_CHANGE | pyuv.fs.UV_RENAME
)
except pyuv.error.FSEventError as e:
code = e.args[0]
if code == pyuv.errno.UV_ENOENT:
raise OSError('No such file or directory: ' + path)
else:
raise
def unwatch(self, path):
with self.lock:
try:
watch = self.watches.pop(path)
except KeyError:
return
watch.close(partial(self._stopped_watching, path))
def __del__(self):
try:
lock = self.lock
except AttributeError:
pass
else:
with lock:
while self.watches:
path, watch = self.watches.popitem()
watch.close(partial(self._stopped_watching, path))
class UvFileWatcher(UvWatcher):
def __init__(self):
super(UvFileWatcher, self).__init__()
self.events = defaultdict(list)
def _record_event(self, path, fsevent_handle, filename, events, error):
with self.lock:
self.events[path].append(events)
if events | pyuv.fs.UV_RENAME:
if not os.path.exists(path):
self.watches.pop(path).close()
def _stopped_watching(self, path, *args):
self.events.pop(path, None)
def __call__(self, path):
with self.lock:
events = self.events.pop(path, None)
if events:
return True
if path not in self.watches:
self.watch(path)
return True
class UvTreeWatcher(UvWatcher):
is_dummy = False
def __init__(self, basedir, ignore_event=None):
super(UvTreeWatcher, self).__init__()
self.ignore_event = ignore_event or (lambda path, name: False)
self.basedir = realpath(basedir)
self.modified = True
self.watch_directory(self.basedir)
def watch_directory(self, path):
os.path.walk(path, self.watch_one_directory, None)
def watch_one_directory(self, arg, dirname, fnames):
try:
self.watch(dirname)
except OSError:
pass
def _stopped_watching(self, path, *args):
pass
def _record_event(self, path, fsevent_handle, filename, events, error):
if not self.ignore_event(path, filename):
self.modified = True
if events == pyuv.fs.UV_CHANGE | pyuv.fs.UV_RENAME:
# Stat changes to watched directory are UV_CHANGE|UV_RENAME. It
# is weird.
pass
elif events | pyuv.fs.UV_RENAME:
if not os.path.isdir(path):
self.unwatch(path)
else:
full_name = os.path.join(path, filename)
if os.path.isdir(full_name):
# For some reason mkdir and rmdir both fall into this
# category
self.watch_directory(full_name)
def __call__(self):
return self.__dict__.pop('modified', False)