# encoding: binary
# Phusion Passenger - http://www.modrails.com/
# Copyright (c) 2010 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'
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 ClassicRails::FrameworkSpawner to preload and cache
# Ruby on Rails frameworks. ClassicRails::FrameworkSpawner, in turn, uses
# ClassicRails::ApplicationSpawner to preload and cache application code.
#
# In case you're wondering why the namespace is "ClassicRails" and not "Rails":
# it's to work around an obscure bug in ActiveSupport's Dispatcher.
class SpawnManager < AbstractServer
include Utils
def initialize(options = {})
super("", "")
@options = options
@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/app_process'
require 'phusion_passenger/classic_rails/framework_spawner'
require 'phusion_passenger/classic_rails/application_spawner'
require 'phusion_passenger/rack/application_spawner'
require 'phusion_passenger/html_template'
require 'phusion_passenger/platform_info'
require 'phusion_passenger/exceptions'
end
end
# Spawns an application with the given spawn options. When successful, an
# AppProcess object will be returned, which represents the spawned application
# process.
#
# Most options are explained in PoolOptions.h.
#
# Mandatory options:
# - 'app_root'
#
# Optional options:
# - 'app_type'
# - 'environment'
# - 'spawn_method'
# - 'user',
# - 'group'
# - 'default_user'
# - 'default_group'
# - 'framework_spawner_timeout'
# - 'app_spawner_timeout'
# - 'environment_variables': Environment variables which should be passed
# to the spawned application process. This is NULL-seperated string of
# key-value pairs, encoded in base64. The last byte in the unencoded
# data must be a NULL.
# - 'base_uri'
# - 'print_exceptions'
#
# <b>Exceptions:</b>
# - 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)
case options["app_type"]
when "rails"
if !defined?(ClassicRails::FrameworkSpawner)
require 'phusion_passenger/classic_rails/framework_spawner'
require 'phusion_passenger/classic_rails/application_spawner'
end
return spawn_rails_application(options)
when "rack"
if !defined?(Rack::ApplicationSpawner)
require 'phusion_passenger/rack/application_spawner'
end
return spawn_rack_application(options)
when "wsgi"
if !defined?(WSGI::ApplicationSpawner)
require 'phusion_passenger/wsgi/application_spawner'
end
return WSGI::ApplicationSpawner.spawn_application(options)
else
raise ArgumentError, "Unknown 'app_type' value '#{options["app_type"]}'."
end
end
# Remove the cached application instances at the given group name.
# If nil is specified as group name, then all cached application
# instances will be removed, no matter the group name.
#
# <b>Long description:</b>
# 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_group_name = nil)
@spawners.synchronize do
if app_group_name
# Stop and delete associated ApplicationSpawner.
@spawners.delete("app:#{app_group_name}")
# Propagate reload command to associated FrameworkSpawner.
@spawners.each do |spawner|
if spawner.respond_to?(:reload)
spawner.reload(app_group_name)
end
end
else
# Stop and delete all spawners.
@spawners.clear
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)
app_root = options["app_root"]
app_group_name = options["app_group_name"]
spawn_method = options["spawn_method"]
spawner = nil
create_spawner = nil
key = nil
case spawn_method
when nil, "", "smart", "smart-lv2"
if spawn_method != "smart-lv2"
framework_version = AppProcess.detect_framework_version(app_root)
end
if framework_version.nil? || framework_version == :vendor
key = "app:#{app_group_name}"
create_spawner = proc do
ClassicRails::ApplicationSpawner.new(@options.merge(options))
end
spawner_timeout = options["app_spawner_timeout"]
else
key = "version:#{framework_version}"
create_spawner = proc do
options["framework_version"] = framework_version
ClassicRails::FrameworkSpawner.new(@options.merge(options))
end
spawner_timeout = options["framework_spawner_timeout"]
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
spawner.start
spawner
end
begin
return spawner.spawn_application(options)
rescue AbstractServer::ServerError
@spawners.delete(key)
raise
end
end
else
return ClassicRails::ApplicationSpawner.spawn_application(
@options.merge(options))
end
end
def spawn_rack_application(options)
app_group_name = options["app_group_name"]
spawn_method = options["spawn_method"]
spawner = nil
create_spawner = nil
key = nil
case spawn_method
when nil, "", "smart", "smart-lv2"
@spawners.synchronize do
key = "app:#{app_group_name}"
spawner = @spawners.lookup_or_add(key) do
spawner_timeout = options["app_spawner_timeout"]
spawner = Rack::ApplicationSpawner.new(
@options.merge(options))
if spawner_timeout != -1
spawner.max_idle_time = spawner_timeout
end
spawner.start
spawner
end
begin
return spawner.spawn_application(options)
rescue AbstractServer::ServerError
@spawners.delete(key)
raise
end
end
else
return Rack::ApplicationSpawner.spawn_application(
@options.merge(options))
end
end
def handle_spawn_application(client, *options)
options = sanitize_spawn_options(Hash[*options])
app_process = nil
app_root = options["app_root"]
app_type = options["app_type"]
begin
app_process = spawn_application(options)
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_process
begin
client.write('ok')
app_process.write_to_channel(client)
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_process.close
end
end
end
def handle_reload(client, app_group_name)
reload(app_group_name)
end
def send_error_page(channel, template_name, options = {})
require 'phusion_passenger/html_template' unless defined?(HTMLTemplate)
if !defined?(PlatformInfo)
require 'phusion_passenger/platform_info'
require 'phusion_passenger/platform_info/ruby'
end
options["enterprisey"] = File.exist?("#{SOURCE_ROOT}/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