2010-04-28 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / layout_tests / port / websocket_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 PyWebSocket server used by layout tests."""
31
32
33 from __future__ import with_statement
34
35 import codecs
36 import logging
37 import optparse
38 import os
39 import subprocess
40 import sys
41 import tempfile
42 import time
43 import urllib
44
45 import factory
46 import http_server
47
48 from webkitpy.common.system.executive import Executive
49
50 _log = logging.getLogger("webkitpy.layout_tests.port.websocket_server")
51
52 _WS_LOG_PREFIX = 'pywebsocket.ws.log-'
53 _WSS_LOG_PREFIX = 'pywebsocket.wss.log-'
54
55 _DEFAULT_WS_PORT = 8880
56 _DEFAULT_WSS_PORT = 9323
57
58
59 def url_is_alive(url):
60     """Checks to see if we get an http response from |url|.
61     We poll the url 5 times with a 1 second delay.  If we don't
62     get a reply in that time, we give up and assume the httpd
63     didn't start properly.
64
65     Args:
66       url: The URL to check.
67     Return:
68       True if the url is alive.
69     """
70     sleep_time = 0.5
71     wait_time = 5
72     while wait_time > 0:
73         try:
74             response = urllib.urlopen(url)
75             # Server is up and responding.
76             return True
77         except IOError:
78             pass
79         # Wait for sleep_time before trying again.
80         wait_time -= sleep_time
81         time.sleep(sleep_time)
82
83     return False
84
85
86 class PyWebSocketNotStarted(Exception):
87     pass
88
89
90 class PyWebSocketNotFound(Exception):
91     pass
92
93
94 class PyWebSocket(http_server.Lighttpd):
95
96     def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT,
97                  root=None, use_tls=False,
98                  register_cygwin=True,
99                  pidfile=None):
100         """Args:
101           output_dir: the absolute path to the layout test result directory
102         """
103         http_server.Lighttpd.__init__(self, port_obj, output_dir,
104                                       port=_DEFAULT_WS_PORT,
105                                       root=root,
106                                       register_cygwin=register_cygwin)
107         self._output_dir = output_dir
108         self._process = None
109         self._port = port
110         self._root = root
111         self._use_tls = use_tls
112         self._private_key = self._pem_file
113         self._certificate = self._pem_file
114         if self._port:
115             self._port = int(self._port)
116         if self._use_tls:
117             self._server_name = 'PyWebSocket(Secure)'
118         else:
119             self._server_name = 'PyWebSocket'
120         self._pidfile = pidfile
121         self._wsout = None
122
123         # Webkit tests
124         if self._root:
125             self._layout_tests = os.path.abspath(self._root)
126             self._web_socket_tests = os.path.abspath(
127                 os.path.join(self._root, 'websocket', 'tests'))
128         else:
129             try:
130                 self._layout_tests = self._port_obj.layout_tests_dir()
131                 self._web_socket_tests = os.path.join(self._layout_tests,
132                      'websocket', 'tests')
133             except:
134                 self._web_socket_tests = None
135
136     def start(self):
137         if not self._web_socket_tests:
138             _log.info('No need to start %s server.' % self._server_name)
139             return
140         if self.is_running():
141             raise PyWebSocketNotStarted('%s is already running.' %
142                                         self._server_name)
143
144         time_str = time.strftime('%d%b%Y-%H%M%S')
145         if self._use_tls:
146             log_prefix = _WSS_LOG_PREFIX
147         else:
148             log_prefix = _WS_LOG_PREFIX
149         log_file_name = log_prefix + time_str
150
151         # Remove old log files. We only need to keep the last ones.
152         self.remove_log_files(self._output_dir, log_prefix)
153
154         error_log = os.path.join(self._output_dir, log_file_name + "-err.txt")
155
156         output_log = os.path.join(self._output_dir, log_file_name + "-out.txt")
157         self._wsout = codecs.open(output_log, "w", "utf-8")
158
159         python_interp = sys.executable
160         pywebsocket_base = os.path.join(
161             os.path.dirname(os.path.dirname(os.path.dirname(
162             os.path.abspath(__file__)))), 'thirdparty', 'pywebsocket')
163         pywebsocket_script = os.path.join(pywebsocket_base, 'mod_pywebsocket',
164             'standalone.py')
165         start_cmd = [
166             python_interp, pywebsocket_script,
167             '--server-host', '127.0.0.1',
168             '--port', str(self._port),
169             '--document-root', self._layout_tests,
170             '--scan-dir', self._web_socket_tests,
171             '--cgi-paths', '/websocket/tests',
172             '--log-file', error_log,
173         ]
174
175         handler_map_file = os.path.join(self._web_socket_tests,
176                                         'handler_map.txt')
177         if os.path.exists(handler_map_file):
178             _log.debug('Using handler_map_file: %s' % handler_map_file)
179             start_cmd.append('--websock-handlers-map-file')
180             start_cmd.append(handler_map_file)
181         else:
182             _log.warning('No handler_map_file found')
183
184         if self._use_tls:
185             start_cmd.extend(['-t', '-k', self._private_key,
186                               '-c', self._certificate])
187
188         # Put the cygwin directory first in the path to find cygwin1.dll
189         env = os.environ
190         if sys.platform in ('cygwin', 'win32'):
191             env['PATH'] = '%s;%s' % (
192                 self._port_obj.path_from_chromium_base('third_party',
193                                                        'cygwin', 'bin'),
194                 env['PATH'])
195             env['CYGWIN_PATH'] = self._port_obj.path_from_chromium_base(
196                 'third_party', 'cygwin', 'bin')
197
198         if sys.platform == 'win32' and self._register_cygwin:
199             setup_mount = self._port_obj.path_from_chromium_base(
200                 'third_party', 'cygwin', 'setup_mount.bat')
201             subprocess.Popen(setup_mount).wait()
202
203         env['PYTHONPATH'] = (pywebsocket_base + os.path.pathsep +
204                              env.get('PYTHONPATH', ''))
205
206         _log.debug('Starting %s server on %d.' % (
207                    self._server_name, self._port))
208         _log.debug('cmdline: %s' % ' '.join(start_cmd))
209         # FIXME: We should direct this call through Executive for testing.
210         # Note: Not thread safe: http://bugs.python.org/issue2320
211         self._process = subprocess.Popen(start_cmd,
212                                          stdin=open(os.devnull, 'r'),
213                                          stdout=self._wsout,
214                                          stderr=subprocess.STDOUT,
215                                          env=env)
216
217         if self._use_tls:
218             url = 'https'
219         else:
220             url = 'http'
221         url = url + '://127.0.0.1:%d/' % self._port
222         if not url_is_alive(url):
223             fp = codecs.open(output_log, "utf-8")
224             try:
225                 for line in fp:
226                     _log.error(line)
227             finally:
228                 fp.close()
229             raise PyWebSocketNotStarted(
230                 'Failed to start %s server on port %s.' %
231                     (self._server_name, self._port))
232
233         # Our process terminated already
234         if self._process.returncode != None:
235             raise PyWebSocketNotStarted(
236                 'Failed to start %s server.' % self._server_name)
237         if self._pidfile:
238             with codecs.open(self._pidfile, "w", "ascii") as file:
239                 file.write("%d" % self._process.pid)
240
241     def stop(self, force=False):
242         if not force and not self.is_running():
243             return
244
245         pid = None
246         if self._process:
247             pid = self._process.pid
248         elif self._pidfile:
249             with codecs.open(self._pidfile, "r", "ascii") as file:
250                 pid = int(file.read().strip())
251
252         if not pid:
253             raise PyWebSocketNotFound(
254                 'Failed to find %s server pid.' % self._server_name)
255
256         _log.debug('Shutting down %s server %d.' % (self._server_name, pid))
257         # FIXME: We should use a non-static Executive for easier testing.
258         Executive().kill_process(pid)
259
260         if self._process:
261             # wait() is not threadsafe and can throw OSError due to:
262             # http://bugs.python.org/issue1731717
263             self._process.wait()
264             self._process = None
265
266         if self._wsout:
267             self._wsout.close()
268             self._wsout = None