# 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 'phusion_passenger/abstract_server'
require 'phusion_passenger/abstract_server_collection'
require 'phusion_passenger/constants'
require 'phusion_passenger/utils'
# Define a constant with a name that's unlikely to clash with anything the
# application defines, so that they can detect whether they're running under
# Phusion Passenger.
IN_PHUSION_PASSENGER = true
module PhusionPassenger
# The spawn manager is capable of spawning Ruby on Rails or Rack application
# instances. It acts like a simple fascade for the rest of the spawn manager
# system.
#
# *Note*: SpawnManager may only be started synchronously with
# AbstractServer#start_synchronously. Starting asynchronously has not been
# tested. Don't forget to call cleanup after the server's main loop has
# finished.
#
# == Ruby on Rails optimizations
#
# Spawning a Ruby on Rails application is usually slow. But SpawnManager
# will preload and cache Ruby on Rails frameworks, as well as application
# code, so subsequent spawns will be very fast.
#
# Internally, SpawnManager uses Railz::FrameworkSpawner to preload and cache
# Ruby on Rails frameworks. Railz::FrameworkSpawner, in turn, uses
# Railz::ApplicationSpawner to preload and cache application code.
#
# In case you're wondering why the namespace is "Railz" and not "Rails":
# it's to work around an obscure bug in ActiveSupport's Dispatcher.
class SpawnManager < AbstractServer
include Utils
def initialize
super()
@spawners = AbstractServerCollection.new
define_message_handler(:spawn_application, :handle_spawn_application)
define_message_handler(:reload, :handle_reload)
define_signal_handler('SIGHUP', :reload)
# Start garbage collector in order to free up some existing
# heap slots. This prevents the heap from growing unnecessarily
# during the startup phase.
GC.start
if GC.copy_on_write_friendly?
# Preload libraries for copy-on-write semantics.
require 'base64'
require 'phusion_passenger/application'
require 'phusion_passenger/railz/framework_spawner'
require 'phusion_passenger/railz/application_spawner'
require 'phusion_passenger/rack/application_spawner'
require 'phusion_passenger/html_template'
require 'phusion_passenger/platform_info'
require 'phusion_passenger/exceptions'
# Commonly used libraries.
['mysql', 'sqlite3'].each do |lib|
begin
require lib
rescue LoadError
# Do nothing; ignore if not present.
end
end
end
end
# Spawn an application with the given spawn options. When successful, an
# Application object will be returned, which represents the spawned application.
# At least one option must be given: +app_root+. This is the application's root
# folder.
#
# Other options are:
#
# ['lower_privilege', 'lowest_user', 'environment', 'environment_variables', 'base_uri' and 'print_exceptions']
# See Railz::ApplicationSpawner.new for an explanation of these options.
#
# ['app_type']
# What kind of application is being spawned. Either "rails" (default), "rack" or "wsgi".
#
# ['spawn_method']
# May be one of "smart", "smart-lv2" or "conservative". When "smart" is specified,
# SpawnManager will internally cache the code of Rails 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 SpawnManager by calling AbstractServer#stop, then AbstractServer#start.
# - Reload the application by calling reload with the correct app_root argument.
#
# "smart" caches the Rails framework code in a framework spawner server, and application
# code in an application spawner server. Sometimes it is desirable to skip the
# framework spawning and going directly for the application spawner instead. The
# "smart-lv2" method allows you to do that.
#
# Caching however can be incompatible with some applications. The "conservative"
# spawning method does not involve any caching at all. Spawning will be slower,
# but is guaranteed to be compatible with all applications.
#
# The default spawn method is "smart-lv2".
#
# ['framework_spawner_timeout' and 'app_spawner_timeout']
# These options allow you to specify the maximum idle timeout, in seconds, of the
# framework spawner servers and application spawner servers that will be started under
# the hood. These options are only used if +app_type+ equals "rails".
#
# A timeout of 0 means that the spawner server should never idle timeout. A timeout of
# -1 means that the default timeout value should be used. The default value is -1.
#
# Exceptions:
# - InvalidPath: +app_root+ doesn't appear to be a valid Ruby on Rails application root.
# - VersionNotFound: The Ruby on Rails framework version that the given application requires
# is not installed.
# - AbstractServer::ServerError: One of the server processes exited unexpectedly.
# - FrameworkInitError: The Ruby on Rails framework that the application requires could not be loaded.
# - AppInitError: The application raised an exception or called exit() during startup.
def spawn_application(options)
if !options["app_root"]
raise ArgumentError, "The 'app_root' option must be given."
end
options = sanitize_spawn_options(options)
if options["app_type"] == "rails"
if !defined?(Railz::FrameworkSpawner)
require 'phusion_passenger/application'
require 'phusion_passenger/railz/framework_spawner'
require 'phusion_passenger/railz/application_spawner'
end
return spawn_rails_application(options)
elsif options["app_type"] == "rack"
if !defined?(Rack::ApplicationSpawner)
require 'phusion_passenger/rack/application_spawner'
end
return Rack::ApplicationSpawner.spawn_application(
options["app_root"], options
)
elsif options["app_type"] == "wsgi"
require 'phusion_passenger/wsgi/application_spawner'
return WSGI::ApplicationSpawner.spawn_application(
options["app_root"],
options["lower_privilege"],
options["lowest_user"],
options["environment"]
)
else
raise ArgumentError, "Unknown 'app_type' value '#{options["app_type"]}'."
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. 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 AbstractServer::SpawnError if something went wrong.
def reload(app_root = nil)
@spawners.synchronize do
if app_root
# Delete associated ApplicationSpawner.
@spawners.delete("app:#{app_root}")
else
# Delete all ApplicationSpawners.
keys_to_delete = []
@spawners.each_pair do |key, spawner|
if spawner.is_a?(Railz::ApplicationSpawner)
keys_to_delete << key
end
end
keys_to_delete.each do |key|
@spawners.delete(key)
end
end
@spawners.each do |spawner|
# Reload all FrameworkSpawners.
if spawner.respond_to?(:reload)
spawner.reload(app_root)
end
end
end
end
# Cleanup resources. Should be called when this SpawnManager is no longer needed.
def cleanup
@spawners.cleanup
end
private
def spawn_rails_application(options)
spawn_method = options["spawn_method"]
app_root = options["app_root"]
if [nil, "", "smart", "smart-lv2"].include?(spawn_method)
spawner_must_be_started = true
if spawn_method != "smart-lv2"
framework_version = Application.detect_framework_version(app_root)
end
if framework_version.nil? || framework_version == :vendor
key = "app:#{app_root}"
create_spawner = proc do
Railz::ApplicationSpawner.new(app_root, options)
end
spawner_timeout = options["app_spawner_timeout"]
else
key = "version:#{framework_version}"
create_spawner = proc do
framework_options = { :version => framework_version }
if options.has_key?(:print_framework_loading_exceptions)
framework_options[:print_framework_loading_exceptions] = options[:print_framework_loading_exceptions]
end
Railz::FrameworkSpawner.new(framework_options)
end
spawner_timeout = options["framework_spawner_timeout"]
end
else
key = "app:#{app_root}"
create_spawner = proc do
Railz::ApplicationSpawner.new(app_root, options)
end
spawner_timeout = options["app_spawner_timeout"]
spawner_must_be_started = false
end
@spawners.synchronize do
spawner = @spawners.lookup_or_add(key) do
spawner = create_spawner.call
if spawner_timeout != -1
spawner.max_idle_time = spawner_timeout
end
if spawner_must_be_started
spawner.start
end
spawner
end
begin
if spawner.is_a?(Railz::FrameworkSpawner)
return spawner.spawn_application(app_root, options)
elsif spawner.started?
return spawner.spawn_application
else
return spawner.spawn_application!
end
rescue AbstractServer::ServerError
@spawners.delete(key)
raise
end
end
end
def handle_spawn_application(*options)
options = sanitize_spawn_options(Hash[*options])
app = nil
app_root = options["app_root"]
app_type = options["app_type"]
begin
app = spawn_application(options)
rescue InvalidPath => e
send_error_page(client, 'invalid_app_root', :error => e, :app_root => app_root)
rescue AbstractServer::ServerError => e
send_error_page(client, 'general_error', :error => e)
rescue VersionNotFound => e
send_error_page(client, 'version_not_found', :error => e, :app_root => app_root)
rescue AppInitError => e
if database_error?(e)
send_error_page(client, 'database_error', :error => e,
:app_root => app_root, :app_name => app_name(app_type),
:app_type => app_type)
elsif load_error?(e)
# 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
send_error_page(client, 'load_error', :error => e, :app_root => app_root,
:app_name => app_name(app_type))
elsif e.child_exception.is_a?(SystemExit)
send_error_page(client, 'app_exited_during_initialization', :error => e,
:app_root => app_root, :app_name => app_name(app_type))
else
send_error_page(client, 'app_init_error', :error => e,
:app_root => app_root, :app_name => app_name(app_type))
end
rescue FrameworkInitError => e
send_error_page(client, 'framework_init_error', :error => e)
end
if app
begin
client.write('ok')
client.write(app.pid, app.listen_socket_name,
app.listen_socket_type)
client.send_io(app.owner_pipe)
rescue Errno::EPIPE
# The Apache module may be interrupted during a spawn command,
# in which case it will close the connection. We ignore this error.
ensure
app.close
end
end
end
def handle_reload(app_root)
reload(app_root)
end
def send_error_page(channel, template_name, options = {})
require 'phusion_passenger/html_template' unless defined?(HTMLTemplate)
require 'phusion_passenger/platform_info' unless defined?(PlatformInfo)
options["enterprisey"] = File.exist?("#{File.dirname(__FILE__)}/../../enterprisey.txt") ||
File.exist?("/etc/passenger_enterprisey.txt")
data = HTMLTemplate.new(template_name, options).result
channel.write('error_page')
channel.write_scalar(data)
end
def database_error?(e)
return ( defined?(Mysql::Error) && e.child_exception.is_a?(Mysql::Error) ) ||
( e.child_exception.is_a?(UnknownError) &&
(
e.child_exception.real_class_name =~ /^ActiveRecord/ ||
e.child_exception.real_class_name =~ /^Mysql::/
)
)
end
def load_error?(e)
return e.child_exception.is_a?(LoadError) || (
e.child_exception.is_a?(UnknownError) &&
e.child_exception.real_class_name == "MissingSourceFile"
)
end
def app_name(app_type)
if app_type == "rails"
return "Ruby on Rails"
else
return "Ruby (Rack)"
end
end
end
end # module PhusionPassenger