# Phusion Passenger - http://www.modrails.com/ # Copyright (c) 2008, 2009 Phusion # # "Phusion Passenger" is a trademark of Hongli Lai & Ninh Bui. # # 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' require 'phusion_passenger/message_channel' require 'phusion_passenger/utils' require 'phusion_passenger/constants' module PhusionPassenger # The request handler is the layer which connects Apache with the underlying application's # request dispatcher (i.e. either Rails's Dispatcher class or Rack). # The request handler's job is to process incoming HTTP requests using the # currently loaded Ruby on Rails application. HTTP requests are forwarded # to the request handler by the web server. HTTP responses generated by the # RoR application are forwarded to the web server, which, in turn, sends the # response back to the HTTP client. # # AbstractRequestHandler is an abstract base class for easing the implementation # of request handlers for Rails and Rack. # # == Design decisions # # Some design decisions are made because we want to decrease system # administrator maintenance overhead. These decisions are documented # in this section. # # === Owner pipes # # Because only the web server communicates directly with a request handler, # we want the request handler to exit if the web server has also exited. # This is implemented by using a so-called _owner pipe_. The writable part # of the pipe will be passed to the web server* via a Unix socket, and the web # server will own that part of the pipe, while AbstractRequestHandler owns # the readable part of the pipe. AbstractRequestHandler will continuously # check whether the other side of the pipe has been closed. If so, then it # knows that the web server has exited, and so the request handler will exit # as well. This works even if the web server gets killed by SIGKILL. # # * It might also be passed to the ApplicationPoolServerExecutable, if the web # server's using ApplicationPoolServer instead of StandardApplicationPool. # # # == Request format # # Incoming "HTTP requests" are not true HTTP requests, i.e. their binary # representation do not conform to RFC 2616. Instead, the request format # is based on CGI, and is similar to that of SCGI. # # The format consists of 3 parts: # - A 32-bit big-endian integer, containing the size of the transformed # headers. # - The transformed HTTP headers. # - The verbatim (untransformed) HTTP request body. # # HTTP headers are transformed to a format that satisfies the following # grammar: # # headers ::= header* # header ::= name NUL value NUL # name ::= notnull+ # value ::= notnull+ # notnull ::= "\x01" | "\x02" | "\x02" | ... | "\xFF" # NUL = "\x00" # # The web server transforms the HTTP request to the aforementioned format, # and sends it to the request handler. class AbstractRequestHandler # Signal which will cause the Rails application to exit immediately. HARD_TERMINATION_SIGNAL = "SIGTERM" # Signal which will cause the Rails application to exit as soon as it's done processing a request. SOFT_TERMINATION_SIGNAL = "SIGUSR1" BACKLOG_SIZE = 100 MAX_HEADER_SIZE = 128 * 1024 # String constants which exist to relieve Ruby's garbage collector. IGNORE = 'IGNORE' # :nodoc: DEFAULT = 'DEFAULT' # :nodoc: NULL = "\0" # :nodoc: X_POWERED_BY = 'X-Powered-By' # :nodoc: REQUEST_METHOD = 'REQUEST_METHOD' # :nodoc: PING = 'ping' # :nodoc: # The name of the socket on which the request handler accepts # new connections. At this moment, this value is always the filename # of a Unix domain socket. # # See also #socket_type. attr_reader :socket_name # The type of socket that #socket_name refers to. At the moment, the # value is always 'unix', which indicates a Unix domain socket. attr_reader :socket_type # Specifies the maximum allowed memory usage, in MB. If after having processed # a request AbstractRequestHandler detects that memory usage has risen above # this limit, then it will gracefully exit (that is, exit after having processed # all pending requests). # # A value of 0 (the default) indicates that there's no limit. attr_accessor :memory_limit # The number of times the main loop has iterated so far. Mostly useful # for unit test assertions. attr_reader :iterations # Number of requests processed so far. This includes requests that raised # exceptions. attr_reader :processed_requests # 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: # - memory_limit: Used to set the +memory_limit+ attribute. def initialize(owner_pipe, options = {}) if should_use_unix_sockets? create_unix_socket_on_filesystem else create_tcp_socket end @socket.close_on_exec! @owner_pipe = owner_pipe @previous_signal_handlers = {} @main_loop_generation = 0 @main_loop_thread_lock = Mutex.new @main_loop_thread_cond = ConditionVariable.new @memory_limit = options["memory_limit"] || 0 @iterations = 0 @processed_requests = 0 @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 @socket.close rescue nil @owner_pipe.close rescue nil File.unlink(@socket_name) rescue nil end # Check whether the main loop's currently running. def main_loop_running? return @main_loop_running end # Enter the request handler's main loop. def 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 end install_useful_signal_handlers while true @iterations += 1 client = accept_connection if client.nil? break end begin headers, input = parse_request(client) if headers if headers[REQUEST_METHOD] == PING process_ping(headers, input, client) else process_request(headers, input, client) end end rescue IOError, SocketError, SystemCallError => e print_exception("Passenger RequestHandler", e) ensure # 'input' is the same as 'client' so we don't # need to close that. # The 'close_write' here prevents forked child # processes from unintentionally keeping the # connection open. client.close_write rescue nil client.close rescue nil end @processed_requests += 1 end rescue EOFError # Exit main loop. rescue Interrupt # Exit main loop. rescue SignalException => signal if signal.message != HARD_TERMINATION_SIGNAL && signal.message != SOFT_TERMINATION_SIGNAL raise end ensure revert_signal_handlers @main_loop_thread_lock.synchronize do @graceful_termination_pipe[0].close rescue nil @graceful_termination_pipe[1].close rescue nil @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 = Thread.new 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 include Utils def should_use_unix_sockets? # There seems to be a bug in MacOS X w.r.t. Unix sockets. # When the Unix socket subsystem is under high stress, a # recv()/read() on a Unix socket can return 0 even when EOF is # not reached. We work around this by using TCP sockets on # MacOS X. return RUBY_PLATFORM !~ /darwin/ end def create_unix_socket_on_filesystem done = false while !done begin if defined?(NativeSupport) unix_path_max = NativeSupport::UNIX_PATH_MAX else unix_path_max = 100 end @socket_name = "#{passenger_tmpdir}/backends/backend.#{generate_random_id(:base64)}" @socket_name = @socket_name.slice(0, unix_path_max - 1) @socket = UNIXServer.new(@socket_name) @socket.listen(BACKLOG_SIZE) @socket_type = "unix" File.chmod(0600, @socket_name) # The SpawnManager class will set tighter permissions on the # socket later on. See sendSpawnCommand in SpawnManager.h. done = true rescue Errno::EADDRINUSE # Do nothing, try again with another name. end end end def create_tcp_socket # We use "127.0.0.1" as address in order to force # TCPv4 instead of TCPv6. @socket = TCPServer.new('127.0.0.1', 0) @socket.listen(BACKLOG_SIZE) @socket_name = "127.0.0.1:#{@socket.addr[1]}" @socket_type = "tcp" 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) end def install_useful_signal_handlers trappable_signals = Signal.list_trappable trap(SOFT_TERMINATION_SIGNAL) do @graceful_termination_pipe[1].close rescue nil end if trappable_signals.has_key?(SOFT_TERMINATION_SIGNAL.sub(/^SIG/, '')) trap('ABRT') do raise SignalException, "SIGABRT" end if trappable_signals.has_key?('ABRT') trap('QUIT') do if Kernel.respond_to?(:caller_for_all_threads) output = "========== Process #{Process.pid}: backtrace dump ==========\n" caller_for_all_threads.each_pair do |thread, stack| output << ("-" * 60) << "\n" output << "# Thread: #{thread.inspect}, " if thread == Thread.main output << "[main thread], " else output << "[current thread], " end output << "alive = #{thread.alive?}\n" output << ("-" * 60) << "\n" output << " " << stack.join("\n ") output << "\n\n" end else output = "========== Process #{Process.pid}: backtrace dump ==========\n" output << ("-" * 60) << "\n" output << "# Current thread: #{Thread.current.inspect}\n" output << ("-" * 60) << "\n" output << " " << caller.join("\n ") end STDERR.puts(output) STDERR.flush 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 accept_connection ios = select([@socket, @owner_pipe, @graceful_termination_pipe[0]]).first if ios.include?(@socket) client = @socket.accept client.close_on_exec! # Some people report that sometimes their Ruby (MRI/REE) # processes get stuck with 100% CPU usage. Upon further # inspection with strace, it turns out that these Ruby # processes are continuously calling lseek() on a socket, # which of course returns ESPIPE as error. gdb reveals # lseek() is called by fwrite(), which in turn is called # by rb_fwrite(). The affected socket is the # AbstractRequestHandler client socket. # # I inspected the MRI source code and didn't find # anything that would explain this behavior. This makes # me think that it's a glibc bug, but that's very # unlikely. # # The rb_fwrite() implementation takes an entirely # different code path if I set 'sync' to true: it will # skip fwrite() and use write() instead. So here we set # 'sync' to true in the hope that this will work around # the problem. client.sync = true # We monkeypatch the 'sync=' method to a no-op so that # sync mode can't be disabled. def client.sync=(value) end # The real input stream is not seekable (calling _seek_ # or _rewind_ on it will raise an exception). But some # frameworks (e.g. Merb) call _rewind_ if the object # responds to it. So we simply undefine _seek_ and # _rewind_. client.instance_eval do undef seek if respond_to?(:seek) undef rewind if respond_to?(:rewind) end # Set encoding for Ruby 1.9 compatibility. client.set_encoding(Encoding::BINARY) if client.respond_to?(:set_encoding) client.binmode return client else # The other end of the owner pipe has been closed, or the # graceful termination pipe has been closed. This is our # call to gracefully terminate (after having processed all # incoming requests). return nil end end # Read the next request from the given socket, and return # a pair [headers, input_stream]. _headers_ is a Hash containing # the request headers, while _input_stream_ is an IO object for # reading HTTP POST data. # # Returns nil if end-of-stream was encountered. def parse_request(socket) channel = MessageChannel.new(socket) headers_data = channel.read_scalar(MAX_HEADER_SIZE) if headers_data.nil? return end headers = Hash[*headers_data.split(NULL)] return [headers, socket] rescue SecurityError => e STDERR.puts("*** Passenger RequestHandler: HTTP header size exceeded maximum.") STDERR.flush print_exception("Passenger RequestHandler", e) end def process_ping(env, input, output) output.write("pong") end # Generate a long, cryptographically secure random ID string, which # is also a valid filename. def generate_random_id(method) case method when :base64 require 'base64' unless defined?(Base64) data = Base64.encode64(File.read("/dev/urandom", 64)) data.gsub!("\n", '') data.gsub!("+", '') data.gsub!("/", '') data.gsub!(/==$/, '') when :hex data = File.read("/dev/urandom", 64).unpack('H*')[0] end return data end def self.determine_passenger_header header = "Phusion Passenger (mod_rails/mod_rack) #{VERSION_STRING}" if File.exist?("#{File.dirname(__FILE__)}/../../enterprisey.txt") || File.exist?("/etc/passenger_enterprisey.txt") header << ", Enterprise Edition" end return header end public PASSENGER_HEADER = determine_passenger_header end end # module PhusionPassenger