mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
IPv6 support (#1896)
* Add initial support for IPv6 * address comments (part 1) * quick clean up of missed fix suggestion * Fix misassigned default serving server address in adapter client * add wrapper method to get host and port from`getsockname`
This commit is contained in:
parent
4bc7343c05
commit
34d5de99a0
13 changed files with 123 additions and 50 deletions
|
|
@ -65,9 +65,10 @@ def main():
|
|||
else:
|
||||
endpoints["client"] = {"host": client_host, "port": client_port}
|
||||
|
||||
localhost = sockets.get_default_localhost()
|
||||
if args.for_server is not None:
|
||||
try:
|
||||
server_host, server_port = servers.serve()
|
||||
server_host, server_port = servers.serve(localhost)
|
||||
except Exception as exc:
|
||||
endpoints = {"error": "Can't listen for server connections: " + str(exc)}
|
||||
else:
|
||||
|
|
@ -80,10 +81,11 @@ def main():
|
|||
)
|
||||
|
||||
try:
|
||||
sock = sockets.create_client()
|
||||
ipv6 = localhost.count(":") > 1
|
||||
sock = sockets.create_client(ipv6)
|
||||
try:
|
||||
sock.settimeout(None)
|
||||
sock.connect(("127.0.0.1", args.for_server))
|
||||
sock.connect((localhost, args.for_server))
|
||||
sock_io = sock.makefile("wb", 0)
|
||||
try:
|
||||
sock_io.write(json.dumps(endpoints).encode("utf-8"))
|
||||
|
|
@ -137,6 +139,10 @@ def main():
|
|||
|
||||
|
||||
def _parse_argv(argv):
|
||||
from debugpy.common import sockets
|
||||
|
||||
host = sockets.get_default_localhost()
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
|
|
@ -154,7 +160,7 @@ def _parse_argv(argv):
|
|||
parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default="127.0.0.1",
|
||||
default=host,
|
||||
metavar="HOST",
|
||||
help="start the adapter in debugServer mode on the specified host",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -404,7 +404,8 @@ class Client(components.Component):
|
|||
self._forward_terminate_request = on_terminate == "KeyboardInterrupt"
|
||||
|
||||
launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__))
|
||||
adapter_host = request("debugAdapterHost", "127.0.0.1")
|
||||
localhost = sockets.get_default_localhost()
|
||||
adapter_host = request("debugAdapterHost", localhost)
|
||||
|
||||
try:
|
||||
servers.serve(adapter_host)
|
||||
|
|
@ -472,20 +473,21 @@ class Client(components.Component):
|
|||
'"processId" and "subProcessId" are mutually exclusive'
|
||||
)
|
||||
|
||||
localhost = sockets.get_default_localhost()
|
||||
if listen != ():
|
||||
if servers.is_serving():
|
||||
raise request.isnt_valid(
|
||||
'Multiple concurrent "listen" sessions are not supported'
|
||||
)
|
||||
host = listen("host", "127.0.0.1")
|
||||
host = listen("host", localhost)
|
||||
port = listen("port", int)
|
||||
adapter.access_token = None
|
||||
self.restart_requested = request("restart", False)
|
||||
host, port = servers.serve(host, port)
|
||||
else:
|
||||
if not servers.is_serving():
|
||||
servers.serve()
|
||||
host, port = servers.listener.getsockname()
|
||||
servers.serve(localhost)
|
||||
host, port = sockets.get_address(servers.listener)
|
||||
|
||||
# There are four distinct possibilities here.
|
||||
#
|
||||
|
|
@ -710,7 +712,7 @@ class Client(components.Component):
|
|||
super().disconnect()
|
||||
|
||||
def report_sockets(self):
|
||||
sockets = [
|
||||
socks = [
|
||||
{
|
||||
"host": host,
|
||||
"port": port,
|
||||
|
|
@ -718,12 +720,12 @@ class Client(components.Component):
|
|||
}
|
||||
for listener in [clients.listener, launchers.listener, servers.listener]
|
||||
if listener is not None
|
||||
for (host, port) in [listener.getsockname()]
|
||||
for (host, port) in [sockets.get_address(listener)]
|
||||
]
|
||||
self.channel.send_event(
|
||||
"debugpySockets",
|
||||
{
|
||||
"sockets": sockets
|
||||
"sockets": socks
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -759,10 +761,11 @@ class Client(components.Component):
|
|||
if "connect" not in body:
|
||||
body["connect"] = {}
|
||||
if "host" not in body["connect"]:
|
||||
body["connect"]["host"] = host if host is not None else "127.0.0.1"
|
||||
localhost = sockets.get_default_localhost()
|
||||
body["connect"]["host"] = host or localhost
|
||||
if "port" not in body["connect"]:
|
||||
if port is None:
|
||||
_, port = listener.getsockname()
|
||||
_, port = sockets.get_address(listener)
|
||||
body["connect"]["port"] = port
|
||||
|
||||
if self.capabilities["supportsStartDebuggingRequest"]:
|
||||
|
|
@ -779,7 +782,7 @@ def serve(host, port):
|
|||
global listener
|
||||
listener = sockets.serve("Client", Client, host, port)
|
||||
sessions.report_sockets()
|
||||
return listener.getsockname()
|
||||
return sockets.get_address(listener)
|
||||
|
||||
|
||||
def stop_serving():
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ def spawn_debuggee(
|
|||
|
||||
arguments = dict(start_request.arguments)
|
||||
if not session.no_debug:
|
||||
_, arguments["port"] = servers.listener.getsockname()
|
||||
_, arguments["port"] = sockets.get_address(servers.listener)
|
||||
arguments["adapterAccessToken"] = adapter.access_token
|
||||
|
||||
def on_launcher_connected(sock):
|
||||
|
|
@ -108,10 +108,11 @@ def spawn_debuggee(
|
|||
sessions.report_sockets()
|
||||
|
||||
try:
|
||||
launcher_host, launcher_port = listener.getsockname()
|
||||
launcher_host, launcher_port = sockets.get_address(listener)
|
||||
localhost = sockets.get_default_localhost()
|
||||
launcher_addr = (
|
||||
launcher_port
|
||||
if launcher_host == "127.0.0.1"
|
||||
if launcher_host == localhost
|
||||
else f"{launcher_host}:{launcher_port}"
|
||||
)
|
||||
cmdline += [str(launcher_addr), "--"]
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ def serve(host="127.0.0.1", port=0):
|
|||
global listener
|
||||
listener = sockets.serve("Server", Connection, host, port)
|
||||
sessions.report_sockets()
|
||||
return listener.getsockname()
|
||||
return sockets.get_address(listener)
|
||||
|
||||
|
||||
def is_serving():
|
||||
|
|
@ -475,7 +475,7 @@ def dont_wait_for_first_connection():
|
|||
|
||||
|
||||
def inject(pid, debugpy_args, on_output):
|
||||
host, port = listener.getsockname()
|
||||
host, port = sockets.get_address(listener)
|
||||
|
||||
cmdline = [
|
||||
sys.executable,
|
||||
|
|
|
|||
|
|
@ -9,18 +9,68 @@ import threading
|
|||
from debugpy.common import log
|
||||
from debugpy.common.util import hide_thread_from_debugger
|
||||
|
||||
def can_bind_ipv4_localhost():
|
||||
"""Check if we can bind to IPv4 localhost."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# Try to bind to IPv4 localhost on port 0 (any available port)
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
sock.close()
|
||||
return True
|
||||
except (socket.error, OSError, AttributeError):
|
||||
return False
|
||||
|
||||
def can_bind_ipv6_localhost():
|
||||
"""Check if we can bind to IPv6 localhost."""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# Try to bind to IPv6 localhost on port 0 (any available port)
|
||||
sock.bind(("::1", 0))
|
||||
sock.close()
|
||||
return True
|
||||
except (socket.error, OSError, AttributeError):
|
||||
return False
|
||||
|
||||
def get_default_localhost():
|
||||
"""Get the default localhost address.
|
||||
Defaults to IPv4 '127.0.0.1', but falls back to IPv6 '::1' if IPv4 is unavailable.
|
||||
"""
|
||||
# First try IPv4 (preferred default)
|
||||
if can_bind_ipv4_localhost():
|
||||
return "127.0.0.1"
|
||||
|
||||
# Fall back to IPv6 if IPv4 is not available
|
||||
if can_bind_ipv6_localhost():
|
||||
return "::1"
|
||||
|
||||
# If neither works, still return IPv4 as a last resort
|
||||
# (this is a very unusual situation)
|
||||
return "127.0.0.1"
|
||||
|
||||
def get_address(sock):
|
||||
"""Gets the socket address host and port."""
|
||||
try:
|
||||
host, port = sock.getsockname()[:2]
|
||||
except Exception as exc:
|
||||
log.swallow_exception("Failed to get socket address:")
|
||||
raise RuntimeError(f"Failed to get socket address: {exc}") from exc
|
||||
|
||||
return host, port
|
||||
|
||||
def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
|
||||
"""Return a local server socket listening on the given port."""
|
||||
|
||||
assert backlog > 0
|
||||
if host is None:
|
||||
host = "127.0.0.1"
|
||||
host = get_default_localhost()
|
||||
if port is None:
|
||||
port = 0
|
||||
ipv6 = host.count(":") > 1
|
||||
|
||||
try:
|
||||
server = _new_sock()
|
||||
server = _new_sock(ipv6)
|
||||
if port != 0:
|
||||
# If binding to a specific port, make sure that the user doesn't have
|
||||
# to wait until the OS times out the socket to be able to use that port
|
||||
|
|
@ -42,13 +92,14 @@ def create_server(host, port=0, backlog=socket.SOMAXCONN, timeout=None):
|
|||
return server
|
||||
|
||||
|
||||
def create_client():
|
||||
def create_client(ipv6=False):
|
||||
"""Return a client socket that may be connected to a remote address."""
|
||||
return _new_sock()
|
||||
return _new_sock(ipv6)
|
||||
|
||||
|
||||
def _new_sock():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
|
||||
def _new_sock(ipv6=False):
|
||||
address_family = socket.AF_INET6 if ipv6 else socket.AF_INET
|
||||
sock = socket.socket(address_family, socket.SOCK_STREAM, socket.IPPROTO_TCP)
|
||||
|
||||
# Set TCP keepalive on an open socket.
|
||||
# It activates after 1 second (TCP_KEEPIDLE,) of idleness,
|
||||
|
|
@ -102,13 +153,14 @@ def serve(name, handler, host, port=0, backlog=socket.SOMAXCONN, timeout=None):
|
|||
log.reraise_exception(
|
||||
"Error listening for incoming {0} connections on {1}:{2}:", name, host, port
|
||||
)
|
||||
host, port = listener.getsockname()
|
||||
host, port = get_address(listener)
|
||||
log.info("Listening for incoming {0} connections on {1}:{2}...", name, host, port)
|
||||
|
||||
def accept_worker():
|
||||
while True:
|
||||
try:
|
||||
sock, (other_host, other_port) = listener.accept()
|
||||
sock, address = listener.accept()
|
||||
other_host, other_port = address[:2]
|
||||
except (OSError, socket.error):
|
||||
# Listener socket has been closed.
|
||||
break
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ def connect(host, port):
|
|||
|
||||
log.info("Connecting to adapter at {0}:{1}", host, port)
|
||||
|
||||
sock = sockets.create_client()
|
||||
ipv6 = host.count(":") > 1
|
||||
sock = sockets.create_client(ipv6)
|
||||
sock.connect((host, port))
|
||||
adapter_host = host
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import sys
|
|||
|
||||
def main():
|
||||
from debugpy import launcher
|
||||
from debugpy.common import log
|
||||
from debugpy.common import log, sockets
|
||||
from debugpy.launcher import debuggee
|
||||
|
||||
log.to_file(prefix="debugpy.launcher")
|
||||
|
|
@ -38,9 +38,10 @@ def main():
|
|||
# The first argument specifies the host/port on which the adapter is waiting
|
||||
# for launcher to connect. It's either host:port, or just port.
|
||||
adapter = launcher_argv[0]
|
||||
host, sep, port = adapter.partition(":")
|
||||
host, sep, port = adapter.rpartition(":")
|
||||
host.strip("[]")
|
||||
if not sep:
|
||||
host = "127.0.0.1"
|
||||
host = sockets.get_default_localhost()
|
||||
port = adapter
|
||||
port = int(port)
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,8 @@ def _starts_debugging(func):
|
|||
_, port = address
|
||||
except Exception:
|
||||
port = address
|
||||
address = ("127.0.0.1", port)
|
||||
localhost = sockets.get_default_localhost()
|
||||
address = (localhost, port)
|
||||
try:
|
||||
port.__index__() # ensure it's int-like
|
||||
except Exception:
|
||||
|
|
@ -143,8 +144,8 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
|
|||
# Multiple calls to listen() cause the debuggee to hang
|
||||
raise RuntimeError("debugpy.listen() has already been called on this process")
|
||||
|
||||
host, port = address
|
||||
if in_process_debug_adapter:
|
||||
host, port = address
|
||||
log.info("Listening: pydevd without debugpy adapter: {0}:{1}", host, port)
|
||||
settrace_kwargs["patch_multiprocessing"] = False
|
||||
_settrace(
|
||||
|
|
@ -161,13 +162,14 @@ def listen(address, settrace_kwargs, in_process_debug_adapter=False):
|
|||
server_access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")
|
||||
|
||||
try:
|
||||
endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=30)
|
||||
localhost = sockets.get_default_localhost()
|
||||
endpoints_listener = sockets.create_server(localhost, 0, timeout=30)
|
||||
except Exception as exc:
|
||||
log.swallow_exception("Can't listen for adapter endpoints:")
|
||||
raise RuntimeError("can't listen for adapter endpoints: " + str(exc))
|
||||
|
||||
try:
|
||||
endpoints_host, endpoints_port = endpoints_listener.getsockname()
|
||||
endpoints_host, endpoints_port = sockets.get_address(endpoints_listener)
|
||||
log.info(
|
||||
"Waiting for adapter endpoints on {0}:{1}...",
|
||||
endpoints_host,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from _pydevd_bundle import pydevd_runpy as runpy
|
|||
|
||||
import debugpy
|
||||
import debugpy.server
|
||||
from debugpy.common import log
|
||||
from debugpy.common import log, sockets
|
||||
from debugpy.server import api
|
||||
|
||||
|
||||
|
|
@ -104,9 +104,10 @@ def set_address(mode):
|
|||
|
||||
# It's either host:port, or just port.
|
||||
value = next(it)
|
||||
host, sep, port = value.partition(":")
|
||||
host, sep, port = value.rpartition(":")
|
||||
host = host.strip("[]")
|
||||
if not sep:
|
||||
host = "127.0.0.1"
|
||||
host = sockets.get_default_localhost()
|
||||
port = value
|
||||
try:
|
||||
port = int(port)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class BackChannel(object):
|
|||
|
||||
def listen(self):
|
||||
self._server_socket = sockets.create_server("127.0.0.1", 0, self.TIMEOUT)
|
||||
_, self.port = self._server_socket.getsockname()
|
||||
_, self.port = sockets.get_address(self._server_socket)
|
||||
self._server_socket.listen(0)
|
||||
|
||||
def accept_worker():
|
||||
|
|
|
|||
|
|
@ -464,7 +464,8 @@ class Session(object):
|
|||
|
||||
self.expected_adapter_sockets["client"]["port"] = port
|
||||
|
||||
sock = sockets.create_client()
|
||||
ipv6 = host.count(":") > 1
|
||||
sock = sockets.create_client(ipv6)
|
||||
sock.connect(address)
|
||||
|
||||
stream = messaging.JsonIOStream.from_socket(sock, name=self.adapter_id)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from unittest import mock
|
|||
from debugpy.common import log
|
||||
from tests.patterns import some
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cli(pyfile):
|
||||
@pyfile
|
||||
|
|
@ -89,7 +88,7 @@ def cli(pyfile):
|
|||
# Test a combination of command line switches
|
||||
@pytest.mark.parametrize("target_kind", ["file", "module", "code"])
|
||||
@pytest.mark.parametrize("mode", ["listen", "connect"])
|
||||
@pytest.mark.parametrize("address", ["8888", "localhost:8888"])
|
||||
@pytest.mark.parametrize("address", ["8888", "localhost:8888", "[::1]:8888"])
|
||||
@pytest.mark.parametrize("wait_for_client", ["", "wait_for_client"])
|
||||
@pytest.mark.parametrize("script_args", ["", "script_args"])
|
||||
def test_targets(cli, target_kind, mode, address, wait_for_client, script_args):
|
||||
|
|
@ -101,7 +100,8 @@ def test_targets(cli, target_kind, mode, address, wait_for_client, script_args):
|
|||
|
||||
args = ["--" + mode, address]
|
||||
|
||||
host, sep, port = address.partition(":")
|
||||
host, sep, port = address.rpartition(":")
|
||||
host = host.strip("[]")
|
||||
if sep:
|
||||
expected_options["address"] = (host, int(port))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ from tests.patterns import some
|
|||
@pytest.mark.parametrize("stop_method", ["breakpoint", "pause"])
|
||||
@pytest.mark.skipif(IS_PY312_OR_GREATER, reason="Flakey test on 312 and higher")
|
||||
@pytest.mark.parametrize("is_client_connected", ["is_client_connected", ""])
|
||||
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||
@pytest.mark.parametrize("wait_for_client", ["wait_for_client", pytest.param("", marks=pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Flakey test on Mac"))])
|
||||
def test_attach_api(pyfile, wait_for_client, is_client_connected, stop_method):
|
||||
def test_attach_api(pyfile, host, wait_for_client, is_client_connected, stop_method):
|
||||
@pyfile
|
||||
def code_to_debug():
|
||||
import debuggee
|
||||
|
|
@ -58,7 +59,8 @@ def test_attach_api(pyfile, wait_for_client, is_client_connected, stop_method):
|
|||
time.sleep(0.1)
|
||||
|
||||
with debug.Session() as session:
|
||||
host, port = runners.attach_connect.host, runners.attach_connect.port
|
||||
host = runners.attach_connect.host if host == "127.0.0.1" else host
|
||||
port = runners.attach_connect.port
|
||||
session.config.update({"connect": {"host": host, "port": port}})
|
||||
|
||||
backchannel = session.open_backchannel()
|
||||
|
|
@ -102,7 +104,8 @@ def test_attach_api(pyfile, wait_for_client, is_client_connected, stop_method):
|
|||
|
||||
session.request_continue()
|
||||
|
||||
def test_multiple_listen_raises_exception(pyfile):
|
||||
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||
def test_multiple_listen_raises_exception(pyfile, host):
|
||||
@pyfile
|
||||
def code_to_debug():
|
||||
import debuggee
|
||||
|
|
@ -124,7 +127,8 @@ def test_multiple_listen_raises_exception(pyfile):
|
|||
debugpy.breakpoint()
|
||||
print("break") # @breakpoint
|
||||
|
||||
host, port = runners.attach_connect.host, runners.attach_connect.port
|
||||
host = runners.attach_connect.host if host == "127.0.0.1" else host
|
||||
port = runners.attach_connect.port
|
||||
with debug.Session() as session:
|
||||
backchannel = session.open_backchannel()
|
||||
session.spawn_debuggee(
|
||||
|
|
@ -147,7 +151,6 @@ def test_multiple_listen_raises_exception(pyfile):
|
|||
assert backchannel.receive() == "listen_exception"
|
||||
session.request_continue()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("run", runners.all_attach_connect)
|
||||
def test_reattach(pyfile, target, run):
|
||||
@pyfile
|
||||
|
|
@ -265,7 +268,8 @@ def test_attach_pid_client(pyfile, target, pid_type):
|
|||
session2.request_continue()
|
||||
|
||||
|
||||
def test_cancel_wait(pyfile):
|
||||
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||
def test_cancel_wait(pyfile, host):
|
||||
@pyfile
|
||||
def code_to_debug():
|
||||
import debugpy
|
||||
|
|
@ -287,7 +291,8 @@ def test_cancel_wait(pyfile):
|
|||
backchannel.send("exit")
|
||||
|
||||
with debug.Session() as session:
|
||||
host, port = runners.attach_connect.host, runners.attach_connect.port
|
||||
host = runners.attach_connect.host if host == "127.0.0.1" else host
|
||||
port = runners.attach_connect.port
|
||||
session.config.update({"connect": {"host": host, "port": port}})
|
||||
session.expected_exit_code = None
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue