Skip to content

[lldb] Updated TCPSocket to listen multiple ports on the same single thread #104797

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

slydiman
Copy link
Contributor

@slydiman slydiman commented Aug 19, 2024

Currently the class TCPSocket already contains a list of NativeSocket m_listen_sockets for different addresses.
I just added different ports. So Accept() can wait for connections to different ports in a single thread.

This is the prerequisite for #104238 (see the implementation and usage of the class Acceptor).

@llvmbot
Copy link
Member

llvmbot commented Aug 19, 2024

@llvm/pr-subscribers-lldb

Author: Dmitry Vasilyev (slydiman)

Changes

This is prerequisite for #104238.


Full diff: https://github.com/llvm/llvm-project/pull/104797.diff

3 Files Affected:

  • (modified) lldb/include/lldb/Host/common/TCPSocket.h (+3)
  • (modified) lldb/source/Host/common/TCPSocket.cpp (+71-34)
  • (modified) lldb/unittests/Host/SocketTest.cpp (+13)
diff --git a/lldb/include/lldb/Host/common/TCPSocket.h b/lldb/include/lldb/Host/common/TCPSocket.h
index b782c9e6096c44..b45fdae00c6b43 100644
--- a/lldb/include/lldb/Host/common/TCPSocket.h
+++ b/lldb/include/lldb/Host/common/TCPSocket.h
@@ -24,6 +24,9 @@ class TCPSocket : public Socket {
   // returns port number or 0 if error
   uint16_t GetLocalPortNumber() const;
 
+  // returns port numbers of all listening sockets
+  std::set<uint16_t> GetLocalPortNumbers() const;
+
   // returns ip address string or empty string if error
   std::string GetLocalIPAddress() const;
 
diff --git a/lldb/source/Host/common/TCPSocket.cpp b/lldb/source/Host/common/TCPSocket.cpp
index df4737216ed3ac..e659b20bb0045e 100644
--- a/lldb/source/Host/common/TCPSocket.cpp
+++ b/lldb/source/Host/common/TCPSocket.cpp
@@ -17,6 +17,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 
+#include "llvm/ADT/StringExtras.h"
 #include "llvm/Config/llvm-config.h"
 #include "llvm/Support/Errno.h"
 #include "llvm/Support/WindowsError.h"
@@ -94,6 +95,25 @@ uint16_t TCPSocket::GetLocalPortNumber() const {
   return 0;
 }
 
+// Return all the port numbers that is being used by the socket.
+std::set<uint16_t> TCPSocket::GetLocalPortNumbers() const {
+  std::set<uint16_t> ports;
+  if (m_socket != kInvalidSocketValue) {
+    SocketAddress sock_addr;
+    socklen_t sock_addr_len = sock_addr.GetMaxLength();
+    if (::getsockname(m_socket, sock_addr, &sock_addr_len) == 0)
+      ports.insert(sock_addr.GetPort());
+  } else if (!m_listen_sockets.empty()) {
+    SocketAddress sock_addr;
+    socklen_t sock_addr_len = sock_addr.GetMaxLength();
+    for (auto listen_socket : m_listen_sockets) {
+      if (::getsockname(listen_socket.first, sock_addr, &sock_addr_len) == 0)
+        ports.insert(sock_addr.GetPort());
+    }
+  }
+  return ports;
+}
+
 std::string TCPSocket::GetLocalIPAddress() const {
   // We bound to port zero, so we need to figure out which port we actually
   // bound to
@@ -196,49 +216,66 @@ Status TCPSocket::Listen(llvm::StringRef name, int backlog) {
   if (!host_port)
     return Status(host_port.takeError());
 
+  llvm::SmallVector<uint16_t, 2> ports;
+  ports.push_back(host_port->port);
+
+  llvm::SmallVector<llvm::StringRef, 2> extra_ports;
+  name.split(extra_ports, ',', -1, false);
+  if (extra_ports.size() > 1) {
+    for (auto i = extra_ports.begin() + 1; i != extra_ports.end(); ++i) {
+      uint16_t port;
+      if (!llvm::to_integer(*i, port, 10))
+        return Status("invalid extra port number %s", i->str().c_str());
+      ports.push_back(port);
+    }
+  }
+
   if (host_port->hostname == "*")
     host_port->hostname = "0.0.0.0";
   std::vector<SocketAddress> addresses = SocketAddress::GetAddressInfo(
       host_port->hostname.c_str(), nullptr, AF_UNSPEC, SOCK_STREAM, IPPROTO_TCP);
   for (SocketAddress &address : addresses) {
-    int fd = Socket::CreateSocket(address.GetFamily(), kType, IPPROTO_TCP,
-                                  m_child_processes_inherit, error);
-    if (error.Fail() || fd < 0)
-      continue;
-
-    // enable local address reuse
-    int option_value = 1;
-    set_socket_option_arg_type option_value_p =
-        reinterpret_cast<set_socket_option_arg_type>(&option_value);
-    if (::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, option_value_p,
-                     sizeof(option_value)) == -1) {
-      CLOSE_SOCKET(fd);
-      continue;
-    }
-
-    SocketAddress listen_address = address;
-    if(!listen_address.IsLocalhost())
-      listen_address.SetToAnyAddress(address.GetFamily(), host_port->port);
-    else
-      listen_address.SetPort(host_port->port);
+    for (size_t i = 0; i < ports.size(); ++i) {
+      int fd = Socket::CreateSocket(address.GetFamily(), kType, IPPROTO_TCP,
+                                    m_child_processes_inherit, error);
+      if (error.Fail() || fd < 0)
+        continue;
+
+      // enable local address reuse
+      int option_value = 1;
+      set_socket_option_arg_type option_value_p =
+          reinterpret_cast<set_socket_option_arg_type>(&option_value);
+      if (::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, option_value_p,
+                       sizeof(option_value)) == -1) {
+        CLOSE_SOCKET(fd);
+        continue;
+      }
 
-    int err =
-        ::bind(fd, &listen_address.sockaddr(), listen_address.GetLength());
-    if (err != -1)
-      err = ::listen(fd, backlog);
+      SocketAddress listen_address = address;
+      if (!listen_address.IsLocalhost())
+        listen_address.SetToAnyAddress(address.GetFamily(), ports[i]);
+      else
+        listen_address.SetPort(ports[i]);
+
+      int err =
+          ::bind(fd, &listen_address.sockaddr(), listen_address.GetLength());
+      if (err != -1)
+        err = ::listen(fd, backlog);
+
+      if (err == -1) {
+        error = GetLastSocketError();
+        CLOSE_SOCKET(fd);
+        continue;
+      }
 
-    if (err == -1) {
-      error = GetLastSocketError();
-      CLOSE_SOCKET(fd);
-      continue;
-    }
+      if (ports[i] == 0) {
+        socklen_t sa_len = address.GetLength();
+        if (getsockname(fd, &address.sockaddr(), &sa_len) == 0)
+          ports[i] = address.GetPort();
+      }
 
-    if (host_port->port == 0) {
-      socklen_t sa_len = address.GetLength();
-      if (getsockname(fd, &address.sockaddr(), &sa_len) == 0)
-        host_port->port = address.GetPort();
+      m_listen_sockets[fd] = address;
     }
-    m_listen_sockets[fd] = address;
   }
 
   if (m_listen_sockets.empty()) {
diff --git a/lldb/unittests/Host/SocketTest.cpp b/lldb/unittests/Host/SocketTest.cpp
index 78e1e11df06562..3e87af57d3ce51 100644
--- a/lldb/unittests/Host/SocketTest.cpp
+++ b/lldb/unittests/Host/SocketTest.cpp
@@ -136,6 +136,19 @@ TEST_P(SocketTest, TCPListen0GetPort) {
   EXPECT_NE(sock.get()->GetLocalPortNumber(), 0);
 }
 
+TEST_P(SocketTest, TCPListen00GetPort) {
+  if (!HostSupportsIPv4())
+    return;
+  llvm::Expected<std::unique_ptr<TCPSocket>> sock =
+      Socket::TcpListen("10.10.12.3:0,0", false);
+  ASSERT_THAT_EXPECTED(sock, llvm::Succeeded());
+  ASSERT_TRUE(sock.get()->IsValid());
+  std::set<uint16_t> ports = sock.get()->GetLocalPortNumbers();
+  ASSERT_EQ(2, ports.size());
+  EXPECT_NE(*ports.begin(), 0);
+  EXPECT_NE(*std::next(ports.begin()), 0);
+}
+
 TEST_P(SocketTest, TCPGetConnectURI) {
   std::unique_ptr<TCPSocket> socket_a_up;
   std::unique_ptr<TCPSocket> socket_b_up;

@labath
Copy link
Collaborator

labath commented Aug 27, 2024

Having a single socket listen on multiple ports sounds like a bad idea to me. The socket class is complicated enough as it is, and it's not the way the MainLoop class was meant to be used (the idea being for it to be created at the topmost level possible, so that any number of users could register their handlers).

I think that a better and more versatile API would be something like:

std::vector<MainLoop::ReadHandleUP> /*or sth like that*/ TCPSocket::Accept(MainLoop &mainloop, std::function<void(std::unique_ptr<TCPSocket> accepted_socket)> /*or similar*/ socket_cb);

Then we could create two sockets and have them listen on the same main loop instance. It should be possible to reimplement the existing TCPSocket::Accept function on top of this API.

@slydiman
Copy link
Contributor Author

slydiman commented Aug 27, 2024

@labath

Having a single socket listen on multiple ports sounds like a bad idea to me.

Note that currently the class TCPSocket already contains a list of NativeSocket m_listen_sockets.
We do not need 2 TCPSocket instances with 2 separated lists of native sockets even with a common MainLoop.

We have 2 options:

  • A) 2 threads - first one calls TCPSocket::Accept() for the platform connection, second calls TCPSocket::Accept() for the gdb connection
  • B) 1 thread - a common TCPSocket::Accept() can accept platform and gdb connections from both ports

I have implemented the option B. It was easy because the class TCPSocket already contains m_listen_sockets.

Then we could create two sockets and have them listen on the same main loop instance.

It is already implemented inside TCPSocket but for different addresses. I just added different ports.

The changes are minimal really. Unfortunately the diff viewer does not detect the block shifting (2 spaces right).

@labath
Copy link
Collaborator

labath commented Aug 29, 2024

@labath

Having a single socket listen on multiple ports sounds like a bad idea to me.

Note that currently the class TCPSocket already contains a list of NativeSocket m_listen_sockets.

I am aware of that, and I'm not entirely happy with how the class implements listening. I think it would be better to have a separate class for a listening socket, because those need a completely different APIs. But, even ignoring that, I think there's a difference between listening on two addresses with the same port (I believe the only way to reach that state is if a hostname resolves to multiple addresses, in which case one really could argue that it's the same "logical" address), and two addresses with completely unrelated ports.

We do not need 2 TCPSocket instances with 2 separated lists of native sockets even with a common MainLoop.

We have 2 options:

* A) 2 threads - first one calls TCPSocket::Accept() for the platform connection, second calls TCPSocket::Accept() for the gdb connection

* B) 1 thread - a common TCPSocket::Accept() can accept platform and gdb connections from both ports

We have (at least) three options, the third one being what I outlined in the previous message. I'm sorry this work of yours is going to waste. I could've been more explicit about what I wanted to do on the first PR, but I was more focused on what I don't want to do.

I have implemented the option B. It was easy because the class TCPSocket already contains m_listen_sockets.

Then we could create two sockets and have them listen on the same main loop instance.

It is already implemented inside TCPSocket but for different addresses.

That's true, but I draw a different conclusion from that. Instead of saying "this should be extended to support multiple ports", my take is that "this wasn't a good place for multiplexing to begin with".

I just added different ports.

The changes are minimal really.

Yes, but that's because you extended a stringly typed api to do that you wanted. That function is used from other places as well, and this means that other places in lldb (including user-facing code, I believe) can accept these multi-port connection strings. I don't think we want to support that.

@slydiman
Copy link
Contributor Author

slydiman commented Aug 29, 2024

I have updated #104238. The class Acceptor uses the MainLoop and contains the own list of listen sockets. So, this PR may be closed.

@slydiman slydiman closed this Sep 3, 2024
@slydiman slydiman deleted the lldb-tcpsocket-listen-multiple-ports branch April 18, 2025 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants