# Phusion Passenger - https://www.phusionpassenger.com/
# Copyright (c) 2010-2017 Phusion Holding B.V.
#
# "Passenger", "Phusion Passenger" and "Union Station" are registered
# trademarks of Phusion Holding B.V.
#
# 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.
PhusionPassenger.require_passenger_lib 'constants'
PhusionPassenger.require_passenger_lib 'console_text_template'
PhusionPassenger.require_passenger_lib 'platform_info'
PhusionPassenger.require_passenger_lib 'platform_info/operating_system'
PhusionPassenger.require_passenger_lib 'utils/ansi_colors'
PhusionPassenger.require_passenger_lib 'utils/download'
require 'fileutils'
require 'logger'
require 'etc'
# IMPORTANT: do not directly or indirectly require native_support; we can't compile
# it yet until we have a compiler, and installers usually check whether a compiler
# is installed.
module PhusionPassenger
# Abstract base class for text mode installers. Used by
# passenger-install-apache2-module and passenger-install-nginx-module.
#
# Subclasses must at least implement the #run_steps method which handles
# the installation itself.
#
# Usage:
#
# installer = ConcereteInstallerClass.new(options...)
# installer.run
class AbstractInstaller
PASSENGER_WEBSITE = "https://www.phusionpassenger.com"
PASSENGER_LIBRARY_URL = "https://www.phusionpassenger.com/library/"
PHUSION_WEBSITE = "www.phusion.nl"
# Create an AbstractInstaller. All options will be stored as instance
# variables, for example:
#
# installer = AbstractInstaller.new(:foo => "bar")
# installer.instance_variable_get(:"@foo") # => "bar"
def initialize(options = {})
@stdout = STDOUT
@stderr = STDERR
@auto = !STDIN.tty?
@colors = Utils::AnsiColors.new(options[:colorize] || :auto)
options.each_pair do |key, value|
instance_variable_set(:"@#{key}", value)
end
end
# Start the installation by calling the #install! method.
def run
before_install
run_steps
return true
rescue Abort
puts
return false
rescue SignalException, SystemExit
raise
rescue PlatformInfo::RuntimeError => e
new_screen
puts "An error occurred"
puts
puts e.message
exit 1
rescue Exception => e
show_support_options_for_installer_bug(e)
exit 2
ensure
after_install
end
protected
class Abort < StandardError
end
class CommandError < Abort
end
def interactive?
return !@auto
end
def non_interactive?
return !interactive?
end
def before_install
if STDOUT.respond_to?(:set_encoding)
STDOUT.set_encoding("UTF-8")
end
STDOUT.write(@colors.default_terminal_color)
STDOUT.flush
end
def after_install
STDOUT.write(@colors.reset)
STDOUT.flush
end
def install_doc_url
"https://www.phusionpassenger.com/library/install/"
end
def troubleshooting_doc_url
"https://www.phusionpassenger.com/library/admin/troubleshooting/"
end
def dependencies
return [[], []]
end
def check_dependencies(show_new_screen = true)
new_screen if show_new_screen
puts "Checking for required software..."
puts
PhusionPassenger.require_passenger_lib 'platform_info/depcheck'
specs, ids = dependencies
runner = PlatformInfo::Depcheck::ConsoleRunner.new(@colors)
specs.each do |spec|
PlatformInfo::Depcheck.load(spec)
end
ids.each do |id|
runner.add(id)
end
if runner.check_all
return true
else
puts
puts "Some required software is not installed."
puts "But don't worry, this installer will tell you how to install them.\n"
puts "Press Enter to continue, or Ctrl-C to abort."
if PhusionPassenger.originally_packaged?
wait
else
wait(10)
end
line
puts
puts "Installation instructions for required software"
puts
runner.missing_dependencies.each do |dep|
puts " * To install #{dep.name}:"
puts " #{dep.install_instructions}"
puts
end
puts "If the aforementioned instructions didn't solve your problem, then please take"
puts "a look at our documentation for troubleshooting tips:"
puts
puts " #{install_doc_url}"
puts " #{troubleshooting_doc_url}"
return false
end
end
def check_whether_os_is_broken
# No known broken OSes at the moment.
end
def check_gem_install_permission_problems
return true if PhusionPassenger.custom_packaged?
begin
require 'rubygems'
rescue LoadError
return true
end
if Process.uid != 0 &&
PhusionPassenger.build_system_dir =~ /^#{Regexp.escape home_dir}\// &&
PhusionPassenger.build_system_dir =~ /^#{Regexp.escape Gem.dir}\// &&
File.stat(PhusionPassenger.build_system_dir).uid == 0
new_screen
render_template 'installer_common/gem_install_permission_problems'
return false
else
return true
end
end
def check_directory_accessible_by_web_server
return true if PhusionPassenger.custom_packaged?
inaccessible_directories = []
list_parent_directories(PhusionPassenger.build_system_dir).each do |path|
if !world_executable?(path)
inaccessible_directories << path
end
end
if !inaccessible_directories.empty?
new_screen
render_template 'installer_common/world_inaccessible_directories',
:directories => inaccessible_directories
wait
end
end
def check_whether_system_has_enough_ram(required = 1024)
begin
meminfo = File.read("/proc/meminfo")
if meminfo =~ /^MemTotal: *(\d+) kB$/
ram_mb = $1.to_i / 1024
if meminfo =~ /^SwapTotal: *(\d+) kB$/
swap_mb = $1.to_i / 1024
else
swap_mb = 0
end
end
rescue Errno::ENOENT, Errno::EACCES
# Don't do anything on systems without memory information.
ram_mb = nil
swap_mb = nil
end
if ram_mb && swap_mb && ram_mb + swap_mb < required
new_screen
render_template 'installer_common/low_amount_of_memory_warning',
:required => required,
:current => ram_mb + swap_mb,
:ram => ram_mb,
:swap => swap_mb,
:install_doc_url => install_doc_url
wait
end
end
def show_support_options_for_installer_bug(e)
# We do not use template rendering here. Since we've determined that there's
# a bug, *anything* may be broken, so we use the safest codepath to ensure that
# the user sees the proper messages.
begin
line
@stderr.puts "*** EXCEPTION: #{e} (#{e.class})\n " +
e.backtrace.join("\n ")
new_screen
puts 'Oops, something went wrong :-('
puts
puts "We're sorry, but it looks like this installer ran into an unexpected problem.\n" +
"Please visit the following website for support. We'll do our best to help you.\n\n" +
" #{SUPPORT_URL}\n\n" +
"When submitting a support inquiry, please copy and paste the entire installer\n" +
"output."
rescue Exception => e2
# Raise original exception so that it doesn't get lost.
raise e
end
end
def use_stderr
old_stdout = @stdout
begin
@stdout = @stderr
yield
ensure
@stdout = old_stdout
end
end
def print(text)
@stdout.write(@colors.ansi_colorize(text))
@stdout.flush
end
def puts(text = nil)
if text
@stdout.puts(@colors.ansi_colorize(text.to_s))
else
@stdout.puts
end
@stdout.flush
end
def puts_error(text)
@stderr.puts(@colors.ansi_colorize("#{text}"))
@stderr.flush
end
def render_template(name, options = {})
options.merge!(:colors => @colors)
puts ConsoleTextTemplate.new({ :file => name }, options).result
end
def new_screen
puts
line
puts
end
def line
puts "--------------------------------------------"
end
def prompt(message, default_value = nil)
done = false
while !done
print "#{message}: "
if non_interactive? && default_value
puts default_value
return default_value
end
begin
result = STDIN.readline
rescue EOFError
exit 2
end
result.strip!
if result.empty?
if default_value
result = default_value
done = true
else
done = !block_given? || yield(result)
end
else
done = !block_given? || yield(result)
end
end
return result
rescue Interrupt
raise Abort
end
def prompt_confirmation(message)
result = prompt("#{message} [y/n]") do |value|
if value.downcase == 'y' || value.downcase == 'n'
true
else
puts_error "Invalid input '#{value}'; please enter either 'y' or 'n'."
false
end
end
return result.downcase == 'y'
rescue Interrupt
raise Abort
end
def prompt_confirmation_with_default(message, default)
if default
default_str = "[Y/n]"
else
default_str = "[y/N]"
end
result = prompt("#{message} #{default_str}") do |value|
if value.downcase == 'y' || value.downcase == 'n'
true
elsif value.empty?
true
else
puts_error "Invalid input '#{value}'; please enter either 'y' or 'n'."
false
end
end
if result.empty?
return default
else
return result.downcase == 'y'
end
rescue Interrupt
raise Abort
end
def wait(timeout = nil)
if interactive?
if timeout
require 'timeout' unless defined?(Timeout)
begin
Timeout.timeout(timeout) do
STDIN.readline
end
rescue Timeout::Error
# Do nothing.
end
else
STDIN.readline
end
end
rescue Interrupt
raise Abort
end
def home_dir
return PhusionPassenger.home_dir
end
def sh(*args)
puts "# #{args.join(' ')}"
result = system(*args)
if result
return true
elsif $?.signaled? && $?.termsig == Signal.list["INT"]
raise Interrupt
else
return false
end
end
def sh!(*args)
if !sh(*args)
puts_error "*** Command failed: #{args.join(' ')}"
raise CommandError
end
end
def rake(*args)
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
if !PlatformInfo.rake_command
puts_error 'Cannot find Rake.'
raise Abort
end
sh("#{PlatformInfo.rake_command} #{args.join(' ')}")
end
def rake!(*args)
PhusionPassenger.require_passenger_lib 'platform_info/ruby'
if !PlatformInfo.rake_command
puts_error 'Cannot find Rake.'
raise Abort
end
sh!("#{PlatformInfo.rake_command} #{args.join(' ')}")
end
def download(url, output, options = {})
options[:logger] ||= begin
logger = Logger.new(STDOUT)
logger.level = Logger::WARN
logger.formatter = proc { |severity, datetime, progname, msg| "*** #{msg}\n" }
logger
end
return PhusionPassenger::Utils::Download.download(url, output, options)
end
def list_parent_directories(dir)
dirs = []
components = File.expand_path(dir).split(File::SEPARATOR)
components.shift # Remove leading /
components.size.times do |i|
dirs << File::SEPARATOR + components[0 .. i].join(File::SEPARATOR)
end
return dirs.reverse
end
def world_executable?(dir)
begin
stat = File.stat(dir)
rescue Errno::EACCESS
return false
end
return stat.mode & 0000001 != 0
end
end
end # module PhusionPassenger