diff options
author | pliard@chromium.org <pliard@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-31 14:42:49 +0000 |
---|---|---|
committer | pliard@chromium.org <pliard@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-08-31 14:42:49 +0000 |
commit | 6d3ac14dc1646ed7f4b1773ee1f5462ae3620e52 (patch) | |
tree | a8dcaed4bba319a2772de2311e4f32fe80dbeff3 /build/android/pylib/chrome_test_server_spawner.py | |
parent | 0955c5b0aff1150f05cbf74ec00025e4c91f9cd0 (diff) | |
download | chromium_src-6d3ac14dc1646ed7f4b1773ee1f5462ae3620e52.zip chromium_src-6d3ac14dc1646ed7f4b1773ee1f5462ae3620e52.tar.gz chromium_src-6d3ac14dc1646ed7f4b1773ee1f5462ae3620e52.tar.bz2 |
Upstream chrome_test_server_spawner.py for Android.
BUG=142571
Review URL: https://chromiumcodereview.appspot.com/10885005
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@154429 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'build/android/pylib/chrome_test_server_spawner.py')
-rw-r--r-- | build/android/pylib/chrome_test_server_spawner.py | 431 |
1 files changed, 360 insertions, 71 deletions
diff --git a/build/android/pylib/chrome_test_server_spawner.py b/build/android/pylib/chrome_test_server_spawner.py index 75a61c1..0b5e4e8 100644 --- a/build/android/pylib/chrome_test_server_spawner.py +++ b/build/android/pylib/chrome_test_server_spawner.py @@ -9,106 +9,395 @@ chrome test server on the host. """ import BaseHTTPServer +import json import logging import os -import sys +import select +import struct +import subprocess import threading import time import urlparse -# Path that are needed to import testserver -cr_src = os.path.join(os.path.abspath(os.path.dirname(__file__)), - '..', '..', '..') -sys.path.append(os.path.join(cr_src, 'third_party')) -sys.path.append(os.path.join(cr_src, 'third_party', 'tlslite')) -sys.path.append(os.path.join(cr_src, 'third_party', 'pyftpdlib', 'src')) -sys.path.append(os.path.join(cr_src, 'net', 'tools', 'testserver')) -import testserver +import constants +from forwarder import Forwarder +import ports -_test_servers = [] -class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): - """A handler used to process http GET request. +# Path that are needed to import necessary modules when running testserver.py. +os.environ['PYTHONPATH'] += ':%s:%s:%s:%s' % ( + os.path.join(constants.CHROME_DIR, 'third_party'), + os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'), + os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'), + os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver')) + + +SERVER_TYPES = { + 'http': '', + 'ftp': '-f', + 'sync': '--sync', + 'tcpecho': '--tcp-echo', + 'udpecho': '--udp-echo', +} + + +# The timeout (in seconds) of starting up the Python test server. +TEST_SERVER_STARTUP_TIMEOUT = 10 + + +def _CheckPortStatus(port, expected_status): + """Returns True if port has expected_status. + + Args: + port: the port number. + expected_status: boolean of expected status. + + Returns: + Returns True if the status is expected. Otherwise returns False. + """ + for timeout in range(1, 5): + if ports.IsHostPortUsed(port) == expected_status: + return True + time.sleep(timeout) + return False + + +def _GetServerTypeCommandLine(server_type): + """Returns the command-line by the given server type. + + Args: + server_type: the server type to be used (e.g. 'http'). + + Returns: + A string containing the command-line argument. """ + if server_type not in SERVER_TYPES: + raise NotImplementedError('Unknown server type: %s' % server_type) + if server_type == 'udpecho': + raise Exception('Please do not run UDP echo tests because we do not have ' + 'a UDP forwarder tool.') + return SERVER_TYPES[server_type] + + +class TestServerThread(threading.Thread): + """A thread to run the test server in a separate process.""" + + def __init__(self, ready_event, arguments, adb, tool, build_type): + """Initialize TestServerThread with the following argument. + + Args: + ready_event: event which will be set when the test server is ready. + arguments: dictionary of arguments to run the test server. + adb: instance of AndroidCommands. + tool: instance of runtime error detection tool. + build_type: 'Release' or 'Debug'. + """ + threading.Thread.__init__(self) + self.wait_event = threading.Event() + self.stop_flag = False + self.ready_event = ready_event + self.ready_event.clear() + self.arguments = arguments + self.adb = adb + self.tool = tool + self.test_server_process = None + self.is_ready = False + self.host_port = self.arguments['port'] + assert isinstance(self.host_port, int) + self._test_server_forwarder = None + # The forwarder device port now is dynamically allocated. + self.forwarder_device_port = 0 + # Anonymous pipe in order to get port info from test server. + self.pipe_in = None + self.pipe_out = None + self.command_line = [] + self.build_type = build_type + + def _WaitToStartAndGetPortFromTestServer(self): + """Waits for the Python test server to start and gets the port it is using. + + The port information is passed by the Python test server with a pipe given + by self.pipe_out. It is written as a result to |self.host_port|. + + Returns: + Whether the port used by the test server was successfully fetched. + """ + assert self.host_port == 0 and self.pipe_out and self.pipe_in + (in_fds, _, _) = select.select([self.pipe_in, ], [], [], + TEST_SERVER_STARTUP_TIMEOUT) + if len(in_fds) == 0: + logging.error('Failed to wait to the Python test server to be started.') + return False + # First read the data length as an unsigned 4-byte value. This + # is _not_ using network byte ordering since the Python test server packs + # size as native byte order and all Chromium platforms so far are + # configured to use little-endian. + # TODO(jnd): Change the Python test server and local_test_server_*.cc to + # use a unified byte order (either big-endian or little-endian). + data_length = os.read(self.pipe_in, struct.calcsize('=L')) + if data_length: + (data_length,) = struct.unpack('=L', data_length) + assert data_length + if not data_length: + logging.error('Failed to get length of server data.') + return False + port_json = os.read(self.pipe_in, data_length) + if not port_json: + logging.error('Failed to get server data.') + return False + logging.info('Got port json data: %s', port_json) + port_json = json.loads(port_json) + if port_json.has_key('port') and isinstance(port_json['port'], int): + self.host_port = port_json['port'] + return _CheckPortStatus(self.host_port, True) + logging.error('Failed to get port information from the server data.') + return False + + def _GenerateCommandLineArguments(self): + """Generates the command line to run the test server. + + Note that all options are processed by following the definitions in + testserver.py. + """ + if self.command_line: + return + # The following arguments must exist. + type_cmd = _GetServerTypeCommandLine(self.arguments['server-type']) + if type_cmd: + self.command_line.append(type_cmd) + self.command_line.append('--port=%d' % self.host_port) + # Use a pipe to get the port given by the instance of Python test server + # if the test does not specify the port. + if self.host_port == 0: + (self.pipe_in, self.pipe_out) = os.pipe() + self.command_line.append('--startup-pipe=%d' % self.pipe_out) + self.command_line.append('--host=%s' % self.arguments['host']) + data_dir = self.arguments['data-dir'] or 'chrome/test/data' + if not os.path.isabs(data_dir): + data_dir = os.path.join(constants.CHROME_DIR, data_dir) + self.command_line.append('--data-dir=%s' % data_dir) + # The following arguments are optional depending on the individual test. + if self.arguments.has_key('log-to-console'): + self.command_line.append('--log-to-console') + if self.arguments.has_key('auth-token'): + self.command_line.append('--auth-token=%s' % self.arguments['auth-token']) + if self.arguments.has_key('https'): + self.command_line.append('--https') + if self.arguments.has_key('cert-and-key-file'): + self.command_line.append('--cert-and-key-file=%s' % os.path.join( + constants.CHROME_DIR, self.arguments['cert-and-key-file'])) + if self.arguments.has_key('ocsp'): + self.command_line.append('--ocsp=%s' % self.arguments['ocsp']) + if self.arguments.has_key('https-record-resume'): + self.command_line.append('--https-record-resume') + if self.arguments.has_key('ssl-client-auth'): + self.command_line.append('--ssl-client-auth') + if self.arguments.has_key('tls-intolerant'): + self.command_line.append('--tls-intolerant') + if self.arguments.has_key('ssl-client-ca'): + for ca in self.arguments['ssl-client-ca']: + self.command_line.append('--ssl-client-ca=%s' % + os.path.join(constants.CHROME_DIR, ca)) + if self.arguments.has_key('ssl-bulk-cipher'): + for bulk_cipher in self.arguments['ssl-bulk-cipher']: + self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher) + + def run(self): + logging.info('Start running the thread!') + self.wait_event.clear() + self._GenerateCommandLineArguments() + command = '%s %s' % ( + os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver', + 'testserver.py'), + ' '.join(self.command_line)) + logging.info(command) + self.process = subprocess.Popen(command, shell=True) + if self.process: + if self.pipe_out: + self.is_ready = self._WaitToStartAndGetPortFromTestServer() + else: + self.is_ready = _CheckPortStatus(self.host_port, True) + if self.is_ready: + self._test_server_forwarder = Forwarder( + self.adb, [(0, self.host_port)], self.tool, '127.0.0.1', + self.build_type) + # Check whether the forwarder is ready on the device. + self.is_ready = False + device_port = self._test_server_forwarder.DevicePortForHostPort( + self.host_port) + if device_port: + for timeout in range(1, 5): + if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'): + self.is_ready = True + self.forwarder_device_port = device_port + break + time.sleep(timeout) + # Wake up the request handler thread. + self.ready_event.set() + # Keep thread running until Stop() gets called. + while not self.stop_flag: + time.sleep(1) + if self.process.poll() is None: + self.process.kill() + if self._test_server_forwarder: + self._test_server_forwarder.Close() + self.process = None + self.is_ready = False + if self.pipe_out: + os.close(self.pipe_in) + os.close(self.pipe_out) + self.pipe_in = None + self.pipe_out = None + logging.info('Test-server has died.') + self.wait_event.set() + + def Stop(self): + """Blocks until the loop has finished. + + Note that this must be called in another thread. + """ + if not self.process: + return + self.stop_flag = True + self.wait_event.wait() + - def GetServerType(self, server_type): - """Returns the server type to use when starting the test server. +class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """A handler used to process http GET/POST request.""" + + def _SendResponse(self, response_code, response_reason, additional_headers, + contents): + """Generates a response sent to the client from the provided parameters. - This function translate the command-line argument into the appropriate - numerical constant. - # TODO(yfriedman): Do that translation! + Args: + response_code: number of the response status. + response_reason: string of reason description of the response. + additional_headers: dict of additional headers. Each key is the name of + the header, each value is the content of the header. + contents: string of the contents we want to send to client. """ - if server_type: - pass - return 0 + self.send_response(response_code, response_reason) + self.send_header('Content-Type', 'text/html') + # Specify the content-length as without it the http(s) response will not + # be completed properly (and the browser keeps expecting data). + self.send_header('Content-Length', len(contents)) + for header_name in additional_headers: + self.send_header(header_name, additional_headers[header_name]) + self.end_headers() + self.wfile.write(contents) + self.wfile.flush() + + def _StartTestServer(self): + """Starts the test server thread.""" + logging.info('Handling request to spawn a test server.') + content_type = self.headers.getheader('content-type') + if content_type != 'application/json': + raise Exception('Bad content-type for start request.') + content_length = self.headers.getheader('content-length') + if not content_length: + content_length = 0 + try: + content_length = int(content_length) + except: + raise Exception('Bad content-length for start request.') + logging.info(content_length) + test_server_argument_json = self.rfile.read(content_length) + logging.info(test_server_argument_json) + assert not self.server.test_server_instance + ready_event = threading.Event() + self.server.test_server_instance = TestServerThread( + ready_event, + json.loads(test_server_argument_json), + self.server.adb, + self.server.tool, + self.server.build_type) + self.server.test_server_instance.setDaemon(True) + self.server.test_server_instance.start() + ready_event.wait() + if self.server.test_server_instance.is_ready: + self._SendResponse(200, 'OK', {}, json.dumps( + {'port': self.server.test_server_instance.forwarder_device_port, + 'message': 'started'})) + logging.info('Test server is running on port: %d.', + self.server.test_server_instance.host_port) + else: + self.server.test_server_instance.Stop() + self.server.test_server_instance = None + self._SendResponse(500, 'Test Server Error.', {}, '') + logging.info('Encounter problem during starting a test server.') + + def _KillTestServer(self): + """Stops the test server instance.""" + # There should only ever be one test server at a time. This may do the + # wrong thing if we try and start multiple test servers. + if not self.server.test_server_instance: + return + port = self.server.test_server_instance.host_port + logging.info('Handling request to kill a test server on port: %d.', port) + self.server.test_server_instance.Stop() + # Make sure the status of test server is correct before sending response. + if _CheckPortStatus(port, False): + self._SendResponse(200, 'OK', {}, 'killed') + logging.info('Test server on port %d is killed', port) + else: + self._SendResponse(500, 'Test Server Error.', {}, '') + logging.info('Encounter problem during killing a test server.') + self.server.test_server_instance = None + + def do_POST(self): + parsed_path = urlparse.urlparse(self.path) + action = parsed_path.path + logging.info('Action for POST method is: %s.', action) + if action == '/start': + self._StartTestServer() + else: + self._SendResponse(400, 'Unknown request.', {}, '') + logging.info('Encounter unknown request: %s.', action) def do_GET(self): parsed_path = urlparse.urlparse(self.path) action = parsed_path.path params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) - logging.info('Action is: %s' % action) - if action == '/killserver': - # There should only ever be one test server at a time. This may do the - # wrong thing if we try and start multiple test servers. - _test_servers.pop().Stop() - elif action == '/start': - logging.info('Handling request to spawn a test webserver') - for param in params: - logging.info('%s=%s' % (param, params[param][0])) - s_type = 0 - doc_root = None - if 'server_type' in params: - s_type = self.GetServerType(params['server_type'][0]) - if 'doc_root' in params: - doc_root = params['doc_root'][0] - self.webserver_thread = threading.Thread( - target=self.SpawnTestWebServer, args=(s_type, doc_root)) - self.webserver_thread.setDaemon(True) - self.webserver_thread.start() - self.send_response(200, 'OK') - self.send_header('Content-type', 'text/html') - self.end_headers() - self.wfile.write('<html><head><title>started</title></head></html>') - logging.info('Returned OK!!!') - - def SpawnTestWebServer(self, s_type, doc_root): - class Options(object): - log_to_console = True - server_type = s_type - port = self.server.test_server_port - data_dir = doc_root or 'chrome/test/data' - file_root_url = '/files/' - cert = False - policy_keys = None - policy_user = None - startup_pipe = None - options = Options() - logging.info('Listening on %d, type %d, data_dir %s' % (options.port, - options.server_type, options.data_dir)) - testserver.main(options, None, server_list=_test_servers) - logging.info('Test-server has died.') + logging.info('Action for GET method is: %s.', action) + for param in params: + logging.info('%s=%s', param, params[param][0]) + if action == '/kill': + self._KillTestServer() + elif action == '/ping': + # The ping handler is used to check whether the spawner server is ready + # to serve the requests. We don't need to test the status of the test + # server when handling ping request. + self._SendResponse(200, 'OK', {}, 'ready') + logging.info('Handled ping request and sent response.') + else: + self._SendResponse(400, 'Unknown request', {}, '') + logging.info('Encounter unknown request: %s.', action) class SpawningServer(object): - """The class used to start/stop a http server. - """ + """The class used to start/stop a http server.""" - def __init__(self, test_server_spawner_port, test_server_port): - logging.info('Creating new spawner %d', test_server_spawner_port) - self.server = testserver.StoppableHTTPServer(('', test_server_spawner_port), - SpawningServerRequestHandler) + def __init__(self, test_server_spawner_port, adb, tool, build_type): + logging.info('Creating new spawner on port: %d.', test_server_spawner_port) + self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port), + SpawningServerRequestHandler) self.port = test_server_spawner_port - self.server.test_server_port = test_server_port + self.server.adb = adb + self.server.tool = tool + self.server.test_server_instance = None + self.server.build_type = build_type - def Listen(self): + def _Listen(self): logging.info('Starting test server spawner') self.server.serve_forever() def Start(self): - listener_thread = threading.Thread(target=self.Listen) + listener_thread = threading.Thread(target=self._Listen) listener_thread.setDaemon(True) listener_thread.start() time.sleep(1) def Stop(self): - self.server.Stop() + if self.server.test_server_instance: + self.server.test_server_instance.Stop() + self.server.shutdown() |