You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
224 lines
6.9 KiB
224 lines
6.9 KiB
# -*- coding: utf-8 -*-
|
|
"""
|
|
livereload.server
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
WSGI app server for livereload.
|
|
|
|
:copyright: (c) 2013 by Hsiaoming Yang
|
|
"""
|
|
|
|
import os
|
|
import logging
|
|
from subprocess import Popen, PIPE
|
|
import time
|
|
import threading
|
|
import webbrowser
|
|
|
|
from tornado import escape
|
|
from tornado.wsgi import WSGIContainer
|
|
from tornado.ioloop import IOLoop
|
|
from tornado.web import Application, FallbackHandler
|
|
from .handlers import LiveReloadHandler, LiveReloadJSHandler
|
|
from .handlers import ForceReloadHandler, StaticHandler
|
|
from .watcher import Watcher
|
|
from ._compat import text_types
|
|
from tornado.log import enable_pretty_logging
|
|
enable_pretty_logging()
|
|
|
|
|
|
def shell(command, output=None, mode='w'):
|
|
"""Command shell command.
|
|
|
|
You can add a shell command::
|
|
|
|
server.watch(
|
|
'style.less', shell('lessc style.less', output='style.css')
|
|
)
|
|
|
|
:param command: a shell command, string or list
|
|
:param output: output stdout to the given file
|
|
:param mode: only works with output, mode ``w`` means write,
|
|
mode ``a`` means append
|
|
"""
|
|
if not output:
|
|
output = os.devnull
|
|
else:
|
|
folder = os.path.dirname(output)
|
|
if folder and not os.path.isdir(folder):
|
|
os.makedirs(folder)
|
|
|
|
if isinstance(command, (list, tuple)):
|
|
cmd = command
|
|
else:
|
|
cmd = command.split()
|
|
|
|
def run_shell():
|
|
try:
|
|
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
|
except OSError as e:
|
|
logging.error(e)
|
|
if e.errno == os.errno.ENOENT: # file (command) not found
|
|
logging.error("maybe you haven't installed %s", cmd[0])
|
|
return e
|
|
stdout, stderr = p.communicate()
|
|
if stderr:
|
|
logging.error(stderr)
|
|
return stderr
|
|
#: stdout is bytes, decode for python3
|
|
code = stdout.decode()
|
|
with open(output, mode) as f:
|
|
f.write(code)
|
|
|
|
return run_shell
|
|
|
|
|
|
class WSGIWrapper(WSGIContainer):
|
|
"""Insert livereload scripts into response body."""
|
|
|
|
def __call__(self, request):
|
|
data = {}
|
|
response = []
|
|
|
|
def start_response(status, response_headers, exc_info=None):
|
|
data["status"] = status
|
|
data["headers"] = response_headers
|
|
return response.append
|
|
app_response = self.wsgi_application(
|
|
WSGIContainer.environ(request), start_response)
|
|
try:
|
|
response.extend(app_response)
|
|
body = b"".join(response)
|
|
finally:
|
|
if hasattr(app_response, "close"):
|
|
app_response.close()
|
|
if not data:
|
|
raise Exception("WSGI app did not call start_response")
|
|
|
|
status_code = int(data["status"].split()[0])
|
|
headers = data["headers"]
|
|
header_set = set(k.lower() for (k, v) in headers)
|
|
body = escape.utf8(body)
|
|
body = body.replace(
|
|
b'</head>',
|
|
b'<script src="/livereload.js"></script></head>'
|
|
)
|
|
|
|
if status_code != 304:
|
|
if "content-length" not in header_set:
|
|
headers.append(("Content-Length", str(len(body))))
|
|
if "content-type" not in header_set:
|
|
headers.append(("Content-Type", "text/html; charset=UTF-8"))
|
|
if "server" not in header_set:
|
|
headers.append(("Server", "livereload-tornado"))
|
|
|
|
parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
|
|
for key, value in headers:
|
|
if key.lower() == 'content-length':
|
|
value = str(len(body))
|
|
parts.append(
|
|
escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n"
|
|
)
|
|
parts.append(b"\r\n")
|
|
parts.append(body)
|
|
request.write(b"".join(parts))
|
|
request.finish()
|
|
self._log(status_code, request)
|
|
|
|
|
|
class Server(object):
|
|
"""Livereload server interface.
|
|
|
|
Initialize a server and watch file changes::
|
|
|
|
server = Server(wsgi_app)
|
|
server.serve()
|
|
|
|
:param app: a wsgi application instance
|
|
:param watcher: A Watcher instance, you don't have to initialize
|
|
it by yourself. Under Linux, you will want to install
|
|
pyinotify and use INotifyWatcher() to avoid wasted
|
|
CPU usage.
|
|
"""
|
|
def __init__(self, app=None, watcher=None):
|
|
self.app = app
|
|
self.port = 5500
|
|
self.root = None
|
|
if not watcher:
|
|
watcher = Watcher()
|
|
self.watcher = watcher
|
|
|
|
def watch(self, filepath, func=None):
|
|
"""Add the given filepath for watcher list.
|
|
|
|
Once you have intialized a server, watch file changes before
|
|
serve the server::
|
|
|
|
server.watch('static/*.stylus', 'make static')
|
|
def alert():
|
|
print('foo')
|
|
server.watch('foo.txt', alert)
|
|
server.serve()
|
|
|
|
:param filepath: files to be watched, it can be a filepath,
|
|
a directory, or a glob pattern
|
|
:param func: the function to be called, it can be a string of
|
|
shell command, or any callable object without
|
|
parameters
|
|
"""
|
|
if isinstance(func, text_types):
|
|
func = shell(func)
|
|
|
|
self.watcher.watch(filepath, func)
|
|
|
|
def application(self, debug=True):
|
|
LiveReloadHandler.watcher = self.watcher
|
|
handlers = [
|
|
(r'/livereload', LiveReloadHandler),
|
|
(r'/forcereload', ForceReloadHandler),
|
|
(r'/livereload.js', LiveReloadJSHandler, dict(port=self.port)),
|
|
]
|
|
|
|
if self.app:
|
|
self.app = WSGIWrapper(self.app)
|
|
handlers.append(
|
|
(r'.*', FallbackHandler, dict(fallback=self.app))
|
|
)
|
|
else:
|
|
handlers.append(
|
|
(r'(.*)', StaticHandler, dict(root=self.root or '.')),
|
|
)
|
|
return Application(handlers=handlers, debug=debug)
|
|
|
|
def serve(self, port=None, host=None, root=None, debug=True, open_url=False):
|
|
"""Start serve the server with the given port.
|
|
|
|
:param port: serve on this port, default is 5500
|
|
:param host: serve on this hostname, default is 0.0.0.0
|
|
:param root: serve static on this root directory
|
|
:param open_url: open system browser
|
|
"""
|
|
if root:
|
|
self.root = root
|
|
if port:
|
|
self.port = port
|
|
if host is None:
|
|
host = ''
|
|
|
|
self.application(debug=debug).listen(self.port, address=host)
|
|
logging.getLogger().setLevel(logging.INFO)
|
|
|
|
host = host or '127.0.0.1'
|
|
print('Serving on %s:%s' % (host, self.port))
|
|
|
|
# Async open web browser after 5 sec timeout
|
|
if open_url:
|
|
def opener():
|
|
time.sleep(5)
|
|
webbrowser.open('http://%s:%s' % (host, self.port))
|
|
threading.Thread(target=opener).start()
|
|
|
|
try:
|
|
IOLoop.instance().start()
|
|
except KeyboardInterrupt:
|
|
print('Shutting down...')
|
|
|