Source code for hupper.reloader

from __future__ import print_function

from glob import glob
import signal
import sys
import threading
import time

from .compat import is_watchdog_supported
from .compat import queue
from .ipc import ProcessGroup
from .worker import (
    Worker,
    is_active,
    get_reloader,
)


class FileMonitorProxy(object):
    """
    Wrap an :class:`hupper.interfaces.IFileMonitor` into an object that
    exposes a thread-safe interface back to the reloader to detect
    when it should reload.

    """
    def __init__(self, monitor_factory, verbose=1):
        self.monitor = monitor_factory(self.file_changed)
        self.verbose = verbose
        self.change_event = threading.Event()
        self.changed_paths = set()

    def out(self, msg):
        if self.verbose > 0:
            print(msg)

    def add_path(self, path):
        # if the glob does not match any files then go ahead and pass
        # the pattern to the monitor anyway incase it is just a file that
        # is currently missing
        for p in glob(path) or [path]:
            self.monitor.add_path(p)

    def start(self):
        self.monitor.start()

    def stop(self):
        self.monitor.stop()
        self.monitor.join()

    def file_changed(self, path):
        if path not in self.changed_paths:
            self.changed_paths.add(path)
            self.out('%s changed; reloading ...' % (path,))
        self.set_changed()

    def is_changed(self):
        return self.change_event.is_set()

    def wait_for_change(self, timeout=None):
        return self.change_event.wait(timeout)

    def clear_changes(self):
        self.change_event.clear()
        self.changed_paths.clear()

    def set_changed(self):
        self.change_event.set()


[docs] class Reloader(object): """ A wrapper class around a file monitor which will handle changes by restarting a new worker process. """ def __init__(self, worker_path, monitor_factory, reload_interval=1, verbose=1, worker_args=None, worker_kwargs=None, ): self.worker_path = worker_path self.worker_args = worker_args self.worker_kwargs = worker_kwargs self.monitor_factory = monitor_factory self.reload_interval = reload_interval self.verbose = verbose self.monitor = None self.worker = None self.group = ProcessGroup() def out(self, msg): if self.verbose > 0: print(msg)
[docs] def run(self): """ Execute the reloader forever, blocking the current thread. This will invoke ``sys.exit(1)`` if interrupted. """ self._capture_signals() self._start_monitor() try: while True: if not self._run_worker(): self._wait_for_changes() time.sleep(self.reload_interval) except KeyboardInterrupt: pass finally: self._stop_monitor() self._restore_signals() sys.exit(1)
[docs] def run_once(self): """ Execute the worker once. This method will return after a file change is detected. """ self._capture_signals() self._start_monitor() try: self._run_worker() except KeyboardInterrupt: return finally: self._stop_monitor() self._restore_signals()
def _run_worker(self): self.worker = Worker( self.worker_path, args=self.worker_args, kwargs=self.worker_kwargs, ) self.worker.start() try: # register the worker with the process group self.group.add_child(self.worker.pid) self.out("Starting monitor for PID %s." % self.worker.pid) self.monitor.clear_changes() while not self.monitor.is_changed() and self.worker.is_alive(): try: cmd = self.worker.pipe.recv(timeout=self.reload_interval) except queue.Empty: continue if not cmd or cmd[0] == 'reload': break if cmd[0] == 'watch': for path in cmd[1]: self.monitor.add_path(path) else: raise RuntimeError('received unknown command') except KeyboardInterrupt: if self.worker.is_alive(): self.out('Waiting for server to exit ...') time.sleep(self.reload_interval) raise finally: if self.worker.is_alive(): self.out('Killing server with PID %s.' % self.worker.pid) self.worker.terminate() self.worker.join() else: self.worker.join() self.out('Server with PID %s exited with code %d.' % (self.worker.pid, self.worker.exitcode)) self.monitor.clear_changes() force_restart = self.worker.terminated self.worker = None return force_restart def _wait_for_changes(self): self.out('Waiting for changes before reloading.') while ( not self.monitor.wait_for_change(self.reload_interval) ): # pragma: nocover pass self.monitor.clear_changes() def _start_monitor(self): self.monitor = FileMonitorProxy(self.monitor_factory, self.verbose) self.monitor.start() def _stop_monitor(self): if self.monitor: self.monitor.stop() self.monitor = None def _capture_signals(self): # SIGHUP is not supported on windows if hasattr(signal, 'SIGHUP'): signal.signal(signal.SIGHUP, self._signal_sighup) def _signal_sighup(self, signum, frame): self.out('Received SIGHUP, triggering a reload.') self.monitor.set_changed() def _restore_signals(self): if hasattr(signal, 'SIGHUP'): signal.signal(signal.SIGHUP, signal.SIG_DFL)
[docs] def start_reloader( worker_path, reload_interval=1, verbose=1, monitor_factory=None, worker_args=None, worker_kwargs=None, ): """ Start a monitor and then fork a worker process which starts by executing the importable function at ``worker_path``. If this function is called from a worker process that is already being monitored then it will return a reference to the current :class:`hupper.interfaces.IReloaderProxy` which can be used to communicate with the monitor. ``worker_path`` must be a dotted string pointing to a globally importable function that will be executed to start the worker. An example could be ``myapp.cli.main``. In most cases it will point at the same function that is invoking ``start_reloader`` in the first place. ``reload_interval`` is a value in seconds and will be used to throttle restarts. ``verbose`` controls the output. Set to ``0`` to turn off any logging of activity and turn up to ``2`` for extra output. ``monitor_factory`` is an instance of :class:`hupper.interfaces.IFileMonitorFactory`. If left unspecified, this will try to create a :class:`hupper.watchdog.WatchdogFileMonitor` if `watchdog <https://pypi.org/project/watchdog/>`_ is installed and will fallback to the less efficient :class:`hupper.polling.PollingFileMonitor` otherwise. """ if is_active(): return get_reloader() if monitor_factory is None: if is_watchdog_supported(): from .watchdog import WatchdogFileMonitor def monitor_factory(callback): return WatchdogFileMonitor(callback) if verbose > 1: print('File monitor backend: watchdog') else: from .polling import PollingFileMonitor def monitor_factory(callback): return PollingFileMonitor(callback, reload_interval) if verbose > 1: print('File monitor backend: polling') reloader = Reloader( worker_path=worker_path, worker_args=worker_args, worker_kwargs=worker_kwargs, reload_interval=reload_interval, verbose=verbose, monitor_factory=monitor_factory, ) return reloader.run()