481 lines
13 KiB
Ruby
481 lines
13 KiB
Ruby
# Phusion Passenger - https://www.phusionpassenger.com/
|
|
# Copyright (c) 2010-2025 Asynchronous B.V.
|
|
#
|
|
# "Passenger", "Phusion Passenger" and "Union Station" are registered
|
|
# trademarks of Asynchronous 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 "<red>An error occurred</red>"
|
|
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 "<banner>Checking for required software...</banner>"
|
|
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 "<red>Some required software is not installed.</red>"
|
|
puts "But don't worry, this installer will tell you how to install them.\n"
|
|
puts "<b>Press Enter to continue, or Ctrl-C to abort.</b>"
|
|
if PhusionPassenger.originally_packaged?
|
|
wait
|
|
else
|
|
wait(10)
|
|
end
|
|
|
|
line
|
|
puts
|
|
puts "<banner>Installation instructions for required software</banner>"
|
|
puts
|
|
runner.missing_dependencies.each do |dep|
|
|
puts " * To install <yellow>#{dep.name}</yellow>:"
|
|
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 " <yellow>#{install_doc_url}</yellow>"
|
|
puts " <yellow>#{troubleshooting_doc_url}</yellow>"
|
|
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 '<red>Oops, something went wrong :-(</red>'
|
|
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" +
|
|
" <b>#{SUPPORT_URL}</b>\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("<red>#{text}</red>"))
|
|
@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
|