• 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"""Functions that deal with local and device ports."""
5
6import contextlib
7import fcntl
8import httplib
9import logging
10import os
11import socket
12import traceback
13
14logger = logging.getLogger(__name__)
15
16# The net test server is started from port 10201.
17_TEST_SERVER_PORT_FIRST = 10201
18_TEST_SERVER_PORT_LAST = 30000
19# A file to record next valid port of test server.
20_TEST_SERVER_PORT_FILE = '/tmp/test_server_port'
21_TEST_SERVER_PORT_LOCKFILE = '/tmp/test_server_port.lock'
22
23
24# The following two methods are used to allocate the port source for various
25# types of test servers. Because some net-related tests can be run on shards at
26# same time, it's important to have a mechanism to allocate the port
27# process-safe. In here, we implement the safe port allocation by leveraging
28# flock.
29def ResetTestServerPortAllocation():
30  """Resets the port allocation to start from TEST_SERVER_PORT_FIRST.
31
32  Returns:
33    Returns True if reset successes. Otherwise returns False.
34  """
35  try:
36    with open(_TEST_SERVER_PORT_FILE, 'w') as fp:
37      fp.write('%d' % _TEST_SERVER_PORT_FIRST)
38    return True
39  except Exception:  # pylint: disable=broad-except
40    logger.exception('Error while resetting port allocation')
41  return False
42
43
44def AllocateTestServerPort():
45  """Allocates a port incrementally.
46
47  Returns:
48    Returns a valid port which should be in between TEST_SERVER_PORT_FIRST and
49    TEST_SERVER_PORT_LAST. Returning 0 means no more valid port can be used.
50  """
51  port = 0
52  ports_tried = []
53  try:
54    fp_lock = open(_TEST_SERVER_PORT_LOCKFILE, 'w')
55    fcntl.flock(fp_lock, fcntl.LOCK_EX)
56    # Get current valid port and calculate next valid port.
57    if not os.path.exists(_TEST_SERVER_PORT_FILE):
58      ResetTestServerPortAllocation()
59    with open(_TEST_SERVER_PORT_FILE, 'r+') as fp:
60      port = int(fp.read())
61      ports_tried.append(port)
62      while not IsHostPortAvailable(port):
63        port += 1
64        ports_tried.append(port)
65      if port > _TEST_SERVER_PORT_LAST or port < _TEST_SERVER_PORT_FIRST:
66        port = 0
67      else:
68        fp.seek(0, os.SEEK_SET)
69        fp.write('%d' % (port + 1))
70  except Exception:  # pylint: disable=broad-except
71    logger.exception('Error while allocating port')
72  finally:
73    if fp_lock:
74      fcntl.flock(fp_lock, fcntl.LOCK_UN)
75      fp_lock.close()
76  if port:
77    logger.info('Allocate port %d for test server.', port)
78  else:
79    logger.error(
80        'Could not allocate port for test server. '
81        'List of ports tried: %s', str(ports_tried))
82  return port
83
84
85def IsHostPortAvailable(host_port):
86  """Checks whether the specified host port is available.
87
88  Args:
89    host_port: Port on host to check.
90
91  Returns:
92    True if the port on host is available, otherwise returns False.
93  """
94  s = socket.socket()
95  try:
96    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
97    s.bind(('', host_port))
98    s.close()
99    return True
100  except socket.error:
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_urls = ('127.0.0.1:%d' % device_port, 'localhost:%d' % device_port)
117  netstat_results = device.RunShellCommand(['netstat', '-an'],
118                                           check_return=True,
119                                           large_output=True)
120  for single_connect in netstat_results:
121    # Column 3 is the local address which we want to check with.
122    connect_results = single_connect.split()
123    if connect_results[0] != 'tcp':
124      continue
125    if len(connect_results) < 6:
126      raise Exception('Unexpected format while parsing netstat line: ' +
127                      single_connect)
128    is_state_match = connect_results[5] == state if state else True
129    if connect_results[3] in base_urls and is_state_match:
130      return True
131  return False
132
133
134def IsHttpServerConnectable(host,
135                            port,
136                            tries=3,
137                            command='GET',
138                            path='/',
139                            expected_read='',
140                            timeout=2):
141  """Checks whether the specified http server is ready to serve request or not.
142
143  Args:
144    host: Host name of the HTTP server.
145    port: Port number of the HTTP server.
146    tries: How many times we want to test the connection. The default value is
147           3.
148    command: The http command we use to connect to HTTP server. The default
149             command is 'GET'.
150    path: The path we use when connecting to HTTP server. The default path is
151          '/'.
152    expected_read: The content we expect to read from the response. The default
153                   value is ''.
154    timeout: Timeout (in seconds) for each http connection. The default is 2s.
155
156  Returns:
157    Tuple of (connect status, client error). connect status is a boolean value
158    to indicate whether the server is connectable. client_error is the error
159    message the server returns when connect status is false.
160  """
161  assert tries >= 1
162  for i in range(0, tries):
163    client_error = None
164    try:
165      with contextlib.closing(
166          httplib.HTTPConnection(host, port, timeout=timeout)) as http:
167        # Output some debug information when we have tried more than 2 times.
168        http.set_debuglevel(i >= 2)
169        http.request(command, path)
170        r = http.getresponse()
171        content = r.read()
172        if r.status == 200 and r.reason == 'OK' and content == expected_read:
173          return (True, '')
174        client_error = ('Bad response: %s %s version %s\n  ' %
175                        (r.status, r.reason, r.version) + '\n  '.join(
176                            [': '.join(h) for h in r.getheaders()]))
177    except (httplib.HTTPException, socket.error) as e:
178      # Probably too quick connecting: try again.
179      exception_error_msgs = traceback.format_exception_only(type(e), e)
180      if exception_error_msgs:
181        client_error = ''.join(exception_error_msgs)
182  # Only returns last client_error.
183  return (False, client_error or 'Timeout')
184