Module: PhusionPassenger::Utils

Module PhusionPassenger::Utils |
In: | lib/phusion_passenger/utils.rb

Utility functions.

Methods

- assert_valid_app_root
- assert_valid_directory
- assert_valid_file
- assert_valid_groupname
- assert_valid_username
- canonicalize_path
- close_all_io_objects_for_fds
- lower_privilege
- marshal_exception
- passenger_tmpdir
- passenger_tmpdir
- passenger_tmpdir=
- print_exception
- report_app_init_status
- safe_fork
- sanitize_spawn_options
- switch_to_user
- to_boolean
- unmarshal_and_raise_errors
- unmarshal_exception

Classes and Modules
Class PhusionPassenger::Utils::PseudoIO
Protected Class methods
passenger_tmpdir(create = true)

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 420
    420:         def self.passenger_tmpdir(create = true)
    421:                 dir = @@passenger_tmpdir
    422:                 if dir.nil? || dir.empty?
    423:                         dir = "#{Dir.tmpdir}/passenger.#{Process.pid}"
    424:                         @@passenger_tmpdir = dir
    425:                 end
    426:                 if create && !File.exist?(dir)
    427:                         # This is a very minimal implementation of the function
    428:                         # passengerCreateTempDir() in Utils.cpp. This implementation
    429:                         # is only meant to make the unit tests pass. For production
    430:                         # systems one should pre-create the temp directory with
    431:                         # passengerCreateTempDir().
    432:                         system("mkdir", "-p", "-m", "u=wxs,g=wx,o=wx", dir)
    433:                         system("mkdir", "-p", "-m", "u=wxs,g=wx,o=wx", "#{dir}/backends")
    434:                 end
    435:                 return dir
    436:         end

passenger_tmpdir=(dir)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 438
    438:         def self.passenger_tmpdir=(dir)
    439:                 @@passenger_tmpdir = dir
    440:         end

Protected Instance methods
assert_valid_app_root(app_root)

Assert that `app_root` is a valid Ruby on Rails application root. Raises InvalidPath if that is not the case.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 60
    60:         def assert_valid_app_root(app_root)
    61:                 assert_valid_directory(app_root)
    62:                 assert_valid_file("#{app_root}/config/environment.rb")
    63:         end

assert_valid_directory(path)

Assert that `path` is a directory. Raises `InvalidPath` if it isn‘t.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 66
    66:         def assert_valid_directory(path)
    67:                 if !File.directory?(path)
    68:                         raise InvalidPath, "'#{path}' is not a valid directory."
    69:                 end
    70:         end

assert_valid_file(path)

Assert that `path` is a file. Raises `InvalidPath` if it isn‘t.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 73
    73:         def assert_valid_file(path)
    74:                 if !File.file?(path)
    75:                         raise InvalidPath, "'#{path}' is not a valid file."
    76:                 end
    77:         end

assert_valid_groupname(groupname)

Assert that `groupname` is a valid group name. Raises ArgumentError if that is not the case.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 88
    88:         def assert_valid_groupname(groupname)
    89:                 # If groupname does not exist then getgrnam() will raise an ArgumentError.
    90:                 groupname && Etc.getgrnam(groupname)
    91:         end

assert_valid_username(username)

Assert that `username` is a valid username. Raises ArgumentError if that is not the case.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 81
    81:         def assert_valid_username(username)
    82:                 # If username does not exist then getpwnam() will raise an ArgumentError.
    83:                 username && Etc.getpwnam(username)
    84:         end

canonicalize_path(path)

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.

[ show source ]

        # File lib/phusion_passenger/utils.rb, line 51
    51:         def canonicalize_path(path)
    52:                 raise ArgumentError, "The 'path' argument may not be nil" if path.nil?
    53:                 return Pathname.new(path).realpath.to_s
    54:         rescue Errno::ENOENT => e
    55:                 raise InvalidAPath, e.message
    56:         end

close_all_io_objects_for_fds(file_descriptors_to_leave_open)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 93
     93:         def close_all_io_objects_for_fds(file_descriptors_to_leave_open)
     94:                 ObjectSpace.each_object(IO) do |io|
     95:                         begin
     96:                                 if !file_descriptors_to_leave_open.include?(io.fileno) && !io.closed?
     97:                                         io.close
     98:                                 end
     99:                         rescue
    100:                         end
    101:                 end
    102:         end

lower_privilege(filename, lowest_user = "nobody")

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 343
    343:         def lower_privilege(filename, lowest_user = "nobody")
    344:                 stat = File.lstat(filename)
    345:                 begin
    346:                         if !switch_to_user(stat.uid)
    347:                                 switch_to_user(lowest_user)
    348:                         end
    349:                 rescue Errno::EPERM
    350:                         # No problem if we were unable to switch user.
    351:                 end
    352:         end

