1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3"""
4A file monitor and server restarter.
5
6Use this like:
7
8..code-block:: Python
9
10    import reloader
11    reloader.install()
12
13Then make sure your server is installed with a shell script like::
14
15    err=3
16    while test "$err" -eq 3 ; do
17        python server.py
18        err="$?"
19    done
20
21or is run from this .bat file (if you use Windows)::
22
23    @echo off
24    :repeat
25        python server.py
26    if %errorlevel% == 3 goto repeat
27
28or run a monitoring process in Python (``paster serve --reload`` does
29this).
30
31Use the ``watch_file(filename)`` function to cause a reload/restart for
32other other non-Python files (e.g., configuration files).  If you have
33a dynamic set of files that grows over time you can use something like::
34
35    def watch_config_files():
36        return CONFIG_FILE_CACHE.keys()
37    paste.reloader.add_file_callback(watch_config_files)
38
39Then every time the reloader polls files it will call
40``watch_config_files`` and check all the filenames it returns.
41"""
42
43from __future__ import print_function
44import os
45import sys
46import time
47import threading
48import traceback
49from paste.util.classinstance import classinstancemethod
50
51def install(poll_interval=1):
52    """
53    Install the reloading monitor.
54
55    On some platforms server threads may not terminate when the main
56    thread does, causing ports to remain open/locked.  The
57    ``raise_keyboard_interrupt`` option creates a unignorable signal
58    which causes the whole application to shut-down (rudely).
59    """
60    mon = Monitor(poll_interval=poll_interval)
61    t = threading.Thread(target=mon.periodic_reload)
62    t.setDaemon(True)
63    t.start()
64
65class Monitor(object):
66
67    instances = []
68    global_extra_files = []
69    global_file_callbacks = []
70
71    def __init__(self, poll_interval):
72        self.module_mtimes = {}
73        self.keep_running = True
74        self.poll_interval = poll_interval
75        self.extra_files = list(self.global_extra_files)
76        self.instances.append(self)
77        self.file_callbacks = list(self.global_file_callbacks)
78
79    def periodic_reload(self):
80        while True:
81            if not self.check_reload():
82                # use os._exit() here and not sys.exit() since within a
83                # thread sys.exit() just closes the given thread and
84                # won't kill the process; note os._exit does not call
85                # any atexit callbacks, nor does it do finally blocks,
86                # flush open files, etc.  In otherwords, it is rude.
87                os._exit(3)
88                break
89            time.sleep(self.poll_interval)
90
91    def check_reload(self):
92        filenames = list(self.extra_files)
93        for file_callback in self.file_callbacks:
94            try:
95                filenames.extend(file_callback())
96            except:
97                print("Error calling paste.reloader callback %r:" % file_callback,
98                      file=sys.stderr)
99                traceback.print_exc()
100        for module in sys.modules.values():
101            try:
102                filename = module.__file__
103            except (AttributeError, ImportError):
104                continue
105            if filename is not None:
106                filenames.append(filename)
107        for filename in filenames:
108            try:
109                stat = os.stat(filename)
110                if stat:
111                    mtime = stat.st_mtime
112                else:
113                    mtime = 0
114            except (OSError, IOError):
115                continue
116            if filename.endswith('.pyc') and os.path.exists(filename[:-1]):
117                mtime = max(os.stat(filename[:-1]).st_mtime, mtime)
118            elif filename.endswith('$py.class') and \
119                    os.path.exists(filename[:-9] + '.py'):
120                mtime = max(os.stat(filename[:-9] + '.py').st_mtime, mtime)
121            if filename not in self.module_mtimes:
122                self.module_mtimes[filename] = mtime
123            elif self.module_mtimes[filename] < mtime:
124                print("%s changed; reloading..." % filename, file=sys.stderr)
125                return False
126        return True
127
128    def watch_file(self, cls, filename):
129        """Watch the named file for changes"""
130        filename = os.path.abspath(filename)
131        if self is None:
132            for instance in cls.instances:
133                instance.watch_file(filename)
134            cls.global_extra_files.append(filename)
135        else:
136            self.extra_files.append(filename)
137
138    watch_file = classinstancemethod(watch_file)
139
140    def add_file_callback(self, cls, callback):
141        """Add a callback -- a function that takes no parameters -- that will
142        return a list of filenames to watch for changes."""
143        if self is None:
144            for instance in cls.instances:
145                instance.add_file_callback(callback)
146            cls.global_file_callbacks.append(callback)
147        else:
148            self.file_callbacks.append(callback)
149
150    add_file_callback = classinstancemethod(add_file_callback)
151
152if sys.platform.startswith('java'):
153    try:
154        from _systemrestart import SystemRestart
155    except ImportError:
156        pass
157    else:
158        class JythonMonitor(Monitor):
159
160            """
161            Monitor that utilizes Jython's special
162            ``_systemrestart.SystemRestart`` exception.
163
164            When raised from the main thread it causes Jython to reload
165            the interpreter in the existing Java process (avoiding
166            startup time).
167
168            Note that this functionality of Jython is experimental and
169            may change in the future.
170            """
171
172            def periodic_reload(self):
173                while True:
174                    if not self.check_reload():
175                        raise SystemRestart()
176                    time.sleep(self.poll_interval)
177
178watch_file = Monitor.watch_file
179add_file_callback = Monitor.add_file_callback
180