관리-도구
편집 파일: abstract_installer.rb
# 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 "<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