# encoding: binary
# 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 'rubygems'
require 'thread'
if (!defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby") && RUBY_VERSION < "1.8.7"
require 'fastthread'
end
require 'pathname'
require 'etc'
require 'fcntl'
require 'tempfile'
require 'stringio'
require 'phusion_passenger/exceptions'
if !defined?(RUBY_ENGINE) || RUBY_ENGINE == "ruby"
require 'phusion_passenger/native_support'
end
module PhusionPassenger
# Utility functions.
module Utils
protected
# Return the canonicalized version of +path+. This path is guaranteed to
# to be "normal", i.e. it doesn't contain stuff like ".." or "/",
# and it fully resolves symbolic links.
#
# Raises SystemCallError if something went wrong. Raises ArgumentError
# if +path+ is nil. Raises InvalidPath if +path+ does not appear
# to be a valid path.
def canonicalize_path(path)
raise ArgumentError, "The 'path' argument may not be nil" if path.nil?
return Pathname.new(path).realpath.to_s
rescue Errno::ENOENT => e
raise InvalidAPath, e.message
end
# Assert that +app_root+ is a valid Ruby on Rails application root.
# Raises InvalidPath if that is not the case.
def assert_valid_app_root(app_root)
assert_valid_directory(app_root)
assert_valid_file("#{app_root}/config/environment.rb")
end
# Assert that +path+ is a directory. Raises +InvalidPath+ if it isn't.
def assert_valid_directory(path)
if !File.directory?(path)
raise InvalidPath, "'#{path}' is not a valid directory."
end
end
# Assert that +path+ is a file. Raises +InvalidPath+ if it isn't.
def assert_valid_file(path)
if !File.file?(path)
raise InvalidPath, "'#{path}' is not a valid file."
end
end
# Assert that +username+ is a valid username. Raises
# ArgumentError if that is not the case.
def assert_valid_username(username)
# If username does not exist then getpwnam() will raise an ArgumentError.
username && Etc.getpwnam(username)
end
# Assert that +groupname+ is a valid group name. Raises
# ArgumentError if that is not the case.
def assert_valid_groupname(groupname)
# If groupname does not exist then getgrnam() will raise an ArgumentError.
groupname && Etc.getgrnam(groupname)
end
def close_all_io_objects_for_fds(file_descriptors_to_leave_open)
ObjectSpace.each_object(IO) do |io|
begin
if !file_descriptors_to_leave_open.include?(io.fileno) && !io.closed?
io.close
end
rescue
end
end
end
def marshal_exception(exception)
data = {
:message => exception.message,
:class => exception.class.to_s,
:backtrace => exception.backtrace
}
if exception.is_a?(InitializationError)
data[:is_initialization_error] = true
if exception.child_exception
data[:child_exception] = marshal_exception(exception.child_exception)
end
else
begin
data[:exception] = Marshal.dump(exception)
rescue ArgumentError, TypeError
e = UnknownError.new(exception.message, exception.class.to_s,
exception.backtrace)
data[:exception] = Marshal.dump(e)
end
end
return Marshal.dump(data)
end
def unmarshal_exception(data)
hash = Marshal.load(data)
if hash[:is_initialization_error]
if hash[:child_exception]
child_exception = unmarshal_exception(hash[:child_exception])
else
child_exception = nil
end
case hash[:class]
when AppInitError.to_s
exception_class = AppInitError
when FrameworkInitError.to_s
exception_class = FrameworkInitError
else
exception_class = InitializationError
end
return exception_class.new(hash[:message], child_exception)
else
begin
return Marshal.load(hash[:exception])
rescue ArgumentError, TypeError
return UnknownError.new(hash[:message], hash[:class], hash[:backtrace])
end
end
end
# Print the given exception, including the stack trace, to STDERR.
#
# +current_location+ is a string which describes where the code is
# currently at. Usually the current class name will be enough.
def print_exception(current_location, exception, destination = STDERR)
if !exception.is_a?(SystemExit)
destination.puts(exception.backtrace_string(current_location))
destination.flush if destination.respond_to?(:flush)
end
end
# Fork a new process and run the given block inside the child process, just like
# fork(). Unlike fork(), this method is safe, i.e. there's no way for the child
# process to escape the block. Any uncaught exceptions in the child process will
# be printed to standard output, citing +current_location+ as the source.
# Futhermore, the child process will exit by calling Kernel#exit!, thereby
# bypassing any at_exit or ensure blocks.
#
# If +double_fork+ is true, then the child process will fork and immediately exit.
# This technique can be used to avoid zombie processes, at the expense of not
# being able to waitpid() the second child.
def safe_fork(current_location = self.class, double_fork = false)
pid = fork
if pid.nil?
begin
if double_fork
pid2 = fork
if pid2.nil?
srand
yield
end
else
srand
yield
end
rescue Exception => e
print_exception(current_location.to_s, e)
ensure
exit!
end
else
if double_fork
Process.waitpid(pid) rescue nil
return pid
else
return pid
end
end
end
class PseudoIO
def initialize(sink)
@sink = sink || File.open("/dev/null", "w")
@buffer = StringIO.new
end
def done!
result = @buffer.string
@buffer = nil
return result
end
def method_missing(*args, &block)
@buffer.send(*args, &block) if @buffer && args.first != :reopen
return @sink.send(*args, &block)
end
def respond_to?(symbol, include_private = false)
return @sink.respond_to?(symbol, include_private)
end
end
# Run the given block. A message will be sent through +channel+ (a
# MessageChannel object), telling the remote side whether the block
# raised an exception, called exit(), or succeeded.
#
# If _sink_ is non-nil, then every operation on $stderr/STDERR inside
# the block will be performed on _sink_ as well. If _sink_ is nil
# then all operations on $stderr/STDERR inside the block will be
# silently discarded, i.e. if one writes to $stderr/STDERR then nothing
# will be actually written to the console.
#
# Returns whether the block succeeded, i.e. whether it didn't raise an
# exception.
#
# Exceptions are not propagated, except SystemExit and a few
# non-StandardExeption classes such as SignalException. Of the
# exceptions that are propagated, only SystemExit will be reported.
def report_app_init_status(channel, sink = STDERR)
begin
old_global_stderr = $stderr
old_stderr = STDERR
stderr_output = ""
pseudo_stderr = PseudoIO.new(sink)
Object.send(:remove_const, 'STDERR') rescue nil
Object.const_set('STDERR', pseudo_stderr)
$stderr = pseudo_stderr
begin
yield
ensure
Object.send(:remove_const, 'STDERR') rescue nil
Object.const_set('STDERR', old_stderr)
$stderr = old_global_stderr
stderr_output = pseudo_stderr.done!
end
channel.write('success')
return true
rescue StandardError, ScriptError, NoMemoryError => e
if ENV['TESTING_PASSENGER'] == '1'
print_exception(self.class.to_s, e)
end
channel.write('exception')
channel.write_scalar(marshal_exception(e))
channel.write_scalar(stderr_output)
return false
rescue SystemExit => e
channel.write('exit')
channel.write_scalar(marshal_exception(e))
channel.write_scalar(stderr_output)
raise
end
end
# Receive status information that was sent to +channel+ by
# report_app_init_status. If an error occured according to the
# received information, then an appropriate exception will be
# raised.
#
# If print_exception evaluates to true, then the
# exception message and the backtrace will also be printed.
# Where it is printed to depends on the type of
# print_exception:
# - If it responds to #puts, then the exception information will
# be printed using this method.
# - If it responds to #to_str, then the exception information
# will be appended to the file whose filename equals the return
# value of the #to_str call.
# - Otherwise, it will be printed to STDERR.
#
# Raises:
# - AppInitError: this class wraps the exception information
# received through the channel.
# - IOError, SystemCallError, SocketError: these errors are
# raised if an error occurred while receiving the information
# through the channel.
def unmarshal_and_raise_errors(channel, print_exception = nil, app_type = "rails")
args = channel.read
if args.nil?
raise EOFError, "Unexpected end-of-file detected."
end
status = args[0]
if status == 'exception'
child_exception = unmarshal_exception(channel.read_scalar)
stderr = channel.read_scalar
exception = AppInitError.new(
"Application '#{@app_root}' raised an exception: " <<
"#{child_exception.class} (#{child_exception.message})",
child_exception,
app_type,
stderr.empty? ? nil : stderr)
elsif status == 'exit'
child_exception = unmarshal_exception(channel.read_scalar)
stderr = channel.read_scalar
exception = AppInitError.new("Application '#{@app_root}' exited during startup",
child_exception, app_type, stderr.empty? ? nil : stderr)
else
exception = nil
end
if print_exception && exception
if print_exception.respond_to?(:puts)
print_exception(self.class.to_s, child_exception, print_exception)
elsif print_exception.respond_to?(:to_str)
filename = print_exception.to_str
File.open(filename, 'a') do |f|
print_exception(self.class.to_s, child_exception, f)
end
else
print_exception(self.class.to_s, child_exception)
end
end
raise exception if exception
end
# Lower the current process's privilege to the owner of the given file.
# No exceptions will be raised in the event that privilege lowering fails.
def lower_privilege(filename, lowest_user = "nobody")
stat = File.lstat(filename)
begin
if !switch_to_user(stat.uid)
switch_to_user(lowest_user)
end
rescue Errno::EPERM
# No problem if we were unable to switch user.
end
end
def switch_to_user(user)
begin
if user.is_a?(String)
pw = Etc.getpwnam(user)
username = user
uid = pw.uid
gid = pw.gid
else
pw = Etc.getpwuid(user)
username = pw.name
uid = user
gid = pw.gid
end
rescue
return false
end
if uid == 0
return false
else
# Some systems are broken. initgroups can fail because of
# all kinds of stupid reasons. So we ignore any errors
# raised by initgroups.
begin
Process.groups = Process.initgroups(username, gid)
rescue
end
Process::Sys.setgid(gid)
Process::Sys.setuid(uid)
ENV['HOME'] = pw.dir
return true
end
end
def to_boolean(value)
return !(value.nil? || value == false || value == "false")
end
def sanitize_spawn_options(options)
defaults = {
"lower_privilege" => true,
"lowest_user" => "nobody",
"environment" => "production",
"app_type" => "rails",
"spawn_method" => "smart-lv2",
"framework_spawner_timeout" => -1,
"app_spawner_timeout" => -1,
"print_exceptions" => true
}
options = defaults.merge(options)
options["lower_privilege"] = to_boolean(options["lower_privilege"])
options["framework_spawner_timeout"] = options["framework_spawner_timeout"].to_i
options["app_spawner_timeout"] = options["app_spawner_timeout"].to_i
# Force this to be a boolean for easy use with Utils#unmarshal_and_raise_errors.
options["print_exceptions"] = to_boolean(options["print_exceptions"])
return options
end
@@passenger_tmpdir = nil
def passenger_tmpdir(create = true)
PhusionPassenger::Utils.passenger_tmpdir(create)
end
# Returns the directory in which to store Phusion Passenger-specific
# temporary files. If +create+ is true, then this method creates the
# directory if it doesn't exist.
def self.passenger_tmpdir(create = true)
dir = @@passenger_tmpdir
if dir.nil? || dir.empty?
dir = "#{Dir.tmpdir}/passenger.#{Process.pid}"
@@passenger_tmpdir = dir
end
if create && !File.exist?(dir)
# This is a very minimal implementation of the function
# passengerCreateTempDir() in Utils.cpp. This implementation
# is only meant to make the unit tests pass. For production
# systems one should pre-create the temp directory with
# passengerCreateTempDir().
system("mkdir", "-p", "-m", "u=wxs,g=wx,o=wx", dir)
system("mkdir", "-p", "-m", "u=wxs,g=wx,o=wx", "#{dir}/backends")
end
return dir
end
def self.passenger_tmpdir=(dir)
@@passenger_tmpdir = dir
end
####################################
end
end # module PhusionPassenger
class Exception
def backtrace_string(current_location = nil)
if current_location.nil?
location = nil
else
location = "in #{current_location} "
end
return "*** Exception #{self.class} #{location}" <<
"(#{self}) (process #{$$}):\n" <<
"\tfrom " << backtrace.join("\n\tfrom ")
end
end
class ConditionVariable
# This is like ConditionVariable.wait(), but allows one to wait a maximum
# amount of time. Returns true if this condition was signaled, false if a
# timeout occurred.
def timed_wait(mutex, secs)
if secs > 100000000
# NOTE: If one calls timeout() on FreeBSD 5 with an
# argument of more than 100000000, then MRI will become
# stuck in an infite loop, blocking all threads. It seems
# that MRI uses select() to implement sleeping.
# I think that a value of more than 100000000 overflows
# select()'s data structures, causing it to behave incorrectly.
# So we just make sure we can't sleep more than 100000000
# seconds.
secs = 100000000
end
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
if secs > 0
return wait(mutex, secs)
else
return wait(mutex)
end
else
require 'timeout' unless defined?(Timeout)
if secs > 0
Timeout.timeout(secs) do
wait(mutex)
end
else
wait(mutex)
end
return true
end
rescue Timeout::Error
return false
end
# This is like ConditionVariable.wait(), but allows one to wait a maximum
# amount of time. Raises Timeout::Error if the timeout has elapsed.
def timed_wait!(mutex, secs)
require 'timeout' unless defined?(Timeout)
if secs > 100000000
# See the corresponding note for timed_wait().
secs = 100000000
end
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
if secs > 0
if !wait(mutex, secs)
raise Timeout::Error, "Timeout"
end
else
wait(mutex)
end
else
if secs > 0
Timeout.timeout(secs) do
wait(mutex)
end
else
wait(mutex)
end
end
return nil
end
end
class IO
if defined?(PhusionPassenger::NativeSupport)
# Send an IO object (i.e. a file descriptor) over this IO channel.
# This only works if this IO channel is a Unix socket.
#
# Raises SystemCallError if something went wrong.
def send_io(io)
PhusionPassenger::NativeSupport.send_fd(self.fileno, io.fileno)
end
# Receive an IO object (i.e. a file descriptor) from this IO channel.
# This only works if this IO channel is a Unix socket.
#
# Raises SystemCallError if something went wrong.
def recv_io
return IO.new(PhusionPassenger::NativeSupport.recv_fd(self.fileno))
end
end
def close_on_exec!
if defined?(Fcntl::F_SETFD)
fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
end
end
end
module Signal
# Like Signal.list, but only returns signals that we can actually trap.
def self.list_trappable
ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby"
case ruby_engine
when "ruby"
result = Signal.list
result.delete("ALRM")
result.delete("VTALRM")
return result
when "jruby"
result = Signal.list
result.delete("QUIT")
result.delete("ILL")
result.delete("FPE")
result.delete("KILL")
result.delete("SEGV")
result.delete("STOP")
result.delete("USR1")
return result
else
return Signal.list
end
end
end
# Ruby's implementation of UNIXSocket#recv_io and UNIXSocket#send_io
# are broken on 64-bit FreeBSD 7, OpenBSD and x86_64/ppc64 OS X. So we override them
# with our own implementation.
if RUBY_PLATFORM =~ /freebsd/ || RUBY_PLATFORM =~ /openbsd/ || (RUBY_PLATFORM =~ /darwin/ && RUBY_PLATFORM !~ /universal/)
require 'socket'
UNIXSocket.class_eval do
def recv_io
super
end
def send_io(io)
super
end
end
end
module GC
if !respond_to?(:copy_on_write_friendly?)
# Checks whether the current Ruby interpreter's garbage
# collector is copy-on-write friendly.
def self.copy_on_write_friendly?
return false
end
end
end