2011-07-05 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / Tools / Scripts / webkitpy / layout_tests / servers / apache_http_server.py
1 #!/usr/bin/env python
2 # Copyright (C) 2011 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 import logging
34 import os
35 import re
36 import sys
37
38 from webkitpy.layout_tests.servers import http_server_base
39
40 _log = logging.getLogger("webkitpy.layout_tests.servers.apache_http_server")
41
42
43 class LayoutTestApacheHttpd(http_server_base.HttpServerBase):
44
45     def __init__(self, port_obj, output_dir):
46         """Args:
47           port_obj: handle to the platform-specific routines
48           output_dir: the absolute path to the layout test result directory
49         """
50         http_server_base.HttpServerBase.__init__(self, port_obj)
51         # We use the name "httpd" instead of "apache" to make our paths (e.g. the pid file: /tmp/WebKit/httpd.pid)
52         # match old-run-webkit-tests: https://bugs.webkit.org/show_bug.cgi?id=63956
53         self._name = 'httpd'
54         self._mappings = [{'port': 8000},
55                           {'port': 8080},
56                           {'port': 8081},
57                           {'port': 8443, 'sslcert': True}]
58         self._output_dir = output_dir
59         port_obj.maybe_make_directory(output_dir)
60
61         self._pid_file = self._filesystem.join(self._runtime_path, '%s.pid' % self._name)
62
63         test_dir = self._port_obj.layout_tests_dir()
64         js_test_resources_dir = self._cygwin_safe_join(test_dir, "fast", "js",
65             "resources")
66         media_resources_dir = self._cygwin_safe_join(test_dir, "media")
67         mime_types_path = self._cygwin_safe_join(test_dir, "http", "conf",
68             "mime.types")
69         cert_file = self._cygwin_safe_join(test_dir, "http", "conf",
70             "webkit-httpd.pem")
71         access_log = self._cygwin_safe_join(output_dir, "access_log.txt")
72         error_log = self._cygwin_safe_join(output_dir, "error_log.txt")
73         document_root = self._cygwin_safe_join(test_dir, "http", "tests")
74
75         # FIXME: We shouldn't be calling a protected method of _port_obj!
76         executable = self._port_obj._path_to_apache()
77         if self._is_cygwin():
78             executable = self._get_cygwin_path(executable)
79
80         start_cmd = [executable,
81             '-f', "\"%s\"" % self._get_apache_config_file_path(test_dir, output_dir),
82             '-C', "\'DocumentRoot \"%s\"\'" % document_root,
83             '-c', "\'Alias /js-test-resources \"%s\"'" % js_test_resources_dir,
84             '-c', "\'Alias /media-resources \"%s\"'" % media_resources_dir,
85             '-C', "\'Listen %s\'" % "127.0.0.1:8000",
86             '-C', "\'Listen %s\'" % "127.0.0.1:8081",
87             '-c', "\'TypesConfig \"%s\"\'" % mime_types_path,
88             '-c', "\'CustomLog \"%s\" common\'" % access_log,
89             '-c', "\'ErrorLog \"%s\"\'" % error_log,
90             '-C', "\'User \"%s\"\'" % os.environ.get("USERNAME",
91                 os.environ.get("USER", "")),
92             '-c', "\'PidFile %s'" % self._pid_file,
93             '-k', "start"]
94
95         stop_cmd = [executable,
96             '-f', "\"%s\"" % self._get_apache_config_file_path(test_dir, output_dir),
97             '-c', "\'PidFile %s'" % self._pid_file,
98             '-k', "stop"]
99
100         if self._is_cygwin():
101             cygbin = self._port_obj._path_from_base('third_party', 'cygwin',
102                 'bin')
103             # Not entirely sure why, but from cygwin we need to run the
104             # httpd command through bash.
105             self._start_cmd = [
106                 os.path.join(cygbin, 'bash.exe'),
107                 '-c',
108                 'PATH=%s %s' % (self._get_cygwin_path(cygbin), " ".join(start_cmd)),
109               ]
110             self._stop_cmd = [
111                 os.path.join(cygbin, 'bash.exe'),
112                 '-c',
113                 'PATH=%s %s' % (self._get_cygwin_path(cygbin), " ".join(stop_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             start_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(start_cmd)
123             self._stop_cmd = " ".join(stop_cmd)
124
125     def _is_cygwin(self):
126         return sys.platform in ("win32", "cygwin")
127
128     def _cygwin_safe_join(self, *parts):
129         """Returns a platform appropriate path."""
130         path = os.path.join(*parts)
131         if self._is_cygwin():
132             return self._get_cygwin_path(path)
133         return path
134
135     def _get_cygwin_path(self, path):
136         """Convert a Windows path to a cygwin path.
137
138         The cygpath utility insists on converting paths that it thinks are
139         Cygwin root paths to what it thinks the correct roots are.  So paths
140         such as "C:\b\slave\webkit-release\build\third_party\cygwin\bin"
141         are converted to plain "/usr/bin".  To avoid this, we
142         do the conversion manually.
143
144         The path is expected to be an absolute path, on any drive.
145         """
146         drive_regexp = re.compile(r'([a-z]):[/\\]', re.IGNORECASE)
147
148         def lower_drive(matchobj):
149             return '/cygdrive/%s/' % matchobj.group(1).lower()
150         path = drive_regexp.sub(lower_drive, path)
151         return path.replace('\\', '/')
152
153     def _get_apache_config_file_path(self, test_dir, output_dir):
154         """Returns the path to the apache config file to use.
155         Args:
156           test_dir: absolute path to the LayoutTests directory.
157           output_dir: absolute path to the layout test results directory.
158         """
159         httpd_config = self._port_obj._path_to_apache_config_file()
160         httpd_config_copy = os.path.join(output_dir, "httpd.conf")
161         httpd_conf = self._filesystem.read_text_file(httpd_config)
162         if self._is_cygwin():
163             # This is a gross hack, but it lets us use the upstream .conf file
164             # and our checked in cygwin. This tells the server the root
165             # directory to look in for .so modules. It will use this path
166             # plus the relative paths to the .so files listed in the .conf
167             # file. We have apache/cygwin checked into our tree so
168             # people don't have to install it into their cygwin.
169             cygusr = self._port_obj._path_from_base('third_party', 'cygwin',
170                 'usr')
171             httpd_conf = httpd_conf.replace('ServerRoot "/usr"',
172                 'ServerRoot "%s"' % self._get_cygwin_path(cygusr))
173
174         self._filesystem.write_text_file(httpd_config_copy, httpd_conf)
175
176         if self._is_cygwin():
177             return self._get_cygwin_path(httpd_config_copy)
178         return httpd_config_copy
179
180     def _spawn_process(self):
181         _log.debug('Starting %s server, cmd="%s"' % (self._name, str(self._start_cmd)))
182         retval, err = self._run(self._start_cmd)
183         if retval or len(err):
184             raise http_server_base.ServerError('Failed to start %s: %s' % (self._name, err))
185
186         # For some reason apache isn't guaranteed to have created the pid file before
187         # the process exits, so we wait a little while longer.
188         if not self._wait_for_action(lambda: self._filesystem.exists(self._pid_file)):
189             raise http_server_base.ServerError('Failed to start %s: no pid file found' % self._name)
190
191         return int(self._filesystem.read_text_file(self._pid_file))
192
193     def _stop_running_server(self):
194         retval, err = self._run(self._stop_cmd)
195         if retval or len(err):
196             raise http_server_base.ServerError('Failed to stop %s: %s' % (self._name, err))
197
198         # For some reason apache isn't guaranteed to have actually stopped after
199         # the stop command returns, so we wait a little while longer for the
200         # pid file to be removed.
201         if not self._wait_for_action(lambda: not self._filesystem.exists(self._pid_file)):
202             raise http_server_base.ServerError('Failed to stop %s: pid file still exists' % self._name)
203
204     def _run(self, cmd):
205         # Use shell=True because we join the arguments into a string for
206         # the sake of Window/Cygwin and it needs quoting that breaks
207         # shell=False.
208         # FIXME: We should not need to be joining shell arguments into strings.
209         # shell=True is a trail of tears.
210         # Note: Not thread safe: http://bugs.python.org/issue2320
211         process = self._executive.popen(cmd, shell=True, stderr=self._executive.PIPE)
212         process.wait()
213         retval = process.returncode
214         err = process.stderr.read()
215         return (retval, err)