Merge pull request #906 from ZyX-I/uv-watcher

Add pyuv-based watcher
This commit is contained in:
Nikolai Aleksandrovich Pavlov 2014-08-30 16:01:07 +04:00
commit d714fb0d88
5 changed files with 298 additions and 86 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)

View File

@ -4,6 +4,7 @@ from __future__ import division
import threading import threading
import os import os
import re import re
import shutil
from time import sleep from time import sleep
from subprocess import call, PIPE from subprocess import call, PIPE
@ -581,12 +582,7 @@ class TestVCS(TestCase):
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(cls):
for repo_dir in [GIT_REPO] + ([HG_REPO] if use_mercurial else []) + ([BZR_REPO] if use_bzr else []): for repo_dir in [GIT_REPO] + ([HG_REPO] if use_mercurial else []) + ([BZR_REPO] if use_bzr else []):
for root, dirs, files in list(os.walk(repo_dir, topdown=False)): shutil.rmtree(repo_dir)
for file in files:
os.remove(os.path.join(root, file))
for dir in dirs:
os.rmdir(os.path.join(root, dir))
os.rmdir(repo_dir)
if use_mercurial: if use_mercurial:
if cls.powerline_old_HGRCPATH is None: if cls.powerline_old_HGRCPATH is None:
os.environ.pop('HGRCPATH') os.environ.pop('HGRCPATH')

View File

