port_server.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. #!/usr/bin/env python3
  2. # Copyright 2015 gRPC authors.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """Manage TCP ports for unit tests; started by run_tests.py"""
  16. from __future__ import print_function
  17. import argparse
  18. import hashlib
  19. import os
  20. import platform
  21. import random
  22. import socket
  23. import sys
  24. import threading
  25. import time
  26. from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
  27. from six.moves.BaseHTTPServer import HTTPServer
  28. from six.moves.socketserver import ThreadingMixIn
  29. # increment this number whenever making a change to ensure that
  30. # the changes are picked up by running CI servers
  31. # note that all changes must be backwards compatible
  32. _MY_VERSION = 21
  33. if len(sys.argv) == 2 and sys.argv[1] == 'dump_version':
  34. print(_MY_VERSION)
  35. sys.exit(0)
  36. argp = argparse.ArgumentParser(description='Server for httpcli_test')
  37. argp.add_argument('-p', '--port', default=12345, type=int)
  38. argp.add_argument('-l', '--logfile', default=None, type=str)
  39. args = argp.parse_args()
  40. if args.logfile is not None:
  41. sys.stdin.close()
  42. sys.stderr.close()
  43. sys.stdout.close()
  44. sys.stderr = open(args.logfile, 'w')
  45. sys.stdout = sys.stderr
  46. print('port server running on port %d' % args.port)
  47. pool = []
  48. in_use = {}
  49. mu = threading.Lock()
  50. # Cronet restricts the following ports to be used (see
  51. # https://cs.chromium.org/chromium/src/net/base/port_util.cc). When one of these
  52. # ports is used in a Cronet test, the test would fail (see issue #12149). These
  53. # ports must be excluded from pool.
  54. cronet_restricted_ports = [
  55. 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 77, 79, 87,
  56. 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 139,
  57. 143, 179, 389, 465, 512, 513, 514, 515, 526, 530, 531, 532, 540, 556, 563,
  58. 587, 601, 636, 993, 995, 2049, 3659, 4045, 6000, 6665, 6666, 6667, 6668,
  59. 6669, 6697
  60. ]
  61. def can_connect(port):
  62. # this test is only really useful on unices where SO_REUSE_PORT is available
  63. # so on Windows, where this test is expensive, skip it
  64. if platform.system() == 'Windows':
  65. return False
  66. s = socket.socket()
  67. try:
  68. s.connect(('localhost', port))
  69. return True
  70. except socket.error as e:
  71. return False
  72. finally:
  73. s.close()
  74. def can_bind(port, proto):
  75. s = socket.socket(proto, socket.SOCK_STREAM)
  76. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  77. try:
  78. s.bind(('localhost', port))
  79. return True
  80. except socket.error as e:
  81. return False
  82. finally:
  83. s.close()
  84. def refill_pool(max_timeout, req):
  85. """Scan for ports not marked for being in use"""
  86. chk = [
  87. port for port in range(1025, 32766)
  88. if port not in cronet_restricted_ports
  89. ]
  90. random.shuffle(chk)
  91. for i in chk:
  92. if len(pool) > 100:
  93. break
  94. if i in in_use:
  95. age = time.time() - in_use[i]
  96. if age < max_timeout:
  97. continue
  98. req.log_message("kill old request %d" % i)
  99. del in_use[i]
  100. if can_bind(i, socket.AF_INET) and can_bind(
  101. i, socket.AF_INET6) and not can_connect(i):
  102. req.log_message("found available port %d" % i)
  103. pool.append(i)
  104. def allocate_port(req):
  105. global pool
  106. global in_use
  107. global mu
  108. mu.acquire()
  109. max_timeout = 600
  110. while not pool:
  111. refill_pool(max_timeout, req)
  112. if not pool:
  113. req.log_message("failed to find ports: retrying soon")
  114. mu.release()
  115. time.sleep(1)
  116. mu.acquire()
  117. max_timeout /= 2
  118. port = pool[0]
  119. pool = pool[1:]
  120. in_use[port] = time.time()
  121. mu.release()
  122. return port
  123. keep_running = True
  124. class Handler(BaseHTTPRequestHandler):
  125. def setup(self):
  126. # If the client is unreachable for 5 seconds, close the connection
  127. self.timeout = 5
  128. BaseHTTPRequestHandler.setup(self)
  129. def do_GET(self):
  130. global keep_running
  131. global mu
  132. if self.path == '/get':
  133. # allocate a new port, it will stay bound for ten minutes and until
  134. # it's unused
  135. self.send_response(200)
  136. self.send_header('Content-Type', 'text/plain')
  137. self.end_headers()
  138. p = allocate_port(self)
  139. self.log_message('allocated port %d' % p)
  140. self.wfile.write(str(p).encode('ascii'))
  141. elif self.path[0:6] == '/drop/':
  142. self.send_response(200)
  143. self.send_header('Content-Type', 'text/plain')
  144. self.end_headers()
  145. p = int(self.path[6:])
  146. mu.acquire()
  147. if p in in_use:
  148. del in_use[p]
  149. pool.append(p)
  150. k = 'known'
  151. else:
  152. k = 'unknown'
  153. mu.release()
  154. self.log_message('drop %s port %d' % (k, p))
  155. elif self.path == '/version_number':
  156. # fetch a version string and the current process pid
  157. self.send_response(200)
  158. self.send_header('Content-Type', 'text/plain')
  159. self.end_headers()
  160. self.wfile.write(str(_MY_VERSION).encode('ascii'))
  161. elif self.path == '/dump':
  162. # yaml module is not installed on Macs and Windows machines by default
  163. # so we import it lazily (/dump action is only used for debugging)
  164. import yaml
  165. self.send_response(200)
  166. self.send_header('Content-Type', 'text/plain')
  167. self.end_headers()
  168. mu.acquire()
  169. now = time.time()
  170. out = yaml.dump({
  171. 'pool': pool,
  172. 'in_use': dict((k, now - v) for k, v in list(in_use.items()))
  173. })
  174. mu.release()
  175. self.wfile.write(out.encode('ascii'))
  176. elif self.path == '/quitquitquit':
  177. self.send_response(200)
  178. self.end_headers()
  179. self.server.shutdown()
  180. class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
  181. """Handle requests in a separate thread"""
  182. ThreadedHTTPServer(('', args.port), Handler).serve_forever()