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 help start/stop the lighttpd server used by layout tests.""" 31 32from __future__ import with_statement 33 34import codecs 35import logging 36import optparse 37import os 38import shutil 39import subprocess 40import sys 41import tempfile 42import time 43import urllib 44 45import factory 46import http_server_base 47 48_log = logging.getLogger("webkitpy.layout_tests.port.http_server") 49 50 51class HttpdNotStarted(Exception): 52 pass 53 54 55class Lighttpd(http_server_base.HttpServerBase): 56 57 def __init__(self, port_obj, output_dir, background=False, port=None, 58 root=None, run_background=None, layout_tests_dir=None): 59 """Args: 60 output_dir: the absolute path to the layout test result directory 61 """ 62 # Webkit tests 63 http_server_base.HttpServerBase.__init__(self, port_obj) 64 self._output_dir = output_dir 65 self._process = None 66 self._port = port 67 self._root = root 68 self._run_background = run_background 69 self._layout_tests_dir = layout_tests_dir 70 71 if self._port: 72 self._port = int(self._port) 73 74 if not self._layout_tests_dir: 75 self._layout_tests_dir = self._port_obj.layout_tests_dir() 76 77 try: 78 self._webkit_tests = os.path.join( 79 self._layout_tests_dir, 'http', 'tests') 80 self._js_test_resource = os.path.join( 81 self._layout_tests_dir, 'fast', 'js', 'resources') 82 self._media_resource = os.path.join( 83 self._layout_tests_dir, 'media') 84 85 except: 86 self._webkit_tests = None 87 self._js_test_resource = None 88 self._media_resource = None 89 90 # Self generated certificate for SSL server (for client cert get 91 # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt) 92 self._pem_file = os.path.join( 93 os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem') 94 95 # One mapping where we can get to everything 96 self.VIRTUALCONFIG = [] 97 98 if self._webkit_tests: 99 self.VIRTUALCONFIG.extend( 100 # Three mappings (one with SSL) for LayoutTests http tests 101 [{'port': 8000, 'docroot': self._webkit_tests}, 102 {'port': 8080, 'docroot': self._webkit_tests}, 103 {'port': 8443, 'docroot': self._webkit_tests, 104 'sslcert': self._pem_file}]) 105 106 def is_running(self): 107 return self._process != None 108 109 def start(self): 110 if self.is_running(): 111 raise 'Lighttpd already running' 112 113 base_conf_file = self._port_obj.path_from_webkit_base('Tools', 114 'Scripts', 'webkitpy', 'layout_tests', 'port', 'lighttpd.conf') 115 out_conf_file = os.path.join(self._output_dir, 'lighttpd.conf') 116 time_str = time.strftime("%d%b%Y-%H%M%S") 117 access_file_name = "access.log-" + time_str + ".txt" 118 access_log = os.path.join(self._output_dir, access_file_name) 119 log_file_name = "error.log-" + time_str + ".txt" 120 error_log = os.path.join(self._output_dir, log_file_name) 121 122 # Remove old log files. We only need to keep the last ones. 123 self.remove_log_files(self._output_dir, "access.log-") 124 self.remove_log_files(self._output_dir, "error.log-") 125 126 # Write out the config 127 with codecs.open(base_conf_file, "r", "utf-8") as file: 128 base_conf = file.read() 129 130 # FIXME: This should be re-worked so that this block can 131 # use with open() instead of a manual file.close() call. 132 # lighttpd.conf files seem to be UTF-8 without BOM: 133 # http://redmine.lighttpd.net/issues/992 134 f = codecs.open(out_conf_file, "w", "utf-8") 135 f.write(base_conf) 136 137 # Write out our cgi handlers. Run perl through env so that it 138 # processes the #! line and runs perl with the proper command 139 # line arguments. Emulate apache's mod_asis with a cat cgi handler. 140 f.write(('cgi.assign = ( ".cgi" => "/usr/bin/env",\n' 141 ' ".pl" => "/usr/bin/env",\n' 142 ' ".asis" => "/bin/cat",\n' 143 ' ".php" => "%s" )\n\n') % 144 self._port_obj._path_to_lighttpd_php()) 145 146 # Setup log files 147 f.write(('server.errorlog = "%s"\n' 148 'accesslog.filename = "%s"\n\n') % (error_log, access_log)) 149 150 # Setup upload folders. Upload folder is to hold temporary upload files 151 # and also POST data. This is used to support XHR layout tests that 152 # does POST. 153 f.write(('server.upload-dirs = ( "%s" )\n\n') % (self._output_dir)) 154 155 # Setup a link to where the js test templates are stored 156 f.write(('alias.url = ( "/js-test-resources" => "%s" )\n\n') % 157 (self._js_test_resource)) 158 159 # Setup a link to where the media resources are stored. 160 f.write(('alias.url += ( "/media-resources" => "%s" )\n\n') % 161 (self._media_resource)) 162 163 # dump out of virtual host config at the bottom. 164 if self._root: 165 if self._port: 166 # Have both port and root dir. 167 mappings = [{'port': self._port, 'docroot': self._root}] 168 else: 169 # Have only a root dir - set the ports as for LayoutTests. 170 # This is used in ui_tests to run http tests against a browser. 171 172 # default set of ports as for LayoutTests but with a 173 # specified root. 174 mappings = [{'port': 8000, 'docroot': self._root}, 175 {'port': 8080, 'docroot': self._root}, 176 {'port': 8443, 'docroot': self._root, 177 'sslcert': self._pem_file}] 178 else: 179 mappings = self.VIRTUALCONFIG 180 for mapping in mappings: 181 ssl_setup = '' 182 if 'sslcert' in mapping: 183 ssl_setup = (' ssl.engine = "enable"\n' 184 ' ssl.pemfile = "%s"\n' % mapping['sslcert']) 185 186 f.write(('$SERVER["socket"] == "127.0.0.1:%d" {\n' 187 ' server.document-root = "%s"\n' + 188 ssl_setup + 189 '}\n\n') % (mapping['port'], mapping['docroot'])) 190 f.close() 191 192 executable = self._port_obj._path_to_lighttpd() 193 module_path = self._port_obj._path_to_lighttpd_modules() 194 start_cmd = [executable, 195 # Newly written config file 196 '-f', os.path.join(self._output_dir, 'lighttpd.conf'), 197 # Where it can find its module dynamic libraries 198 '-m', module_path] 199 200 if not self._run_background: 201 start_cmd.append(# Don't background 202 '-D') 203 204 # Copy liblightcomp.dylib to /tmp/lighttpd/lib to work around the 205 # bug that mod_alias.so loads it from the hard coded path. 206 if sys.platform == 'darwin': 207 tmp_module_path = '/tmp/lighttpd/lib' 208 if not os.path.exists(tmp_module_path): 209 os.makedirs(tmp_module_path) 210 lib_file = 'liblightcomp.dylib' 211 shutil.copyfile(os.path.join(module_path, lib_file), 212 os.path.join(tmp_module_path, lib_file)) 213 214 env = self._port_obj.setup_environ_for_server() 215 _log.debug('Starting http server, cmd="%s"' % str(start_cmd)) 216 # FIXME: Should use Executive.run_command 217 self._process = subprocess.Popen(start_cmd, env=env, stdin=subprocess.PIPE) 218 219 # Wait for server to start. 220 self.mappings = mappings 221 server_started = self.wait_for_action( 222 self.is_server_running_on_all_ports) 223 224 # Our process terminated already 225 if not server_started or self._process.returncode != None: 226 raise google.httpd_utils.HttpdNotStarted('Failed to start httpd.') 227 228 _log.debug("Server successfully started") 229 230 # TODO(deanm): Find a nicer way to shutdown cleanly. Our log files are 231 # probably not being flushed, etc... why doesn't our python have os.kill ? 232 233 def stop(self, force=False): 234 if not force and not self.is_running(): 235 return 236 237 httpd_pid = None 238 if self._process: 239 httpd_pid = self._process.pid 240 self._port_obj._shut_down_http_server(httpd_pid) 241 242 if self._process: 243 # wait() is not threadsafe and can throw OSError due to: 244 # http://bugs.python.org/issue1731717 245 self._process.wait() 246 self._process = None 247