|  | @@ -0,0 +1,178 @@
 | 
	
		
			
				|  |  | +# -*- coding: utf-8 -*-
 | 
	
		
			
				|  |  | +"""
 | 
	
		
			
				|  |  | +    celery.worker.autoreload
 | 
	
		
			
				|  |  | +    ~~~~~~~~~~~~~~~~~~~~~~~~
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    This module implements the automatic module reloading
 | 
	
		
			
				|  |  | +"""
 | 
	
		
			
				|  |  | +from __future__ import with_statement
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import os
 | 
	
		
			
				|  |  | +import sys
 | 
	
		
			
				|  |  | +import time
 | 
	
		
			
				|  |  | +import select
 | 
	
		
			
				|  |  | +import hashlib
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +from collections import defaultdict
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +from celery.task.control import broadcast
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +def file_hash(filename, algorithm='md5'):
 | 
	
		
			
				|  |  | +    hobj = hashlib.new(algorithm)
 | 
	
		
			
				|  |  | +    with open(filename, 'rb') as f:
 | 
	
		
			
				|  |  | +        for chunk in iter(lambda: f.read(2 ** 20), ''):
 | 
	
		
			
				|  |  | +            hobj.update(chunk)
 | 
	
		
			
				|  |  | +    return hobj.digest()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class StatMonitor(object):
 | 
	
		
			
				|  |  | +    """File change monitor based on `stat` system call"""
 | 
	
		
			
				|  |  | +    def __init__(self, files, on_change=None, interval=0.5):
 | 
	
		
			
				|  |  | +        self._files = files
 | 
	
		
			
				|  |  | +        self._interval = interval
 | 
	
		
			
				|  |  | +        self._on_change = on_change
 | 
	
		
			
				|  |  | +        self._modify_times = defaultdict(int)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def start(self):
 | 
	
		
			
				|  |  | +        while True:
 | 
	
		
			
				|  |  | +            modified = {}
 | 
	
		
			
				|  |  | +            for m in self._files:
 | 
	
		
			
				|  |  | +                mt = self._mtime(m)
 | 
	
		
			
				|  |  | +                if mt is None:
 | 
	
		
			
				|  |  | +                    break
 | 
	
		
			
				|  |  | +                if self._modify_times[m] != mt:
 | 
	
		
			
				|  |  | +                    modified[m] = mt
 | 
	
		
			
				|  |  | +            else:
 | 
	
		
			
				|  |  | +                if modified:
 | 
	
		
			
				|  |  | +                    self.on_change(modified.keys())
 | 
	
		
			
				|  |  | +                    self._modify_times.update(modified)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            time.sleep(self._interval)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def on_change(self, modified):
 | 
	
		
			
				|  |  | +        if self._on_change:
 | 
	
		
			
				|  |  | +            return self._on_change(modified)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @classmethod
 | 
	
		
			
				|  |  | +    def _mtime(cls, path):
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            return os.stat(path).st_mtime
 | 
	
		
			
				|  |  | +        except:
 | 
	
		
			
				|  |  | +            return
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class KQueueMonitor(object):
 | 
	
		
			
				|  |  | +    """File change monitor based on BSD kernel event notifications"""
 | 
	
		
			
				|  |  | +    def __init__(self, files, on_change=None):
 | 
	
		
			
				|  |  | +        assert hasattr(select, 'kqueue')
 | 
	
		
			
				|  |  | +        self._files = dict([(f, None) for f in files])
 | 
	
		
			
				|  |  | +        self._on_change = on_change
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def start(self):
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            self._kq = select.kqueue()
 | 
	
		
			
				|  |  | +            kevents = []
 | 
	
		
			
				|  |  | +            for f in self._files:
 | 
	
		
			
				|  |  | +                self._files[f] = fd = os.open(f, os.O_RDONLY)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                ev = select.kevent(fd,
 | 
	
		
			
				|  |  | +                        filter=select.KQ_FILTER_VNODE,
 | 
	
		
			
				|  |  | +                        flags=select.KQ_EV_ADD |
 | 
	
		
			
				|  |  | +                              select.KQ_EV_ENABLE |
 | 
	
		
			
				|  |  | +                              select.KQ_EV_CLEAR,
 | 
	
		
			
				|  |  | +                        fflags=select.KQ_NOTE_WRITE |
 | 
	
		
			
				|  |  | +                               select.KQ_NOTE_EXTEND)
 | 
	
		
			
				|  |  | +                kevents.append(ev)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            events = self._kq.control(kevents, 0)
 | 
	
		
			
				|  |  | +            while True:
 | 
	
		
			
				|  |  | +                events = self._kq.control(kevents, 1)
 | 
	
		
			
				|  |  | +                fds = [e.ident for e in events]
 | 
	
		
			
				|  |  | +                modified = [k for k, v in self._files.iteritems()
 | 
	
		
			
				|  |  | +                                            if v in fds]
 | 
	
		
			
				|  |  | +                self.on_change(modified)
 | 
	
		
			
				|  |  | +        finally:
 | 
	
		
			
				|  |  | +            self.close()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def close(self):
 | 
	
		
			
				|  |  | +        self._kq.close()
 | 
	
		
			
				|  |  | +        for f in self._files:
 | 
	
		
			
				|  |  | +            if self._files[f] is not None:
 | 
	
		
			
				|  |  | +                os.close(self._files[f])
 | 
	
		
			
				|  |  | +                self._files[f] = None
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def on_change(self, modified):
 | 
	
		
			
				|  |  | +        if self._on_change:
 | 
	
		
			
				|  |  | +            return self._on_change(modified)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +try:
 | 
	
		
			
				|  |  | +    import pyinotify
 | 
	
		
			
				|  |  | +except ImportError:
 | 
	
		
			
				|  |  | +    pyinotify = None    # noqa
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class InotifyMonitor(pyinotify and pyinotify.ProcessEvent or object):
 | 
	
		
			
				|  |  | +    """File change monitor based on  Linux kernel `inotify` subsystem"""
 | 
	
		
			
				|  |  | +    def __init__(self, modules, on_change=None):
 | 
	
		
			
				|  |  | +        assert pyinotify
 | 
	
		
			
				|  |  | +        self._modules = modules
 | 
	
		
			
				|  |  | +        self._on_change = on_change
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def start(self):
 | 
	
		
			
				|  |  | +        try:
 | 
	
		
			
				|  |  | +            self._wm = pyinotify.WatchManager()
 | 
	
		
			
				|  |  | +            self._notifier = pyinotify.Notifier(self._wm)
 | 
	
		
			
				|  |  | +            for m in self._modules:
 | 
	
		
			
				|  |  | +                self._wm.add_watch(m, pyinotify.IN_MODIFY)
 | 
	
		
			
				|  |  | +            self._notifier.loop()
 | 
	
		
			
				|  |  | +        finally:
 | 
	
		
			
				|  |  | +            self.close()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def close(self):
 | 
	
		
			
				|  |  | +        self._notifier.stop()
 | 
	
		
			
				|  |  | +        self._wm.close()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def process_IN_MODIFY(self, event):
 | 
	
		
			
				|  |  | +        self.on_change(event.pathname)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def on_change(self, modified):
 | 
	
		
			
				|  |  | +        if self._on_change:
 | 
	
		
			
				|  |  | +            self._on_change(modified)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +if hasattr(select, 'kqueue'):
 | 
	
		
			
				|  |  | +    _monitor_cls = KQueueMonitor
 | 
	
		
			
				|  |  | +elif sys.platform.startswith('linux') and pyinotify:
 | 
	
		
			
				|  |  | +    _monitor_cls = InotifyMonitor
 | 
	
		
			
				|  |  | +else:
 | 
	
		
			
				|  |  | +    _monitor_cls = StatMonitor
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +class AutoReloader(object):
 | 
	
		
			
				|  |  | +    """Tracks changes in modules and fires reload commands"""
 | 
	
		
			
				|  |  | +    def __init__(self, modules, monitor_cls=_monitor_cls, *args, **kwargs):
 | 
	
		
			
				|  |  | +        self._monitor = monitor_cls(modules, self.on_change, *args, **kwargs)
 | 
	
		
			
				|  |  | +        self._hashes = dict([(f, file_hash(f)) for f in modules])
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def start(self):
 | 
	
		
			
				|  |  | +        self._monitor.start()
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def on_change(self, files):
 | 
	
		
			
				|  |  | +        modified = []
 | 
	
		
			
				|  |  | +        for f in files:
 | 
	
		
			
				|  |  | +            fhash = file_hash(f)
 | 
	
		
			
				|  |  | +            if fhash != self._hashes[f]:
 | 
	
		
			
				|  |  | +                modified.append(f)
 | 
	
		
			
				|  |  | +                self._hashes[f] = fhash
 | 
	
		
			
				|  |  | +        if modified:
 | 
	
		
			
				|  |  | +            self._reload(map(self._module_name, modified))
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def _reload(self, modules):
 | 
	
		
			
				|  |  | +        broadcast("pool_restart",
 | 
	
		
			
				|  |  | +                arguments={"imports": modules, "reload_modules": True})
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @classmethod
 | 
	
		
			
				|  |  | +    def _module_name(cls, path):
 | 
	
		
			
				|  |  | +        return os.path.splitext(os.path.basename(path))[0]
 |