Add LiveReload functionality to Jekyll. (#5142)
Merge pull request 5142
This commit is contained in:
parent
9ec9273ed9
commit
9d68b1b134
1
Gemfile
1
Gemfile
|
@ -24,6 +24,7 @@ end
|
|||
group :test do
|
||||
gem "codeclimate-test-reporter", "~> 1.0.5"
|
||||
gem "cucumber", RUBY_VERSION >= "2.2" ? "~> 3.0" : "3.0.1"
|
||||
gem "httpclient"
|
||||
gem "jekyll_test_plugin"
|
||||
gem "jekyll_test_plugin_malicious"
|
||||
# nokogiri v1.8 does not work with ruby 2.1 and below
|
||||
|
|
|
@ -32,6 +32,7 @@ Gem::Specification.new do |s|
|
|||
|
||||
s.add_runtime_dependency("addressable", "~> 2.4")
|
||||
s.add_runtime_dependency("colorator", "~> 1.0")
|
||||
s.add_runtime_dependency("em-websocket", "~> 0.5")
|
||||
s.add_runtime_dependency("i18n", "~> 0.7")
|
||||
s.add_runtime_dependency("jekyll-sass-converter", "~> 1.0")
|
||||
s.add_runtime_dependency("jekyll-watch", "~> 2.0")
|
||||
|
|
|
@ -1,20 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "thread"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve < Command
|
||||
# Similar to the pattern in Utils::ThreadEvent except we are maintaining the
|
||||
# state of @running instead of just signaling an event. We have to maintain this
|
||||
# state since Serve is just called via class methods instead of an instance
|
||||
# being created each time.
|
||||
@mutex = Mutex.new
|
||||
@run_cond = ConditionVariable.new
|
||||
@running = false
|
||||
|
||||
class << self
|
||||
COMMAND_OPTIONS = {
|
||||
"ssl_cert" => ["--ssl-cert [CERT]", "X.509 (SSL) certificate."],
|
||||
"host" => ["host", "-H", "--host [HOST]", "Host to bind to"],
|
||||
"open_url" => ["-o", "--open-url", "Launch your site in a browser"],
|
||||
"detach" => ["-B", "--detach", "Run the server in the background"],
|
||||
"ssl_key" => ["--ssl-key [KEY]", "X.509 (SSL) Private Key."],
|
||||
"port" => ["-P", "--port [PORT]", "Port to listen on"],
|
||||
"show_dir_listing" => ["--show-dir-listing",
|
||||
"ssl_cert" => ["--ssl-cert [CERT]", "X.509 (SSL) certificate."],
|
||||
"host" => ["host", "-H", "--host [HOST]", "Host to bind to"],
|
||||
"open_url" => ["-o", "--open-url", "Launch your site in a browser"],
|
||||
"detach" => ["-B", "--detach",
|
||||
"Run the server in the background",],
|
||||
"ssl_key" => ["--ssl-key [KEY]", "X.509 (SSL) Private Key."],
|
||||
"port" => ["-P", "--port [PORT]", "Port to listen on"],
|
||||
"show_dir_listing" => ["--show-dir-listing",
|
||||
"Show a directory listing instead of loading your index file.",],
|
||||
"skip_initial_build" => ["skip_initial_build", "--skip-initial-build",
|
||||
"skip_initial_build" => ["skip_initial_build", "--skip-initial-build",
|
||||
"Skips the initial site build which occurs before the server is started.",],
|
||||
"livereload" => ["-l", "--livereload",
|
||||
"Use LiveReload to automatically refresh browsers",],
|
||||
"livereload_ignore" => ["--livereload-ignore ignore GLOB1[,GLOB2[,...]]",
|
||||
Array,
|
||||
"Files for LiveReload to ignore. Remember to quote the values so your shell "\
|
||||
"won't expand them",],
|
||||
"livereload_min_delay" => ["--livereload-min-delay [SECONDS]",
|
||||
"Minimum reload delay",],
|
||||
"livereload_max_delay" => ["--livereload-max-delay [SECONDS]",
|
||||
"Maximum reload delay",],
|
||||
"livereload_port" => ["--livereload-port [PORT]", Integer,
|
||||
"Port for LiveReload to listen on",],
|
||||
}.freeze
|
||||
|
||||
DIRECTORY_INDEX = %w(
|
||||
|
@ -26,7 +49,11 @@ module Jekyll
|
|||
index.json
|
||||
).freeze
|
||||
|
||||
#
|
||||
LIVERELOAD_PORT = 35_729
|
||||
LIVERELOAD_DIR = File.join(__dir__, "serve", "livereload_assets")
|
||||
|
||||
attr_reader :mutex, :run_cond, :running
|
||||
alias_method :running?, :running
|
||||
|
||||
def init_with_program(prog)
|
||||
prog.command(:serve) do |cmd|
|
||||
|
@ -41,20 +68,34 @@ module Jekyll
|
|||
end
|
||||
|
||||
cmd.action do |_, opts|
|
||||
opts["livereload_port"] ||= LIVERELOAD_PORT
|
||||
opts["serving"] = true
|
||||
opts["watch" ] = true unless opts.key?("watch")
|
||||
|
||||
config = configuration_from_options(opts)
|
||||
if Jekyll.env == "development"
|
||||
config["url"] = default_url(config)
|
||||
end
|
||||
[Build, Serve].each { |klass| klass.process(config) }
|
||||
start(opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
def start(opts)
|
||||
# Set the reactor to nil so any old reactor will be GCed.
|
||||
# We can't unregister a hook so in testing when Serve.start is
|
||||
# called multiple times we don't want to inadvertently keep using
|
||||
# a reactor created by a previous test when our test might not
|
||||
@reload_reactor = nil
|
||||
|
||||
register_reload_hooks(opts) if opts["livereload"]
|
||||
config = configuration_from_options(opts)
|
||||
if Jekyll.env == "development"
|
||||
config["url"] = default_url(config)
|
||||
end
|
||||
[Build, Serve].each { |klass| klass.process(config) }
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
def process(opts)
|
||||
opts = configuration_from_options(opts)
|
||||
destination = opts["destination"]
|
||||
|
@ -63,6 +104,76 @@ module Jekyll
|
|||
start_up_webrick(opts, destination)
|
||||
end
|
||||
|
||||
def shutdown
|
||||
@server.shutdown if running?
|
||||
end
|
||||
|
||||
# Perform logical validation of CLI options
|
||||
|
||||
private
|
||||
def validate_options(opts)
|
||||
if opts["livereload"]
|
||||
if opts["detach"]
|
||||
Jekyll.logger.warn "Warning:",
|
||||
"--detach and --livereload are mutually exclusive. Choosing --livereload"
|
||||
opts["detach"] = false
|
||||
end
|
||||
if opts["ssl_cert"] || opts["ssl_key"]
|
||||
# This is not technically true. LiveReload works fine over SSL, but
|
||||
# EventMachine's SSL support in Windows requires building the gem's
|
||||
# native extensions against OpenSSL and that proved to be a process
|
||||
# so tedious that expecting users to do it is a non-starter.
|
||||
Jekyll.logger.abort_with "Error:", "LiveReload does not support SSL"
|
||||
end
|
||||
unless opts["watch"]
|
||||
# Using livereload logically implies you want to watch the files
|
||||
opts["watch"] = true
|
||||
end
|
||||
elsif %w(livereload_min_delay
|
||||
livereload_max_delay
|
||||
livereload_ignore
|
||||
livereload_port).any? { |o| opts[o] }
|
||||
Jekyll.logger.abort_with "--livereload-min-delay, "\
|
||||
"--livereload-max-delay, --livereload-ignore, and "\
|
||||
"--livereload-port require the --livereload option."
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
private
|
||||
# rubocop:disable Metrics/AbcSize
|
||||
def register_reload_hooks(opts)
|
||||
require_relative "serve/live_reload_reactor"
|
||||
@reload_reactor = LiveReloadReactor.new
|
||||
|
||||
Jekyll::Hooks.register(:site, :post_render) do |site|
|
||||
regenerator = Jekyll::Regenerator.new(site)
|
||||
@changed_pages = site.pages.select do |p|
|
||||
regenerator.regenerate?(p)
|
||||
end
|
||||
end
|
||||
|
||||
# A note on ignoring files: LiveReload errs on the side of reloading when it
|
||||
# comes to the message it gets. If, for example, a page is ignored but a CSS
|
||||
# file linked in the page isn't, the page will still be reloaded if the CSS
|
||||
# file is contained in the message sent to LiveReload. Additionally, the
|
||||
# path matching is very loose so that a message to reload "/" will always
|
||||
# lead the page to reload since every page starts with "/".
|
||||
Jekyll::Hooks.register(:site, :post_write) do
|
||||
if @changed_pages && @reload_reactor && @reload_reactor.running?
|
||||
ignore, @changed_pages = @changed_pages.partition do |p|
|
||||
Array(opts["livereload_ignore"]).any? do |filter|
|
||||
File.fnmatch(filter, Jekyll.sanitized_path(p.relative_path))
|
||||
end
|
||||
end
|
||||
Jekyll.logger.debug "LiveReload:", "Ignoring #{ignore.map(&:relative_path)}"
|
||||
@reload_reactor.reload(@changed_pages)
|
||||
end
|
||||
@changed_pages = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Do a base pre-setup of WEBRick so that everything is in place
|
||||
# when we get ready to party, checking for an setting up an error page
|
||||
# and making sure our destination exists.
|
||||
|
@ -92,6 +203,7 @@ module Jekyll
|
|||
:MimeTypes => mime_types,
|
||||
:DocumentRoot => opts["destination"],
|
||||
:StartCallback => start_callback(opts["detach"]),
|
||||
:StopCallback => stop_callback(opts["detach"]),
|
||||
:BindAddress => opts["host"],
|
||||
:Port => opts["port"],
|
||||
:DirectoryIndex => DIRECTORY_INDEX,
|
||||
|
@ -108,11 +220,16 @@ module Jekyll
|
|||
|
||||
private
|
||||
def start_up_webrick(opts, destination)
|
||||
server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }
|
||||
server.mount(opts["baseurl"].to_s, Servlet, destination, file_handler_opts)
|
||||
Jekyll.logger.info "Server address:", server_address(server, opts)
|
||||
launch_browser server, opts if opts["open_url"]
|
||||
boot_or_detach server, opts
|
||||
if opts["livereload"]
|
||||
@reload_reactor.start(opts)
|
||||
end
|
||||
|
||||
@server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }
|
||||
@server.mount(opts["baseurl"].to_s, Servlet, destination, file_handler_opts)
|
||||
|
||||
Jekyll.logger.info "Server address:", server_address(@server, opts)
|
||||
launch_browser @server, opts if opts["open_url"]
|
||||
boot_or_detach @server, opts
|
||||
end
|
||||
|
||||
# Recreate NondisclosureName under utf-8 circumstance
|
||||
|
@ -227,7 +344,29 @@ module Jekyll
|
|||
def start_callback(detached)
|
||||
unless detached
|
||||
proc do
|
||||
Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
|
||||
mutex.synchronize do
|
||||
# Block until EventMachine reactor starts
|
||||
@reload_reactor.started_event.wait unless @reload_reactor.nil?
|
||||
@running = true
|
||||
Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
|
||||
@run_cond.broadcast
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def stop_callback(detached)
|
||||
unless detached
|
||||
proc do
|
||||
mutex.synchronize do
|
||||
unless @reload_reactor.nil?
|
||||
@reload_reactor.stop
|
||||
@reload_reactor.stopped_event.wait
|
||||
end
|
||||
@running = false
|
||||
@run_cond.broadcast
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
require "em-websocket"
|
||||
|
||||
require_relative "websockets"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
class LiveReloadReactor
|
||||
attr_reader :started_event
|
||||
attr_reader :stopped_event
|
||||
attr_reader :thread
|
||||
|
||||
def initialize
|
||||
@thread = nil
|
||||
@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(ws)
|
||||
ws.onopen do |handshake|
|
||||
connect(ws, handshake)
|
||||
end
|
||||
|
||||
ws.onclose do
|
||||
disconnect(ws)
|
||||
end
|
||||
|
||||
ws.onmessage do |msg|
|
||||
print_message(msg)
|
||||
end
|
||||
|
||||
ws.onerror do |error|
|
||||
log_error(error)
|
||||
end
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def start(opts)
|
||||
@thread = Thread.new do
|
||||
# Use epoll if the kernel supports it
|
||||
EM.epoll
|
||||
EM.run do
|
||||
EM.error_handler do |e|
|
||||
log_error(e)
|
||||
end
|
||||
|
||||
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 do
|
||||
@started_event.set
|
||||
end
|
||||
|
||||
EM.add_shutdown_hook do
|
||||
@stopped_event.set
|
||||
end
|
||||
|
||||
Jekyll.logger.info(
|
||||
"LiveReload address:", "#{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|
|
||||
msg = {
|
||||
:command => "reload",
|
||||
:path => p.url,
|
||||
:liveCSS => true,
|
||||
}
|
||||
|
||||
Jekyll.logger.debug("LiveReload:", "Reloading #{p.url}")
|
||||
Jekyll.logger.debug(JSON.dump(msg))
|
||||
@websockets.each do |ws|
|
||||
ws.send(JSON.dump(msg))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def connect(ws, handshake)
|
||||
@connections_count += 1
|
||||
if @connections_count == 1
|
||||
message = "Browser connected"
|
||||
message += " over SSL/TLS" if handshake.secure?
|
||||
Jekyll.logger.info("LiveReload:", message)
|
||||
end
|
||||
ws.send(
|
||||
JSON.dump(
|
||||
:command => "hello",
|
||||
:protocols => ["http://livereload.com/protocols/official-7"],
|
||||
:serverName => "jekyll"
|
||||
)
|
||||
)
|
||||
|
||||
@websockets << ws
|
||||
end
|
||||
|
||||
private
|
||||
def disconnect(ws)
|
||||
@websockets.delete(ws)
|
||||
end
|
||||
|
||||
private
|
||||
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.
|
||||
if msg["command"] == "url"
|
||||
Jekyll.logger.info("LiveReload:", "Browser URL: #{msg["url"]}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def log_error(e)
|
||||
Jekyll.logger.warn(
|
||||
"LiveReload experienced an error. "\
|
||||
"Run with --verbose for more information."
|
||||
)
|
||||
Jekyll.logger.debug("LiveReload Error:", e.message)
|
||||
Jekyll.logger.debug("LiveReload Error:", e.backtrace.join("\n"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,128 @@ 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"] =~ %r!^inline!
|
||||
end
|
||||
|
||||
def bad_browser?
|
||||
BAD_USER_AGENTS.any? { |pattern| @request["User-Agent"] =~ pattern }
|
||||
end
|
||||
|
||||
def html?
|
||||
@response["Content-Type"] =~ %r!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)][^<]*>!
|
||||
|
||||
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
|
||||
|
||||
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 = <<-TEMPLATE
|
||||
<script>
|
||||
document.write(
|
||||
'<script src="http://' +
|
||||
(location.host || 'localhost').split(':')[0] +
|
||||
':<%=@options["livereload_port"] %>/livereload.js?snipver=1<%= livereload_args %>"' +
|
||||
'></' +
|
||||
'script>');
|
||||
</script>
|
||||
TEMPLATE
|
||||
ERB.new(Jekyll::Utils.strip_heredoc(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
|
||||
if @options["livereload_port"]
|
||||
src += "&port=#{@options["livereload_port"]}"
|
||||
end
|
||||
src
|
||||
end
|
||||
end
|
||||
|
||||
class Servlet < WEBrick::HTTPServlet::FileHandler
|
||||
DEFAULTS = {
|
||||
"Cache-Control" => "private, max-age=0, proxy-revalidate, " \
|
||||
|
@ -34,6 +156,21 @@ module Jekyll
|
|||
# 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
|
||||
|
||||
validate_and_ensure_charset(req, res)
|
||||
res.header.merge!(@headers)
|
||||
rtn
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
# 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 =~ %r!^\/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
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,7 @@ module Jekyll
|
|||
autoload :Internet, "jekyll/utils/internet"
|
||||
autoload :Platforms, "jekyll/utils/platforms"
|
||||
autoload :Rouge, "jekyll/utils/rouge"
|
||||
autoload :ThreadEvent, "jekyll/utils/thread_event"
|
||||
autoload :WinTZ, "jekyll/utils/win_tz"
|
||||
|
||||
# Constants for use in #slugify
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "thread"
|
||||
|
||||
module Jekyll
|
||||
module Utils
|
||||
# Based on the pattern and code from
|
||||
# https://emptysqua.re/blog/an-event-synchronization-primitive-for-ruby/
|
||||
class ThreadEvent
|
||||
attr_reader :flag
|
||||
|
||||
def initialize
|
||||
@lock = Mutex.new
|
||||
@cond = ConditionVariable.new
|
||||
@flag = false
|
||||
end
|
||||
|
||||
def set
|
||||
@lock.synchronize do
|
||||
yield if block_given?
|
||||
@flag = true
|
||||
@cond.broadcast
|
||||
end
|
||||
end
|
||||
|
||||
def wait
|
||||
@lock.synchronize do
|
||||
unless @flag
|
||||
@cond.wait(@lock)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,7 +3,10 @@
|
|||
require "webrick"
|
||||
require "mercenary"
|
||||
require "helper"
|
||||
require "httpclient"
|
||||
require "openssl"
|
||||
require "thread"
|
||||
require "tmpdir"
|
||||
|
||||
class TestCommandsServe < JekyllUnitTest
|
||||
def custom_opts(what)
|
||||
|
@ -12,6 +15,128 @@ class TestCommandsServe < JekyllUnitTest
|
|||
)
|
||||
end
|
||||
|
||||
def start_server(opts)
|
||||
@thread = Thread.new do
|
||||
merc = nil
|
||||
cmd = Jekyll::Commands::Serve
|
||||
Mercenary.program(:jekyll) do |p|
|
||||
merc = cmd.init_with_program(p)
|
||||
end
|
||||
merc.execute(:serve, opts)
|
||||
end
|
||||
@thread.abort_on_exception = true
|
||||
|
||||
Jekyll::Commands::Serve.mutex.synchronize do
|
||||
unless Jekyll::Commands::Serve.running?
|
||||
Jekyll::Commands::Serve.run_cond.wait(Jekyll::Commands::Serve.mutex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def serve(opts)
|
||||
allow(Jekyll).to receive(:configuration).and_return(opts)
|
||||
allow(Jekyll::Commands::Build).to receive(:process)
|
||||
|
||||
start_server(opts)
|
||||
|
||||
opts
|
||||
end
|
||||
|
||||
context "using LiveReload" do
|
||||
setup do
|
||||
@temp_dir = Dir.mktmpdir("jekyll_livereload_test")
|
||||
@destination = File.join(@temp_dir, "_site")
|
||||
Dir.mkdir(@destination) || flunk("Could not make directory #{@destination}")
|
||||
@client = HTTPClient.new
|
||||
@client.connect_timeout = 5
|
||||
@standard_options = {
|
||||
"port" => 4000,
|
||||
"host" => "localhost",
|
||||
"baseurl" => "",
|
||||
"detach" => false,
|
||||
"livereload" => true,
|
||||
"source" => @temp_dir,
|
||||
"destination" => @destination,
|
||||
}
|
||||
|
||||
site = instance_double(Jekyll::Site)
|
||||
simple_page = <<-HTML.gsub(%r!^\s*!, "")
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hello! I am a simple web page.</p>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
File.open(File.join(@destination, "hello.html"), "w") do |f|
|
||||
f.write(simple_page)
|
||||
end
|
||||
allow(Jekyll::Site).to receive(:new).and_return(site)
|
||||
end
|
||||
|
||||
teardown do
|
||||
capture_io do
|
||||
Jekyll::Commands::Serve.shutdown
|
||||
end
|
||||
|
||||
Jekyll::Commands::Serve.mutex.synchronize do
|
||||
if Jekyll::Commands::Serve.running?
|
||||
Jekyll::Commands::Serve.run_cond.wait(Jekyll::Commands::Serve.mutex)
|
||||
end
|
||||
end
|
||||
|
||||
FileUtils.remove_entry_secure(@temp_dir, true)
|
||||
end
|
||||
|
||||
should "serve livereload.js over HTTP on the default LiveReload port" do
|
||||
skip_if_windows "EventMachine support on Windows is limited"
|
||||
opts = serve(@standard_options)
|
||||
content = @client.get_content(
|
||||
"http://#{opts["host"]}:#{opts["livereload_port"]}/livereload.js"
|
||||
)
|
||||
assert_match(%r!LiveReload.on!, content)
|
||||
end
|
||||
|
||||
should "serve nothing else over HTTP on the default LiveReload port" do
|
||||
skip_if_windows "EventMachine support on Windows is limited"
|
||||
opts = serve(@standard_options)
|
||||
res = @client.get("http://#{opts["host"]}:#{opts["livereload_port"]}/")
|
||||
assert_equal(400, res.status_code)
|
||||
assert_match(%r!only serves livereload.js!, res.content)
|
||||
end
|
||||
|
||||
should "insert the LiveReload script tags" do
|
||||
skip_if_windows "EventMachine support on Windows is limited"
|
||||
opts = serve(@standard_options)
|
||||
content = @client.get_content(
|
||||
"http://#{opts["host"]}:#{opts["port"]}/#{opts["baseurl"]}/hello.html"
|
||||
)
|
||||
assert_match(
|
||||
%r!livereload.js\?snipver=1&port=#{opts["livereload_port"]}!,
|
||||
content
|
||||
)
|
||||
assert_match(%r!I am a simple web page!, content)
|
||||
end
|
||||
|
||||
should "apply the max and min delay options" do
|
||||
skip_if_windows "EventMachine support on Windows is limited"
|
||||
opts = serve(@standard_options.merge(
|
||||
"livereload_max_delay" => "1066",
|
||||
"livereload_min_delay" => "3"
|
||||
))
|
||||
content = @client.get_content(
|
||||
"http://#{opts["host"]}:#{opts["port"]}/#{opts["baseurl"]}/hello.html"
|
||||
)
|
||||
assert_match(%r!&mindelay=3!, content)
|
||||
assert_match(%r!&maxdelay=1066!, content)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a program" do
|
||||
setup do
|
||||
@merc = nil
|
||||
|
|
Loading…
Reference in New Issue