1 #!/usr/bin/env python 2 # Copyright (C) 2010 Google Inc. All rights reserved. 3 # 4 # Redistribution and use in source and binary forms, with or without 5 # modification, are permitted provided that the following conditions are 6 # met: 7 # 8 # * Redistributions of source code must retain the above copyright 9 # notice, this list of conditions and the following disclaimer. 10 # * Redistributions in binary form must reproduce the above 11 # copyright notice, this list of conditions and the following disclaimer 12 # in the documentation and/or other materials provided with the 13 # distribution. 14 # * Neither the name of Google Inc. nor the names of its 15 # contributors may be used to endorse or promote products derived from 16 # this software without specific prior written permission. 17 # 18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30 """A class to start/stop the apache http server used by layout tests.""" 31 32 33 from __future__ import with_statement 34 35 import codecs 36 import logging 37 import optparse 38 import os 39 import re 40 import subprocess 41 import sys 42 43 import http_server_base 44 45 _log = logging.getLogger("webkitpy.layout_tests.port.apache_http_server") 46 47 48 class LayoutTestApacheHttpd(http_server_base.HttpServerBase): 49 50 def __init__(self, port_obj, output_dir): 51 """Args: 52 port_obj: handle to the platform-specific routines 53 output_dir: the absolute path to the layout test result directory 54 """ 55 http_server_base.HttpServerBase.__init__(self, port_obj) 56 self._output_dir = output_dir 57 self._httpd_proc = None 58 port_obj.maybe_make_directory(output_dir) 59 60 self.mappings = [{'port': 8000}, 61 {'port': 8080}, 62 {'port': 8081}, 63 {'port': 8443, 'sslcert': True}] 64 65 # The upstream .conf file assumed the existence of /tmp/WebKit for 66 # placing apache files like the lock file there. 67 self._runtime_path = os.path.join("/tmp", "WebKit") 68 port_obj.maybe_make_directory(self._runtime_path) 69 70 # The PID returned when Apache is started goes away (due to dropping 71 # privileges?). The proper controlling PID is written to a file in the 72 # apache runtime directory. 73 self._pid_file = os.path.join(self._runtime_path, 'httpd.pid') 74 75 test_dir = self._port_obj.layout_tests_dir() 76 js_test_resources_dir = self._cygwin_safe_join(test_dir, "fast", "js", 77 "resources") 78 media_resources_dir = self._cygwin_safe_join(test_dir, "media") 79 mime_types_path = self._cygwin_safe_join(test_dir, "http", "conf", 80 "mime.types") 81 cert_file = self._cygwin_safe_join(test_dir, "http", "conf", 82 "webkit-httpd.pem") 83 access_log = self._cygwin_safe_join(output_dir, "access_log.txt") 84 error_log = self._cygwin_safe_join(output_dir, "error_log.txt") 85 document_root = self._cygwin_safe_join(test_dir, "http", "tests") 86 87 # FIXME: We shouldn't be calling a protected method of _port_obj! 88 executable = self._port_obj._path_to_apache() 89 if self._is_cygwin(): 90 executable = self._get_cygwin_path(executable) 91 92 cmd = [executable, 93 '-f', "\"%s\"" % self._get_apache_config_file_path(test_dir, output_dir), 94 '-C', "\'DocumentRoot \"%s\"\'" % document_root, 95 '-c', "\'Alias /js-test-resources \"%s\"'" % js_test_resources_dir, 96 '-c', "\'Alias /media-resources \"%s\"'" % media_resources_dir, 97 '-C', "\'Listen %s\'" % "127.0.0.1:8000", 98 '-C', "\'Listen %s\'" % "127.0.0.1:8081", 99 '-c', "\'TypesConfig \"%s\"\'" % mime_types_path, 100 '-c', "\'CustomLog \"%s\" common\'" % access_log, 101 '-c', "\'ErrorLog \"%s\"\'" % error_log, 102 '-C', "\'User \"%s\"\'" % os.environ.get("USERNAME", 103 os.environ.get("USER", ""))] 104 105 if self._is_cygwin(): 106 cygbin = self._port_obj._path_from_base('third_party', 'cygwin', 107 'bin') 108 # Not entirely sure why, but from cygwin we need to run the 109 # httpd command through bash. 110 self._start_cmd = [ 111 os.path.join(cygbin, 'bash.exe'), 112 '-c', 113 'PATH=%s %s' % (self._get_cygwin_path(cygbin), " ".join(cmd)), 114 ] 115 else: 116 # TODO(ojan): When we get cygwin using Apache 2, use set the 117 # cert file for cygwin as well. 118 cmd.extend(['-c', "\'SSLCertificateFile %s\'" % cert_file]) 119 # Join the string here so that Cygwin/Windows and Mac/Linux 120 # can use the same code. Otherwise, we could remove the single 121 # quotes above and keep cmd as a sequence. 122 self._start_cmd = " ".join(cmd) 123 124 def _is_cygwin(self): 125 return sys.platform in ("win32", "cygwin") 126 127 def _cygwin_safe_join(self, *parts): 128 """Returns a platform appropriate path.""" 129 path = os.path.join(*parts) 130 if self._is_cygwin(): 131 return self._get_cygwin_path(path) 132 return path 133 134 def _get_cygwin_path(self, path): 135 """Convert a Windows path to a cygwin path. 136 137 The cygpath utility insists on converting paths that it thinks are 138 Cygwin root paths to what it thinks the correct roots are. So paths 139 such as "C:\b\slave\webkit-release\build\third_party\cygwin\bin" 140 are converted to plain "/usr/bin". To avoid this, we 141 do the conversion manually. 142 143 The path is expected to be an absolute path, on any drive. 144 """ 145 drive_regexp = re.compile(r'([a-z]):[/\\]', re.IGNORECASE) 146 147 def lower_drive(matchobj): 148 return '/cygdrive/%s/' % matchobj.group(1).lower() 149 path = drive_regexp.sub(lower_drive, path) 150 return path.replace('\\', '/') 151 152 def _get_apache_config_file_path(self, test_dir, output_dir): 153 """Returns the path to the apache config file to use. 154 Args: 155 test_dir: absolute path to the LayoutTests directory. 156 output_dir: absolute path to the layout test results directory. 157 """ 158 httpd_config = self._port_obj._path_to_apache_config_file() 159 httpd_config_copy = os.path.join(output_dir, "httpd.conf") 160 # httpd.conf is always utf-8 according to http://archive.apache.org/gnats/10125 161 with codecs.open(httpd_config, "r", "utf-8") as httpd_config_file: 162 httpd_conf = httpd_config_file.read() 163 if self._is_cygwin(): 164 # This is a gross hack, but it lets us use the upstream .conf file 165 # and our checked in cygwin. This tells the server the root 166 # directory to look in for .so modules. It will use this path 167 # plus the relative paths to the .so files listed in the .conf 168 # file. We have apache/cygwin checked into our tree so 169 # people don't have to install it into their cygwin. 170 cygusr = self._port_obj._path_from_base('third_party', 'cygwin', 171 'usr') 172 httpd_conf = httpd_conf.replace('ServerRoot "/usr"', 173 'ServerRoot "%s"' % self._get_cygwin_path(cygusr)) 174 175 with codecs.open(httpd_config_copy, "w", "utf-8") as file: 176 file.write(httpd_conf) 177 178 if self._is_cygwin(): 179 return self._get_cygwin_path(httpd_config_copy) 180 return httpd_config_copy 181 182 def _get_virtual_host_config(self, document_root, port, ssl=False): 183 """Returns a <VirtualHost> directive block for an httpd.conf file. 184 It will listen to 127.0.0.1 on each of the given port. 185 """ 186 return '\n'.join(('<VirtualHost 127.0.0.1:%s>' % port, 187 'DocumentRoot "%s"' % document_root, 188 ssl and 'SSLEngine On' or '', 189 '</VirtualHost>', '')) 190 191 def _start_httpd_process(self): 192 """Starts the httpd process and returns whether there were errors.""" 193 # Use shell=True because we join the arguments into a string for 194 # the sake of Window/Cygwin and it needs quoting that breaks 195 # shell=False. 196 # FIXME: We should not need to be joining shell arguments into strings. 197 # shell=True is a trail of tears. 198 # Note: Not thread safe: http://bugs.python.org/issue2320 199 _log.debug('Starting http server, cmd="%s"' % str(self._start_cmd)) 200 self._httpd_proc = subprocess.Popen(self._start_cmd, 201 stderr=subprocess.PIPE, 202 shell=True) 203 err = self._httpd_proc.stderr.read() 204 if len(err): 205 _log.debug(err) 206 return False 207 return True 208 209 def start(self): 210 """Starts the apache http server.""" 211 # Stop any currently running servers. 212 self.stop() 213 214 _log.debug("Starting apache http server") 215 server_started = self.wait_for_action(self._start_httpd_process) 216 if server_started: 217 _log.debug("Apache started. Testing ports") 218 server_started = self.wait_for_action( 219 self.is_server_running_on_all_ports) 220 221 if server_started: 222 _log.debug("Server successfully started") 223 else: 224 raise Exception('Failed to start http server') 225 226 def stop(self): 227 """Stops the apache http server.""" 228 _log.debug("Shutting down any running http servers") 229 httpd_pid = None 230 if os.path.exists(self._pid_file): 231 httpd_pid = int(open(self._pid_file).readline()) 232 # FIXME: We shouldn't be calling a protected method of _port_obj! 233 self._port_obj._shut_down_http_server(httpd_pid) 234