관리-도구
편집 파일: apache2_controller.rb
require 'erb' require 'fileutils' require 'time' PhusionPassenger.require_passenger_lib 'platform_info/apache' PhusionPassenger.require_passenger_lib 'platform_info/ruby' # A class for starting, stopping and restarting Apache, and for manipulating # its configuration file. This is used by the integration tests. # # Before a test begins, the test instructs Apache2Controller to create an Apache # configuration folder, which contains an Apache configuration file and other # configuration resources that Apache needs. The Apache configuration file is # created from a template (see Apache2Controller::STUB_DIR). # The test can define configuration customizations. For example, it can tell # Apache2Controller to add configuration options, virtual host definitions, etc. # # After the configuration folder has been created, Apache2Controller will start # Apache. After Apache has been started, the test will be run. Apache2Controller # will stop Apache after the test is done. # # Apache2Controller ensures that starting, stopping and restarting are not prone # to race conditions. For example, it ensures that when #start returns, Apache # really is listening on its server socket instead of still initializing. # # == Usage # # Suppose that you want to test a hypothetical "AlwaysPrintHelloWorld" # Apache configuration option. Then you can write the following test: # # apache = Apache2Controller.new # # # Add a configuration option to the configuration file. # apache << "AlwaysPrintHelloWorld on" # # # Write configuration file and start Apache with that configuration file. # apache.start # # begin # response_body = http_get("http://localhost:#{apache.port}/some/url") # response_body.should == "hello world!" # ensure # apache.stop # end class Apache2Controller include PhusionPassenger STUB_DIR = File.expand_path(File.dirname(__FILE__) + "/../stub/apache2") class VHost attr_accessor :domain attr_accessor :document_root attr_accessor :additional_configs def initialize(domain, document_root) @domain = domain @document_root = document_root @additional_configs = [] end def <<(config) @additional_configs << config end end attr_accessor :port attr_accessor :vhosts attr_reader :server_root def initialize(options = nil) set(options) if options @port ||= 64506 @vhosts = [] @extra = [] @server_root = File.expand_path('tmp.apache2') @passenger_root = File.expand_path(PhusionPassenger.install_spec) @mod_passenger = PhusionPassenger.apache2_module_path end def set(options) options.each_pair do |key, value| instance_variable_set("@#{key}", value) end end # Create an Apache configuration folder and start Apache on that # configuration folder. This method does not return until Apache # has done initializing. # # If Apache is already started, this this method will stop Apache first. def start if running? stop else File.unlink("#{@server_root}/httpd.pid") rescue nil end if @codesigning_identity require 'open3' stdout, stderr, status = Open3.capture3("codesign", "--force", "-s", @codesigning_identity, "--keychain", File.expand_path("~/Library/Keychains/login.keychain-db"), @mod_passenger) if !status.success? raise "Could not sign Apache module at #{@mod_passenger} with authority #{@codesigning_identity}: #{stderr}" end end if File.exist?(@server_root) FileUtils.rm_r(@server_root) end FileUtils.mkdir_p(@server_root) write_config_file FileUtils.cp("#{STUB_DIR}/mime.types", @server_root) command = [PlatformInfo.httpd, "-f", "#{@server_root}/httpd.conf", "-k", "start"] if boolean_option('VALGRIND') command = ['valgrind', '--dsymutil=yes', '--vgdb=yes', '--vgdb-error=1', '--trace-children=no'] + command end prev_error_log_position = error_log_position if !system(*command) raise [ "Could not start an Apache server: #{$?}", "\t---------------- Begin logs -------------------", read_error_log_starting_from(prev_error_log_position).split("\n").map{ |l| "\t#{l}" }.join("\n"), "\t---------------- End logs -------------------", ].join("\n") end # Wait until the PID file has been created. wait_start_time = Time.now begin Timeout::timeout(20) do while !File.exist?("#{@server_root}/httpd.pid") sleep(0.1) end end rescue Timeout::Error end_start_time = Time.now raise [ "Timeout waiting for an Apache server report its PID:", "\t---------------- Begin logs -------------------", read_error_log_starting_from(prev_error_log_position).split("\n").map{ |l| "\t#{l}" }.join("\n"), "\t---------------- End logs -------------------", "Started waiting at #{wait_start_time.iso8601(3)}", " Ended waiting at #{end_start_time.iso8601(3)}", ].join("\n") end # Wait until Apache is listening on the server port. wait_start_time = Time.now begin Timeout::timeout(30) do done = false while !done begin socket = TCPSocket.new('localhost', @port) socket.close done = true rescue Errno::ECONNREFUSED sleep(0.1) end end end rescue Timeout::Error end_start_time = Time.now raise [ "Timeout waiting for an Apache server to listen on port #{@port}:", "\t---------------- Begin logs -------------------", read_error_log_starting_from(prev_error_log_position).split("\n").map{ |l| "\t#{l}" }.join("\n"), "\t---------------- End logs -------------------", "Started waiting at #{wait_start_time.iso8601(3)}", " Ended waiting at #{end_start_time.iso8601(3)}", ].join("\n") end Dir["#{@server_root}/*"].each do |filename| if File.file?(filename) File.chmod(0666, filename) end end end def graceful_restart write_config_file if !system(PlatformInfo.httpd, "-f", "#{@server_root}/httpd.conf", "-k", "graceful") raise "Cannot restart Apache." end end # Stop Apache and delete its configuration folder. This method waits # until Apache is done with its shutdown procedure. # # This method does nothing if Apache is already stopped. def stop pid_file = "#{@server_root}/httpd.pid" if File.exist?(pid_file) begin pid = File.read(pid_file).strip.to_i Process.kill('SIGTERM', pid) rescue Errno::ESRCH # Looks like a stale pid file. FileUtils.rm_r(@server_root) return end end begin # Wait until the PID file is removed. Timeout::timeout(17) do while File.exist?(pid_file) sleep(0.1) end end # Wait until the server socket is closed. Timeout::timeout(7) do done = false while !done begin socket = TCPSocket.new('localhost', @port) socket.close sleep(0.1) rescue SystemCallError done = true end end end rescue Timeout::Error raise "Unable to stop Apache." end if File.exist?(@server_root) FileUtils.chmod_R(0777, @server_root) FileUtils.rm_r(@server_root) end end # Define a virtual host configuration block for the Apache configuration # file. If there was already a vhost definition with the same domain name, # then it will be overwritten. # # The given document root will be created if it doesn't exist. def set_vhost(domain, document_root) FileUtils.mkdir_p(document_root) vhost = VHost.new(domain, document_root) if block_given? yield vhost end vhosts.reject! {|host| host.domain == domain} vhosts << vhost end # Checks whether this Apache instance is running. def running? if File.exist?("#{@server_root}/httpd.pid") pid = File.read("#{@server_root}/httpd.pid").strip begin Process.kill(0, pid.to_i) return true rescue Errno::ESRCH return false rescue SystemCallError return true end else return false end end # Defines a configuration snippet to be added to the Apache configuration file. def <<(line) @extra << line end private def get_binding return binding end def write_config_file template = ERB.new(File.read("#{STUB_DIR}/httpd.conf.erb")) File.open("#{@server_root}/httpd.conf", 'w') do |f| f.write(template.result(get_binding)) end end def error_log_position if @log_file File.open(@log_file, 'rb') do |f| f.seek(0, IO::SEEK_END) f.pos end end rescue Errno::ENOENT nil end def read_error_log_starting_from(prev_pos) File.open(@log_file, 'r:utf-8') do |f| f.seek(prev_pos || 0, IO::SEEK_SET) f.read end rescue Errno::ENOENT "(no log file)" end def modules_dir PlatformInfo.apache2_modulesdir end def builtin_modules @@builtin_modules ||= `#{PlatformInfo.httpd} -l`.split("\n").grep(/\.c$/).map do |line| line.strip end end def has_builtin_module?(name) return builtin_modules.include?(name) end def has_module?(name) return File.exist?("#{modules_dir}/#{name}") end def we_are_root? return Process.uid == 0 end def boolean_option(name, default_value = false) value = ENV[name] if value.nil? || value.empty? default_value else value == "yes" || value == "on" || value == "true" || value == "1" end end end