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

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

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

mercurial