media/webrtc/trunk/build/android/pylib/chrome_test_server_spawner.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 # Copyright (c) 2012 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.
     5 """A "Test Server Spawner" that handles killing/stopping per-test test servers.
     7 It's used to accept requests from the device to spawn and kill instances of the
     8 chrome test server on the host.
     9 """
    11 import BaseHTTPServer
    12 import json
    13 import logging
    14 import os
    15 import select
    16 import struct
    17 import subprocess
    18 import threading
    19 import time
    20 import urlparse
    22 import constants
    23 from forwarder import Forwarder
    24 import ports
    27 # Path that are needed to import necessary modules when running testserver.py.
    28 os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + ':%s:%s:%s:%s' % (
    29     os.path.join(constants.CHROME_DIR, 'third_party'),
    30     os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'),
    31     os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'),
    32     os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver'))
    35 SERVER_TYPES = {
    36     'http': '',
    37     'ftp': '-f',
    38     'sync': '--sync',
    39     'tcpecho': '--tcp-echo',
    40     'udpecho': '--udp-echo',
    41 }
    44 # The timeout (in seconds) of starting up the Python test server.
    45 TEST_SERVER_STARTUP_TIMEOUT = 10
    48 def _CheckPortStatus(port, expected_status):
    49   """Returns True if port has expected_status.
    51   Args:
    52     port: the port number.
    53     expected_status: boolean of expected status.
    55   Returns:
    56     Returns True if the status is expected. Otherwise returns False.
    57   """
    58   for timeout in range(1, 5):
    59     if ports.IsHostPortUsed(port) == expected_status:
    60       return True
    61     time.sleep(timeout)
    62   return False
    65 def _GetServerTypeCommandLine(server_type):
    66   """Returns the command-line by the given server type.
    68   Args:
    69     server_type: the server type to be used (e.g. 'http').
    71   Returns:
    72     A string containing the command-line argument.
    73   """
    74   if server_type not in SERVER_TYPES:
    75     raise NotImplementedError('Unknown server type: %s' % server_type)
    76   if server_type == 'udpecho':
    77     raise Exception('Please do not run UDP echo tests because we do not have '
    78                     'a UDP forwarder tool.')
    79   return SERVER_TYPES[server_type]
    82 class TestServerThread(threading.Thread):
    83   """A thread to run the test server in a separate process."""
    85   def __init__(self, ready_event, arguments, adb, tool, build_type):
    86     """Initialize TestServerThread with the following argument.
    88     Args:
    89       ready_event: event which will be set when the test server is ready.
    90       arguments: dictionary of arguments to run the test server.
    91       adb: instance of AndroidCommands.
    92       tool: instance of runtime error detection tool.
    93       build_type: 'Release' or 'Debug'.
    94     """
    95     threading.Thread.__init__(self)
    96     self.wait_event = threading.Event()
    97     self.stop_flag = False
    98     self.ready_event = ready_event
    99     self.ready_event.clear()
   100     self.arguments = arguments
   101     self.adb = adb
   102     self.tool = tool
   103     self.test_server_process = None
   104     self.is_ready = False
   105     self.host_port = self.arguments['port']
   106     assert isinstance(self.host_port, int)
   107     self._test_server_forwarder = None
   108     # The forwarder device port now is dynamically allocated.
   109     self.forwarder_device_port = 0
   110     # Anonymous pipe in order to get port info from test server.
   111     self.pipe_in = None
   112     self.pipe_out = None
   113     self.command_line = []
   114     self.build_type = build_type
   116   def _WaitToStartAndGetPortFromTestServer(self):
   117     """Waits for the Python test server to start and gets the port it is using.
   119     The port information is passed by the Python test server with a pipe given
   120     by self.pipe_out. It is written as a result to |self.host_port|.
   122     Returns:
   123       Whether the port used by the test server was successfully fetched.
   124     """
   125     assert self.host_port == 0 and self.pipe_out and self.pipe_in
   126     (in_fds, _, _) = select.select([self.pipe_in, ], [], [],
   127                                    TEST_SERVER_STARTUP_TIMEOUT)
   128     if len(in_fds) == 0:
   129       logging.error('Failed to wait to the Python test server to be started.')
   130       return False
   131     # First read the data length as an unsigned 4-byte value.  This
   132     # is _not_ using network byte ordering since the Python test server packs
   133     # size as native byte order and all Chromium platforms so far are
   134     # configured to use little-endian.
   135     # TODO(jnd): Change the Python test server and local_test_server_*.cc to
   136     # use a unified byte order (either big-endian or little-endian).
   137     data_length = os.read(self.pipe_in, struct.calcsize('=L'))
   138     if data_length:
   139       (data_length,) = struct.unpack('=L', data_length)
   140       assert data_length
   141     if not data_length:
   142       logging.error('Failed to get length of server data.')
   143       return False
   144     port_json = os.read(self.pipe_in, data_length)
   145     if not port_json:
   146       logging.error('Failed to get server data.')
   147       return False
   148     logging.info('Got port json data: %s', port_json)
   149     port_json = json.loads(port_json)
   150     if port_json.has_key('port') and isinstance(port_json['port'], int):
   151       self.host_port = port_json['port']
   152       return _CheckPortStatus(self.host_port, True)
   153     logging.error('Failed to get port information from the server data.')
   154     return False
   156   def _GenerateCommandLineArguments(self):
   157     """Generates the command line to run the test server.
   159     Note that all options are processed by following the definitions in
   160     testserver.py.
   161     """
   162     if self.command_line:
   163       return
   164     # The following arguments must exist.
   165     type_cmd = _GetServerTypeCommandLine(self.arguments['server-type'])
   166     if type_cmd:
   167       self.command_line.append(type_cmd)
   168     self.command_line.append('--port=%d' % self.host_port)
   169     # Use a pipe to get the port given by the instance of Python test server
   170     # if the test does not specify the port.
   171     if self.host_port == 0:
   172       (self.pipe_in, self.pipe_out) = os.pipe()
   173       self.command_line.append('--startup-pipe=%d' % self.pipe_out)
   174     self.command_line.append('--host=%s' % self.arguments['host'])
   175     data_dir = self.arguments['data-dir'] or 'chrome/test/data'
   176     if not os.path.isabs(data_dir):
   177       data_dir = os.path.join(constants.CHROME_DIR, data_dir)
   178     self.command_line.append('--data-dir=%s' % data_dir)
   179     # The following arguments are optional depending on the individual test.
   180     if self.arguments.has_key('log-to-console'):
   181       self.command_line.append('--log-to-console')
   182     if self.arguments.has_key('auth-token'):
   183       self.command_line.append('--auth-token=%s' % self.arguments['auth-token'])
   184     if self.arguments.has_key('https'):
   185       self.command_line.append('--https')
   186       if self.arguments.has_key('cert-and-key-file'):
   187         self.command_line.append('--cert-and-key-file=%s' % os.path.join(
   188             constants.CHROME_DIR, self.arguments['cert-and-key-file']))
   189       if self.arguments.has_key('ocsp'):
   190         self.command_line.append('--ocsp=%s' % self.arguments['ocsp'])
   191       if self.arguments.has_key('https-record-resume'):
   192         self.command_line.append('--https-record-resume')
   193       if self.arguments.has_key('ssl-client-auth'):
   194         self.command_line.append('--ssl-client-auth')
   195       if self.arguments.has_key('tls-intolerant'):
   196         self.command_line.append('--tls-intolerant=%s' %
   197                                  self.arguments['tls-intolerant'])
   198       if self.arguments.has_key('ssl-client-ca'):
   199         for ca in self.arguments['ssl-client-ca']:
   200           self.command_line.append('--ssl-client-ca=%s' %
   201                                    os.path.join(constants.CHROME_DIR, ca))
   202       if self.arguments.has_key('ssl-bulk-cipher'):
   203         for bulk_cipher in self.arguments['ssl-bulk-cipher']:
   204           self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher)
   206   def run(self):
   207     logging.info('Start running the thread!')
   208     self.wait_event.clear()
   209     self._GenerateCommandLineArguments()
   210     command = [os.path.join(constants.CHROME_DIR, 'net', 'tools',
   211                             'testserver', 'testserver.py')] + self.command_line
   212     logging.info('Running: %s', command)
   213     self.process = subprocess.Popen(command)
   214     if self.process:
   215       if self.pipe_out:
   216         self.is_ready = self._WaitToStartAndGetPortFromTestServer()
   217       else:
   218         self.is_ready = _CheckPortStatus(self.host_port, True)
   219     if self.is_ready:
   220       self._test_server_forwarder = Forwarder(
   221           self.adb, [(0, self.host_port)], self.tool, '127.0.0.1',
   222           self.build_type)
   223       # Check whether the forwarder is ready on the device.
   224       self.is_ready = False
   225       device_port = self._test_server_forwarder.DevicePortForHostPort(
   226           self.host_port)
   227       if device_port:
   228         for timeout in range(1, 5):
   229           if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'):
   230             self.is_ready = True
   231             self.forwarder_device_port = device_port
   232             break
   233           time.sleep(timeout)
   234     # Wake up the request handler thread.
   235     self.ready_event.set()
   236     # Keep thread running until Stop() gets called.
   237     while not self.stop_flag:
   238       time.sleep(1)
   239     if self.process.poll() is None:
   240       self.process.kill()
   241     if self._test_server_forwarder:
   242       self._test_server_forwarder.Close()
   243     self.process = None
   244     self.is_ready = False
   245     if self.pipe_out:
   246       os.close(self.pipe_in)
   247       os.close(self.pipe_out)
   248       self.pipe_in = None
   249       self.pipe_out = None
   250     logging.info('Test-server has died.')
   251     self.wait_event.set()
   253   def Stop(self):
   254     """Blocks until the loop has finished.
   256     Note that this must be called in another thread.
   257     """
   258     if not self.process:
   259       return
   260     self.stop_flag = True
   261     self.wait_event.wait()
   264 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
   265   """A handler used to process http GET/POST request."""
   267   def _SendResponse(self, response_code, response_reason, additional_headers,
   268                     contents):
   269     """Generates a response sent to the client from the provided parameters.
   271     Args:
   272       response_code: number of the response status.
   273       response_reason: string of reason description of the response.
   274       additional_headers: dict of additional headers. Each key is the name of
   275                           the header, each value is the content of the header.
   276       contents: string of the contents we want to send to client.
   277     """
   278     self.send_response(response_code, response_reason)
   279     self.send_header('Content-Type', 'text/html')
   280     # Specify the content-length as without it the http(s) response will not
   281     # be completed properly (and the browser keeps expecting data).
   282     self.send_header('Content-Length', len(contents))
   283     for header_name in additional_headers:
   284       self.send_header(header_name, additional_headers[header_name])
   285     self.end_headers()
   286     self.wfile.write(contents)
   287     self.wfile.flush()
   289   def _StartTestServer(self):
   290     """Starts the test server thread."""
   291     logging.info('Handling request to spawn a test server.')
   292     content_type = self.headers.getheader('content-type')
   293     if content_type != 'application/json':
   294       raise Exception('Bad content-type for start request.')
   295     content_length = self.headers.getheader('content-length')
   296     if not content_length:
   297       content_length = 0
   298     try:
   299       content_length = int(content_length)
   300     except:
   301       raise Exception('Bad content-length for start request.')
   302     logging.info(content_length)
   303     test_server_argument_json = self.rfile.read(content_length)
   304     logging.info(test_server_argument_json)
   305     assert not self.server.test_server_instance
   306     ready_event = threading.Event()
   307     self.server.test_server_instance = TestServerThread(
   308         ready_event,
   309         json.loads(test_server_argument_json),
   310         self.server.adb,
   311         self.server.tool,
   312         self.server.build_type)
   313     self.server.test_server_instance.setDaemon(True)
   314     self.server.test_server_instance.start()
   315     ready_event.wait()
   316     if self.server.test_server_instance.is_ready:
   317       self._SendResponse(200, 'OK', {}, json.dumps(
   318           {'port': self.server.test_server_instance.forwarder_device_port,
   319            'message': 'started'}))
   320       logging.info('Test server is running on port: %d.',
   321                    self.server.test_server_instance.host_port)
   322     else:
   323       self.server.test_server_instance.Stop()
   324       self.server.test_server_instance = None
   325       self._SendResponse(500, 'Test Server Error.', {}, '')
   326       logging.info('Encounter problem during starting a test server.')
   328   def _KillTestServer(self):
   329     """Stops the test server instance."""
   330     # There should only ever be one test server at a time. This may do the
   331     # wrong thing if we try and start multiple test servers.
   332     if not self.server.test_server_instance:
   333       return
   334     port = self.server.test_server_instance.host_port
   335     logging.info('Handling request to kill a test server on port: %d.', port)
   336     self.server.test_server_instance.Stop()
   337     # Make sure the status of test server is correct before sending response.
   338     if _CheckPortStatus(port, False):
   339       self._SendResponse(200, 'OK', {}, 'killed')
   340       logging.info('Test server on port %d is killed', port)
   341     else:
   342       self._SendResponse(500, 'Test Server Error.', {}, '')
   343       logging.info('Encounter problem during killing a test server.')
   344     self.server.test_server_instance = None
   346   def do_POST(self):
   347     parsed_path = urlparse.urlparse(self.path)
   348     action = parsed_path.path
   349     logging.info('Action for POST method is: %s.', action)
   350     if action == '/start':
   351       self._StartTestServer()
   352     else:
   353       self._SendResponse(400, 'Unknown request.', {}, '')
   354       logging.info('Encounter unknown request: %s.', action)
   356   def do_GET(self):
   357     parsed_path = urlparse.urlparse(self.path)
   358     action = parsed_path.path
   359     params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1)
   360     logging.info('Action for GET method is: %s.', action)
   361     for param in params:
   362       logging.info('%s=%s', param, params[param][0])
   363     if action == '/kill':
   364       self._KillTestServer()
   365     elif action == '/ping':
   366       # The ping handler is used to check whether the spawner server is ready
   367       # to serve the requests. We don't need to test the status of the test
   368       # server when handling ping request.
   369       self._SendResponse(200, 'OK', {}, 'ready')
   370       logging.info('Handled ping request and sent response.')
   371     else:
   372       self._SendResponse(400, 'Unknown request', {}, '')
   373       logging.info('Encounter unknown request: %s.', action)
   376 class SpawningServer(object):
   377   """The class used to start/stop a http server."""
   379   def __init__(self, test_server_spawner_port, adb, tool, build_type):
   380     logging.info('Creating new spawner on port: %d.', test_server_spawner_port)
   381     self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port),
   382                                             SpawningServerRequestHandler)
   383     self.port = test_server_spawner_port
   384     self.server.adb = adb
   385     self.server.tool = tool
   386     self.server.test_server_instance = None
   387     self.server.build_type = build_type
   389   def _Listen(self):
   390     logging.info('Starting test server spawner')
   391     self.server.serve_forever()
   393   def Start(self):
   394     listener_thread = threading.Thread(target=self._Listen)
   395     listener_thread.setDaemon(True)
   396     listener_thread.start()
   397     time.sleep(1)
   399   def Stop(self):
   400     if self.server.test_server_instance:
   401       self.server.test_server_instance.Stop()
   402     self.server.shutdown()

mercurial