# Phusion Passenger - http://www.modrails.com/ # Copyright (C) 2008 Phusion # # Phusion Passenger is a trademark of Hongli Lai & Ninh Bui. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. require 'rubygems' require 'passenger/abstract_server' require 'passenger/railz/application_spawner' require 'passenger/exceptions' require 'passenger/constants' require 'passenger/utils' module Passenger module Railz # This class is capable of spawning Ruby on Rails application instances # quickly. This is done by preloading the Ruby on Rails framework into memory, # before spawning the application instances. # # A single FrameworkSpawner instance can only hold a single Ruby on Rails # framework version. So be careful when using FrameworkSpawner: the applications # that you spawn through it must require the same RoR version. To handle multiple # RoR versions, use multiple FrameworkSpawner instances. # # FrameworkSpawner uses ApplicationSpawner internally. # # *Note*: FrameworkSpawner may only be started asynchronously with AbstractServer#start. # Starting it synchronously with AbstractServer#start_synchronously has not been tested. class FrameworkSpawner < AbstractServer include Utils # This exception means that the FrameworkSpawner server process exited unexpectedly. class Error < AbstractServer::ServerError end # An attribute, used internally. This should not be used outside Passenger. attr_accessor :time # Creates a new instance of FrameworkSpawner. # # Valid options: # - :version: The Ruby on Rails version to use. It is not checked whether # this version is actually installed. # - :vendor: The directory to the vendor Rails framework to use. This is # usually something like "/webapps/foo/vendor/rails". # # It is not allowed to specify both +version+ and +vendor+. # # Note that the specified Rails framework will be loaded during the entire life time # of the FrameworkSpawner server. If you wish to reload the Rails framework's code, # then restart the server by calling AbstractServer#stop and AbstractServer#start. def initialize(options = {}) if !options.respond_to?(:'[]') raise ArgumentError, "The 'options' argument not seem to be an options hash" end @version = options[:version] @vendor = options[:vendor] if !@version && !@vendor raise ArgumentError, "Either the 'version' or the 'vendor' option must specified" elsif @version && @vendor raise ArgumentError, "It is not allowed to specify both the 'version' and the 'vendor' options" end super() define_message_handler(:spawn_application, :handle_spawn_application) define_message_handler(:reload, :handle_reload) end # Overrided from AbstractServer#start. # # May raise these additional exceptions: # - FrameworkInitError: The specified Ruby on Rails framework could not be loaded. # - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly. def start super begin status = server.read[0] if status == 'exception' child_exception = unmarshal_exception(server.read_scalar) stop if @version message = "Could not load Ruby on Rails framework version #{@version}: " << "#{child_exception.class} (#{child_exception.message})" else message = "Could not load Ruby on Rails framework at '#{@vendor}': " << "#{child_exception.class} (#{child_exception.message})" end options = { :vendor => @vendor, :version => @version } raise FrameworkInitError.new(message, child_exception, options) end rescue IOError, SystemCallError, SocketError stop raise Error, "The framework spawner server exited unexpectedly" end end # Spawn a RoR application using the Ruby on Rails framework # version associated with this FrameworkSpawner. # When successful, an Application object will be returned, which represents # the spawned RoR application. # # See ApplicationSpawner.new for an explanation of the +lower_privilege+, # +lowest_user+ and +environment+ parameters. # # FrameworkSpawner will internally cache the code of applications, in order to # speed up future spawning attempts. This implies that, if you've changed # the application's code, you must do one of these things: # - Restart this FrameworkSpawner by calling AbstractServer#stop, then AbstractServer#start. # - Reload the application by calling reload with the correct app_root argument. # # Raises: # - AbstractServer::ServerNotStarted: The FrameworkSpawner server hasn't already been started. # - ArgumentError: +app_root+ doesn't appear to be a valid Ruby on Rails application root. # - AppInitError: The application raised an exception or called exit() during startup. # - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly. # - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly. def spawn_application(app_root, lower_privilege = true, lowest_user = "nobody", environment = "production") app_root = normalize_path(app_root) assert_valid_app_root(app_root) exception_to_propagate = nil begin server.write("spawn_application", app_root, lower_privilege, lowest_user, environment) result = server.read if result.nil? raise IOError, "Connection closed" end if result[0] == 'exception' raise unmarshal_exception(server.read_scalar) else pid, listen_socket_name, using_abstract_namespace = server.read if pid.nil? raise IOError, "Connection closed" end owner_pipe = server.recv_io return Application.new(app_root, pid, listen_socket_name, using_abstract_namespace == "true", owner_pipe) end rescue SystemCallError, IOError, SocketError => e raise Error, "The framework spawner server exited unexpectedly" end end # Remove the cached application instances at the given application root. # If nil is specified as application root, then all cached application # instances will be removed, no matter the application root. # # Long description: # Application code might be cached in memory by a FrameworkSpawner. But # once it a while, it will be necessary to reload the code for an # application, such as after deploying a new version of the application. # This method makes sure that any cached application code is removed, so # that the next time an application instance is spawned, the application # code will be freshly loaded into memory. # # Raises: # - ArgumentError: +app_root+ doesn't appear to be a valid Ruby on Rails # application root. # - FrameworkSpawner::Error: The FrameworkSpawner server exited unexpectedly. def reload(app_root = nil) if app_root.nil? server.write("reload") else server.write("reload", normalize_path(app_root)) end rescue SystemCallError, IOError, SocketError raise Error, "The framework spawner server exited unexpectedly" end protected # Overrided method. def before_fork # :nodoc: if GC.copy_on_write_friendly? # Garbage collect now so that the child process doesn't have to # do that (to prevent making pages dirty). GC.start end end # Overrided method. def initialize_server # :nodoc: $0 = "Passenger FrameworkSpawner: #{@version || @vendor}" @spawners = {} @spawners_lock = Mutex.new @spawners_cond = ConditionVariable.new @spawners_cleaner = Thread.new do begin spawners_cleaner_main_loop rescue Exception => e print_exception(self.class.to_s, e) end end begin preload_rails rescue StandardError, ScriptError, NoMemoryError => e client.write('exception') client.write_scalar(marshal_exception(e)) return end client.write('success') end # Overrided method. def finalize_server # :nodoc: @spawners_lock.synchronize do @spawners_cond.signal end @spawners_cleaner.join @spawners.each_value do |spawner| spawner.stop end end private def preload_rails Object.const_set(:RAILS_ROOT, ".") if @version gem 'rails', "=#{@version}" require 'initializer' else $LOAD_PATH.unshift("#{@vendor}/railties/builtin/rails_info") Dir["#{@vendor}/*"].each do |entry| next unless File.directory?(entry) $LOAD_PATH.unshift("#{entry}/lib") end require "#{@vendor}/railties/lib/initializer" end require 'active_support' require 'active_record' require 'action_controller' require 'action_view' require 'action_pack' require 'action_mailer' require 'dispatcher' begin if ::Rails::VERSION::MAJOR >= 2 require 'active_resource' else require 'action_web_service' end require 'ruby_version_check' require 'active_support/whiny_nil' rescue NameError # Rails < 1.1 require 'action_web_service' end Object.send(:remove_const, :RAILS_ROOT) end def handle_spawn_application(app_root, lower_privilege, lowest_user, environment) lower_privilege = lower_privilege == "true" @spawners_lock.synchronize do spawner = @spawners[app_root] if spawner.nil? begin spawner = ApplicationSpawner.new(app_root, lower_privilege, lowest_user, environment) spawner.start rescue ArgumentError, AppInitError, ApplicationSpawner::Error => e client.write('exception') client.write_scalar(marshal_exception(e)) if e.child_exception.is_a?(LoadError) # A source file failed to load, maybe because of a # missing gem. If that's the case then the sysadmin # will install probably the gem. So we clear RubyGems's # cache so that it can detect new gems. Gem.clear_paths end return end @spawners[app_root] = spawner end spawner.time = Time.now begin app = spawner.spawn_application rescue ApplicationSpawner::Error => e spawner.stop @spawners.delete(app_root) client.write('exception') client.write_scalar(marshal_exception(e)) return end client.write('success') client.write(app.pid, app.listen_socket_name, app.using_abstract_namespace?) client.send_io(app.owner_pipe) app.close end end def handle_reload(app_root = nil) @spawners_lock.synchronize do if app_root.nil? @spawners.each_value do |spawner| spawner.stop end @spawners.clear else spawner = @spawners[app_root] if spawner spawner.stop @spawners.delete(app_root) end end end end # The main loop for the spawners cleaner thread. # This thread checks the spawners list every APP_SPAWNER_CLEAN_INTERVAL seconds, # and stops application spawners that have been idle for more than # APP_SPAWNER_MAX_IDLE_TIME seconds. def spawners_cleaner_main_loop @spawners_lock.synchronize do while true if @spawners_cond.timed_wait(@spawners_lock, APP_SPAWNER_CLEAN_INTERVAL) break else current_time = Time.now @spawners.keys.each do |key| spawner = @spawners[key] if current_time - spawner.time > APP_SPAWNER_MAX_IDLE_TIME spawner.stop @spawners.delete(key) end end end end end end end end # module Railz end # module Passenger