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