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 33from __future__ import with_statement 34 35import codecs 36import logging 37import optparse 38import os 39import subprocess 40import sys 41import tempfile 42import time 43import urllib 44 45import factory 46import http_server 47 48from webkitpy.common.system.executive import Executive 49from 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 61def 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 88class PyWebSocketNotStarted(Exception): 89 pass 90 91 92class PyWebSocketNotFound(Exception): 93 pass 94 95 96class 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