marshal_exception(exception)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 104
    104:         def marshal_exception(exception)
    105:                 data = {
    106:                         :message => exception.message,
    107:                         :class => exception.class.to_s,
    108:                         :backtrace => exception.backtrace
    109:                 }
    110:                 if exception.is_a?(InitializationError)
    111:                         data[:is_initialization_error] = true
    112:                         if exception.child_exception
    113:                                 data[:child_exception] = marshal_exception(exception.child_exception)
    114:                         end
    115:                 else
    116:                         begin
    117:                                 data[:exception] = Marshal.dump(exception)
    118:                         rescue ArgumentError, TypeError
    119:                                 e = UnknownError.new(exception.message, exception.class.to_s,
    120:                                                         exception.backtrace)
    121:                                 data[:exception] = Marshal.dump(e)
    122:                         end
    123:                 end
    124:                 return Marshal.dump(data)
    125:         end

passenger_tmpdir(create = true)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 413
    413:         def passenger_tmpdir(create = true)
    414:                 PhusionPassenger::Utils.passenger_tmpdir(create)
    415:         end

print_exception(current_location, exception, destination = STDERR)

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 158
    158:         def print_exception(current_location, exception, destination = STDERR)
    159:                 if !exception.is_a?(SystemExit)
    160:                         destination.puts(exception.backtrace_string(current_location))
    161:                         destination.flush if destination.respond_to?(:flush)
    162:                 end
    163:         end

report_app_init_status(channel, sink = STDERR) {|| ...}

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 242
    242:         def report_app_init_status(channel, sink = STDERR)
    243:                 begin
    244:                         old_global_stderr = $stderr
    245:                         old_stderr = STDERR
    246:                         stderr_output = ""
    247:
    248:                         pseudo_stderr = PseudoIO.new(sink)
    249:                         Object.send(:remove_const, 'STDERR') rescue nil
    250:                         Object.const_set('STDERR', pseudo_stderr)
    251:                         $stderr = pseudo_stderr
    252:
    253:                         begin
    254:                                 yield
    255:                         ensure
    256:                                 Object.send(:remove_const, 'STDERR') rescue nil
    257:                                 Object.const_set('STDERR', old_stderr)
    258:                                 $stderr = old_global_stderr
    259:                                 stderr_output = pseudo_stderr.done!
    260:                         end
    261:
    262:                         channel.write('success')
    263:                         return true
    264:                 rescue StandardError, ScriptError, NoMemoryError => e
    265:                         if ENV['TESTING_PASSENGER'] == '1'
    266:                                 print_exception(self.class.to_s, e)
    267:                         end
    268:                         channel.write('exception')
    269:                         channel.write_scalar(marshal_exception(e))
    270:                         channel.write_scalar(stderr_output)
    271:                         return false
    272:                 rescue SystemExit => e
    273:                         channel.write('exit')
    274:                         channel.write_scalar(marshal_exception(e))
    275:                         channel.write_scalar(stderr_output)
    276:                         raise
    277:                 end
    278:         end

safe_fork(current_location = self.class, double_fork = false) {|| ...}

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 175
    175:         def safe_fork(current_location = self.class, double_fork = false)
    176:                 pid = fork
    177:                 if pid.nil?
    178:                         begin
    179:                                 if double_fork
    180:                                         pid2 = fork
    181:                                         if pid2.nil?
    182:                                                 srand
    183:                                                 yield
    184:                                         end
    185:                                 else
    186:                                         srand
    187:                                         yield
    188:                                 end
    189:                         rescue Exception => e
    190:                                 print_exception(current_location.to_s, e)
    191:                         ensure
    192:                                 exit!
    193:                         end
    194:                 else
    195:                         if double_fork
    196:                                 Process.waitpid(pid) rescue nil
    197:                                 return pid
    198:                         else
    199:                                 return pid
    200:                         end
    201:                 end
    202:         end

sanitize_spawn_options(options)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 391
    391:         def sanitize_spawn_options(options)
    392:                 defaults = {
    393:                         "lower_privilege" => true,
    394:                         "lowest_user"     => "nobody",
    395:                         "environment"     => "production",
    396:                         "app_type"        => "rails",
    397:                         "spawn_method"    => "smart-lv2",
    398:                         "framework_spawner_timeout" => -1,
    399:                         "app_spawner_timeout"       => -1,
    400:                         "print_exceptions" => true
    401:                 }
    402:                 options = defaults.merge(options)
    403:                 options["lower_privilege"]           = to_boolean(options["lower_privilege"])
    404:                 options["framework_spawner_timeout"] = options["framework_spawner_timeout"].to_i
    405:                 options["app_spawner_timeout"]       = options["app_spawner_timeout"].to_i
    406:                 # Force this to be a boolean for easy use with Utils#unmarshal_and_raise_errors.
    407:                 options["print_exceptions"]          = to_boolean(options["print_exceptions"])
    408:                 return options
    409:         end

