Use own style reloader
This commit is contained in:
@@ -53,7 +53,7 @@ class ClientScript(Plugin):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
watcher = None
|
watches = {}
|
||||||
|
|
||||||
original_paths = {'style': {}, 'script': {}}
|
original_paths = {'style': {}, 'script': {}}
|
||||||
paths = {'style': {}, 'script': {}}
|
paths = {'style': {}, 'script': {}}
|
||||||
@@ -82,17 +82,7 @@ class ClientScript(Plugin):
|
|||||||
def livereload(self):
|
def livereload(self):
|
||||||
|
|
||||||
if Env.get('dev'):
|
if Env.get('dev'):
|
||||||
from livereload import Server
|
fireEvent('schedule.interval', 'livereload.watcher', self.watcher, seconds = .5)
|
||||||
from livereload.watcher import Watcher
|
|
||||||
|
|
||||||
self.livereload_server = Server()
|
|
||||||
self.livereload_server.watch('%s/minified/*.css' % Env.get('cache_dir'))
|
|
||||||
self.livereload_server.watch('%s/*.css' % os.path.join(Env.get('app_dir'), 'couchpotato', 'static', 'style'))
|
|
||||||
|
|
||||||
self.watcher = Watcher()
|
|
||||||
fireEvent('schedule.interval', 'livereload.watcher', self.watcher.examine, seconds = .5)
|
|
||||||
|
|
||||||
self.livereload_server.serve(port = 35729)
|
|
||||||
|
|
||||||
def addCore(self):
|
def addCore(self):
|
||||||
|
|
||||||
@@ -106,6 +96,39 @@ class ClientScript(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.registerStyle(core_url, file_path, position = 'front')
|
self.registerStyle(core_url, file_path, position = 'front')
|
||||||
|
|
||||||
|
def watcher(self):
|
||||||
|
|
||||||
|
changed = []
|
||||||
|
|
||||||
|
for file_path in self.watches:
|
||||||
|
info = self.watches[file_path]
|
||||||
|
old_time = info['file_time']
|
||||||
|
file_time = os.path.getmtime(file_path)
|
||||||
|
if file_time > old_time:
|
||||||
|
changed.append(info['api_path'])
|
||||||
|
|
||||||
|
if info['compiled_path']:
|
||||||
|
compiler = Scss(live_errors = True, search_paths = [os.path.dirname(file_path)])
|
||||||
|
f = open(file_path, 'r').read()
|
||||||
|
f = compiler.compile(f)
|
||||||
|
|
||||||
|
self.createFile(info['compiled_path'], f.strip())
|
||||||
|
|
||||||
|
|
||||||
|
# Add file to watchlist again, with current filetime
|
||||||
|
self.watches[file_path]['file_time'] = file_time
|
||||||
|
|
||||||
|
# Notify fronted with changes
|
||||||
|
if changed:
|
||||||
|
fireEvent('notify.frontend', type = 'watcher.changed', data = changed)
|
||||||
|
|
||||||
|
def watchFile(self, file_path, api_path, compiled_path = False):
|
||||||
|
self.watches[file_path] = {
|
||||||
|
'file_time': os.path.getmtime(file_path),
|
||||||
|
'api_path': api_path,
|
||||||
|
'compiled_path': compiled_path,
|
||||||
|
}
|
||||||
|
|
||||||
def compile(self):
|
def compile(self):
|
||||||
|
|
||||||
# Create cache dir
|
# Create cache dir
|
||||||
@@ -146,7 +169,6 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
# Reload watcher
|
# Reload watcher
|
||||||
if Env.get('dev'):
|
if Env.get('dev'):
|
||||||
self.watcher.watch(file_path, self.compile)
|
|
||||||
|
|
||||||
compiled_file_name = position + '_%s.css' % url_path.replace('/', '_').split('.scss')[0]
|
compiled_file_name = position + '_%s.css' % url_path.replace('/', '_').split('.scss')[0]
|
||||||
compiled_file_path = os.path.join(minified_dir, compiled_file_name)
|
compiled_file_path = os.path.join(minified_dir, compiled_file_name)
|
||||||
@@ -155,6 +177,8 @@ class ClientScript(Plugin):
|
|||||||
# Remove scss path
|
# Remove scss path
|
||||||
x = (file_path, 'minified/%s?%s' % (compiled_file_name, tryInt(time.time())))
|
x = (file_path, 'minified/%s?%s' % (compiled_file_name, tryInt(time.time())))
|
||||||
|
|
||||||
|
self.watchFile(file_path, x[1], compiled_path = compiled_file_path)
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
|
|
||||||
if file_type == 'script':
|
if file_type == 'script':
|
||||||
@@ -199,6 +223,8 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
def register(self, api_path, file_path, type, location):
|
def register(self, api_path, file_path, type, location):
|
||||||
|
|
||||||
|
self.watchFile(file_path, api_path)
|
||||||
|
|
||||||
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
||||||
|
|
||||||
if not self.original_paths[type].get(location):
|
if not self.original_paths[type].get(location):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ body, html {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #111;
|
background: #111;
|
||||||
@@ -75,6 +75,11 @@ a {
|
|||||||
border-radius: 3px 0 0 3px;
|
border-radius: 3px 0 0 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.pages {
|
.pages {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
widows: 100%;
|
widows: 100%;
|
||||||
@@ -91,6 +96,7 @@ a {
|
|||||||
.page {
|
.page {
|
||||||
|
|
||||||
display: none;
|
display: none;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -8,18 +8,22 @@
|
|||||||
|
|
||||||
|
|
||||||
{% for url in fireEvent('clientscript.get_styles', location = 'front', single = True) %}
|
{% for url in fireEvent('clientscript.get_styles', location = 'front', single = True) %}
|
||||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
<link data-url="{{ url }}" rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||||
{% for url in fireEvent('clientscript.get_scripts', location = 'front', single = True) %}
|
{% for url in fireEvent('clientscript.get_scripts', location = 'front', single = True) %}
|
||||||
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||||
|
|
||||||
{% for url in fireEvent('clientscript.get_scripts', location = 'head', single = True) %}
|
{% for url in fireEvent('clientscript.get_scripts', location = 'head', single = True) %}
|
||||||
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||||
{% for url in fireEvent('clientscript.get_styles', location = 'head', single = True) %}
|
{% for url in fireEvent('clientscript.get_styles', location = 'head', single = True) %}
|
||||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
<link data-url="{{ url }}" rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||||
|
|
||||||
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
|
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||||
<link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
|
<link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
|
||||||
|
|
||||||
|
{% if Env.get('dev') %}
|
||||||
|
<script type="text/javascript" src="{{ Env.get('static_path') }}scripts/reloader.js"></script>
|
||||||
|
{% end %}
|
||||||
|
|
||||||
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
livereload
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
A python version of livereload.
|
|
||||||
|
|
||||||
:copyright: (c) 2013 by Hsiaoming Yang
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = '2.2.0'
|
|
||||||
__author__ = 'Hsiaoming Yang <me@lepture.com>'
|
|
||||||
__homepage__ = 'https://github.com/lepture/python-livereload'
|
|
||||||
|
|
||||||
from .server import Server, shell
|
|
||||||
|
|
||||||
__all__ = ('Server', 'shell')
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
"""
|
|
||||||
livereload._compat
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Compatible module for python2 and python3.
|
|
||||||
|
|
||||||
:copyright: (c) 2013 by Hsiaoming Yang
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
if PY3:
|
|
||||||
unicode_type = str
|
|
||||||
bytes_type = bytes
|
|
||||||
text_types = (str,)
|
|
||||||
else:
|
|
||||||
unicode_type = unicode
|
|
||||||
bytes_type = str
|
|
||||||
text_types = (str, unicode)
|
|
||||||
|
|
||||||
|
|
||||||
def to_unicode(value, encoding='utf-8'):
|
|
||||||
"""Convert different types of objects to unicode."""
|
|
||||||
if isinstance(value, unicode_type):
|
|
||||||
return value
|
|
||||||
|
|
||||||
if isinstance(value, bytes_type):
|
|
||||||
return unicode_type(value, encoding=encoding)
|
|
||||||
|
|
||||||
if isinstance(value, int):
|
|
||||||
return unicode_type(str(value))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def to_bytes(value, encoding='utf-8'):
|
|
||||||
"""Convert different types of objects to bytes."""
|
|
||||||
if isinstance(value, bytes_type):
|
|
||||||
return value
|
|
||||||
return value.encode(encoding)
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
livereload.handlers
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
HTTP and WebSocket handlers for livereload.
|
|
||||||
|
|
||||||
:copyright: (c) 2013 by Hsiaoming Yang
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import mimetypes
|
|
||||||
from tornado import ioloop
|
|
||||||
from tornado import escape
|
|
||||||
from tornado.websocket import WebSocketHandler
|
|
||||||
from tornado.web import RequestHandler
|
|
||||||
from tornado.util import ObjectDict
|
|
||||||
from ._compat import to_bytes
|
|
||||||
|
|
||||||
|
|
||||||
class LiveReloadHandler(WebSocketHandler):
|
|
||||||
waiters = set()
|
|
||||||
watcher = None
|
|
||||||
_last_reload_time = None
|
|
||||||
|
|
||||||
def allow_draft76(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
if self in LiveReloadHandler.waiters:
|
|
||||||
LiveReloadHandler.waiters.remove(self)
|
|
||||||
|
|
||||||
def send_message(self, message):
|
|
||||||
if isinstance(message, dict):
|
|
||||||
message = escape.json_encode(message)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.write_message(message)
|
|
||||||
except:
|
|
||||||
logging.error('Error sending message', exc_info=True)
|
|
||||||
|
|
||||||
def poll_tasks(self):
|
|
||||||
filepath = self.watcher.examine()
|
|
||||||
if not filepath:
|
|
||||||
return
|
|
||||||
logging.info('File %s changed', filepath)
|
|
||||||
self.watch_tasks()
|
|
||||||
|
|
||||||
def watch_tasks(self):
|
|
||||||
if time.time() - self._last_reload_time < 3:
|
|
||||||
# if you changed lot of files in one time
|
|
||||||
# it will refresh too many times
|
|
||||||
logging.info('ignore this reload action')
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.info('Reload %s waiters', len(self.waiters))
|
|
||||||
|
|
||||||
msg = {
|
|
||||||
'command': 'reload',
|
|
||||||
'path': self.watcher.filepath or '*',
|
|
||||||
'liveCSS': True
|
|
||||||
}
|
|
||||||
|
|
||||||
self._last_reload_time = time.time()
|
|
||||||
for waiter in LiveReloadHandler.waiters:
|
|
||||||
try:
|
|
||||||
waiter.write_message(msg)
|
|
||||||
except:
|
|
||||||
logging.error('Error sending message', exc_info=True)
|
|
||||||
LiveReloadHandler.waiters.remove(waiter)
|
|
||||||
|
|
||||||
def on_message(self, message):
|
|
||||||
"""Handshake with livereload.js
|
|
||||||
|
|
||||||
1. client send 'hello'
|
|
||||||
2. server reply 'hello'
|
|
||||||
3. client send 'info'
|
|
||||||
|
|
||||||
http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
|
|
||||||
"""
|
|
||||||
message = ObjectDict(escape.json_decode(message))
|
|
||||||
if message.command == 'hello':
|
|
||||||
handshake = {}
|
|
||||||
handshake['command'] = 'hello'
|
|
||||||
handshake['protocols'] = [
|
|
||||||
'http://livereload.com/protocols/official-7',
|
|
||||||
'http://livereload.com/protocols/official-8',
|
|
||||||
'http://livereload.com/protocols/official-9',
|
|
||||||
'http://livereload.com/protocols/2.x-origin-version-negotiation',
|
|
||||||
'http://livereload.com/protocols/2.x-remote-control'
|
|
||||||
]
|
|
||||||
handshake['serverName'] = 'livereload-tornado'
|
|
||||||
self.send_message(handshake)
|
|
||||||
|
|
||||||
if message.command == 'info' and 'url' in message:
|
|
||||||
logging.info('Browser Connected: %s' % message.url)
|
|
||||||
LiveReloadHandler.waiters.add(self)
|
|
||||||
|
|
||||||
if not LiveReloadHandler._last_reload_time:
|
|
||||||
if not self.watcher._tasks:
|
|
||||||
logging.info('Watch current working directory')
|
|
||||||
self.watcher.watch(os.getcwd())
|
|
||||||
|
|
||||||
LiveReloadHandler._last_reload_time = time.time()
|
|
||||||
logging.info('Start watching changes')
|
|
||||||
if not self.watcher.start(self.poll_tasks):
|
|
||||||
ioloop.PeriodicCallback(self.poll_tasks, 800).start()
|
|
||||||
|
|
||||||
|
|
||||||
class LiveReloadJSHandler(RequestHandler):
|
|
||||||
def initialize(self, port):
|
|
||||||
self._port = port
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
js = os.path.join(
|
|
||||||
os.path.abspath(os.path.dirname(__file__)), 'livereload.js',
|
|
||||||
)
|
|
||||||
self.set_header('Content-Type', 'application/javascript')
|
|
||||||
with open(js, 'r') as f:
|
|
||||||
content = f.read()
|
|
||||||
content = content.replace('{{port}}', str(self._port))
|
|
||||||
self.write(content)
|
|
||||||
|
|
||||||
|
|
||||||
class ForceReloadHandler(RequestHandler):
|
|
||||||
def get(self):
|
|
||||||
msg = {
|
|
||||||
'command': 'reload',
|
|
||||||
'path': self.get_argument('path', default=None) or '*',
|
|
||||||
'liveCSS': True,
|
|
||||||
'liveImg': True
|
|
||||||
}
|
|
||||||
for waiter in LiveReloadHandler.waiters:
|
|
||||||
try:
|
|
||||||
waiter.write_message(msg)
|
|
||||||
except:
|
|
||||||
logging.error('Error sending message', exc_info=True)
|
|
||||||
LiveReloadHandler.waiters.remove(waiter)
|
|
||||||
self.write('ok')
|
|
||||||
|
|
||||||
|
|
||||||
class StaticHandler(RequestHandler):
|
|
||||||
def initialize(self, root, fallback=None):
|
|
||||||
self._root = os.path.abspath(root)
|
|
||||||
self._fallback = fallback
|
|
||||||
|
|
||||||
def filepath(self, url):
|
|
||||||
url = url.lstrip('/')
|
|
||||||
url = os.path.join(self._root, url)
|
|
||||||
|
|
||||||
if url.endswith('/'):
|
|
||||||
url += 'index.html'
|
|
||||||
elif not os.path.exists(url) and not url.endswith('.html'):
|
|
||||||
url += '.html'
|
|
||||||
|
|
||||||
if not os.path.isfile(url):
|
|
||||||
return None
|
|
||||||
return url
|
|
||||||
|
|
||||||
def get(self, path='/'):
|
|
||||||
filepath = self.filepath(path)
|
|
||||||
if not filepath and path.endswith('/'):
|
|
||||||
rootdir = os.path.join(self._root, path.lstrip('/'))
|
|
||||||
return self.create_index(rootdir)
|
|
||||||
|
|
||||||
if not filepath:
|
|
||||||
if self._fallback:
|
|
||||||
self._fallback(self.request)
|
|
||||||
self._finished = True
|
|
||||||
return
|
|
||||||
return self.send_error(404)
|
|
||||||
|
|
||||||
mime_type, encoding = mimetypes.guess_type(filepath)
|
|
||||||
if not mime_type:
|
|
||||||
mime_type = 'text/html'
|
|
||||||
|
|
||||||
self.mime_type = mime_type
|
|
||||||
self.set_header('Content-Type', mime_type)
|
|
||||||
|
|
||||||
with open(filepath, 'r') as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
hasher = hashlib.sha1()
|
|
||||||
hasher.update(to_bytes(data))
|
|
||||||
self.set_header('Etag', '"%s"' % hasher.hexdigest())
|
|
||||||
|
|
||||||
ua = self.request.headers.get('User-Agent', 'bot').lower()
|
|
||||||
if mime_type == 'text/html' and 'msie' not in ua:
|
|
||||||
data = data.replace(
|
|
||||||
'</head>',
|
|
||||||
'<script src="/livereload.js"></script></head>'
|
|
||||||
)
|
|
||||||
self.write(data)
|
|
||||||
|
|
||||||
def create_index(self, root):
|
|
||||||
files = os.listdir(root)
|
|
||||||
self.write('<ul>')
|
|
||||||
for f in files:
|
|
||||||
path = os.path.join(root, f)
|
|
||||||
self.write('<li>')
|
|
||||||
if os.path.isdir(path):
|
|
||||||
self.write('<a href="%s/">%s</a>' % (f, f))
|
|
||||||
else:
|
|
||||||
self.write('<a href="%s">%s</a>' % (f, f))
|
|
||||||
self.write('</li>')
|
|
||||||
self.write('</ul>')
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,224 +0,0 @@
|
|||||||
# -*- 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...')
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
livereload.watcher
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
A file watch management for LiveReload Server.
|
|
||||||
|
|
||||||
:copyright: (c) 2013 by Hsiaoming Yang
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class Watcher(object):
|
|
||||||
"""A file watcher registery."""
|
|
||||||
def __init__(self):
|
|
||||||
self._tasks = {}
|
|
||||||
self._mtimes = {}
|
|
||||||
|
|
||||||
# filepath that is changed
|
|
||||||
self.filepath = None
|
|
||||||
self._start = time.time()
|
|
||||||
|
|
||||||
def ignore(self, filename):
|
|
||||||
"""Ignore a given filename or not."""
|
|
||||||
_, ext = os.path.splitext(filename)
|
|
||||||
return ext in ['.pyc', '.pyo', '.o', '.swp']
|
|
||||||
|
|
||||||
def watch(self, path, func=None):
|
|
||||||
"""Add a task to watcher."""
|
|
||||||
self._tasks[path] = func
|
|
||||||
|
|
||||||
def start(self, callback):
|
|
||||||
"""Start the watcher running, calling callback when changes are observed. If this returns False,
|
|
||||||
regular polling will be used."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def examine(self):
|
|
||||||
"""Check if there are changes, if true, run the given task."""
|
|
||||||
# clean filepath
|
|
||||||
self.filepath = None
|
|
||||||
for path in self._tasks:
|
|
||||||
if self.is_changed(path):
|
|
||||||
func = self._tasks[path]
|
|
||||||
# run function
|
|
||||||
func and func()
|
|
||||||
return self.filepath
|
|
||||||
|
|
||||||
def is_changed(self, path):
|
|
||||||
if os.path.isfile(path):
|
|
||||||
return self.is_file_changed(path)
|
|
||||||
elif os.path.isdir(path):
|
|
||||||
return self.is_folder_changed(path)
|
|
||||||
return self.is_glob_changed(path)
|
|
||||||
|
|
||||||
def is_file_changed(self, path):
|
|
||||||
if not os.path.isfile(path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.ignore(path):
|
|
||||||
return False
|
|
||||||
|
|
||||||
mtime = os.path.getmtime(path)
|
|
||||||
|
|
||||||
if path not in self._mtimes:
|
|
||||||
self._mtimes[path] = mtime
|
|
||||||
self.filepath = path
|
|
||||||
return mtime > self._start
|
|
||||||
|
|
||||||
if self._mtimes[path] != mtime:
|
|
||||||
self._mtimes[path] = mtime
|
|
||||||
self.filepath = path
|
|
||||||
return True
|
|
||||||
|
|
||||||
self._mtimes[path] = mtime
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_folder_changed(self, path):
|
|
||||||
for root, dirs, files in os.walk(path, followlinks=True):
|
|
||||||
if '.git' in dirs:
|
|
||||||
dirs.remove('.git')
|
|
||||||
if '.hg' in dirs:
|
|
||||||
dirs.remove('.hg')
|
|
||||||
if '.svn' in dirs:
|
|
||||||
dirs.remove('.svn')
|
|
||||||
if '.cvs' in dirs:
|
|
||||||
dirs.remove('.cvs')
|
|
||||||
|
|
||||||
for f in files:
|
|
||||||
if self.is_file_changed(os.path.join(root, f)):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_glob_changed(self, path):
|
|
||||||
for f in glob.glob(path):
|
|
||||||
if self.is_file_changed(f):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class INotifyWatcher(Watcher):
|
|
||||||
def __init__(self):
|
|
||||||
Watcher.__init__(self)
|
|
||||||
|
|
||||||
import pyinotify
|
|
||||||
self.wm = pyinotify.WatchManager()
|
|
||||||
self.notifier = None
|
|
||||||
self.callback = None
|
|
||||||
|
|
||||||
def watch(self, path, func=None):
|
|
||||||
import pyinotify
|
|
||||||
flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
|
|
||||||
self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True)
|
|
||||||
Watcher.watch(self, path, func)
|
|
||||||
|
|
||||||
def inotify_event(self, event):
|
|
||||||
self.callback()
|
|
||||||
|
|
||||||
def start(self, callback):
|
|
||||||
if not self.notifier:
|
|
||||||
self.callback = callback
|
|
||||||
|
|
||||||
import pyinotify
|
|
||||||
from tornado import ioloop
|
|
||||||
self.notifier = pyinotify.TornadoAsyncNotifier(
|
|
||||||
self.wm, ioloop.IOLoop.instance(),
|
|
||||||
default_proc_fun=self.inotify_event
|
|
||||||
)
|
|
||||||
callback()
|
|
||||||
return True
|
|
||||||
Reference in New Issue
Block a user