Skip to content

Commit

Permalink
Add LiveReload functionality to Jekyll. (#5142)
Browse files Browse the repository at this point in the history
Merge pull request 5142
  • Loading branch information
awood authored and DirtyF committed Dec 7, 2017
1 parent e3142e4 commit 50ff219
Show file tree
Hide file tree
Showing 10 changed files with 1,875 additions and 20 deletions.
1 change: 1 addition & 0 deletions Gemfile
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions jekyll.gemspec
Expand Up @@ -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")
Expand Down
179 changes: 159 additions & 20 deletions lib/jekyll/commands/serve.rb
@@ -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(
Expand All @@ -26,7 +49,11 @@ class << self
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|
Expand All @@ -41,20 +68,34 @@ def init_with_program(prog)
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"]
Expand All @@ -63,6 +104,76 @@ def process(opts)
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.
Expand Down Expand Up @@ -92,6 +203,7 @@ def webrick_opts(opts)
: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,
Expand All @@ -108,11 +220,16 @@ def webrick_opts(opts)

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
Expand Down Expand Up @@ -227,7 +344,29 @@ def enable_ssl(opts)
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
Expand Down

0 comments on commit 50ff219

Please sign in to comment.