This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "em-websocket"
|
||||
|
||||
require_relative "websockets"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
class LiveReloadReactor
|
||||
attr_reader :started_event, :stopped_event, :thread
|
||||
|
||||
def initialize
|
||||
@websockets = []
|
||||
@connections_count = 0
|
||||
@started_event = Utils::ThreadEvent.new
|
||||
@stopped_event = Utils::ThreadEvent.new
|
||||
end
|
||||
|
||||
def stop
|
||||
# There is only one EventMachine instance per Ruby process so stopping
|
||||
# it here will stop the reactor thread we have running.
|
||||
EM.stop if EM.reactor_running?
|
||||
Jekyll.logger.debug "LiveReload Server:", "halted"
|
||||
end
|
||||
|
||||
def running?
|
||||
EM.reactor_running?
|
||||
end
|
||||
|
||||
def handle_websockets_event(websocket)
|
||||
websocket.onopen { |handshake| connect(websocket, handshake) }
|
||||
websocket.onclose { disconnect(websocket) }
|
||||
websocket.onmessage { |msg| print_message(msg) }
|
||||
websocket.onerror { |error| log_error(error) }
|
||||
end
|
||||
|
||||
def start(opts)
|
||||
@thread = Thread.new do
|
||||
# Use epoll if the kernel supports it
|
||||
EM.epoll
|
||||
EM.run do
|
||||
EM.error_handler { |e| log_error(e) }
|
||||
|
||||
EM.start_server(
|
||||
opts["host"],
|
||||
opts["livereload_port"],
|
||||
HttpAwareConnection,
|
||||
opts
|
||||
) do |ws|
|
||||
handle_websockets_event(ws)
|
||||
end
|
||||
|
||||
# Notify blocked threads that EventMachine has started or shutdown
|
||||
EM.schedule { @started_event.set }
|
||||
EM.add_shutdown_hook { @stopped_event.set }
|
||||
|
||||
Jekyll.logger.info "LiveReload address:",
|
||||
"http://#{opts["host"]}:#{opts["livereload_port"]}"
|
||||
end
|
||||
end
|
||||
@thread.abort_on_exception = true
|
||||
end
|
||||
|
||||
# For a description of the protocol see
|
||||
# http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
|
||||
def reload(pages)
|
||||
pages.each do |p|
|
||||
json_message = JSON.dump(
|
||||
:command => "reload",
|
||||
:path => p.url,
|
||||
:liveCSS => true
|
||||
)
|
||||
|
||||
Jekyll.logger.debug "LiveReload:", "Reloading URL #{p.url.inspect}"
|
||||
@websockets.each { |ws| ws.send(json_message) }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def connect(websocket, handshake)
|
||||
@connections_count += 1
|
||||
if @connections_count == 1
|
||||
message = "Browser connected"
|
||||
message += " over SSL/TLS" if handshake.secure?
|
||||
Jekyll.logger.info "LiveReload:", message
|
||||
end
|
||||
websocket.send(
|
||||
JSON.dump(
|
||||
:command => "hello",
|
||||
:protocols => ["http://livereload.com/protocols/official-7"],
|
||||
:serverName => "jekyll"
|
||||
)
|
||||
)
|
||||
|
||||
@websockets << websocket
|
||||
end
|
||||
|
||||
def disconnect(websocket)
|
||||
@websockets.delete(websocket)
|
||||
end
|
||||
|
||||
def print_message(json_message)
|
||||
msg = JSON.parse(json_message)
|
||||
# Not sure what the 'url' command even does in LiveReload. The spec is silent
|
||||
# on its purpose.
|
||||
Jekyll.logger.info "LiveReload:", "Browser URL: #{msg["url"]}" if msg["command"] == "url"
|
||||
end
|
||||
|
||||
def log_error(error)
|
||||
Jekyll.logger.error "LiveReload experienced an error. " \
|
||||
"Run with --trace for more information."
|
||||
raise error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"application/javascript": "UTF-8",
|
||||
"application/json": "UTF-8",
|
||||
"application/manifest+json": "UTF-8",
|
||||
"application/vnd.syncml+xml": "UTF-8",
|
||||
"application/vnd.syncml.dm+wbxml": "UTF-8",
|
||||
"application/vnd.syncml.dm+xml": "UTF-8",
|
||||
"application/vnd.syncml.dmddf+xml": "UTF-8",
|
||||
"application/vnd.wap.wbxml": "UTF-8",
|
||||
"text/cache-manifest": "UTF-8",
|
||||
"text/calendar": "UTF-8",
|
||||
"text/coffeescript": "UTF-8",
|
||||
"text/css": "UTF-8",
|
||||
"text/csv": "UTF-8",
|
||||
"text/html": "UTF-8",
|
||||
"text/jade": "UTF-8",
|
||||
"text/jsx": "UTF-8",
|
||||
"text/less": "UTF-8",
|
||||
"text/markdown": "UTF-8",
|
||||
"text/mathml": "UTF-8",
|
||||
"text/mdx": "UTF-8",
|
||||
"text/n3": "UTF-8",
|
||||
"text/plain": "UTF-8",
|
||||
"text/prs.lines.tag": "UTF-8",
|
||||
"text/richtext": "UTF-8",
|
||||
"text/sgml": "UTF-8",
|
||||
"text/shex": "UTF-8",
|
||||
"text/slim": "UTF-8",
|
||||
"text/spdx": "UTF-8",
|
||||
"text/stylus": "UTF-8",
|
||||
"text/tab-separated-values": "UTF-8",
|
||||
"text/troff": "UTF-8",
|
||||
"text/turtle": "UTF-8",
|
||||
"text/uri-list": "UTF-8",
|
||||
"text/vcard": "UTF-8",
|
||||
"text/vnd.curl": "UTF-8",
|
||||
"text/vnd.curl.dcurl": "UTF-8",
|
||||
"text/vnd.curl.mcurl": "UTF-8",
|
||||
"text/vnd.curl.scurl": "UTF-8",
|
||||
"text/vnd.familysearch.gedcom": "UTF-8",
|
||||
"text/vnd.fly": "UTF-8",
|
||||
"text/vnd.fmi.flexstor": "UTF-8",
|
||||
"text/vnd.graphviz": "UTF-8",
|
||||
"text/vnd.in3d.3dml": "UTF-8",
|
||||
"text/vnd.in3d.spot": "UTF-8",
|
||||
"text/vnd.sun.j2me.app-descriptor": "UTF-8",
|
||||
"text/vnd.wap.wml": "UTF-8",
|
||||
"text/vnd.wap.wmlscript": "UTF-8",
|
||||
"text/vtt": "UTF-8",
|
||||
"text/x-asm": "UTF-8",
|
||||
"text/x-c": "UTF-8",
|
||||
"text/x-component": "UTF-8",
|
||||
"text/x-fortran": "UTF-8",
|
||||
"text/x-handlebars-template": "UTF-8",
|
||||
"text/x-java-source": "UTF-8",
|
||||
"text/x-lua": "UTF-8",
|
||||
"text/x-markdown": "UTF-8",
|
||||
"text/x-nfo": "UTF-8",
|
||||
"text/x-opml": "UTF-8",
|
||||
"text/x-pascal": "UTF-8",
|
||||
"text/x-processing": "UTF-8",
|
||||
"text/x-sass": "UTF-8",
|
||||
"text/x-scss": "UTF-8",
|
||||
"text/x-setext": "UTF-8",
|
||||
"text/x-sfv": "UTF-8",
|
||||
"text/x-suse-ymp": "UTF-8",
|
||||
"text/x-uuencode": "UTF-8",
|
||||
"text/x-vcalendar": "UTF-8",
|
||||
"text/x-vcard": "UTF-8",
|
||||
"text/yaml": "UTF-8"
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "webrick"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
# This class is used to determine if the Servlet should modify a served file
|
||||
# to insert the LiveReload script tags
|
||||
class SkipAnalyzer
|
||||
BAD_USER_AGENTS = [%r!MSIE!].freeze
|
||||
|
||||
def self.skip_processing?(request, response, options)
|
||||
new(request, response, options).skip_processing?
|
||||
end
|
||||
|
||||
def initialize(request, response, options)
|
||||
@options = options
|
||||
@request = request
|
||||
@response = response
|
||||
end
|
||||
|
||||
def skip_processing?
|
||||
!html? || chunked? || inline? || bad_browser?
|
||||
end
|
||||
|
||||
def chunked?
|
||||
@response["Transfer-Encoding"] == "chunked"
|
||||
end
|
||||
|
||||
def inline?
|
||||
@response["Content-Disposition"].to_s.start_with?("inline")
|
||||
end
|
||||
|
||||
def bad_browser?
|
||||
BAD_USER_AGENTS.any? { |pattern| pattern.match?(@request["User-Agent"]) }
|
||||
end
|
||||
|
||||
def html?
|
||||
@response["Content-Type"].to_s.include?("text/html")
|
||||
end
|
||||
end
|
||||
|
||||
# This class inserts the LiveReload script tags into HTML as it is served
|
||||
class BodyProcessor
|
||||
HEAD_TAG_REGEX = %r!<head>|<head[^(er)][^<]*>!.freeze
|
||||
|
||||
attr_reader :content_length, :new_body, :livereload_added
|
||||
|
||||
def initialize(body, options)
|
||||
@body = body
|
||||
@options = options
|
||||
@processed = false
|
||||
end
|
||||
|
||||
def processed?
|
||||
@processed
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def process!
|
||||
@new_body = []
|
||||
# @body will usually be a File object but Strings occur in rare cases
|
||||
if @body.respond_to?(:each)
|
||||
begin
|
||||
@body.each { |line| @new_body << line.to_s }
|
||||
ensure
|
||||
@body.close
|
||||
end
|
||||
else
|
||||
@new_body = @body.lines
|
||||
end
|
||||
|
||||
@content_length = 0
|
||||
@livereload_added = false
|
||||
|
||||
@new_body.each do |line|
|
||||
if !@livereload_added && line["<head"]
|
||||
line.gsub!(HEAD_TAG_REGEX) do |match|
|
||||
%(#{match}#{template.result(binding)})
|
||||
end
|
||||
|
||||
@livereload_added = true
|
||||
end
|
||||
|
||||
@content_length += line.bytesize
|
||||
@processed = true
|
||||
end
|
||||
@new_body = @new_body.join
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def template
|
||||
# Unclear what "snipver" does. Doc at
|
||||
# https://github.com/livereload/livereload-js states that the recommended
|
||||
# setting is 1.
|
||||
|
||||
# Complicated JavaScript to ensure that livereload.js is loaded from the
|
||||
# same origin as the page. Mostly useful for dealing with the browser's
|
||||
# distinction between 'localhost' and 127.0.0.1
|
||||
@template ||= ERB.new(<<~TEMPLATE)
|
||||
<script>
|
||||
document.write(
|
||||
'<script src="' + location.protocol + '//' +
|
||||
(location.host || 'localhost').split(':')[0] +
|
||||
':<%=@options["livereload_port"] %>/livereload.js?snipver=1<%= livereload_args %>"' +
|
||||
'></' +
|
||||
'script>');
|
||||
</script>
|
||||
TEMPLATE
|
||||
end
|
||||
|
||||
def livereload_args
|
||||
# XHTML standard requires ampersands to be encoded as entities when in
|
||||
# attributes. See http://stackoverflow.com/a/2190292
|
||||
src = ""
|
||||
if @options["livereload_min_delay"]
|
||||
src += "&mindelay=#{@options["livereload_min_delay"]}"
|
||||
end
|
||||
if @options["livereload_max_delay"]
|
||||
src += "&maxdelay=#{@options["livereload_max_delay"]}"
|
||||
end
|
||||
src += "&port=#{@options["livereload_port"]}" if @options["livereload_port"]
|
||||
src
|
||||
end
|
||||
end
|
||||
|
||||
class Servlet < WEBrick::HTTPServlet::FileHandler
|
||||
DEFAULTS = {
|
||||
"Cache-Control" => "private, max-age=0, proxy-revalidate, " \
|
||||
"no-store, no-cache, must-revalidate",
|
||||
}.freeze
|
||||
|
||||
def initialize(server, root, callbacks)
|
||||
# So we can access them easily.
|
||||
@jekyll_opts = server.config[:JekyllOptions]
|
||||
@mime_types_charset = server.config[:MimeTypesCharset]
|
||||
set_defaults
|
||||
super
|
||||
end
|
||||
|
||||
def search_index_file(req, res)
|
||||
super ||
|
||||
search_file(req, res, ".html") ||
|
||||
search_file(req, res, ".xhtml")
|
||||
end
|
||||
|
||||
# Add the ability to tap file.html the same way that Nginx does on our
|
||||
# Docker images (or on GitHub Pages.) The difference is that we might end
|
||||
# up with a different preference on which comes first.
|
||||
|
||||
def search_file(req, res, basename)
|
||||
# /file.* > /file/index.html > /file.html
|
||||
super ||
|
||||
super(req, res, "#{basename}.html") ||
|
||||
super(req, res, "#{basename}.xhtml")
|
||||
end
|
||||
|
||||
# rubocop:disable Naming/MethodName
|
||||
def do_GET(req, res)
|
||||
rtn = super
|
||||
|
||||
if @jekyll_opts["livereload"]
|
||||
return rtn if SkipAnalyzer.skip_processing?(req, res, @jekyll_opts)
|
||||
|
||||
processor = BodyProcessor.new(res.body, @jekyll_opts)
|
||||
processor.process!
|
||||
res.body = processor.new_body
|
||||
res.content_length = processor.content_length.to_s
|
||||
|
||||
if processor.livereload_added
|
||||
# Add a header to indicate that the page content has been modified
|
||||
res["X-Rack-LiveReload"] = "1"
|
||||
end
|
||||
end
|
||||
|
||||
conditionally_inject_charset(res)
|
||||
res.header.merge!(@headers)
|
||||
rtn
|
||||
end
|
||||
# rubocop:enable Naming/MethodName
|
||||
|
||||
private
|
||||
|
||||
# Inject charset based on Jekyll config only if our mime-types database contains
|
||||
# the charset metadata.
|
||||
#
|
||||
# Refer `script/vendor-mimes` in the repository for further details.
|
||||
def conditionally_inject_charset(res)
|
||||
typ = res.header["content-type"]
|
||||
return unless @mime_types_charset.key?(typ)
|
||||
return if %r!;\s*charset=!.match?(typ)
|
||||
|
||||
res.header["content-type"] = "#{typ}; charset=#{@jekyll_opts["encoding"]}"
|
||||
end
|
||||
|
||||
def set_defaults
|
||||
hash_ = @jekyll_opts.fetch("webrick", {}).fetch("headers", {})
|
||||
DEFAULTS.each_with_object(@headers = hash_) do |(key, val), hash|
|
||||
hash[key] = val unless hash.key?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "http/parser"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
# The LiveReload protocol requires the server to serve livereload.js over HTTP
|
||||
# despite the fact that the protocol itself uses WebSockets. This custom connection
|
||||
# class addresses the dual protocols that the server needs to understand.
|
||||
class HttpAwareConnection < EventMachine::WebSocket::Connection
|
||||
attr_reader :reload_body, :reload_size
|
||||
|
||||
def initialize(_opts)
|
||||
# If EventMachine SSL support on Windows ever gets better, the code below will
|
||||
# set up the reactor to handle SSL
|
||||
#
|
||||
# @ssl_enabled = opts["ssl_cert"] && opts["ssl_key"]
|
||||
# if @ssl_enabled
|
||||
# em_opts[:tls_options] = {
|
||||
# :private_key_file => Jekyll.sanitized_path(opts["source"], opts["ssl_key"]),
|
||||
# :cert_chain_file => Jekyll.sanitized_path(opts["source"], opts["ssl_cert"])
|
||||
# }
|
||||
# em_opts[:secure] = true
|
||||
# end
|
||||
|
||||
# This is too noisy even for --verbose, but uncomment if you need it for
|
||||
# a specific WebSockets issue. Adding ?LR-verbose=true onto the URL will
|
||||
# enable logging on the client side.
|
||||
# em_opts[:debug] = true
|
||||
|
||||
em_opts = {}
|
||||
super(em_opts)
|
||||
|
||||
reload_file = File.join(Serve.singleton_class::LIVERELOAD_DIR, "livereload.js")
|
||||
|
||||
@reload_body = File.read(reload_file)
|
||||
@reload_size = @reload_body.bytesize
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def dispatch(data)
|
||||
parser = Http::Parser.new
|
||||
parser << data
|
||||
|
||||
# WebSockets requests will have a Connection: Upgrade header
|
||||
if parser.http_method != "GET" || parser.upgrade?
|
||||
super
|
||||
elsif parser.request_url.start_with?("/livereload.js")
|
||||
headers = [
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Type: application/javascript",
|
||||
"Content-Length: #{reload_size}",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
send_data(headers)
|
||||
|
||||
# stream_file_data would free us from keeping livereload.js in memory
|
||||
# but JRuby blocks on that call and never returns
|
||||
send_data(reload_body)
|
||||
close_connection_after_writing
|
||||
else
|
||||
body = "This port only serves livereload.js over HTTP.\n"
|
||||
headers = [
|
||||
"HTTP/1.1 400 Bad Request",
|
||||
"Content-Type: text/plain",
|
||||
"Content-Length: #{body.bytesize}",
|
||||
"",
|
||||
"",
|
||||
].join("\r\n")
|
||||
send_data(headers)
|
||||
send_data(body)
|
||||
close_connection_after_writing
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user