# 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