1# Copyright (C) 2011 Google Inc. All rights reserved.
2#
3# Redistribution and use in source and binary forms, with or without
4# modification, are permitted provided that the following conditions are
5# met:
6#
7#     * Redistributions of source code must retain the above copyright
8# notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above
10# copyright notice, this list of conditions and the following disclaimer
11# in the documentation and/or other materials provided with the
12# distribution.
13#     * Neither the name of Google Inc. nor the names of its
14# contributors may be used to endorse or promote products derived from
15# this software without specific prior written permission.
16#
17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29"""A class to help start/stop the lighttpd server used by layout tests."""
30
31import logging
32import os
33import time
34
35from webkitpy.layout_tests.servers import http_server_base
36
37
38_log = logging.getLogger(__name__)
39
40
41class Lighttpd(http_server_base.HttpServerBase):
42
43    def __init__(self, port_obj, output_dir, background=False, port=None,
44                 root=None, run_background=None, additional_dirs=None,
45                 layout_tests_dir=None, number_of_servers=None):
46        """Args:
47          output_dir: the absolute path to the layout test result directory
48        """
49        # Webkit tests
50        http_server_base.HttpServerBase.__init__(self, port_obj, number_of_servers)
51        self._name = 'lighttpd'
52        self._output_dir = output_dir
53        self._port = port
54        self._root = root
55        self._run_background = run_background
56        self._additional_dirs = additional_dirs
57        self._layout_tests_dir = layout_tests_dir
58
59        self._pid_file = self._filesystem.join(self._runtime_path, '%s.pid' % self._name)
60
61        if self._port:
62            self._port = int(self._port)
63
64        if not self._layout_tests_dir:
65            self._layout_tests_dir = self._port_obj.layout_tests_dir()
66
67        self._webkit_tests = os.path.join(self._layout_tests_dir, 'http', 'tests')
68        self._js_test_resource = os.path.join(self._layout_tests_dir, 'resources')
69        self._media_resource = os.path.join(self._layout_tests_dir, 'media')
70
71        # Self generated certificate for SSL server (for client cert get
72        # <base-path>\chrome\test\data\ssl\certs\root_ca_cert.crt)
73        self._pem_file = os.path.join(
74            os.path.dirname(os.path.abspath(__file__)), 'httpd2.pem')
75
76        # One mapping where we can get to everything
77        self.VIRTUALCONFIG = []
78
79        if self._webkit_tests:
80            self.VIRTUALCONFIG.extend(
81               # Three mappings (one with SSL) for LayoutTests http tests
82               [{'port': 8000, 'docroot': self._webkit_tests},
83                {'port': 8080, 'docroot': self._webkit_tests},
84                {'port': 8443, 'docroot': self._webkit_tests,
85                 'sslcert': self._pem_file}])
86
87    def _prepare_config(self):
88        base_conf_file = self._port_obj.path_from_webkit_base('Tools',
89            'Scripts', 'webkitpy', 'layout_tests', 'servers', 'lighttpd.conf')
90        out_conf_file = os.path.join(self._output_dir, 'lighttpd.conf')
91        time_str = time.strftime("%d%b%Y-%H%M%S")
92        access_file_name = "access.log-" + time_str + ".txt"
93        access_log = os.path.join(self._output_dir, access_file_name)
94        log_file_name = "error.log-" + time_str + ".txt"
95        error_log = os.path.join(self._output_dir, log_file_name)
96
97        # Write out the config
98        base_conf = self._filesystem.read_text_file(base_conf_file)
99
100        # FIXME: This should be re-worked so that this block can
101        # use with open() instead of a manual file.close() call.
102        f = self._filesystem.open_text_file_for_writing(out_conf_file)
103        f.write(base_conf)
104
105        # Write out our cgi handlers.  Run perl through env so that it
106        # processes the #! line and runs perl with the proper command
107        # line arguments. Emulate apache's mod_asis with a cat cgi handler.
108        f.write(('cgi.assign = ( ".cgi"  => "/usr/bin/env",\n'
109                 '               ".pl"   => "/usr/bin/env",\n'
110                 '               ".asis" => "/bin/cat",\n'
111                 '               ".php"  => "%s" )\n\n') %
112                                     self._port_obj._path_to_lighttpd_php())
113
114        # Setup log files
115        f.write(('server.errorlog = "%s"\n'
116                 'accesslog.filename = "%s"\n\n') % (error_log, access_log))
117
118        # Setup upload folders. Upload folder is to hold temporary upload files
119        # and also POST data. This is used to support XHR layout tests that
120        # does POST.
121        f.write(('server.upload-dirs = ( "%s" )\n\n') % (self._output_dir))
122
123        # Setup a link to where the js test templates are stored
124        f.write(('alias.url = ( "/js-test-resources" => "%s" )\n\n') %
125                    (self._js_test_resource))
126
127        if self._additional_dirs:
128            for alias, path in self._additional_dirs.iteritems():
129                f.write(('alias.url += ( "%s" => "%s" )\n\n') % (alias, path))
130
131        # Setup a link to where the media resources are stored.
132        f.write(('alias.url += ( "/media-resources" => "%s" )\n\n') %
133                    (self._media_resource))
134
135        # dump out of virtual host config at the bottom.
136        if self._root:
137            if self._port:
138                # Have both port and root dir.
139                mappings = [{'port': self._port, 'docroot': self._root}]
140            else:
141                # Have only a root dir - set the ports as for LayoutTests.
142                # This is used in ui_tests to run http tests against a browser.
143
144                # default set of ports as for LayoutTests but with a
145                # specified root.
146                mappings = [{'port': 8000, 'docroot': self._root},
147                            {'port': 8080, 'docroot': self._root},
148                            {'port': 8443, 'docroot': self._root,
149                             'sslcert': self._pem_file}]
150        else:
151            mappings = self.VIRTUALCONFIG
152        for mapping in mappings:
153            ssl_setup = ''
154            if 'sslcert' in mapping:
155                ssl_setup = ('  ssl.engine = "enable"\n'
156                             '  ssl.pemfile = "%s"\n' % mapping['sslcert'])
157
158            f.write(('$SERVER["socket"] == "127.0.0.1:%d" {\n'
159                     '  server.document-root = "%s"\n' +
160                     ssl_setup +
161                     '}\n\n') % (mapping['port'], mapping['docroot']))
162        f.close()
163
164        executable = self._port_obj._path_to_lighttpd()
165        module_path = self._port_obj._path_to_lighttpd_modules()
166        start_cmd = [executable,
167                     # Newly written config file
168                     '-f', os.path.join(self._output_dir, 'lighttpd.conf'),
169                     # Where it can find its module dynamic libraries
170                     '-m', module_path]
171
172        if not self._run_background:
173            start_cmd.append(# Don't background
174                             '-D')
175
176        # Copy liblightcomp.dylib to /tmp/lighttpd/lib to work around the
177        # bug that mod_alias.so loads it from the hard coded path.
178        if self._port_obj.host.platform.is_mac():
179            tmp_module_path = '/tmp/lighttpd/lib'
180            if not self._filesystem.exists(tmp_module_path):
181                self._filesystem.maybe_make_directory(tmp_module_path)
182            lib_file = 'liblightcomp.dylib'
183            self._filesystem.copyfile(self._filesystem.join(module_path, lib_file),
184                                      self._filesystem.join(tmp_module_path, lib_file))
185
186        self._start_cmd = start_cmd
187        self._env = self._port_obj.setup_environ_for_server('lighttpd')
188        self._mappings = mappings
189
190    def _remove_stale_logs(self):
191        # Sometimes logs are open in other processes but they should clear eventually.
192        for log_prefix in ('access.log-', 'error.log-'):
193            try:
194                self._remove_log_files(self._output_dir, log_prefix)
195            except OSError, e:
196                _log.warning('Failed to remove old %s %s files' % (self._name, log_prefix))
197
198    def _spawn_process(self):
199        _log.debug('Starting %s server, cmd="%s"' % (self._name, self._start_cmd))
200        process = self._executive.popen(self._start_cmd, env=self._env, shell=False, stderr=self._executive.PIPE)
201        pid = process.pid
202        self._filesystem.write_text_file(self._pid_file, str(pid))
203        return pid
204
205    def _stop_running_server(self):
206        # FIXME: It would be nice if we had a cleaner way of killing this process.
207        # Currently we throw away the process object created in _spawn_process,
208        # since there doesn't appear to be any way to kill the server any more
209        # cleanly using it than just killing the pid, and we need to support
210        # killing a pid directly anyway for run-webkit-httpd and run-webkit-websocketserver.
211        self._wait_for_action(self._check_and_kill)
212        if self._filesystem.exists(self._pid_file):
213            self._filesystem.remove(self._pid_file)
214
215    def _check_and_kill(self):
216        if self._executive.check_running_pid(self._pid):
217            host = self._port_obj.host
218            if host.platform.is_win() and not host.platform.is_cygwin():
219                # FIXME: https://bugs.webkit.org/show_bug.cgi?id=106838
220                # We need to kill all of the child processes as well as the
221                # parent, so we can't use executive.kill_process().
222                #
223                # If this is actually working, we should figure out a clean API.
224                self._executive.run_command(["taskkill.exe", "/f", "/t", "/pid", self._pid], error_handler=self._executive.ignore_error)
225            else:
226                self._executive.kill_process(self._pid)
227            return False
228        return True
229