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