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