# Copyright (c) 2012 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Functions that deal with local and device ports.""" import contextlib import fcntl import httplib import logging import os import re import socket import traceback import cmd_helper import constants # The following two methods are used to allocate the port source for various # types of test servers. Because some net-related tests can be run on shards at # same time, it's important to have a mechanism to allocate the port # process-safe. In here, we implement the safe port allocation by leveraging # flock. def ResetTestServerPortAllocation(): """Resets the port allocation to start from TEST_SERVER_PORT_FIRST. Returns: Returns True if reset successes. Otherwise returns False. """ try: with open(constants.TEST_SERVER_PORT_FILE, 'w') as fp: fp.write('%d' % constants.TEST_SERVER_PORT_FIRST) if os.path.exists(constants.TEST_SERVER_PORT_LOCKFILE): os.unlink(constants.TEST_SERVER_PORT_LOCKFILE) return True except Exception as e: logging.error(e) return False def AllocateTestServerPort(): """Allocates a port incrementally. Returns: Returns a valid port which should be in between TEST_SERVER_PORT_FIRST and TEST_SERVER_PORT_LAST. Returning 0 means no more valid port can be used. """ port = 0 ports_tried = [] try: fp_lock = open(constants.TEST_SERVER_PORT_LOCKFILE, 'w') fcntl.flock(fp_lock, fcntl.LOCK_EX) # Get current valid port and calculate next valid port. if not os.path.exists(constants.TEST_SERVER_PORT_FILE): ResetTestServerPortAllocation() with open(constants.TEST_SERVER_PORT_FILE, 'r+') as fp: port = int(fp.read()) ports_tried.append(port) while IsHostPortUsed(port): port += 1 ports_tried.append(port) if (port > constants.TEST_SERVER_PORT_LAST or port < constants.TEST_SERVER_PORT_FIRST): port = 0 else: fp.seek(0, os.SEEK_SET) fp.write('%d' % (port + 1)) except Exception as e: logging.info(e) finally: if fp_lock: fcntl.flock(fp_lock, fcntl.LOCK_UN) fp_lock.close() if port: logging.info('Allocate port %d for test server.', port) else: logging.error('Could not allocate port for test server. ' 'List of ports tried: %s', str(ports_tried)) return port def IsHostPortUsed(host_port): """Checks whether the specified host port is used or not. Uses -n -P to inhibit the conversion of host/port numbers to host/port names. Args: host_port: Port on host we want to check. Returns: True if the port on host is already used, otherwise returns False. """ port_info = '(\*)|(127\.0\.0\.1)|(localhost):%d' % host_port # TODO(jnd): Find a better way to filter the port. Note that connecting to the # socket and closing it would leave it in the TIME_WAIT state. Setting # SO_LINGER on it and then closing it makes the Python HTTP server crash. re_port = re.compile(port_info, re.MULTILINE) if re_port.search(cmd_helper.GetCmdOutput(['lsof', '-nPi:%d' % host_port])): return True return False def IsDevicePortUsed(adb, device_port, state=''): """Checks whether the specified device port is used or not. Args: adb: Instance of AndroidCommands for talking to the device. device_port: Port on device we want to check. state: String of the specified state. Default is empty string, which means any state. Returns: True if the port on device is already used, otherwise returns False. """ base_url = '127.0.0.1:%d' % device_port netstat_results = adb.RunShellCommand('netstat', log_result=False) for single_connect in netstat_results: # Column 3 is the local address which we want to check with. connect_results = single_connect.split() if connect_results[0] != 'tcp': continue if len(connect_results) < 6: raise Exception('Unexpected format while parsing netstat line: ' + single_connect) is_state_match = connect_results[5] == state if state else True if connect_results[3] == base_url and is_state_match: return True return False def IsHttpServerConnectable(host, port, tries=3, command='GET', path='/', expected_read='', timeout=2): """Checks whether the specified http server is ready to serve request or not. Args: host: Host name of the HTTP server. port: Port number of the HTTP server. tries: How many times we want to test the connection. The default value is 3. command: The http command we use to connect to HTTP server. The default command is 'GET'. path: The path we use when connecting to HTTP server. The default path is '/'. expected_read: The content we expect to read from the response. The default value is ''. timeout: Timeout (in seconds) for each http connection. The default is 2s. Returns: Tuple of (connect status, client error). connect status is a boolean value to indicate whether the server is connectable. client_error is the error message the server returns when connect status is false. """ assert tries >= 1 for i in xrange(0, tries): client_error = None try: with contextlib.closing(httplib.HTTPConnection( host, port, timeout=timeout)) as http: # Output some debug information when we have tried more than 2 times. http.set_debuglevel(i >= 2) http.request(command, path) r = http.getresponse() content = r.read() if r.status == 200 and r.reason == 'OK' and content == expected_read: return (True, '') client_error = ('Bad response: %s %s version %s\n ' % (r.status, r.reason, r.version) + '\n '.join([': '.join(h) for h in r.getheaders()])) except (httplib.HTTPException, socket.error) as e: # Probably too quick connecting: try again. exception_error_msgs = traceback.format_exception_only(type(e), e) if exception_error_msgs: client_error = ''.join(exception_error_msgs) # Only returns last client_error. return (False, client_error or 'Timeout')