Home | History | Annotate | Download | only in testserver
      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