@ -1,14 +1,16 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import absolute_import, unicode_literals, print_function, division from __future__ import absolute_import, unicode_literals, print_function, division
from powerline.lib.watcher import create_file_watcher, create_tree_watcher, INotifyError
from powerline import get_fallback_logger
from powerline.lib.monotonic import monotonic
import shutil import shutil
import os
from time import sleep from time import sleep
from functools import partial from functools import partial
import os
from powerline.lib.watcher import create_file_watcher, create_tree_watcher, INotifyError
from powerline.lib.watcher.uv import UvNotFound
from powerline import get_fallback_logger
from powerline.lib.monotonic import monotonic
from tests import TestCase, SkipTest from tests import TestCase, SkipTest
@ -16,6 +18,14 @@ from tests import TestCase, SkipTest
INOTIFY_DIR = 'inotify' + os.environ.get('PYTHON', '') INOTIFY_DIR = 'inotify' + os.environ.get('PYTHON', '')
def clear_dir(dir):
for root, dirs, files in list(os.walk(dir, topdown=False)):
for f in files:
os.remove(os.path.join(root, f))
for d in dirs:
os.rmdir(os.path.join(root, d))
class TestFilesystemWatchers(TestCase): class TestFilesystemWatchers(TestCase):
def do_test_for_change(self, watcher, path): def do_test_for_change(self, watcher, path):
st = monotonic() st = monotonic()
@ -30,78 +40,106 @@ class TestFilesystemWatchers(TestCase):
w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify') w = create_file_watcher(pl=get_fallback_logger(), watcher_type='inotify')
except INotifyError: except INotifyError:
raise SkipTest('This test is not suitable for a stat based file watcher') raise SkipTest('This test is not suitable for a stat based file watcher')
f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3)) return self.do_test_file_watcher(w)
with open(f1, 'wb'):
with open(f2, 'wb'): def do_test_file_watcher(self, w):
with open(f3, 'wb'): try:
f1, f2, f3 = map(lambda x: os.path.join(INOTIFY_DIR, 'file%d' % x), (1, 2, 3))
with open(f1, 'wb'):
with open(f2, 'wb'):
with open(f3, 'wb'):
pass
ne = os.path.join(INOTIFY_DIR, 'notexists')
self.assertRaises(OSError, w, ne)
self.assertTrue(w(f1))
self.assertTrue(w(f2))
os.utime(f1, None), os.utime(f2, None)
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2)
# Repeat once
os.utime(f1, None), os.utime(f2, None)
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2)
# Check that no false changes are reported
self.assertFalse(w(f1), 'Spurious change detected')
self.assertFalse(w(f2), 'Spurious change detected')
# Check that open the file with 'w' triggers a change
with open(f1, 'wb'):
with open(f2, 'wb'):
pass pass
ne = os.path.join(INOTIFY_DIR, 'notexists') self.do_test_for_change(w, f1)
self.assertRaises(OSError, w, ne) self.do_test_for_change(w, f2)
self.assertTrue(w(f1)) # Check that writing to a file with 'a' triggers a change
self.assertTrue(w(f2)) with open(f1, 'ab') as f:
os.utime(f1, None), os.utime(f2, None) f.write(b'1')
self.do_test_for_change(w, f1) self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2) # Check that deleting a file registers as a change
# Repeat once os.unlink(f1)
os.utime(f1, None), os.utime(f2, None) self.do_test_for_change(w, f1)
self.do_test_for_change(w, f1) # Test that changing the inode of a file does not cause it to stop
self.do_test_for_change(w, f2) # being watched
# Check that no false changes are reported os.rename(f3, f2)
self.assertFalse(w(f1), 'Spurious change detected') self.do_test_for_change(w, f2)
self.assertFalse(w(f2), 'Spurious change detected') self.assertFalse(w(f2), 'Spurious change detected')
# Check that open the file with 'w' triggers a change os.utime(f2, None)
with open(f1, 'wb'): self.do_test_for_change(w, f2)
with open(f2, 'wb'): finally:
pass clear_dir(INOTIFY_DIR)
self.do_test_for_change(w, f1)
self.do_test_for_change(w, f2) def test_uv_file_watcher(self):
# Check that writing to a file with 'a' triggers a change raise SkipTest('Uv watcher tests are not stable')
with open(f1, 'ab') as f: try:
f.write(b'1') w = create_file_watcher(pl=get_fallback_logger(), watcher_type='uv')
self.do_test_for_change(w, f1) except UvNotFound:
# Check that deleting a file registers as a change raise SkipTest('Pyuv is not available')
os.unlink(f1) return self.do_test_file_watcher(w)
self.do_test_for_change(w, f1)
# Test that changing the inode of a file does not cause it to stop
# being watched
os.rename(f3, f2)
self.do_test_for_change(w, f2)
self.assertFalse(w(f2), 'Spurious change detected')
os.utime(f2, None)
self.do_test_for_change(w, f2)
def test_tree_watcher(self): def test_tree_watcher(self):
tw = create_tree_watcher(get_fallback_logger()) tw = create_tree_watcher(get_fallback_logger())
subdir = os.path.join(INOTIFY_DIR, 'subdir') return self.do_test_tree_watcher(tw)
os.mkdir(subdir)
if tw.watch(INOTIFY_DIR).is_dummy: def do_test_tree_watcher(self, tw):
raise SkipTest('No tree watcher available') try:
self.assertTrue(tw(INOTIFY_DIR)) subdir = os.path.join(INOTIFY_DIR, 'subdir')
self.assertFalse(tw(INOTIFY_DIR)) os.mkdir(subdir)
changed = partial(self.do_test_for_change, tw, INOTIFY_DIR) try:
open(os.path.join(INOTIFY_DIR, 'tree1'), 'w').close() if tw.watch(INOTIFY_DIR).is_dummy:
changed() raise SkipTest('No tree watcher available')
open(os.path.join(subdir, 'tree1'), 'w').close() except UvNotFound:
changed() raise SkipTest('Pyuv is not available')
os.unlink(os.path.join(subdir, 'tree1')) self.assertTrue(tw(INOTIFY_DIR))
changed() self.assertFalse(tw(INOTIFY_DIR))
os.rmdir(subdir) changed = partial(self.do_test_for_change, tw, INOTIFY_DIR)
changed() open(os.path.join(INOTIFY_DIR, 'tree1'), 'w').close()
os.mkdir(subdir) changed()
changed() open(os.path.join(subdir, 'tree1'), 'w').close()
os.rename(subdir, subdir + '1') changed()
changed() os.unlink(os.path.join(subdir, 'tree1'))
shutil.rmtree(subdir + '1') changed()
changed() os.rmdir(subdir)
os.mkdir(subdir) changed()
f = os.path.join(subdir, 'f') os.mkdir(subdir)
open(f, 'w').close() changed()
changed() os.rename(subdir, subdir + '1')
with open(f, 'a') as s: changed()
s.write(' ') shutil.rmtree(subdir + '1')
changed() changed()
os.rename(f, f + '1') os.mkdir(subdir)
changed() f = os.path.join(subdir, 'f')
open(f, 'w').close()
changed()
with open(f, 'a') as s:
s.write(' ')
changed()
os.rename(f, f + '1')
changed()
finally:
clear_dir(INOTIFY_DIR)
def test_uv_tree_watcher(self):
raise SkipTest('Uv watcher tests are not stable')
tw = create_tree_watcher(get_fallback_logger(), 'uv')
return self.do_test_tree_watcher(tw)
old_cwd = None old_cwd = None
@ -115,13 +153,7 @@ def setUpModule():
def tearDownModule(): def tearDownModule():
for d in [INOTIFY_DIR]: shutil.rmtree(INOTIFY_DIR)
for root, dirs, files in list(os.walk(d, topdown=False)):
for file in files:
os.remove(os.path.join(root, file))
for dir in dirs:
os.rmdir(os.path.join(root, dir))
os.rmdir(d)
os.chdir(old_cwd) os.chdir(old_cwd)