613 lines
21 KiB
Ruby
613 lines
21 KiB
Ruby
# encoding: binary
|
|
# Phusion Passenger - https://www.phusionpassenger.com/
|
|
# Copyright (c) 2010-2025 Asynchronous B.V.
|
|
#
|
|
# "Passenger", "Phusion Passenger" and "Union Station" are registered
|
|
# trademarks of Asynchronous B.V.
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
require 'socket'
|
|
require 'fcntl'
|
|
PhusionPassenger.require_passenger_lib 'constants'
|
|
PhusionPassenger.require_passenger_lib 'public_api'
|
|
PhusionPassenger.require_passenger_lib 'message_client'
|
|
PhusionPassenger.require_passenger_lib 'debug_logging'
|
|
PhusionPassenger.require_passenger_lib 'native_support'
|
|
PhusionPassenger.require_passenger_lib 'utils'
|
|
PhusionPassenger.require_passenger_lib 'ruby_core_enhancements'
|
|
PhusionPassenger.require_passenger_lib 'ruby_core_io_enhancements'
|
|
PhusionPassenger.require_passenger_lib 'request_handler/thread_handler'
|
|
|
|
module PhusionPassenger
|
|
|
|
class RequestHandler
|
|
include DebugLogging
|
|
include Utils
|
|
|
|
# Signal which will cause the application to exit immediately.
|
|
HARD_TERMINATION_SIGNAL = "SIGTERM"
|
|
BACKLOG_SIZE = 500
|
|
|
|
# String constants which exist to relieve Ruby's garbage collector.
|
|
IGNORE = 'IGNORE' # :nodoc:
|
|
DEFAULT = 'DEFAULT' # :nodoc:
|
|
|
|
# A hash containing all server sockets that this request handler listens on.
|
|
# The hash is in the form of:
|
|
#
|
|
# {
|
|
# name1 => [socket_address1, socket_type1, socket1],
|
|
# name2 => [socket_address2, socket_type2, socket2],
|
|
# ...
|
|
# }
|
|
#
|
|
# +name+ is a Symbol. +socket_addressx+ is the address of the socket,
|
|
# +socket_typex+ is the socket's type (either 'unix' or 'tcp') and
|
|
# +socketx+ is the actual socket IO objec.
|
|
# There's guaranteed to be at least one server socket, namely one with the
|
|
# name +:main+.
|
|
attr_reader :server_sockets
|
|
|
|
attr_reader :concurrency
|
|
|
|
# A password with which clients must authenticate. Default is unauthenticated.
|
|
attr_accessor :connect_password
|
|
|
|
# Create a new RequestHandler with the given owner pipe.
|
|
# +owner_pipe+ must be the readable part of a pipe IO object.
|
|
#
|
|
# Additionally, the following options may be given:
|
|
# - connect_password
|
|
def initialize(owner_pipe, options = {})
|
|
require_option(options, "app_group_name")
|
|
install_options_as_ivars(self, options,
|
|
"app",
|
|
"app_group_name",
|
|
"connect_password"
|
|
)
|
|
|
|
@keepalive = options.fetch("keepalive", true).to_s == "true"
|
|
@force_http_session = ENV["_PASSENGER_FORCE_HTTP_SESSION"] == "true"
|
|
if @force_http_session
|
|
@connect_password = nil
|
|
end
|
|
@thread_handler = options["thread_handler"] || ThreadHandler
|
|
@concurrency = 1
|
|
|
|
#############
|
|
#############
|
|
|
|
@server_sockets = {}
|
|
|
|
if should_use_unix_sockets?
|
|
@main_socket_address, @main_socket = create_unix_socket_on_filesystem(options)
|
|
else
|
|
@main_socket_address, @main_socket = create_tcp_socket(options)
|
|
end
|
|
@server_sockets[:main] = {
|
|
:address => @main_socket_address,
|
|
:socket => @main_socket,
|
|
:protocol => @force_http_session ? :http : :session,
|
|
:concurrency => @concurrency,
|
|
:accept_http_requests => true
|
|
}
|
|
|
|
@http_socket_address, @http_socket = create_tcp_socket(options)
|
|
@server_sockets[:http] = {
|
|
:address => @http_socket_address,
|
|
:socket => @http_socket,
|
|
:protocol => :http,
|
|
:concurrency => 1
|
|
}
|
|
|
|
@owner_pipe = owner_pipe
|
|
@options = options
|
|
@previous_signal_handlers = {}
|
|
@main_loop_generation = 0
|
|
@main_loop_thread_lock = Mutex.new
|
|
@main_loop_thread_cond = ConditionVariable.new
|
|
@threads = []
|
|
@threads_mutex = Mutex.new
|
|
@main_loop_running = false
|
|
|
|
#############
|
|
end
|
|
|
|
# Clean up temporary stuff created by the request handler.
|
|
#
|
|
# If the main loop was started by #main_loop, then this method may only
|
|
# be called after the main loop has exited.
|
|
#
|
|
# If the main loop was started by #start_main_loop_thread, then this method
|
|
# may be called at any time, and it will stop the main loop thread.
|
|
def cleanup
|
|
if @main_loop_thread
|
|
@main_loop_thread_lock.synchronize do
|
|
@graceful_termination_pipe[1].close rescue nil
|
|
end
|
|
@main_loop_thread.join
|
|
end
|
|
@server_sockets.each_value do |info|
|
|
socket = info[:socket]
|
|
type = get_socket_address_type(info[:address])
|
|
|
|
begin
|
|
socket.close if !socket.closed?
|
|
rescue Exception => e
|
|
# Ignore "stream closed" error, which occurs in some unit tests.
|
|
# We catch Exception here instead of IOError because of a Ruby 1.8.7 bug.
|
|
if e.to_s !~ /stream closed/ && e.message.to_s !~ /stream closed/
|
|
raise e
|
|
end
|
|
end
|
|
if type == :unix
|
|
filename = info[:address].sub(/^unix:/, '')
|
|
File.unlink(filename) rescue nil
|
|
end
|
|
end
|
|
@owner_pipe.close rescue nil
|
|
end
|
|
|
|
# Check whether the main loop's currently running.
|
|
def main_loop_running?
|
|
@main_loop_thread_lock.synchronize do
|
|
return @main_loop_running
|
|
end
|
|
end
|
|
|
|
# Enter the request handler's main loop.
|
|
def main_loop
|
|
debug("Entering request handler main loop")
|
|
reset_signal_handlers
|
|
begin
|
|
@graceful_termination_pipe = IO.pipe
|
|
@graceful_termination_pipe[0].close_on_exec!
|
|
@graceful_termination_pipe[1].close_on_exec!
|
|
|
|
@main_loop_thread_lock.synchronize do
|
|
@main_loop_generation += 1
|
|
@main_loop_running = true
|
|
@main_loop_thread_cond.broadcast
|
|
|
|
@select_timeout = nil
|
|
|
|
@selectable_sockets = []
|
|
@server_sockets.each_value do |value|
|
|
socket = value[2]
|
|
@selectable_sockets << socket if socket
|
|
end
|
|
@selectable_sockets << @owner_pipe
|
|
@selectable_sockets << @graceful_termination_pipe[0]
|
|
end
|
|
|
|
install_useful_signal_handlers
|
|
start_threads
|
|
wait_until_termination_requested
|
|
wait_until_all_threads_are_idle
|
|
terminate_threads
|
|
debug("Request handler main loop exited normally")
|
|
|
|
rescue EOFError
|
|
# Exit main loop.
|
|
trace(2, "Request handler main loop interrupted by EOFError exception")
|
|
rescue Interrupt
|
|
# Exit main loop.
|
|
trace(2, "Request handler main loop interrupted by Interrupt exception")
|
|
rescue SignalException => signal
|
|
trace(2, "Request handler main loop interrupted by SignalException")
|
|
if signal.message != HARD_TERMINATION_SIGNAL
|
|
raise
|
|
end
|
|
rescue Exception => e
|
|
trace(2, "Request handler main loop interrupted by #{e.class} exception")
|
|
raise
|
|
ensure
|
|
debug("Exiting request handler main loop")
|
|
revert_signal_handlers
|
|
@main_loop_thread_lock.synchronize do
|
|
@graceful_termination_pipe[1].close rescue nil
|
|
@graceful_termination_pipe[0].close rescue nil
|
|
@selectable_sockets = []
|
|
@main_loop_generation += 1
|
|
@main_loop_running = false
|
|
@main_loop_thread_cond.broadcast
|
|
end
|
|
end
|
|
end
|
|
|
|
# Start the main loop in a new thread. This thread will be stopped by #cleanup.
|
|
def start_main_loop_thread
|
|
current_generation = @main_loop_generation
|
|
@main_loop_thread = create_thread_and_abort_on_exception do
|
|
main_loop
|
|
end
|
|
@main_loop_thread_lock.synchronize do
|
|
while @main_loop_generation == current_generation
|
|
@main_loop_thread_cond.wait(@main_loop_thread_lock)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
def should_use_unix_sockets?
|
|
# Historical note:
|
|
# There seems to be a bug in MacOS X Leopard w.r.t. Unix server
|
|
# sockets file descriptors that are passed to another process.
|
|
# Usually Unix server sockets work fine, but when they're passed
|
|
# to another process, then clients that connect to the socket
|
|
# can incorrectly determine that the client socket is closed,
|
|
# even though that's not actually the case. More specifically:
|
|
# recv()/read() calls on these client sockets can return 0 even
|
|
# when we know EOF is not reached.
|
|
#
|
|
# The ApplicationPool infrastructure used to connect to a backend
|
|
# process's Unix socket in the Passenger core process, and then
|
|
# pass the connection file descriptor to the web server, which
|
|
# triggers this kernel bug. We used to work around this by using
|
|
# TCP sockets instead of Unix sockets; TCP sockets can still fail
|
|
# with this fake-EOF bug once in a while, but not nearly as often
|
|
# as with Unix sockets.
|
|
#
|
|
# This problem no longer applies today. The web server now passes
|
|
# all I/O through the Passenger core, and the bug is no longer
|
|
# triggered. Nevertheless, we keep this function intact so that
|
|
# if something like this ever happens again, we know why, and we
|
|
# can easily reactivate the workaround. Or maybe if we just need
|
|
# TCP sockets for some other reason.
|
|
|
|
#return RUBY_PLATFORM !~ /darwin/
|
|
|
|
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
|
|
# Unix domain socket implementation on JRuby
|
|
# is still bugged as of version 1.7.0. They can
|
|
# cause unexplicable freezes when used in combination
|
|
# with threading.
|
|
# It's also bugged in Windows Subsystem for Linux
|
|
# as of Windows 10, version 1803 (Fall Creators Update)
|
|
# You will get "Address In Use" errors even if the port is unused.
|
|
return !@force_http_session && ruby_engine != "jruby" && !PlatformInfo.windows_subsystem?
|
|
end
|
|
|
|
def create_unix_socket_on_filesystem(options)
|
|
if defined?(NativeSupport)
|
|
unix_path_max = NativeSupport::UNIX_PATH_MAX
|
|
else
|
|
unix_path_max = options.fetch('UNIX_PATH_MAX', 100).to_i
|
|
end
|
|
if options['socket_dir']
|
|
socket_dir = options['socket_dir']
|
|
socket_prefix = "ruby"
|
|
else
|
|
socket_dir = Dir.tmpdir
|
|
socket_prefix = "PsgRubyApp"
|
|
end
|
|
|
|
retry_at_most(128, Errno::EADDRINUSE) do
|
|
socket_address = "#{socket_dir}/#{socket_prefix}.#{generate_random_id(:base64)}"
|
|
socket_address = socket_address.slice(0, unix_path_max - 10)
|
|
socket = UNIXServer.new(socket_address)
|
|
socket.listen(BACKLOG_SIZE)
|
|
socket.binmode
|
|
socket.sync = true
|
|
socket.close_on_exec!
|
|
File.chmod(0600, socket_address)
|
|
["unix:#{socket_address}", socket]
|
|
end
|
|
end
|
|
|
|
def create_tcp_socket(options)
|
|
# We default to "127.0.0.1" as address in order to force
|
|
# TCPv4 instead of TCPv6.
|
|
bind_address = options.fetch('bind_address', '127.0.0.1')
|
|
socket = TCPServer.new(bind_address, 0)
|
|
socket.listen(BACKLOG_SIZE)
|
|
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
socket.binmode
|
|
socket.sync = true
|
|
socket.close_on_exec!
|
|
socket_address = "tcp://#{bind_address}:#{socket.addr[1]}"
|
|
return [socket_address, socket]
|
|
end
|
|
|
|
# Reset signal handlers to their default handler, and install some
|
|
# special handlers for a few signals. The previous signal handlers
|
|
# will be put back by calling revert_signal_handlers.
|
|
def reset_signal_handlers
|
|
Signal.list_trappable.each_key do |signal|
|
|
begin
|
|
prev_handler = trap(signal, DEFAULT)
|
|
if prev_handler != DEFAULT
|
|
@previous_signal_handlers[signal] = prev_handler
|
|
end
|
|
rescue ArgumentError
|
|
# Signal cannot be trapped; ignore it.
|
|
end
|
|
end
|
|
trap('HUP', IGNORE)
|
|
PhusionPassenger.call_event(:after_installing_signal_handlers)
|
|
end
|
|
|
|
def install_useful_signal_handlers
|
|
trappable_signals = Signal.list_trappable
|
|
|
|
trap('ABRT') do
|
|
print_status_report
|
|
abort
|
|
end if trappable_signals.has_key?('ABRT')
|
|
trap('QUIT') do
|
|
print_status_report
|
|
end if trappable_signals.has_key?('QUIT')
|
|
end
|
|
|
|
def revert_signal_handlers
|
|
@previous_signal_handlers.each_pair do |signal, handler|
|
|
trap(signal, handler)
|
|
end
|
|
end
|
|
|
|
def print_status_report
|
|
warn(Utils.global_backtrace_report)
|
|
warn("Threads: #{@threads.inspect}")
|
|
end
|
|
|
|
def start_threads
|
|
common_options = {
|
|
:app => @app,
|
|
:app_group_name => @app_group_name,
|
|
:connect_password => @connect_password,
|
|
:keepalive_enabled => @keepalive
|
|
}
|
|
main_socket_options = common_options.merge(
|
|
:server_socket => @main_socket,
|
|
:socket_name => "main socket",
|
|
:protocol => @server_sockets[:main][:protocol] == :session ?
|
|
:session :
|
|
:http
|
|
)
|
|
http_socket_options = common_options.merge(
|
|
:server_socket => @http_socket,
|
|
:socket_name => "HTTP socket",
|
|
:protocol => :http
|
|
)
|
|
|
|
# Used for marking threads that have finished initializing,
|
|
# or failed during initialization. Threads that are not yet done
|
|
# are not in `initialization_state`. Threads that have succeeded
|
|
# set their own state to true. Threads that have failed set their
|
|
# own state to false.
|
|
initialization_state_mutex = Mutex.new
|
|
initialization_state_cond = ConditionVariable.new
|
|
initialization_state = {}
|
|
set_initialization_state = lambda do |value|
|
|
initialization_state_mutex.synchronize do
|
|
initialization_state[Thread.current] = value
|
|
initialization_state_cond.signal
|
|
end
|
|
end
|
|
set_initialization_state_to_true = lambda do
|
|
set_initialization_state.call(true)
|
|
end
|
|
|
|
# Actually start all the threads.
|
|
thread_handler = @thread_handler
|
|
expected_nthreads = 0
|
|
|
|
@threads_mutex.synchronize do
|
|
@concurrency.times do |i|
|
|
thread = create_thread_and_abort_on_exception(i) do |number|
|
|
begin
|
|
Thread.current[:name] = "Worker #{number + 1}"
|
|
handler = thread_handler.new(self, main_socket_options)
|
|
handler.install
|
|
handler.main_loop(set_initialization_state_to_true)
|
|
ensure
|
|
set_initialization_state.call(false)
|
|
unregister_current_thread
|
|
end
|
|
end
|
|
@threads << thread
|
|
expected_nthreads += 1
|
|
end
|
|
|
|
thread = create_thread_and_abort_on_exception do
|
|
begin
|
|
Thread.current[:name] = "HTTP helper worker"
|
|
handler = thread_handler.new(self, http_socket_options)
|
|
handler.install
|
|
handler.main_loop(set_initialization_state_to_true)
|
|
ensure
|
|
set_initialization_state.call(false)
|
|
unregister_current_thread
|
|
end
|
|
end
|
|
@threads << thread
|
|
expected_nthreads += 1
|
|
end
|
|
|
|
# Wait until all threads have finished starting.
|
|
initialization_state_mutex.synchronize do
|
|
while initialization_state.size != expected_nthreads
|
|
initialization_state_cond.wait(initialization_state_mutex)
|
|
end
|
|
end
|
|
end
|
|
|
|
def unregister_current_thread
|
|
@threads_mutex.synchronize do
|
|
@threads.delete(Thread.current)
|
|
end
|
|
end
|
|
|
|
def wait_until_termination_requested
|
|
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
|
|
if ruby_engine == "jruby"
|
|
# On JRuby, selecting on an input TTY always returns, so
|
|
# we use threads to do the job.
|
|
owner_pipe_watcher = IO.pipe
|
|
owner_pipe_watcher_thread = create_thread_and_abort_on_exception do
|
|
Thread.current[:name] = "Owner pipe waiter"
|
|
begin
|
|
@owner_pipe.read(1)
|
|
ensure
|
|
owner_pipe_watcher[1].write('x')
|
|
end
|
|
end
|
|
begin
|
|
ios = select([owner_pipe_watcher[0], @graceful_termination_pipe[0]])[0]
|
|
if ios.include?(owner_pipe_watcher[0])
|
|
trace(2, "Owner pipe closed")
|
|
else
|
|
trace(2, "Graceful termination pipe closed")
|
|
end
|
|
ensure
|
|
owner_pipe_watcher_thread.kill
|
|
owner_pipe_watcher_thread.join
|
|
owner_pipe_watcher[0].close if !owner_pipe_watcher[0].closed?
|
|
owner_pipe_watcher[1].close if !owner_pipe_watcher[1].closed?
|
|
end
|
|
else
|
|
ios = select([@owner_pipe, @graceful_termination_pipe[0]])[0]
|
|
if ios.include?(@owner_pipe)
|
|
trace(2, "Owner pipe closed")
|
|
else
|
|
trace(2, "Graceful termination pipe closed")
|
|
end
|
|
end
|
|
end
|
|
|
|
def wakeup_all_threads
|
|
threads = []
|
|
if get_socket_address_type(@server_sockets[:main][:address]) == :unix &&
|
|
!File.exist?(@server_sockets[:main][:address].sub(/^unix:/, ''))
|
|
# It looks like someone deleted the Unix domain socket we listen on.
|
|
# This makes it impossible to wake up the worker threads gracefully,
|
|
# so we hard kill them.
|
|
warn("Unix domain socket gone; force aborting all threads")
|
|
@threads_mutex.synchronize do
|
|
@threads.each do |thread|
|
|
thread.raise(RuntimeError.new("Force abort"))
|
|
end
|
|
end
|
|
else
|
|
@concurrency.times do
|
|
threads << create_thread_and_abort_on_exception(@server_sockets[:main][:address]) do |address|
|
|
begin
|
|
debug("Shutting down worker thread by connecting to #{address}")
|
|
connect_to_server(address).close
|
|
rescue Errno::ECONNREFUSED
|
|
debug("Worker thread listening on #{address} already exited")
|
|
rescue SystemCallError, IOError => e
|
|
debug("Error shutting down worker thread (#{address}): #{e} (#{e.class})")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
threads << create_thread_and_abort_on_exception(@server_sockets[:http][:address]) do |address|
|
|
begin
|
|
debug("Shutting down HTTP thread by connecting to #{address}")
|
|
connect_to_server(address).close
|
|
rescue Errno::ECONNREFUSED
|
|
debug("Worker thread listening on #{address} already exited")
|
|
rescue SystemCallError, IOError => e
|
|
debug("Error shutting down HTTP thread (#{address}): #{e} (#{e.class})")
|
|
end
|
|
end
|
|
return threads
|
|
end
|
|
|
|
def terminate_threads
|
|
debug("Stopping all threads")
|
|
threads = @threads_mutex.synchronize do
|
|
@threads.dup
|
|
end
|
|
threads.each do |thr|
|
|
thr.raise(ThreadHandler::Interrupted.new)
|
|
end
|
|
threads.each do |thr|
|
|
thr.join
|
|
end
|
|
debug("All threads stopped")
|
|
end
|
|
|
|
def wait_until_all_threads_are_idle
|
|
debug("Waiting until all threads have become idle...")
|
|
|
|
# We wait until 100 ms have passed since all handlers have become
|
|
# interruptable and remained in the same iterations.
|
|
|
|
done = false
|
|
|
|
while !done
|
|
handlers = @threads_mutex.synchronize do
|
|
@threads.map do |thr|
|
|
thr[:passenger_thread_handler]
|
|
end
|
|
end
|
|
debug("There are currently #{handlers.size} threads")
|
|
if handlers.empty?
|
|
# There are no threads, so we're done.
|
|
done = true
|
|
break
|
|
end
|
|
|
|
# Record initial state.
|
|
handlers.each { |h| h.stats_mutex.lock }
|
|
iterations = handlers.map { |h| h.iteration }
|
|
handlers.each { |h| h.stats_mutex.unlock }
|
|
|
|
start_time = Time.now
|
|
sleep 0.01
|
|
|
|
while true
|
|
if handlers.size != @threads_mutex.synchronize { @threads.size }
|
|
debug("The number of threads changed. Restarting waiting algorithm")
|
|
break
|
|
end
|
|
|
|
# Record current state.
|
|
handlers.each { |h| h.stats_mutex.lock }
|
|
all_interruptable = handlers.all? { |h| h.interruptable }
|
|
new_iterations = handlers.map { |h| h.iteration }
|
|
|
|
# Are all threads interruptable and has there been no activity
|
|
# since last time we checked?
|
|
if all_interruptable && new_iterations == iterations
|
|
# Yes. If enough time has passed then we're done.
|
|
handlers.each { |h| h.stats_mutex.unlock }
|
|
if Time.now >= start_time + 0.1
|
|
done = true
|
|
break
|
|
end
|
|
else
|
|
# No. We reset the timer and check again later.
|
|
handlers.each { |h| h.stats_mutex.unlock }
|
|
iterations = new_iterations
|
|
start_time = Time.now
|
|
sleep 0.01
|
|
end
|
|
end
|
|
end
|
|
|
|
debug("All threads are now idle")
|
|
end
|
|
end
|
|
|
|
end # module PhusionPassenger
|