• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5"""Functions that deal with local and device ports."""
6
7import contextlib
8import fcntl
9import httplib
10import logging
11import os
12import re
13import socket
14import traceback
15
16from pylib import cmd_helper
17from pylib import constants
18
19
20# The following two methods are used to allocate the port source for various
21# types of test servers. Because some net-related tests can be run on shards at
22# same time, it's important to have a mechanism to allocate the port
23# process-safe. In here, we implement the safe port allocation by leveraging
24# flock.
25def ResetTestServerPortAllocation():
26  """Resets the port allocation to start from TEST_SERVER_PORT_FIRST.
27
28  Returns:
29    Returns True if reset successes. Otherwise returns False.
30  """
31  try:
32    with open(constants.TEST_SERVER_PORT_FILE, 'w') as fp:
33      fp.write('%d' % constants.TEST_SERVER_PORT_FIRST)
34    if os.path.exists(constants.TEST_SERVER_PORT_LOCKFILE):
35      os.unlink(constants.TEST_SERVER_PORT_LOCKFILE)
36    return True
37  except Exception as e:
38    logging.error(e)
39  return False
40
41
42def AllocateTestServerPort():
43  """Allocates a port incrementally.
44
45  Returns:
46    Returns a valid port which should be in between TEST_SERVER_PORT_FIRST and
47    TEST_SERVER_PORT_LAST. Returning 0 means no more valid port can be used.
48  """
49  port = 0
50  ports_tried = []
51  try:
52    fp_lock = open(constants.TEST_SERVER_PORT_LOCKFILE, 'w')
53    fcntl.flock(fp_lock, fcntl.LOCK_EX)
54    # Get current valid port and calculate next valid port.
55    if not os.path.exists(constants.TEST_SERVER_PORT_FILE):
56      ResetTestServerPortAllocation()
57    with open(constants.TEST_SERVER_PORT_FILE, 'r+') as fp:
58      port = int(fp.read())
59      ports_tried.append(port)
60      while IsHostPortUsed(port):
61        port += 1
62        ports_tried.append(port)
63      if (port > constants.TEST_SERVER_PORT_LAST or
64          port < constants.TEST_SERVER_PORT_FIRST):
65        port = 0
66      else:
67        fp.seek(0, os.SEEK_SET)
68        fp.write('%d' % (port + 1))
69  except Exception as e:
70    logging.info(e)
71  finally:
72    if fp_lock:
73      fcntl.flock(fp_lock, fcntl.LOCK_UN)
74      fp_lock.close()
75  if port:
76    logging.info('Allocate port %d for test server.', port)
77  else:
78    logging.error('Could not allocate port for test server. '
79                  'List of ports tried: %s', str(ports_tried))
80  return port
81
82
83def IsHostPortUsed(host_port):
84  """Checks whether the specified host port is used or not.
85
86  Uses -n -P to inhibit the conversion of host/port numbers to host/port names.
87
88  Args:
89    host_port: Port on host we want to check.
90
91  Returns:
92    True if the port on host is already used, otherwise returns False.
93  """
94  port_info = '(\*)|(127\.0\.0\.1)|(localhost):%d' % host_port
95  # TODO(jnd): Find a better way to filter the port. Note that connecting to the
96  # socket and closing it would leave it in the TIME_WAIT state. Setting
97  # SO_LINGER on it and then closing it makes the Python HTTP server crash.
98  re_port = re.compile(port_info, re.MULTILINE)
99  if re_port.search(cmd_helper.GetCmdOutput(['lsof', '-nPi:%d' % host_port])):
100    return True
101  return False
102
103
104def IsDevicePortUsed(device, device_port, state=''):
105  """Checks whether the specified device port is used or not.
106
107  Args:
108    device: A DeviceUtils instance.
109    device_port: Port on device we want to check.
110    state: String of the specified state. Default is empty string, which
111           means any state.
112
113  Returns:
114    True if the port on device is already used, otherwise returns False.
115  """
116  base_url = '127.0.0.1:%d' % device_port
117  netstat_results = device.RunShellCommand('netstat')
118  for single_connect in netstat_results:
119    # Column 3 is the local address which we want to check with.
120    connect_results = single_connect.split()
121    if connect_results[0] != 'tcp':
122      continue
123    if len(connect_results) < 6:
124      raise Exception('Unexpected format while parsing netstat line: ' +
125                      single_connect)
126    is_state_match = connect_results[5] == state if state else True
127    if connect_results[3] == base_url and is_state_match:
128      return True
129  return False
130
131
132def IsHttpServerConnectable(host, port, tries=3, command='GET', path='/',
133                            expected_read='', timeout=2):
134  """Checks whether the specified http server is ready to serve request or not.
135
136  Args:
137    host: Host name of the HTTP server.
138    port: Port number of the HTTP server.
139    tries: How many times we want to test the connection. The default value is
140           3.
141    command: The http command we use to connect to HTTP server. The default
142             command is 'GET'.
143    path: The path we use when connecting to HTTP server. The default path is
144          '/'.
145    expected_read: The content we expect to read from the response. The default
146                   value is ''.
147    timeout: Timeout (in seconds) for each http connection. The default is 2s.
148
149  Returns:
150    Tuple of (connect status, client error). connect status is a boolean value
151    to indicate whether the server is connectable. client_error is the error
152    message the server returns when connect status is false.
153  """
154  assert tries >= 1
155  for i in xrange(0, tries):
156    client_error = None
157    try:
158      with contextlib.closing(httplib.HTTPConnection(
159          host, port, timeout=timeout)) as http:
160        # Output some debug information when we have tried more than 2 times.
161        http.set_debuglevel(i >= 2)
162        http.request(command, path)
163        r = http.getresponse()
164        content = r.read()
165        if r.status == 200 and r.reason == 'OK' and content == expected_read:
166          return (True, '')
167        client_error = ('Bad response: %s %s version %s\n  ' %
168                        (r.status, r.reason, r.version) +
169                        '\n  '.join([': '.join(h) for h in r.getheaders()]))
170    except (httplib.HTTPException, socket.error) as e:
171      # Probably too quick connecting: try again.
172      exception_error_msgs = traceback.format_exception_only(type(e), e)
173      if exception_error_msgs:
174        client_error = ''.join(exception_error_msgs)
175  # Only returns last client_error.
176  return (False, client_error or 'Timeout')
177