1# Copyright 2013 The Chromium Authors 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from six.moves import BaseHTTPServer 6import errno 7import json 8import optparse 9import os 10import re 11import socket 12from six.moves import socketserver as SocketServer 13import struct 14import sys 15import warnings 16 17# Ignore deprecation warnings, they make our output more cluttered. 18warnings.filterwarnings("ignore", category=DeprecationWarning) 19 20if sys.platform == 'win32': 21 import msvcrt 22 23# Using debug() seems to cause hangs on XP: see http://crbug.com/64515. 24debug_output = sys.stderr 25def debug(string): 26 debug_output.write(string + "\n") 27 debug_output.flush() 28 29 30class Error(Exception): 31 """Error class for this module.""" 32 33 34class OptionError(Error): 35 """Error for bad command line options.""" 36 37 38class 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 58class 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 66class 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 if err == errno.ECONNRESET: 85 print("testserver.py: Connection reset by peer") 86 return 87 SocketServer.BaseServer.handle_error(self, request, client_address) 88 89 90class StoppableHTTPServer(BaseHTTPServer.HTTPServer): 91 """This is a specialization of BaseHTTPServer to allow it 92 to be exited cleanly (by setting its "stop" member to True).""" 93 94 def serve_forever(self): 95 self.stop = False 96 self.nonce_time = None 97 while not self.stop: 98 self.handle_request() 99 self.socket.close() 100 101 102def MultiplexerHack(std_fd, log_fd): 103 """Creates a FileMultiplexer that will write to both specified files. 104 105 When running on Windows XP bots, stdout and stderr will be invalid file 106 handles, so log_fd will be returned directly. (This does not occur if you 107 run the test suite directly from a console, but only if the output of the 108 test executable is redirected.) 109 """ 110 if std_fd.fileno() <= 0: 111 return log_fd 112 return FileMultiplexer(std_fd, log_fd) 113 114 115class BasePageHandler(BaseHTTPServer.BaseHTTPRequestHandler): 116 117 def __init__(self, request, client_address, socket_server, 118 connect_handlers, get_handlers, head_handlers, post_handlers, 119 put_handlers): 120 self._connect_handlers = connect_handlers 121 self._get_handlers = get_handlers 122 self._head_handlers = head_handlers 123 self._post_handlers = post_handlers 124 self._put_handlers = put_handlers 125 BaseHTTPServer.BaseHTTPRequestHandler.__init__( 126 self, request, client_address, socket_server) 127 128 def log_request(self, *args, **kwargs): 129 # Disable request logging to declutter test log output. 130 pass 131 132 def _ShouldHandleRequest(self, handler_name): 133 """Determines if the path can be handled by the handler. 134 135 We consider a handler valid if the path begins with the 136 handler name. It can optionally be followed by "?*", "/*". 137 """ 138 139 pattern = re.compile('%s($|\?|/).*' % handler_name) 140 return pattern.match(self.path) 141 142 def do_CONNECT(self): 143 for handler in self._connect_handlers: 144 if handler(): 145 return 146 147 def do_GET(self): 148 for handler in self._get_handlers: 149 if handler(): 150 return 151 152 def do_HEAD(self): 153 for handler in self._head_handlers: 154 if handler(): 155 return 156 157 def do_POST(self): 158 for handler in self._post_handlers: 159 if handler(): 160 return 161 162 def do_PUT(self): 163 for handler in self._put_handlers: 164 if handler(): 165 return 166 167 168class TestServerRunner(object): 169 """Runs a test server and communicates with the controlling C++ test code. 170 171 Subclasses should override the create_server method to create their server 172 object, and the add_options method to add their own options. 173 """ 174 175 def __init__(self): 176 self.option_parser = optparse.OptionParser() 177 self.add_options() 178 179 def main(self): 180 self.options, self.args = self.option_parser.parse_args() 181 182 logfile = open(self.options.log_file, 'w') 183 184 # http://crbug.com/248796 : Error logs streamed to normal sys.stderr will be 185 # written to HTTP response payload when remote test server is used. 186 # For this reason, some tests like ResourceFetcherTests.ResourceFetcher404 187 # were failing on Android because remote test server is being used there. 188 # To fix them, we need to use sys.stdout as sys.stderr if remote test server 189 # is used. 190 if self.options.on_remote_server: 191 sys.stderr = sys.stdout 192 193 sys.stderr = MultiplexerHack(sys.stderr, logfile) 194 if self.options.log_to_console: 195 sys.stdout = MultiplexerHack(sys.stdout, logfile) 196 else: 197 sys.stdout = logfile 198 199 server_data = { 200 'host': self.options.host, 201 } 202 self.server = self.create_server(server_data) 203 self._notify_startup_complete(server_data) 204 self.run_server() 205 206 def create_server(self, server_data): 207 """Creates a server object and returns it. 208 209 Must populate server_data['port'], and can set additional server_data 210 elements if desired.""" 211 raise NotImplementedError() 212 213 def run_server(self): 214 try: 215 self.server.serve_forever() 216 except KeyboardInterrupt: 217 print('shutting down server') 218 self.server.stop = True 219 220 def add_options(self): 221 self.option_parser.add_option('--startup-pipe', type='int', 222 dest='startup_pipe', 223 help='File handle of pipe to parent process') 224 self.option_parser.add_option('--log-to-console', action='store_const', 225 const=True, default=False, 226 dest='log_to_console', 227 help='Enables or disables sys.stdout logging ' 228 'to the console.') 229 self.option_parser.add_option('--log-file', default='testserver.log', 230 dest='log_file', 231 help='The name of the server log file.') 232 self.option_parser.add_option('--port', default=0, type='int', 233 help='Port used by the server. If ' 234 'unspecified, the server will listen on an ' 235 'ephemeral port.') 236 self.option_parser.add_option('--host', default='127.0.0.1', 237 dest='host', 238 help='Hostname or IP upon which the server ' 239 'will listen. Client connections will also ' 240 'only be allowed from this address.') 241 self.option_parser.add_option('--data-dir', dest='data_dir', 242 help='Directory from which to read the ' 243 'files.') 244 self.option_parser.add_option('--on-remote-server', action='store_const', 245 const=True, default=False, 246 dest='on_remote_server', 247 help='Whether remote server is being used or ' 248 'not.') 249 250 def _notify_startup_complete(self, server_data): 251 # Notify the parent that we've started. (BaseServer subclasses 252 # bind their sockets on construction.) 253 if self.options.startup_pipe is not None: 254 server_data_json = json.dumps(server_data).encode() 255 server_data_len = len(server_data_json) 256 print('sending server_data: %s (%d bytes)' % 257 (server_data_json, server_data_len)) 258 if sys.platform == 'win32': 259 fd = msvcrt.open_osfhandle(self.options.startup_pipe, 0) 260 else: 261 fd = self.options.startup_pipe 262 startup_pipe = os.fdopen(fd, "wb") 263 # First write the data length as an unsigned 4-byte value. This 264 # is _not_ using network byte ordering since the other end of the 265 # pipe is on the same machine. 266 startup_pipe.write(struct.pack('=L', server_data_len)) 267 startup_pipe.write(server_data_json) 268 startup_pipe.close() 269