mirror of
https://github.com/middleman/middleman.git
synced 2022-11-09 12:20:27 -05:00
Preview server refactoring (#2497)
* Split preview server by preview and web server classes * Support web server dynamic loading * improve loggin for reload * clean dead threads * use webrick * remove .tool-versions * finish cleanup * finish cleanup * back original logger Co-authored-by: Thomas Reynolds <me@tdreyno.com>
This commit is contained in:
parent
b7d525de46
commit
e1b0de9e11
2 changed files with 165 additions and 140 deletions
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'webrick/https'
|
||||
require 'openssl'
|
||||
require 'middleman-core/meta_pages'
|
||||
require 'middleman-core/logger'
|
||||
|
@ -9,13 +7,14 @@ require 'middleman-core/rack'
|
|||
require 'middleman-core/preview_server/server_information'
|
||||
require 'middleman-core/preview_server/server_url'
|
||||
require 'middleman-core/preview_server/server_information_callback_proxy'
|
||||
require 'middleman-core/preview_server/webrick'
|
||||
|
||||
module Middleman
|
||||
class PreviewServer
|
||||
class << self
|
||||
extend Forwardable
|
||||
|
||||
attr_reader :app, :ssl_certificate, :ssl_private_key, :environment, :server_information
|
||||
attr_reader :app, :web_server, :ssl_certificate, :ssl_private_key, :environment, :server_information
|
||||
|
||||
# Start an instance of Middleman::Application
|
||||
# @return [void]
|
||||
|
@ -32,7 +31,7 @@ module Middleman
|
|||
# New app evaluates the middleman configuration. Since this can be
|
||||
# invalid as well, we need to evaluate the configuration BEFORE
|
||||
# checking for validity
|
||||
the_app = initialize_new_app
|
||||
app = initialize_new_app
|
||||
|
||||
# And now comes the check
|
||||
unless server_information.valid?
|
||||
|
@ -40,8 +39,6 @@ module Middleman
|
|||
exit 1
|
||||
end
|
||||
|
||||
mount_instance(the_app)
|
||||
|
||||
app.logger.debug %(== Server information is provided by #{server_information.handler})
|
||||
app.logger.debug %(== The Middleman is running in "#{environment}" environment)
|
||||
app.logger.debug format('== The Middleman preview server is bound to %<url>s', url: ServerUrl.new(hosts: server_information.listeners, port: server_information.port, https: server_information.https?).to_bind_addresses.join(', '))
|
||||
|
@ -53,8 +50,6 @@ module Middleman
|
|||
|
||||
@initialized = true
|
||||
|
||||
register_signal_handlers
|
||||
|
||||
# Save the last-used @options so it may be re-used when
|
||||
# reloading later on.
|
||||
::Middleman::Profiling.report('server_start')
|
||||
|
@ -76,21 +71,32 @@ module Middleman
|
|||
end
|
||||
end
|
||||
|
||||
loop do
|
||||
@webrick.start
|
||||
signals_thread = stop_on_exit_thread
|
||||
|
||||
# $mm_shutdown is set by the signal handler
|
||||
if $mm_shutdown
|
||||
shutdown
|
||||
exit
|
||||
elsif $mm_reload
|
||||
$mm_reload = false
|
||||
reload
|
||||
init_webserver(app)
|
||||
web_server.start
|
||||
|
||||
signals_thread.join # wait after reload for real exit by signals
|
||||
end
|
||||
|
||||
def stop_on_exit_thread
|
||||
signals_queue = Queue.new
|
||||
|
||||
%w[INT HUP TERM QUIT].each do |sig|
|
||||
next unless Signal.list[sig]
|
||||
|
||||
Signal.trap(sig) do
|
||||
signals_queue << sig # send to queue signal
|
||||
end
|
||||
end
|
||||
|
||||
Thread.new do
|
||||
signals_queue.pop # waiting for kill signal
|
||||
stop # stop web server and app
|
||||
end
|
||||
end
|
||||
|
||||
# Detach the current Middleman::Application instance
|
||||
# Stop web server
|
||||
# @return [void]
|
||||
def stop
|
||||
begin
|
||||
|
@ -99,7 +105,7 @@ module Middleman
|
|||
# if the user closed their terminal STDOUT/STDERR won't exist
|
||||
end
|
||||
|
||||
unmount_instance
|
||||
stop_webserver
|
||||
end
|
||||
|
||||
# Simply stop, then start the server
|
||||
|
@ -110,28 +116,26 @@ module Middleman
|
|||
app.execute_callbacks(:reload)
|
||||
|
||||
begin
|
||||
app = initialize_new_app
|
||||
new_app = initialize_new_app
|
||||
rescue StandardError => e
|
||||
warn "Error reloading Middleman: #{e}\n#{e.backtrace.join("\n")}"
|
||||
app.logger.info '== The Middleman is still running the application from before the error'
|
||||
return
|
||||
end
|
||||
|
||||
unmount_instance
|
||||
stop_webserver
|
||||
|
||||
@webrick.shutdown
|
||||
@webrick = nil
|
||||
|
||||
mount_instance(app)
|
||||
init_webserver(new_app)
|
||||
|
||||
app.logger.info '== The Middleman has reloaded'
|
||||
|
||||
web_server.start
|
||||
end
|
||||
|
||||
# Stop the current instance, exit Webrick
|
||||
# Stop the current instance
|
||||
# @return [void]
|
||||
def shutdown
|
||||
stop
|
||||
@webrick.shutdown
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -202,9 +206,18 @@ module Middleman
|
|||
@ssl_certificate = possible_from_cli(:ssl_certificate, app.config)
|
||||
@ssl_private_key = possible_from_cli(:ssl_private_key, app.config)
|
||||
|
||||
reload_queue = Queue.new
|
||||
|
||||
app.files.on_change :reload do
|
||||
$mm_reload = true
|
||||
@webrick.stop
|
||||
reload_queue << true
|
||||
end
|
||||
|
||||
app.before_shutdown do
|
||||
reload_queue << false # just exit
|
||||
end
|
||||
|
||||
Thread.new do
|
||||
reload if reload_queue.pop # wait for reload signal or just die
|
||||
end
|
||||
|
||||
# Add in the meta pages application
|
||||
|
@ -220,127 +233,28 @@ module Middleman
|
|||
@cli_options[key] || config[key]
|
||||
end
|
||||
|
||||
# Trap some interrupt signals and shut down smoothly
|
||||
# @return [void]
|
||||
def register_signal_handlers
|
||||
%w[INT HUP TERM QUIT].each do |sig|
|
||||
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 = {
|
||||
Port: server_information.port,
|
||||
AccessLog: [],
|
||||
ServerName: server_information.server_name,
|
||||
BindAddress: server_information.bind_address.to_s,
|
||||
DoNotReverseLookup: true
|
||||
}
|
||||
|
||||
if server_information.https?
|
||||
http_opts[:SSLEnable] = true
|
||||
|
||||
if ssl_certificate || ssl_private_key
|
||||
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
|
||||
http_opts[:SSLCertName] = [
|
||||
%w[CN localhost],
|
||||
['CN', server_information.server_name]
|
||||
].uniq
|
||||
cert, key = create_self_signed_cert(4096, [['CN', server_information.server_name]], server_information.site_addresses, 'Middleman Preview Server')
|
||||
http_opts[:SSLCertificate] = cert
|
||||
http_opts[:SSLPrivateKey] = key
|
||||
end
|
||||
end
|
||||
|
||||
http_opts[:Logger] = if is_logging
|
||||
FilteredWebrickLog.new
|
||||
else
|
||||
::WEBrick::Log.new(nil, 0)
|
||||
end
|
||||
|
||||
begin
|
||||
::WEBrick::HTTPServer.new(http_opts)
|
||||
rescue Errno::EADDRINUSE
|
||||
port = http_opts[:Port]
|
||||
warn %(== Port #{port} is already in use. This could mean another instance of middleman is already running. Please make sure port #{port} is free and start `middleman server` again, or choose another port by running `middleman server —-port=#{port + 1}` instead.)
|
||||
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, aliases, 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
|
||||
cert.not_after = Time.now + (365 * 24 * 60 * 60)
|
||||
cert.public_key = rsa.public_key
|
||||
|
||||
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
|
||||
ef.issuer_certificate = cert
|
||||
cert.extensions = [
|
||||
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.add_extension ef.create_extension('subjectAltName', aliases.map { |d| "DNS: #{d}" }.join(','))
|
||||
|
||||
cert.sign(rsa, OpenSSL::Digest.new('SHA256'))
|
||||
|
||||
[cert, rsa]
|
||||
end
|
||||
|
||||
# Attach a new Middleman::Application instance
|
||||
# Init web server
|
||||
# @param [Middleman::Application] app
|
||||
# @return [void]
|
||||
def mount_instance(app)
|
||||
def init_webserver(app)
|
||||
@app = app
|
||||
|
||||
@webrick ||= setup_webrick(@options[:debug] || false)
|
||||
|
||||
rack_app = ::Middleman::Rack.new(@app).to_app
|
||||
@webrick.mount '/', ::Rack::Handler::WEBrick, rack_app
|
||||
@web_server = Webrick.new(
|
||||
::Middleman::Rack.new(app).to_app,
|
||||
server_information,
|
||||
@options[:debug] || false
|
||||
)
|
||||
end
|
||||
|
||||
# Detach the current Middleman::Application instance
|
||||
# Stop web server and app
|
||||
# @return [void]
|
||||
def unmount_instance
|
||||
@webrick.unmount '/'
|
||||
|
||||
@app.shutdown!
|
||||
def stop_webserver
|
||||
web_server.shutdown!
|
||||
app.shutdown!
|
||||
|
||||
@web_server = nil
|
||||
@app = nil
|
||||
end
|
||||
end
|
||||
|
||||
class FilteredWebrickLog < ::WEBrick::Log
|
||||
def log(level, data)
|
||||
super(level, data) unless /Could not determine content-length of response body./.match?(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
111
middleman-core/lib/middleman-core/preview_server/webrick.rb
Normal file
111
middleman-core/lib/middleman-core/preview_server/webrick.rb
Normal file
|
@ -0,0 +1,111 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'webrick/https'
|
||||
|
||||
module Middleman
|
||||
class PreviewServer
|
||||
class Webrick
|
||||
attr_reader :webrick
|
||||
|
||||
def initialize(rack_app, server_information, is_debug)
|
||||
@webrick = setup_webrick(server_information, is_debug)
|
||||
webrick.mount '/', ::Rack::Handler::WEBrick, rack_app
|
||||
end
|
||||
|
||||
def start
|
||||
webrick.start
|
||||
end
|
||||
|
||||
def shutdown!
|
||||
webrick.shutdown
|
||||
@webrick = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_webrick(server_information, is_debug)
|
||||
http_opts = {
|
||||
Port: server_information.port,
|
||||
AccessLog: [],
|
||||
ServerName: server_information.server_name,
|
||||
BindAddress: server_information.bind_address.to_s,
|
||||
DoNotReverseLookup: true
|
||||
}
|
||||
|
||||
if server_information.https?
|
||||
http_opts[:SSLEnable] = true
|
||||
|
||||
if ssl_certificate || ssl_private_key
|
||||
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
|
||||
http_opts[:SSLCertName] = [
|
||||
%w[CN localhost],
|
||||
['CN', server_information.server_name]
|
||||
].uniq
|
||||
cert, key = create_self_signed_cert(4096, [['CN', server_information.server_name]], server_information.site_addresses, 'Middleman Preview Server')
|
||||
http_opts[:SSLCertificate] = cert
|
||||
http_opts[:SSLPrivateKey] = key
|
||||
end
|
||||
end
|
||||
|
||||
http_opts[:Logger] = if is_debug
|
||||
FilteredWebrickLog.new
|
||||
else
|
||||
::WEBrick::Log.new(nil, 0)
|
||||
end
|
||||
|
||||
begin
|
||||
::WEBrick::HTTPServer.new(http_opts)
|
||||
rescue Errno::EADDRINUSE
|
||||
port = http_opts[:Port]
|
||||
warn %(== Port #{port} is already in use. This could mean another instance of middleman is already running. Please make sure port #{port} is free and start `middleman server` again, or choose another port by running `middleman server —-port=#{port + 1}` instead.)
|
||||
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, aliases, 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
|
||||
cert.not_after = Time.now + (365 * 24 * 60 * 60)
|
||||
cert.public_key = rsa.public_key
|
||||
|
||||
ef = OpenSSL::X509::ExtensionFactory.new(nil, cert)
|
||||
ef.issuer_certificate = cert
|
||||
cert.extensions = [
|
||||
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.add_extension ef.create_extension('subjectAltName', aliases.map { |d| "DNS: #{d}" }.join(','))
|
||||
|
||||
cert.sign(rsa, OpenSSL::Digest.new('SHA256'))
|
||||
|
||||
[cert, rsa]
|
||||
end
|
||||
|
||||
class FilteredWebrickLog < ::WEBrick::Log
|
||||
def log(level, data)
|
||||
super(level, data) unless /Could not determine content-length of response body./.match?(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue