-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from alfred82santa/feature/runner-and-reloader
Runner and reloader
- Loading branch information
Showing
7 changed files
with
374 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import asyncio | ||
import os | ||
import sys | ||
from hachiko.hachiko import AIOEventHandler | ||
from werkzeug._internal import _log | ||
from werkzeug._reloader import ReloaderLoop, _find_observable_paths | ||
|
||
__author__ = 'alfred' | ||
|
||
|
||
EVENT_TYPE_MOVED = 'moved' | ||
EVENT_TYPE_DELETED = 'deleted' | ||
EVENT_TYPE_CREATED = 'created' | ||
EVENT_TYPE_MODIFIED = 'modified' | ||
|
||
|
||
class AIOReloaderLoop(ReloaderLoop): | ||
|
||
def __init__(self, extra_files=None, interval=1, loop=None): | ||
super(AIOReloaderLoop, self).__init__(extra_files=extra_files, interval=interval) | ||
self.loop = loop | ||
self.process = None | ||
|
||
@asyncio.coroutine | ||
def restart_with_reloader(self): | ||
"""Spawn a new Python interpreter with the same arguments as this one, | ||
but running the reloader thread. | ||
""" | ||
_log('info', ' * Restarting with %s' % self.name) | ||
args = [sys.executable] + sys.argv | ||
new_environ = os.environ.copy() | ||
new_environ['WERKZEUG_RUN_MAIN'] = 'true' | ||
|
||
exit_code = 3 | ||
while exit_code == 3: | ||
self.process = yield from asyncio.create_subprocess_shell(' '.join(args), env=new_environ, | ||
cwd=os.getcwd(), | ||
stdout=sys.stdout) | ||
exit_code = yield from self.process.wait() | ||
return exit_code | ||
|
||
def terminate(self): | ||
if self.process and self.pid: | ||
self.process.terminate() | ||
|
||
|
||
class HachikoReloaderLoop(AIOReloaderLoop): | ||
|
||
def __init__(self, *args, **kwargs): | ||
super(HachikoReloaderLoop, self).__init__(*args, **kwargs) | ||
from watchdog.observers import Observer | ||
self.observable_paths = set() | ||
|
||
@asyncio.coroutine | ||
def _check_modification(filename): | ||
if filename in self.extra_files: | ||
yield from self.trigger_reload(filename) | ||
dirname = os.path.dirname(filename) | ||
if dirname.startswith(tuple(self.observable_paths)): | ||
if filename.endswith(('.pyc', '.pyo')): | ||
yield from self.trigger_reload(filename[:-1]) | ||
elif filename.endswith('.py'): | ||
yield from self.trigger_reload(filename) | ||
|
||
class _CustomHandler(AIOEventHandler): | ||
|
||
@asyncio.coroutine | ||
def on_created(self, event): | ||
yield from _check_modification(event.src_path) | ||
|
||
@asyncio.coroutine | ||
def on_modified(self, event): | ||
yield from _check_modification(event.src_path) | ||
|
||
@asyncio.coroutine | ||
def on_moved(self, event): | ||
yield from _check_modification(event.src_path) | ||
yield from _check_modification(event.dest_path) | ||
|
||
@asyncio.coroutine | ||
def on_deleted(self, event): | ||
yield from _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(loop=self.loop) | ||
self.should_reload = asyncio.Event(loop=self.loop) | ||
|
||
@asyncio.coroutine | ||
def trigger_reload(self, filename): | ||
# This is called inside an event handler, which means we can't throw | ||
# SystemExit here. https://github.com/gorakhargosh/watchdog/issues/294 | ||
self.should_reload.set() | ||
filename = os.path.abspath(filename) | ||
_log('info', ' * Detected change in %r, reloading' % filename) | ||
|
||
@asyncio.coroutine | ||
def run(self): | ||
watches = {} | ||
observer = self.observer_class() | ||
observer.start() | ||
|
||
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 | ||
|
||
yield from self.should_reload.wait() | ||
|
||
sys.exit(3) | ||
|
||
def terminate(self): | ||
pass | ||
|
||
|
||
reloader_loops = { | ||
'hachiko': HachikoReloaderLoop | ||
} | ||
|
||
|
||
reloader_loops['auto'] = reloader_loops['hachiko'] | ||
|
||
|
||
def run_with_reloader(main_func, extra_files=None, interval=1, | ||
reloader_type='auto', loop=None): | ||
|
||
loop = loop or asyncio.get_event_loop() | ||
|
||
reloader = reloader_loops[reloader_type](extra_files, interval, loop=loop) | ||
|
||
import signal | ||
loop.add_signal_handler(signal.SIGTERM, lambda *args: loop.stop()) | ||
try: | ||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': | ||
main_func(loop=loop) | ||
loop.run_until_complete(reloader.run()) | ||
else: | ||
resultcode = loop.run_until_complete(reloader.restart_with_reloader()) | ||
sys.exit(resultcode) | ||
except KeyboardInterrupt: | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import asyncio | ||
import socket | ||
import os | ||
import sys | ||
from aiohttp.wsgi import WSGIServerHttpProtocol | ||
from werkzeug._internal import _log | ||
from werkzeug.serving import select_ip_version | ||
|
||
__author__ = 'alfred' | ||
|
||
|
||
def make_server(host, port, app=None, threaded=False, processes=1, | ||
request_handler=None, passthrough_errors=False, | ||
ssl_context=None, loop=None): | ||
if threaded or processes > 1: | ||
raise ValueError("Multi-thread or process servers not supported.") | ||
|
||
loop = loop or asyncio.get_event_loop() | ||
asyncio.async(asyncio.get_event_loop().create_server(lambda: WSGIServerHttpProtocol(app, readpayload=True), | ||
host, port), | ||
loop=loop) | ||
|
||
|
||
def run_simple(hostname, port, application, use_reloader=False, | ||
use_debugger=False, use_evalex=True, | ||
extra_files=None, reloader_interval=1, | ||
reloader_type='auto', threaded=False, processes=1, | ||
request_handler=None, static_files=None, | ||
passthrough_errors=False, ssl_context=None, loop=None): | ||
"""Start a WSGI application. Optional features include a reloader, | ||
multithreading and fork support. | ||
This function has a command-line interface too:: | ||
python -m werkzeug.serving --help | ||
.. versionadded:: 0.5 | ||
`static_files` was added to simplify serving of static files as well | ||
as `passthrough_errors`. | ||
.. versionadded:: 0.6 | ||
support for SSL was added. | ||
.. versionadded:: 0.8 | ||
Added support for automatically loading a SSL context from certificate | ||
file and private key. | ||
.. versionadded:: 0.9 | ||
Added command-line interface. | ||
.. versionadded:: 0.10 | ||
Improved the reloader and added support for changing the backend | ||
through the `reloader_type` parameter. See :ref:`reloader` | ||
for more information. | ||
:param hostname: The host for the application. eg: ``'localhost'`` | ||
:param port: The port for the server. eg: ``8080`` | ||
:param application: the WSGI application to execute | ||
:param use_reloader: should the server automatically restart the python | ||
process if modules were changed? | ||
:param use_debugger: should the werkzeug debugging system be used? | ||
:param use_evalex: should the exception evaluation feature be enabled? | ||
:param extra_files: a list of files the reloader should watch | ||
additionally to the modules. For example configuration | ||
files. | ||
:param reloader_interval: the interval for the reloader in seconds. | ||
:param reloader_type: the type of reloader to use. The default is | ||
auto detection. Valid values are ``'stat'`` and | ||
``'watchdog'``. See :ref:`reloader` for more | ||
information. | ||
:param threaded: should the process handle each request in a separate | ||
thread? | ||
:param processes: if greater than 1 then handle each request in a new process | ||
up to this maximum number of concurrent processes. | ||
:param request_handler: optional parameter that can be used to replace | ||
the default one. You can use this to replace it | ||
with a different | ||
:class:`~BaseHTTPServer.BaseHTTPRequestHandler` | ||
subclass. | ||
:param static_files: a dict of paths for static files. This works exactly | ||
like :class:`SharedDataMiddleware`, it's actually | ||
just wrapping the application in that middleware before | ||
serving. | ||
:param passthrough_errors: set this to `True` to disable the error catching. | ||
This means that the server will die on errors but | ||
it can be useful to hook debuggers in (pdb etc.) | ||
:param ssl_context: an SSL context for the connection. Either an | ||
:class:`ssl.SSLContext`, a tuple in the form | ||
``(cert_file, pkey_file)``, the string ``'adhoc'`` if | ||
the server should automatically create one, or ``None`` | ||
to disable SSL (which is the default). | ||
""" | ||
loop = loop or asyncio.get_event_loop() | ||
|
||
if use_debugger: | ||
raise NotImplemented("Debugger not implemented with asyncio") | ||
if static_files: | ||
raise NotImplemented("Static files not implemented with asyncio") | ||
|
||
def inner(loop): | ||
make_server(hostname, port, application, threaded, | ||
processes, request_handler, | ||
passthrough_errors, ssl_context, loop) | ||
|
||
if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': | ||
display_hostname = hostname != '*' and hostname or 'localhost' | ||
if ':' in display_hostname: | ||
display_hostname = '[%s]' % display_hostname | ||
quit_msg = '(Press CTRL+C to quit)' | ||
_log('info', ' * Running on %s://%s:%d/ %s', ssl_context is None | ||
and 'http' or 'https', display_hostname, port, quit_msg) | ||
if use_reloader: | ||
# Create and destroy a socket so that any exceptions are raised before | ||
# we spawn a separate Python interpreter and lose this ability. | ||
address_family = select_ip_version(hostname, port) | ||
test_socket = socket.socket(address_family, socket.SOCK_STREAM) | ||
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
test_socket.bind((hostname, port)) | ||
test_socket.close() | ||
|
||
from ._reloader import run_with_reloader | ||
run_with_reloader(inner, extra_files, reloader_interval, | ||
reloader_type, loop) | ||
else: | ||
inner(loop) | ||
loop.run_forever() | ||
|
||
|
||
def run_with_reloader(*args, **kwargs): | ||
# People keep using undocumented APIs. Do not use this function | ||
# please, we do not guarantee that it continues working. | ||
from ._reloader import run_with_reloader | ||
return run_with_reloader(*args, **kwargs) | ||
|
||
|
||
def main(): | ||
'''A simple command-line interface for :py:func:`run_simple`.''' | ||
|
||
# in contrast to argparse, this works at least under Python < 2.7 | ||
import optparse | ||
from werkzeug.utils import import_string | ||
|
||
parser = optparse.OptionParser( | ||
usage='Usage: %prog [options] app_module:app_object') | ||
parser.add_option('-b', '--bind', dest='address', | ||
help='The hostname:port the app should listen on.') | ||
parser.add_option('-d', '--debug', dest='use_debugger', | ||
action='store_true', default=False, | ||
help='Use Werkzeug\'s debugger.') | ||
parser.add_option('-r', '--reload', dest='use_reloader', | ||
action='store_true', default=False, | ||
help='Reload Python process if modules change.') | ||
options, args = parser.parse_args() | ||
|
||
hostname, port = None, None | ||
if options.address: | ||
address = options.address.split(':') | ||
hostname = address[0] | ||
if len(address) > 1: | ||
port = address[1] | ||
|
||
if len(args) != 1: | ||
sys.stdout.write('No application supplied, or too much. See --help\n') | ||
sys.exit(1) | ||
app = import_string(args[0]) | ||
|
||
run_simple( | ||
hostname=(hostname or '127.0.0.1'), port=int(port or 5000), | ||
application=app, use_reloader=options.use_reloader, | ||
use_debugger=options.use_debugger | ||
) | ||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
Werkzeug>=0.7 | ||
hachiko | ||
aiohttp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.