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