#  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 'rubygems'
require 'socket'
require 'etc'
require 'fcntl'
require 'phusion_passenger/abstract_server'
require 'phusion_passenger/app_process'
require 'phusion_passenger/constants'
require 'phusion_passenger/debug_logging'
require 'phusion_passenger/classic_rails/request_handler'
require 'phusion_passenger/rack/request_handler'
require 'phusion_passenger/public_api'
require 'phusion_passenger/exceptions'
require 'phusion_passenger/utils'

module PhusionPassenger
module ClassicRails

# Spawning of Rails 1 and Rails 2 applications.
#
# ClassicRails::ApplicationSpawner can operate in two modes:
# - Smart mode. In this mode, the Rails application's code is first preloaded into
#   a temporary process, which can then further fork off application processes.
#   Once the code has been preloaded, forking off application processes is very fast,
#   and all the forked off application processes can share code memory with each other.
#   To use this mode, create an ApplicationSpawner object, start it, and call
#   #spawn_application on it.
#   A single ApplicationSpawner object can only handle a single Rails application.
# - Conservative mode. In this mode, a Rails app process is directly spawned
#   without any preloading. This increases compatibility with applications. To use this
#   mode, call ApplicationSpawner.spawn_application.
class ApplicationSpawner < AbstractServer
	include Utils
	extend Utils
	include DebugLogging
	
	# This exception means that the ApplicationSpawner server process exited unexpectedly.
	class Error < AbstractServer::ServerError
	end
	
	# The application root of this spawner.
	attr_reader :app_root
	
	# Spawns an instance of a Rails application. When successful, an AppProcess object
	# will be returned, which represents the spawned Rails application.
	#
	# This method spawns the application directly, without preloading its code.
	# This method may only be called if no Rails framework has been loaded in the current
	# Ruby VM.
	#
	# The "app_root" option must be given. All other options are passed to the request
	# handler's constructor.
	#
	# Raises:
	# - AppInitError: The Ruby on Rails application raised an exception
	#   or called exit() during startup.
	# - SystemCallError, IOError, SocketError: Something went wrong.
	def self.spawn_application(options)
		options = sanitize_spawn_options(options)
		
		a, b = UNIXSocket.pair
		pid = safe_fork('application', true) do
			a.close
			
			file_descriptors_to_leave_open = [0, 1, 2, b.fileno]
			NativeSupport.close_all_file_descriptors(file_descriptors_to_leave_open)
			close_all_io_objects_for_fds(file_descriptors_to_leave_open)
			
			channel = MessageChannel.new(b)
			success = report_app_init_status(channel) do
				prepare_app_process('config/environment.rb', options)
				require File.expand_path('config/environment')
				require 'dispatcher'
				after_loading_app_code(options)
			end
			if success
				start_request_handler(channel, false, options)
			end
		end
		b.close
		Process.waitpid(pid) rescue nil
		
		channel = MessageChannel.new(a)
		unmarshal_and_raise_errors(channel, options["print_exceptions"])
		
		# No exception was raised, so spawning succeeded.
		return AppProcess.read_from_channel(channel)
	end
	
	# The following options are accepted:
	# - 'app_root'
	#
	# See SpawnManager#spawn_application for information about the options.
	def initialize(options)
		super()
		@options          = sanitize_spawn_options(options)
		@app_root         = @options["app_root"]
		@canonicalized_app_root = canonicalize_path(@app_root)
		self.max_idle_time = DEFAULT_APP_SPAWNER_MAX_IDLE_TIME
		define_message_handler(:spawn_application, :handle_spawn_application)
	end
	
	# Spawns an instance of the Rails application. When successful, an AppProcess object
	# will be returned, which represents the spawned Rails application.
	#
	# +options+ will be passed to the request handler's constructor.
	#
	# Raises:
	# - AbstractServer::ServerNotStarted: The ApplicationSpawner server hasn't already been started.
	# - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly.
	def spawn_application(options = {})
		connect do |channel|
			channel.write("spawn_application", *options.to_a.flatten)
			return AppProcess.read_from_channel(channel)
		end
	rescue SystemCallError, IOError, SocketError => e
		raise Error, "The application spawner server exited unexpectedly: #{e}"
	end
	
	# Overrided from AbstractServer#start.
	#
	# May raise these additional exceptions:
	# - AppInitError: The Ruby on Rails application raised an exception
	#   or called exit() during startup.
	# - ApplicationSpawner::Error: The ApplicationSpawner server exited unexpectedly.
	def start
		super
		begin
			channel = MessageChannel.new(@owner_socket)
			unmarshal_and_raise_errors(channel, @options["print_exceptions"])
		rescue IOError, SystemCallError, SocketError => e
			stop if started?
			raise Error, "The application spawner server exited unexpectedly: #{e}"
		rescue
			stop if started?
			raise
		end
	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:
		report_app_init_status(MessageChannel.new(@owner_socket)) do
			$0 = "Passenger ApplicationSpawner: #{@options['app_group_name']}"
			prepare_app_process('config/environment.rb', @options)
			if defined?(RAILS_ENV)
				Object.send(:remove_const, :RAILS_ENV)
				Object.const_set(:RAILS_ENV, ENV['RAILS_ENV'])
			end
			preload_application
			after_loading_app_code(@options)
		end
	end
	
