Your IP : 3.133.133.251
# encoding: binary
# Phusion Passenger - https://www.phusionpassenger.com/
# Copyright (c) 2010-2018 Phusion Holding B.V.
#
# "Passenger", "Phusion Passenger" and "Union Station" are registered
# trademarks of Phusion Holding 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