switch_to_user(user)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 354
    354:         def switch_to_user(user)
    355:                 begin
    356:                         if user.is_a?(String)
    357:                                 pw = Etc.getpwnam(user)
    358:                                 username = user
    359:                                 uid = pw.uid
    360:                                 gid = pw.gid
    361:                         else
    362:                                 pw = Etc.getpwuid(user)
    363:                                 username = pw.name
    364:                                 uid = user
    365:                                 gid = pw.gid
    366:                         end
    367:                 rescue
    368:                         return false
    369:                 end
    370:                 if uid == 0
    371:                         return false
    372:                 else
    373:                         # Some systems are broken. initgroups can fail because of
    374:                         # all kinds of stupid reasons. So we ignore any errors
    375:                         # raised by initgroups.
    376:                         begin
    377:                                 Process.groups = Process.initgroups(username, gid)
    378:                         rescue
    379:                         end
    380:                         Process::Sys.setgid(gid)
    381:                         Process::Sys.setuid(uid)
    382:                         ENV['HOME'] = pw.dir
    383:                         return true
    384:                 end
    385:         end

to_boolean(value)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 387
    387:         def to_boolean(value)
    388:                 return !(value.nil? || value == false || value == "false")
    389:         end

unmarshal_and_raise_errors(channel, print_exception = nil, app_type = "rails")

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.

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 302
    302:         def unmarshal_and_raise_errors(channel, print_exception = nil, app_type = "rails")
    303:                 args = channel.read
    304:                 if args.nil?
    305:                         raise EOFError, "Unexpected end-of-file detected."
    306:                 end
    307:                 status = args[0]
    308:                 if status == 'exception'
    309:                         child_exception = unmarshal_exception(channel.read_scalar)
    310:                         stderr = channel.read_scalar
    311:                         exception = AppInitError.new(
    312:                                 "Application '#{@app_root}' raised an exception: " <<
    313:                                 "#{child_exception.class} (#{child_exception.message})",
    314:                                 child_exception,
    315:                                 app_type,
    316:                                 stderr.empty? ? nil : stderr)
    317:                 elsif status == 'exit'
    318:                         child_exception = unmarshal_exception(channel.read_scalar)
    319:                         stderr = channel.read_scalar
    320:                         exception = AppInitError.new("Application '#{@app_root}' exited during startup",
    321:                                 child_exception, app_type, stderr.empty? ? nil : stderr)
    322:                 else
    323:                         exception = nil
    324:                 end
    325:
    326:                 if print_exception && exception
    327:                         if print_exception.respond_to?(:puts)
    328:                                 print_exception(self.class.to_s, child_exception, print_exception)
    329:                         elsif print_exception.respond_to?(:to_str)
    330:                                 filename = print_exception.to_str
    331:                                 File.open(filename, 'a') do |f|
    332:                                         print_exception(self.class.to_s, child_exception, f)
    333:                                 end
    334:                         else
    335:                                 print_exception(self.class.to_s, child_exception)
    336:                         end
    337:                 end
    338:                 raise exception if exception
    339:         end

unmarshal_exception(data)

[ show source ]

         # File lib/phusion_passenger/utils.rb, line 127
    127:         def unmarshal_exception(data)
    128:                 hash = Marshal.load(data)
    129:                 if hash[:is_initialization_error]
    130:                         if hash[:child_exception]
    131:                                 child_exception = unmarshal_exception(hash[:child_exception])
    132:                         else
    133:                                 child_exception = nil
    134:                         end
    135:
    136:                         case hash[:class]
    137:                         when AppInitError.to_s
    138:                                 exception_class = AppInitError
    139:                         when FrameworkInitError.to_s
    140:                                 exception_class = FrameworkInitError
    141:                         else
    142:                                 exception_class = InitializationError
    143:                         end
    144:                         return exception_class.new(hash[:message], child_exception)
    145:                 else
    146:                         begin
    147:                                 return Marshal.load(hash[:exception])
    148:                         rescue ArgumentError, TypeError
    149:                                 return UnknownError.new(hash[:message], hash[:class], hash[:backtrace])
    150:                         end
    151:                 end
    152:         end