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