| | import os |
| | import sys |
| | import time |
| | import subprocess |
| | import threading |
| | from itertools import chain |
| | |
| | from werkzeug._internal import _log |
| | from werkzeug._compat import PY2, iteritems, text_type |
| | |
| | |
| | def _iter_module_files(): |
| | """This iterates over all relevant Python files. It goes through all |
| | loaded files from modules, all files in folders of already loaded modules |
| | as well as all files reachable through a package. |
| | """ |
| | # The list call is necessary on Python 3 in case the module |
| | # dictionary modifies during iteration. |
| | for module in list(sys.modules.values()): |
| | if module is None: |
| | continue |
| | filename = getattr(module, '__file__',None) |
| | if filename: |
| | old = None |
| | while not os.path.isfile(filename): |
| | old = filename |
| | filename = os.path.dirname(filename) |
| | if filename == old: |
| | break |
| | else: |
| | if filename[-4:]in ('.pyc','.pyo'): |
| | filename = filename[:-1] |
| | yield filename |
| | |
| | |
| | def _find_observable_paths(extra_files=None): |
| | """Finds all paths that should be observed.""" |
| | rv = set(os.path.abspath(x) for xin sys.path) |
| | |
| | for filename in extra_files or (): |
| | rv.add(os.path.dirname(os.path.abspath(filename))) |
| | |
| | for module in list(sys.modules.values()): |
| | fn = getattr(module, '__file__',None) |
| | if fn is None: |
| | continue |
| | fn = os.path.abspath(fn) |
| | rv.add(os.path.dirname(fn)) |
| | |
| | return _find_common_roots(rv) |
| | |
| | |
| | def _find_common_roots(paths): |
| | """Out of some paths it finds the common roots that need monitoring.""" |
| | paths = [x.split(os.path.sep)for x in paths] |
| | root = {} |
| | for chunks in sorted(paths, key=len,reverse=True): |
| | node = root |
| | for chunk in chunks: |
| | node = node.setdefault(chunk, {}) |
| | node.clear() |
| | |
| | rv = set() |
| | |
| | def _walk(node, path): |
| | for prefix, child in iteritems(node): |
| | _walk(child, path + (prefix,)) |
| | if not node: |
| | rv.add('/'.join(path)) |
| | _walk(root, ()) |
| | return rv |
| | |
| | |
| | class ReloaderLoop(object): |
| | name = None |
| | |
| | # monkeypatched by testsuite. wrapping with `staticmethod` is required in |
| | # case time.sleep has been replaced by a non-c function (e.g. by |
| | # `eventlet.monkey_patch`) before we get here |
| | _sleep = staticmethod(time.sleep) |
| | |
| | def __init__(self, extra_files=None,interval=1): |
| | self.extra_files = set(os.path.abspath(x) |
| | for x in extra_files or ()) |
| | self.interval = interval |
| | |
| | def run(self): |
| | pass |
| | |
| | def restart_with_reloader(self): |
| | """Spawn a new Python interpreter with the same arguments as this one, |
| | but running the reloader thread. |
| | """ |
| | while 1: |
| | _log('info',' * Restarting with%s'% self.name) |
| | args = [sys.executable] + sys.argv |
| | new_environ = os.environ.copy() |
| | new_environ['WERKZEUG_RUN_MAIN']= 'true' |
| | |
| | # a weird bug on windows. sometimes unicode strings end up in the |
| | # environment and subprocess.call does not like this, encode them |
| | # to latin1 and continue. |
| | if os.name == 'nt'and PY2: |
| | for key, value in iteritems(new_environ): |
| | if isinstance(value, text_type): |
| | new_environ[key] = value.encode('iso-8859-1') |
| | |
| | exit_code = subprocess.call(args,env=new_environ) |
| | if exit_code != 3: |
| | return exit_code |
| | |
| | def trigger_reload(self,filename): |
| | self.log_reload(filename) |
| | sys.exit(3) |
| | |
| | def log_reload(self, filename): |
| | filename = os.path.abspath(filename) |
| | _log('info',' * Detected change in%r, reloading'% filename) |
| | |
| | |
| | class StatReloaderLoop(ReloaderLoop): |
| | name = 'stat' |
| | |
| | def run(self): |
| | mtimes = {} |
| | while 1: |
| | for filename in chain(_iter_module_files(), |
| | self.extra_files): |
| | try: |
| | mtime = os.stat(filename).st_mtime |
| | except OSError: |
| | continue |
| | |
| | old_time = mtimes.get(filename) |
| | if old_time is None: |
| | mtimes[filename] = mtime |
| | continue |
| | elif mtime > old_time: |
| | self.trigger_reload(filename) |
| | self._sleep(self.interval) |
| | |
| | |
| | class WatchdogReloaderLoop(ReloaderLoop): |
| | |
| | def __init__(self, *args, **kwargs): |
| | ReloaderLoop.__init__(self,*args, **kwargs) |
| | from watchdog.observers import Observer |
| | from watchdog.events import FileSystemEventHandler |
| | self.observable_paths = set() |
| | |
| | def _check_modification(filename): |
| | if filename in self.extra_files: |
| | self.trigger_reload(filename) |
| | dirname = os.path.dirname(filename) |
| | if dirname.startswith(tuple(self.observable_paths)): |
| | if filename.endswith(('.pyc','.pyo')): |
| | self.trigger_reload(filename[:-1]) |
| | elif filename.endswith('.py'): |
| | self.trigger_reload(filename) |
| | |
| | class _CustomHandler(FileSystemEventHandler): |
| | |
| | def on_created(self, event): |
| | _check_modification(event.src_path) |
| | |
| | def on_modified(self, event): |
| | _check_modification(event.src_path) |
| | |
| | def on_moved(self, event): |
| | _check_modification(event.src_path) |
| | _check_modification(event.dest_path) |
| | |
| | def on_deleted(self, event): |
| | _check_modification(event.src_path) |
| | |
| | reloader_name = Observer.__name__.lower() |
| | if reloader_name.endswith('observer'): |
| | reloader_name = reloader_name[:-8] |
| | reloader_name += ' reloader' |
| | |
| | self.name = reloader_name |
| | |
| | self.observer_class = Observer |
| | self.event_handler = _CustomHandler() |
| | self.should_reload = False |
| | |
| | def trigger_reload(self,filename): |
| | # This is called inside an event handler, which means throwing |
| | # SystemExit has no effect. |
| | # https://github.com/gorakhargosh/watchdog/issues/294 |
| | self.should_reload = True |
| | self.log_reload(filename) |
| | |
| | def run(self): |
| | watches = {} |
| | observer = self.observer_class() |
| | observer.start() |
| | |
| | while not self.should_reload: |
| | to_delete = set(watches) |
| | paths = _find_observable_paths(self.extra_files) |
| | for path in paths: |
| | if path not in watches: |
| | try: |
| | watches[path] = observer.schedule( |
| | self.event_handler, path,recursive=True) |
| | except OSError as e: |
| | message = str(e) |
| | |
| | if message != "Path is not a directory": |
| | # Log the exception |
| | _log('error', message) |
| | |
| | # Clear this path from list of watches We don't want |
| | # the same error message showing again in the next |
| | # iteration. |
| | watches[path] = None |
| | to_delete.discard(path) |
| | for path in to_delete: |
| | watch = watches.pop(path, None) |
| | if watch is not None: |
| | observer.unschedule(watch) |
| | self.observable_paths = paths |
| | self._sleep(self.interval) |
| | |
| | sys.exit(3) |
| | |
| | |
| | reloader_loops = { |
| | 'stat': StatReloaderLoop, |
| | 'watchdog': WatchdogReloaderLoop, |
| | } |
| | |
| | try: |
| | __import__('watchdog.observers') |
| | except ImportError: |
| | reloader_loops['auto']= reloader_loops['stat'] |
| | else: |
| | reloader_loops['auto']= reloader_loops['watchdog'] |
| | |
| | |
| | def run_with_reloader(main_func,extra_files=None,interval=1, |
| | reloader_type='auto'): |
| | """Run the given function in an independent python interpreter.""" |
| | import signal |
| | reloader = reloader_loops[reloader_type](extra_files, interval) |
| | signal.signal(signal.SIGTERM,lambda *args: sys.exit(0)) |
| | try: |
| | if os.environ.get('WERKZEUG_RUN_MAIN')== 'true': |
| | t = threading.Thread(target=main_func,args=()) |
| | t.setDaemon(True) |
| | t.start() |
| | reloader.run() |
| | else: |
| | sys.exit(reloader.restart_with_reloader()) |
| | except KeyboardInterrupt: |
| | pass |