Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
299 additions
and
299 deletions.
There are no files selected for viewing
File renamed without changes.
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,299 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright © 2012-2015 Roberto Alsina and others. | ||
|
||
# Permission is hereby granted, free of charge, to any | ||
# person obtaining a copy of this software and associated | ||
# documentation files (the "Software"), to deal in the | ||
# Software without restriction, including without limitation | ||
# the rights to use, copy, modify, merge, publish, | ||
# distribute, sublicense, and/or sell copies of the | ||
# Software, and to permit persons to whom the Software is | ||
# furnished to do so, subject to the following conditions: | ||
# | ||
# The above copyright notice and this permission notice | ||
# shall be included in all copies or substantial portions of | ||
# the Software. | ||
# | ||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY | ||
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE | ||
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | ||
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS | ||
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR | ||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR | ||
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | ||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
|
||
from __future__ import print_function | ||
|
||
import json | ||
import mimetypes | ||
import os | ||
import re | ||
import subprocess | ||
try: | ||
from urlparse import urlparse | ||
except ImportError: | ||
from urllib.parse import urlparse # NOQA | ||
import webbrowser | ||
from wsgiref.simple_server import make_server | ||
import wsgiref.util | ||
|
||
from blinker import signal | ||
import pyinotify | ||
try: | ||
from ws4py.websocket import WebSocket | ||
from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler | ||
from ws4py.server.wsgiutils import WebSocketWSGIApplication | ||
from ws4py.messaging import TextMessage | ||
except ImportError: | ||
WebSocket = None | ||
|
||
from nikola.plugin_categories import Command | ||
from nikola.utils import req_missing | ||
|
||
LRJS_PATH = os.path.join(os.path.dirname(__file__), 'livereload.js') | ||
MASK = pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_MODIFY | ||
error_signal = signal('error') | ||
refresh_signal = signal('refresh') | ||
|
||
|
||
class CommandAuto(Command): | ||
"""Start debugging console.""" | ||
name = "auto" | ||
doc_purpose = "builds and serves a site; automatically detects site changes, rebuilds, and optionally refreshes a browser" | ||
cmd_options = [ | ||
{ | ||
'name': 'port', | ||
'short': 'p', | ||
'long': 'port', | ||
'default': 8000, | ||
'type': int, | ||
'help': 'Port nummber (default: 8000)', | ||
}, | ||
{ | ||
'name': 'address', | ||
'short': 'a', | ||
'long': 'address', | ||
'type': str, | ||
'default': '0.0.0.0', | ||
'help': 'Address to bind (default: 0.0.0.0 – all local IPv4 interfaces)', | ||
}, | ||
{ | ||
'name': 'browser', | ||
'short': 'b', | ||
'type': bool, | ||
'help': 'Start a web browser.', | ||
'default': False, | ||
}, | ||
{ | ||
'name': 'ipv6', | ||
'short': '6', | ||
'long': 'ipv6', | ||
'default': False, | ||
'type': bool, | ||
'help': 'Use IPv6', | ||
}, | ||
] | ||
|
||
def _execute(self, options, args): | ||
"""Start the watcher.""" | ||
|
||
if WebSocket is None: | ||
req_missing(['ws4py'], 'use the "auto" command') | ||
return | ||
|
||
arguments = ['build'] | ||
if self.site.configuration_filename != 'conf.py': | ||
arguments = ['--conf=' + self.site.configuration_filename] + arguments | ||
|
||
self.command_line = 'nikola ' + ' '.join(arguments) | ||
|
||
# Run an initial build so we are up-to-date | ||
subprocess.call(["nikola"] + arguments) | ||
|
||
port = options and options.get('port') | ||
self.snippet = '''<script>document.write('<script src="http://' | ||
+ (location.host || 'localhost').split(':')[0] | ||
+ ':{0}/livereload.js?snipver=1"></' | ||
+ 'script>')</script> | ||
</head>'''.format(port) | ||
|
||
watched = [ | ||
self.site.configuration_filename, | ||
'themes/', | ||
'templates/', | ||
] | ||
for item in self.site.config['post_pages']: | ||
watched.append(os.path.dirname(item[0])) | ||
for item in self.site.config['FILES_FOLDERS']: | ||
watched.append(item) | ||
for item in self.site.config['GALLERY_FOLDERS']: | ||
watched.append(item) | ||
for item in self.site.config['LISTINGS_FOLDERS']: | ||
watched.append(item) | ||
|
||
out_folder = self.site.config['OUTPUT_FOLDER'] | ||
if options and options.get('browser'): | ||
browser = True | ||
else: | ||
browser = False | ||
|
||
if options['ipv6']: | ||
dhost = '::' | ||
else: | ||
dhost = None | ||
|
||
host = options['address'].strip('[').strip(']') or dhost | ||
|
||
# Start watchers that trigger reloads | ||
reload_wm = pyinotify.WatchManager() | ||
reload_notifier = pyinotify.ThreadedNotifier(reload_wm, self.do_refresh) | ||
reload_notifier.start() | ||
reload_wm.add_watch(out_folder, MASK, rec=True) # Watch output folders | ||
|
||
# Start watchers that trigger rebuilds | ||
rebuild_wm = pyinotify.WatchManager() | ||
rebuild_notifier = pyinotify.ThreadedNotifier(rebuild_wm, self.do_rebuild) | ||
rebuild_notifier.start() | ||
for p in watched: | ||
if os.path.exists(p): | ||
rebuild_wm.add_watch(p, MASK, rec=True) # Watch input folders | ||
|
||
parent = self | ||
|
||
class Mixed(WebSocketWSGIApplication): | ||
"""A class that supports WS and HTTP protocols in the same port.""" | ||
def __call__(self, environ, start_response): | ||
if environ.get('HTTP_UPGRADE') is None: | ||
return parent.serve_static(environ, start_response) | ||
return super(Mixed, self).__call__(environ, start_response) | ||
|
||
ws = make_server( | ||
host, port, server_class=WSGIServer, | ||
handler_class=WebSocketWSGIRequestHandler, | ||
app=Mixed(handler_cls=LRSocket) | ||
) | ||
ws.initialize_websockets_manager() | ||
print("Serving on port {0}...".format(port)) | ||
|
||
# Yes, this is racy | ||
if browser: | ||
webbrowser.open('http://{0}:{1}'.format(host, port)) | ||
|
||
try: | ||
ws.serve_forever() | ||
except KeyboardInterrupt: | ||
ws.server_close() | ||
|
||
def do_rebuild(self, event): | ||
p = subprocess.Popen(self.command_line, shell=True, stderr=subprocess.PIPE) | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
if p.wait() != 0: | ||
error_signal.send(error=p.stderr.read()) | ||
|
||
def do_refresh(self, event): | ||
print('REFRESHING: ', event.pathname) | ||
p = os.path.relpath(event.pathname, os.path.abspath(self.site.config['OUTPUT_FOLDER'])) | ||
refresh_signal.send(path=p) | ||
|
||
def serve_static(self, environ, start_response): | ||
"""Trivial static file server.""" | ||
uri = wsgiref.util.request_uri(environ) | ||
print('====>', uri) | ||
p_uri = urlparse(uri) | ||
f_path = os.path.join(self.site.config['OUTPUT_FOLDER'], *p_uri.path.split('/')) | ||
mimetype = mimetypes.guess_type(uri)[0] or b'text/html' | ||
|
||
if os.path.isdir(f_path): | ||
f_path = os.path.join(f_path, self.site.config['INDEX_FILE']) | ||
|
||
if os.path.isfile(f_path): | ||
with open(f_path) as fd: | ||
start_response(b'200 OK', [(b'Content-type', mimetype)]) | ||
return self.inject_js(mimetype, fd.read()) | ||
elif p_uri.path == '/livereload.js': | ||
with open(LRJS_PATH) as fd: | ||
start_response(b'200 OK', [(b'Content-type', mimetype)]) | ||
return self.inject_js(mimetype, fd.read()) | ||
start_response(b'404 ERR', []) | ||
return ['404 {0}'.format(uri)] | ||
|
||
def inject_js(self, mimetype, data): | ||
"""Inject livereload.js in HTML files.""" | ||
if mimetype == 'text/html': | ||
data = re.sub('</head>', self.snippet, data, 1, re.IGNORECASE) | ||
return data | ||
|
||
|
||
pending = [] | ||
|
||
|
||
class LRSocket(WebSocket): | ||
"""Speak Livereload protocol.""" | ||
|
||
def __init__(self, *a, **kw): | ||
refresh_signal.connect(self.notify) | ||
error_signal.connect(self.send_error) | ||
super(LRSocket, self).__init__(*a, **kw) | ||
|
||
def received_message(self, message): | ||
message = json.loads(message.data) | ||
print('<---', message) | ||
response = None | ||
if message['command'] == 'hello': # Handshake | ||
response = { | ||
'command': 'hello', | ||
'protocols': [ | ||
'http://livereload.com/protocols/official-7', | ||
], | ||
'serverName': 'nikola-livereload', | ||
} | ||
elif message['command'] == 'info': # Someone connected | ||
print('****** ', 'Browser Connected: %s' % message.get('url')) | ||
print('****** ', 'sending {0} pending messages'.format(len(pending))) | ||
while pending: | ||
msg = pending.pop() | ||
print('--->', msg.data) | ||
self.send(msg, msg.is_binary) | ||
else: | ||
response = { | ||
'command': 'alert', | ||
'message': 'HEY', | ||
} | ||
if response is not None: | ||
response = json.dumps(response) | ||
print('--->', response) | ||
response = TextMessage(response) | ||
self.send(response, response.is_binary) | ||
|
||
def notify(self, sender, path): | ||
"""Send reload requests to the client.""" | ||
p = os.path.join('/', path) | ||
message = { | ||
'command': 'reload', | ||
'liveCSS': True, | ||
'path': p, | ||
} | ||
response = json.dumps(message) | ||
print('--->', p) | ||
response = TextMessage(response) | ||
if self.stream is None: # No client connected or whatever | ||
pending.append(response) | ||
else: | ||
self.send(response, response.is_binary) | ||
|
||
def send_error(self, sender, error=None): | ||
"""Send reload requests to the client.""" | ||
print('ERRRRRRRR', error) | ||
if self.stream is None: # No client connected or whatever | ||
return | ||
message = { | ||
'command': 'alert', | ||
'message': error, | ||
} | ||
response = json.dumps(message) | ||
response = TextMessage(response) | ||
if self.stream is None: # No client connected or whatever | ||
pending.append(response) | ||
else: | ||
self.send(response, response.is_binary) |
Oops, something went wrong.
I never got to reviewing this branch, but now I noticed this. Why are we doing
shell=True
with a command line here andshell=False
with arguments (the recommended way) on the initial build?