private
	def preload_application
		Object.const_set(:RAILS_ROOT, @canonicalized_app_root)
		if defined?(::Rails::Initializer)
			::Rails::Initializer.run(:set_load_path)
			
			# The Rails framework is loaded at the moment.
			# environment.rb may set ENV['RAILS_ENV']. So we re-initialize
			# RAILS_ENV in Rails::Initializer.load_environment.
			::Rails::Initializer.class_eval do
				def load_environment_with_passenger
					using_default_log_path =
						configuration.log_path ==
						configuration.send(:default_log_path)
					
					if defined?(::RAILS_ENV)
						Object.send(:remove_const, :RAILS_ENV)
					end
					Object.const_set(:RAILS_ENV, (ENV['RAILS_ENV'] || 'development').dup)
					
					if using_default_log_path
						# We've changed the environment, so open the
						# correct log file.
						configuration.log_path = configuration.send(:default_log_path)
					end
					
					load_environment_without_passenger
				end
				
				alias_method :load_environment_without_passenger, :load_environment
				alias_method :load_environment, :load_environment_with_passenger
			end
		end
		if File.exist?('config/preinitializer.rb')
			require File.expand_path('config/preinitializer')
		end
		require File.expand_path('config/environment')
		if ActionController::Base.page_cache_directory.blank?
			ActionController::Base.page_cache_directory = "#{RAILS_ROOT}/public"
		end
		if defined?(ActionController::Dispatcher) \
		   && ActionController::Dispatcher.respond_to?(:error_file_path)
			ActionController::Dispatcher.error_file_path = "#{RAILS_ROOT}/public"
		end
		
		require 'rails/version' if !defined?(::Rails::VERSION)
		if !defined?(Dispatcher)
			begin
				require 'dispatcher'
			rescue LoadError
				# Early versions of Rails 3 still had the dispatcher, but
				# later versions disposed of it, in which case we'll need
				# to use the application object.
				raise if Rails::VERSION::MAJOR < 3
			end
		end
		
		# - No point in preloading the application sources if the garbage collector
		#   isn't copy-on-write friendly.
		# - Rails >= 2.2 already preloads application sources by default, so no need
		#   to do that again.
		if GC.copy_on_write_friendly? && !rails_will_preload_app_code?
			# Rails 2.2+ uses application_controller.rb while olde
			# versions use application.rb.
			require_dependency 'application'
			['models','controllers','helpers'].each do |section|
				Dir.glob("app/#{section}}/*.rb").each do |file|
					require_dependency canonicalize_path(file)
				end
			end
		end
	end
	
	def rails_will_preload_app_code?
		if defined?(Rails::Initializer)
			return ::Rails::Initializer.method_defined?(:load_application_classes)
		else
			return Rails::VERSION::MAJOR >= 3
		end
	end

	def handle_spawn_application(client, *options)
		options = sanitize_spawn_options(Hash[*options])
		a, b = UNIXSocket.pair
		safe_fork('application', true) do
			begin
				a.close
				client.close
				options = @options.merge(options)
				self.class.send(:start_request_handler, MessageChannel.new(b),
					true, options)
			rescue SignalException => e
				if e.message != AbstractRequestHandler::HARD_TERMINATION_SIGNAL &&
				   e.message != AbstractRequestHandler::SOFT_TERMINATION_SIGNAL
					raise
				end
			end
		end
		
		b.close
		worker_channel = MessageChannel.new(a)
		app_process = AppProcess.read_from_channel(worker_channel)
		app_process.write_to_channel(client)
	ensure
		a.close if a
		b.close if b && !b.closed?
		app_process.close if app_process
	end
	
	# Initialize the request handler and enter its main loop.
	# Spawn information will be sent back via +channel+.
	# The +forked+ argument indicates whether a new process was forked off
	# after loading environment.rb (i.e. whether smart spawning is being
	# used).
	def self.start_request_handler(channel, forked, options)
		app_root = options["app_root"]
		$0 = "Rails: #{options['app_group_name']}"
		reader, writer = IO.pipe
		begin
			reader.close_on_exec!
			
			if Rails::VERSION::STRING >= '2.3.0'
				rack_app = find_rack_app
				handler = Rack::RequestHandler.new(reader, rack_app, options)
			else
				handler = RequestHandler.new(reader, options)
			end
			
			app_process = AppProcess.new(app_root, Process.pid, writer,
				handler.server_sockets)
			app_process.write_to_channel(channel)
			writer.close
			channel.close
			
			before_handling_requests(forked, options)
			handler.main_loop
		ensure
			channel.close rescue nil
			writer.close rescue nil
			handler.cleanup rescue nil
			after_handling_requests
		end
	end
	private_class_method :start_request_handler
	
	def self.find_rack_app
		if Rails::VERSION::MAJOR >= 3
			File.read("config/application.rb") =~ /^module (.+)$/
			app_module = Object.const_get($1)
			return app_module::Application
		else
			return ActionController::Dispatcher.new
		end
	end
	private_class_method :find_rack_app
end

end # module ClassicRails
end # module PhusionPassenger
