1 # Copyright 2013 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import BaseHTTPServer 6 import errno 7 import json 8 import optparse 9 import os 10 import re 11 import socket 12 import SocketServer 13 import struct 14 import sys 15 import warnings 16 17 # Ignore deprecation warnings, they make our output more cluttered. 18 warnings.filterwarnings("ignore", category=DeprecationWarning) 19 20 if sys.platform == 'win32': 21 import msvcrt 22 23 # Using debug() seems to cause hangs on XP: see http://crbug.com/64515. 24 debug_output = sys.stderr 25 def debug(string): 26 debug_output.write(string + "\n") 27 debug_output.flush() 28 29 30 class Error(Exception): 31 """Error class for this module.""" 32 33 34 class OptionError(Error): 35 """Error for bad command line options.""" 36 37 38 class FileMultiplexer(object): 39 def __init__(self, fd1, fd2) : 40 self.__fd1 = fd1 41 self.__fd2 = fd2 42 43 def __del__(self) : 44 if self.__fd1 != sys.stdout and self.__fd1 != sys.stderr: 45 self.__fd1.close() 46 if self.__fd2 != sys.stdout and self.__fd2 != sys.stderr: 47 self.__fd2.close() 48 49 def write(self, text) : 50 self.__fd1.write(text) 51 self.__fd2.write(text) 52 53 def flush(self) : 54 self.__fd1.flush() 55 self.__fd2.flush() 56 57 58 class ClientRestrictingServerMixIn: 59 """Implements verify_request to limit connections to our configured IP 60 address.""" 61 62 def verify_request(self, _request, client_address): 63 return client_address[0] == self.server_address[0] 64 65 66 class BrokenPipeHandlerMixIn: 67 """Allows the server to deal with "broken pipe" errors (which happen if the 68 browser quits with outstanding requests, like for the favicon). This mix-in 69 requires the class to derive from SocketServer.BaseServer and not override its 70 handle_error() method. """ 71 72 def handle_error(self, request, client_address): 73 value = sys.exc_info()[1] 74 if isinstance(value, socket.error): 75 err = value.args[0] 76 if sys.platform in ('win32', 'cygwin'): 77 # "An established connection was aborted by the software in your host." 78 pipe_err = 10053 79 else: 80 pipe_err = errno.EPIPE 81 if err == pipe_err: 82 print "testserver.py: Broken pipe" 83 return 84 SocketServer.BaseServer.handle_error(self, request, client_address) 85 86 87 class StoppableHTTPServer(BaseHTTPServer.HTTPServer): 88 """This is a specialization of BaseHTTPServer to allow it 89 to be exited cleanly (by setting its "stop" member to True).""" 90 91 def serve_forever(self): 92 self.stop = False 93 self.nonce_time = None 94 while not self.stop: 95 self.handle_request() 96 self.socket.close() 97 98 99 def MultiplexerHack(std_fd, log_fd): 100 """Creates a FileMultiplexer that will write to both specified files. 101 102 When running on Windows XP bots, stdout and stderr will be invalid file 103 handles, so log_fd will be returned directly. (This does not occur if you 104 run the test suite directly from a console, but only if the output of the 105 test executable is redirected.) 106 """ 107 if std_fd.fileno() <= 0: 108 return log_fd 109 return FileMultiplexer(std_fd, log_fd) 110 111 112 class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler): 113 114 def __init__(self, request, client_address, socket_server, 115 connect_handlers, get_handlers, head_handlers, post_handlers, 116 put_handlers): 117 self._connect_handlers = connect_handlers 118 self._get_handlers = get_handlers 119 self._head_handlers = head_handlers 120 self._post_handlers = post_handlers 121 self._put_handlers = put_handlers 122 BaseHTTPServer.BaseHTTPRequestHandler.__init__( 123 self, request, client_address, socket_server) 124 125 def log_request(self, *args, **kwargs): 126 # Disable request logging to declutter test log output. 127 pass 128 129 def _ShouldHandleRequest(self, handler_name): 130 """Determines if the path can be handled by the handler. 131 132 We consider a handler valid if the path begins with the 133 handler name. It can optionally be followed by "?*", "/*". 134 """ 135 136 pattern = re.compile('%s($|\?|/).*' % handler_name) 137 return pattern.match(self.path) 138 139 def do_CONNECT(self): 140 for handler in self._connect_handlers: 141 if handler(): 142 return 143 144 def do_GET(self): 145 for handler in self._get_handlers: 146 if handler(): 147 return 148 149 def do_HEAD(self): 150 for handler in self._head_handlers: 151 if handler(): 152 return 153 154 def do_POST(self): 155 for handler in self._post_handlers: 156 if handler(): 157 return 158 159 def do_PUT(self): 160 for handler in self._put_handlers: 161 if handler(): 162 return 163 164 165 class TestServerRunner(object): 166 """Runs a test server and communicates with the controlling C++ test code. 167 168 Subclasses should override the create_server method to create their server 169 object, and the add_options method to add their own options. 170 """ 171 172 def __init__(self): 173 self.option_parser = optparse.OptionParser() 174 self.add_options() 175 176 def main(self): 177 self.options, self.args = self.option_parser.parse_args() 178 179 logfile = open(self.options.log_file, 'w') 180 sys.stderr = MultiplexerHack(sys.stderr, logfile) 181 if self.options.log_to_console: 182 sys.stdout = MultiplexerHack(sys.stdout, logfile) 183 else: 184 sys.stdout = logfile 185 186 server_data = { 187 'host': self.options.host, 188 } 189 self.server = self.create_server(server_data) 190 self._notify_startup_complete(server_data) 191 self.run_server() 192 193 def create_server(self, server_data): 194 """Creates a server object and returns it. 195 196 Must populate server_data['port'], and can set additional server_data 197 elements if desired.""" 198 raise NotImplementedError() 199 200 def run_server(self): 201 try: 202 self.server.serve_forever() 203 except KeyboardInterrupt: 204 print 'shutting down server' 205 self.server.stop = True 206 207 def add_options(self): 208 self.option_parser.add_option('--startup-pipe', type='int', 209 dest='startup_pipe', 210 help='File handle of pipe to parent process') 211 self.option_parser.add_option('--log-to-console', action='store_const', 212 const=True, default=False, 213 dest='log_to_console', 214 help='Enables or disables sys.stdout logging ' 215 'to the console.') 216 self.option_parser.add_option('--log-file', default='testserver.log', 217 dest='log_file', 218 help='The name of the server log file.') 219 self.option_parser.add_option('--port', default=0, type='int', 220 help='Port used by the server. If ' 221 'unspecified, the server will listen on an ' 222 'ephemeral port.') 223 self.option_parser.add_option('--host', default='127.0.0.1', 224 dest='host', 225 help='Hostname or IP upon which the server ' 226 'will listen. Client connections will also ' 227 'only be allowed from this address.') 228 self.option_parser.add_option('--data-dir', dest='data_dir', 229 help='Directory from which to read the ' 230 'files.') 231 232 def _notify_startup_complete(self, server_data): 233 # Notify the parent that we've started. (BaseServer subclasses 234 # bind their sockets on construction.) 235 if self.options.startup_pipe is not None: 236 server_data_json = json.dumps(server_data) 237 server_data_len = len(server_data_json) 238 print 'sending server_data: %s (%d bytes)' % ( 239 server_data_json, server_data_len) 240 if sys.platform == 'win32': 241 fd = msvcrt.open_osfhandle(self.options.startup_pipe, 0) 242 else: 243 fd = self.options.startup_pipe 244 startup_pipe = os.fdopen(fd, "w") 245 # First write the data length as an unsigned 4-byte value. This 246 # is _not_ using network byte ordering since the other end of the 247 # pipe is on the same machine. 248 startup_pipe.write(struct.pack('=L', server_data_len)) 249 startup_pipe.write(server_data_json) 250 startup_pipe.close() 251