1
0
Fork 0
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:
Alexey Vasiliev 2021-11-23 00:41:02 +02:00 committed by GitHub
parent b7d525de46
commit e1b0de9e11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 165 additions and 140 deletions

View file

@ -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

View 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