middleman/middleman-core/lib/middleman-core/preview_server.rb

341 lines
10 KiB
Ruby
Raw Normal View History

require 'webrick'
require 'webrick/https'
require 'openssl'
require 'socket'
2012-10-13 22:12:47 +02:00
require 'middleman-core/meta_pages'
require 'middleman-core/logger'
2014-04-29 19:44:24 +02:00
# rubocop:disable GlobalVars
module Middleman
module PreviewServer
class << self
2015-05-08 10:12:57 +02:00
attr_reader :app, :host, :port, :ssl_certificate, :ssl_private_key, :environment
2014-04-29 19:44:24 +02:00
delegate :logger, to: :app
def https?
@https
end
# Start an instance of Middleman::Application
# @return [void]
2012-09-13 19:13:57 +02:00
def start(opts={})
@options = opts
mount_instance(new_app)
2015-05-08 10:12:57 +02:00
logger.debug %(== The Middleman is running in "#{environment}" environment)
logger.info "== The Middleman is standing watch at #{uri} (#{uri(public_ip)})"
logger.info "== Inspect your site configuration at #{uri + '__middleman'}"
@initialized ||= false
2014-07-02 19:11:52 +02:00
return if @initialized
@initialized = true
2014-07-02 19:11:52 +02:00
register_signal_handlers
2014-07-02 19:11:52 +02:00
# Save the last-used @options so it may be re-used when
# reloading later on.
::Middleman::Profiling.report('server_start')
2014-07-02 19:11:52 +02:00
loop do
@webrick.start
2014-07-02 19:11:52 +02:00
# $mm_shutdown is set by the signal handler
if $mm_shutdown
shutdown
exit
elsif $mm_reload
$mm_reload = false
reload
end
end
end
# Detach the current Middleman::Application instance
# @return [void]
def stop
begin
logger.info '== The Middleman is shutting down'
rescue
# if the user closed their terminal STDOUT/STDERR won't exist
end
if @listener
@listener.stop
@listener = nil
end
unmount_instance
end
# Simply stop, then start the server
# @return [void]
def reload
logger.info '== The Middleman is reloading'
begin
app = new_app
2014-04-29 01:02:18 +02:00
rescue => e
logger.error "Error reloading Middleman: #{e}\n#{e.backtrace.join("\n")}"
logger.info '== The Middleman is still running the application from before the error'
return
end
2012-09-13 19:13:57 +02:00
unmount_instance
2015-05-11 18:24:22 +02:00
@webrick.shutdown
@webrick = nil
mount_instance(app)
logger.info '== The Middleman has reloaded'
end
# Stop the current instance, exit Webrick
# @return [void]
def shutdown
stop
@webrick.shutdown
end
2014-04-29 01:02:18 +02:00
private
2014-04-29 19:44:24 +02:00
2012-09-13 19:13:57 +02:00
def new_app
opts = @options.dup
server = ::Middleman::Application.server
# Add in the meta pages application
meta_app = Middleman::MetaPages::Application.new(server)
server.map '/__middleman' do
run meta_app
end
@app = server.inst do
::Middleman::Logger.singleton(
opts[:debug] ? 0 : 1,
opts[:instrumenting] || false
)
2014-07-02 19:11:52 +02:00
config[:environment] = opts[:environment].to_sym if opts[:environment]
config[:port] = opts[:port] if opts[:port]
2015-05-07 09:21:11 +02:00
config[:host] = opts[:host].presence || Socket.gethostname.tr(' ', '+')
config[:https] = opts[:https] unless opts[:https].nil?
config[:ssl_certificate] = opts[:ssl_certificate] if opts[:ssl_certificate]
config[:ssl_private_key] = opts[:ssl_private_key] if opts[:ssl_private_key]
2012-09-13 19:13:57 +02:00
end
2015-05-08 10:12:57 +02:00
@host = @app.config[:host]
@port = @app.config[:port]
@https = @app.config[:https]
@environment = @app.config[:environment]
@ssl_certificate = @app.config[:ssl_certificate]
@ssl_private_key = @app.config[:ssl_private_key]
@app
2012-09-13 19:13:57 +02:00
end
def start_file_watcher
return if @listener || @options[:disable_watcher]
2014-07-02 19:26:18 +02:00
# Watcher Library
require 'listen'
options = { force_polling: @options[:force_polling] }
2014-07-02 19:26:18 +02:00
options[:latency] = @options[:latency] if @options[:latency]
@listener = Listen.to(::Middleman::Util.current_directory, options) do |modified, added, removed|
added_and_modified = (modified + added)
# See if the changed file is config.rb or lib/*.rb
2013-03-22 18:28:38 +01:00
if needs_to_reload?(added_and_modified + removed)
$mm_reload = true
@webrick.stop
else
wd = Pathname(::Middleman::Util.current_directory)
added_and_modified.each do |path|
relative_path = Pathname(path).relative_path_from(wd).to_s
2014-07-02 19:26:18 +02:00
next if app.files.ignored?(relative_path)
app.files.did_change(relative_path)
end
removed.each do |path|
relative_path = Pathname(path).relative_path_from(wd).to_s
2014-07-02 19:26:18 +02:00
next if app.files.ignored?(relative_path)
app.files.did_delete(relative_path)
end
end
end
# Don't block this thread
2014-07-02 19:26:18 +02:00
@listener.start
end
# Trap some interupt signals and shut down smoothly
# @return [void]
def register_signal_handlers
%w(INT HUP TERM QUIT).each do |sig|
2014-07-02 19:11:52 +02:00
next unless Signal.list[sig]
Signal.trap(sig) do
# Do as little work as possible in the signal context
$mm_shutdown = true
@webrick.stop
end
end
end
# Initialize webrick
# @return [void]
def setup_webrick(is_logging)
http_opts = {
2014-04-29 19:44:24 +02:00
Port: port,
AccessLog: [],
2015-05-07 09:21:11 +02:00
ServerName: host,
2014-04-29 19:44:24 +02:00
DoNotReverseLookup: true
}
if https?
http_opts[:SSLEnable] = true
if ssl_certificate || ssl_private_key
2015-05-16 22:21:12 +02:00
raise 'You must provide both :ssl_certificate and :ssl_private_key' unless ssl_private_key && ssl_certificate
http_opts[:SSLCertificate] = OpenSSL::X509::Certificate.new File.read ssl_certificate
http_opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new File.read ssl_private_key
else
# use a generated self-signed cert
2015-05-16 22:21:12 +02:00
cert, key = create_self_signed_cert(1024, [['CN', host]], 'Middleman Preview Server')
http_opts[:SSLCertificate] = cert
http_opts[:SSLPrivateKey] = key
end
end
if is_logging
http_opts[:Logger] = FilteredWebrickLog.new
else
http_opts[:Logger] = ::WEBrick::Log.new(nil, 0)
end
attempts_left = 4
tried_ports = []
2012-09-13 19:13:57 +02:00
begin
::WEBrick::HTTPServer.new(http_opts)
rescue Errno::EADDRINUSE
logger.error "== Port #{port} is unavailable. Either close the instance of Middleman already running on #{port} or start this Middleman on a new port with: --port=#{unused_tcp_port}"
2012-09-13 19:13:57 +02:00
exit(1)
end
end
# Copy of https://github.com/nahi/ruby/blob/webrick_trunk/lib/webrick/ssl.rb#L39
# that uses a different serial number each time the cert is generated in order to
# avoid errors in Firefox. Also doesn't print out stuff to $stderr unnecessarily.
def create_self_signed_cert(bits, cn, comment)
rsa = OpenSSL::PKey::RSA.new(bits)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = Time.now.to_i % (1 << 20)
name = OpenSSL::X509::Name.new(cn)
cert.subject = name
cert.issuer = name
cert.not_before = Time.now
2015-05-16 22:21:12 +02:00
cert.not_after = Time.now + (365 * 24 * 60 * 60)
cert.public_key = rsa.public_key
2015-05-16 22:21:12 +02:00
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
ef.issuer_certificate = cert
cert.extensions = [
2015-05-16 22:21:12 +02:00
ef.create_extension('basicConstraints', 'CA:FALSE'),
ef.create_extension('keyUsage', 'keyEncipherment'),
ef.create_extension('subjectKeyIdentifier', 'hash'),
ef.create_extension('extendedKeyUsage', 'serverAuth'),
ef.create_extension('nsComment', comment)
]
aki = ef.create_extension('authorityKeyIdentifier',
'keyid:always,issuer:always')
cert.add_extension(aki)
cert.sign(rsa, OpenSSL::Digest::SHA1.new)
2015-05-16 22:21:12 +02:00
[cert, rsa]
end
# Attach a new Middleman::Application instance
# @param [Middleman::Application] app
# @return [void]
def mount_instance(app)
@app = app
@webrick ||= setup_webrick(@options[:debug] || false)
start_file_watcher
2013-03-22 18:28:38 +01:00
2012-10-13 22:12:47 +02:00
rack_app = app.class.to_rack_app
@webrick.mount '/', ::Rack::Handler::WEBrick, rack_app
end
# Detach the current Middleman::Application instance
# @return [void]
def unmount_instance
@webrick.unmount '/'
@app = nil
end
# Whether the passed files are config.rb, lib/*.rb or helpers
# @param [Array<String>] paths Array of paths to check
# @return [Boolean] Whether the server needs to reload
def needs_to_reload?(paths)
relative_paths = paths.map do |p|
Pathname(p).relative_path_from(Pathname(app.root)).to_s
end
2012-09-13 19:13:57 +02:00
match_against = [
%r{^config\.rb$},
%r{^lib/[^\.](.*)\.rb$},
%r{^helpers/[^\.](.*)\.rb$}
2012-09-13 19:13:57 +02:00
]
2012-09-13 19:13:57 +02:00
if @options[:reload_paths]
@options[:reload_paths].split(',').each do |part|
match_against << %r{^#{part}}
end
end
relative_paths.any? do |path|
2012-09-13 19:13:57 +02:00
match_against.any? do |matcher|
path =~ matcher
2012-09-13 19:13:57 +02:00
end
end
end
# Returns the URI the preview server will run on
# @return [URI]
2015-05-06 00:56:08 +02:00
def uri(host=@host)
scheme = https? ? 'https' : 'http'
URI("#{scheme}://#{host}:#{@port}/")
end
# An IPv4 address on this machine which should be externally addressable.
# @return [String]
def public_ip
ip = Socket.ip_address_list.find { |ai| ai.ipv4? && !ai.ipv4_loopback? }
ip ? ip.ip_address : '127.0.0.1'
end
# Returns unused TCP port
# @return [Fixnum]
def unused_tcp_port
server = TCPServer.open(0)
port = server.addr[1]
server.close
port
end
end
class FilteredWebrickLog < ::WEBrick::Log
def log(level, data)
2014-07-02 19:11:52 +02:00
super(level, data) unless data =~ %r{Could not determine content-length of response body.}
end
end
end
end