sockets.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. # Copyright (c) Microsoft Corporation. All rights reserved.
  2. # Licensed under the MIT License. See LICENSE in the project root
  3. # for license information.
  4. import socket
  5. import sys
  6. import threading
  7. from debugpy.common import log
  8. from debugpy.common.util import hide_thread_from_debugger
  9. def can_bind_ipv4_localhost():
  10. """Check if we can bind to IPv4 localhost."""
  11. try:
  12. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  13. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  14. # Try to bind to IPv4 localhost on port 0 (any available port)
  15. sock.bind(("127.0.0.1", 0))
  16. sock.close()
  17. return True
  18. except (socket.error, OSError, AttributeError):
  19. return False
  20. def can_bind_ipv6_localhost():
  21. """Check if we can bind to IPv6 localhost."""
  22. try:
  23. sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  24. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  25. # Try to bind to IPv6 localhost on port 0 (any available port)
  26. sock.bind(("::1", 0))
  27. sock.close()
  28. return True
  29. except (socket.error, OSError, AttributeError):
  30. return False
  31. def get_default_localhost():
  32. """Get the default localhost address.
  33. Defaults to IPv4 '127.0.0.1', but falls back to IPv6 '::1' if IPv4 is unavailable.
  34. """
  35. # First try IPv4 (preferred default)
  36. if can_bind_ipv4_localhost():
  37. return "127.0.0.1"
  38. # Fall back to IPv6 if IPv4 is not available
  39. if can_bind_ipv6_localhost():
  40. return "::1"
  41. # If neither works, still return IPv4 as a last resort
  42. # (this is a very unusual situation)
  43. return "127.0.0.1"
  44. def get_address(sock):
  45. """Gets the socket address host and port."""
  46. try:
  47. host, port = sock.getsockname()[:2]
  48. except Exception as exc:
  49. log.swallow_exception("Failed to get socket address:")
  50. raise RuntimeError(f"Failed to get socket address: {exc}") from exc
  51. return host, port
  52. def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
  53. """Return a local server socket listening on the given port."""
  54. assert backlog > 0
  55. if host is None:
  56. host = get_default_localhost()
  57. if port is None:
  58. port = 0
  59. ipv6 = host.count(":") > 1
  60. try:
  61. server = _new_sock(ipv6)
  62. if port != 0:
  63. # If binding to a specific port, make sure that the user doesn't have
  64. # to wait until the OS times out the socket to be able to use that port
  65. # again.if the server or the adapter crash or are force-killed.
  66. if sys.platform == "win32":
  67. server.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
  68. else:
  69. try:
  70. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  71. except (AttributeError, OSError): # pragma: no cover
  72. pass # Not available everywhere
  73. server.bind((host, port))
  74. if timeout is not None:
  75. server.settimeout(timeout)
  76. server.listen(backlog)
  77. except Exception: # pragma: no cover
  78. server.close()
  79. raise
  80. return server
  81. def create_client(ipv6=False):
  82. """Return a client socket that may be connected to a remote address."""
  83. return _new_sock(ipv6)
  84. def _new_sock(ipv6=False):
  85. address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
  86. sock = socket.socket(address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP)
  87. # Set TCP keepalive on an open socket.
  88. # It activates after 1 second (TCP_KEEPIDLE,) of idleness,
  89. # then sends a keepalive ping once every 3 seconds (TCP_KEEPINTVL),
  90. # and closes the connection after 5 failed ping (TCP_KEEPCNT), or 15 seconds
  91. try:
  92. sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
  93. except (AttributeError, OSError): # pragma: no cover
  94. pass # May not be available everywhere.
  95. try:
  96. sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
  97. except (AttributeError, OSError): # pragma: no cover
  98. pass # May not be available everywhere.
  99. try:
  100. sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
  101. except (AttributeError, OSError): # pragma: no cover
  102. pass # May not be available everywhere.
  103. try:
  104. sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
  105. except (AttributeError, OSError): # pragma: no cover
  106. pass # May not be available everywhere.
  107. return sock
  108. def shut_down(sock, how=socket.SHUT_RDWR):
  109. """Shut down the given socket."""
  110. sock.shutdown(how)
  111. def close_socket(sock):
  112. """Shutdown and close the socket."""
  113. try:
  114. shut_down(sock)
  115. except Exception: # pragma: no cover
  116. pass
  117. sock.close()
  118. def serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None):
  119. """Accepts TCP connections on the specified host and port, and invokes the
  120. provided handler function for every new connection.
  121. Returns the created server socket.
  122. """
  123. assert backlog > 0
  124. try:
  125. listener = create_server(host, port, backlog, timeout)
  126. except Exception: # pragma: no cover
  127. log.reraise_exception(
  128. "Error listening for incoming {0} connections on {1}:{2}:", name, host, port
  129. )
  130. host, port = get_address(listener)
  131. log.info("Listening for incoming {0} connections on {1}:{2}...", name, host, port)
  132. def accept_worker():
  133. while True:
  134. try:
  135. sock, address = listener.accept()
  136. other_host, other_port = address[:2]
  137. except (OSError, socket.error):
  138. # Listener socket has been closed.
  139. break
  140. log.info(
  141. "Accepted incoming {0} connection from {1}:{2}.",
  142. name,
  143. other_host,
  144. other_port,
  145. )
  146. handler(sock)
  147. thread = threading.Thread(target=accept_worker)
  148. thread.daemon = True
  149. hide_thread_from_debugger(thread)
  150. thread.start()
  151. return listener