[lldb/test] Test lldb-server named pipe functionality on windows
authorPavel Labath <pavel@labath.sk>
Mon, 8 Feb 2021 09:28:42 +0000 (10:28 +0100)
committerPavel Labath <pavel@labath.sk>
Tue, 16 Feb 2021 14:47:39 +0000 (15:47 +0100)
lldb-server can use a named pipe to communicate the port number it is
listening on. This windows bits of this are already implemented, but we
did not have a test for that, most likely because python does not have
native pipe functionality.

This patch implements the windows bits necessary to test this. I'm using
the ctypes package to call the native APIs directly to avoid a
dependency to non-standard python packages. This introduces some amount
of boilerplate, but our named pipe use case is fairly limited, so we
should not end up needing to wrap large chunks of windows APIs.

Surprisingly to changes to lldb-server were needed to make the test
pass.

Differential Revision: https://reviews.llvm.org/D96260

lldb/test/API/tools/lldb-server/commandline/TestGdbRemoteConnection.py

index 5a7220f..c9799d1 100644 (file)
@@ -5,6 +5,122 @@ import select
 import socket
 from lldbsuite.test.decorators import *
 from lldbsuite.test.lldbtest import *
+import lldbsuite.test.lldbplatformutil
+import random
+
+if lldbplatformutil.getHostPlatform() == "windows":
+    import ctypes
+    import ctypes.wintypes
+    from ctypes.wintypes import (BOOL, DWORD, HANDLE, LPCWSTR, LPDWORD, LPVOID)
+
+    kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+
+    PIPE_ACCESS_INBOUND = 1
+    FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000
+    FILE_FLAG_OVERLAPPED = 0x40000000
+    PIPE_TYPE_BYTE = 0
+    PIPE_REJECT_REMOTE_CLIENTS = 8
+    INVALID_HANDLE_VALUE = -1
+    ERROR_ACCESS_DENIED = 5
+    ERROR_IO_PENDING = 997
+
+
+    class OVERLAPPED(ctypes.Structure):
+        _fields_ = [("Internal", LPVOID), ("InternalHigh", LPVOID), ("Offset",
+            DWORD), ("OffsetHigh", DWORD), ("hEvent", HANDLE)]
+
+        def __init__(self):
+            super(OVERLAPPED, self).__init__(Internal=0, InternalHigh=0,
+                Offset=0, OffsetHigh=0, hEvent=None)
+    LPOVERLAPPED = ctypes.POINTER(OVERLAPPED)
+
+    CreateNamedPipe = kernel32.CreateNamedPipeW
+    CreateNamedPipe.restype = HANDLE
+    CreateNamedPipe.argtypes = (LPCWSTR, DWORD, DWORD, DWORD, DWORD, DWORD,
+            DWORD, LPVOID)
+
+    ConnectNamedPipe = kernel32.ConnectNamedPipe
+    ConnectNamedPipe.restype = BOOL
+    ConnectNamedPipe.argtypes = (HANDLE, LPOVERLAPPED)
+
+    CreateEvent = kernel32.CreateEventW
+    CreateEvent.restype = HANDLE
+    CreateEvent.argtypes = (LPVOID, BOOL, BOOL, LPCWSTR)
+
+    GetOverlappedResultEx = kernel32.GetOverlappedResultEx
+    GetOverlappedResultEx.restype = BOOL
+    GetOverlappedResultEx.argtypes = (HANDLE, LPOVERLAPPED, LPDWORD, DWORD,
+        BOOL)
+
+    ReadFile = kernel32.ReadFile
+    ReadFile.restype = BOOL
+    ReadFile.argtypes = (HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED)
+
+    CloseHandle = kernel32.CloseHandle
+    CloseHandle.restype = BOOL
+    CloseHandle.argtypes = (HANDLE,)
+
+    class Pipe(object):
+        def __init__(self, prefix):
+            while True:
+                self.name = "lldb-" + str(random.randrange(1e10))
+                full_name = "\\\\.\\pipe\\" + self.name
+                self._handle = CreateNamedPipe(full_name, PIPE_ACCESS_INBOUND |
+                        FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED,
+                        PIPE_TYPE_BYTE | PIPE_REJECT_REMOTE_CLIENTS, 1, 4096,
+                        4096, 0, None)
+                if self._handle != INVALID_HANDLE_VALUE:
+                    break
+                if ctypes.get_last_error() != ERROR_ACCESS_DENIED:
+                    raise ctypes.WinError(ctypes.get_last_error())
+
+            self._overlapped = OVERLAPPED()
+            self._overlapped.hEvent = CreateEvent(None, True, False, None)
+            result = ConnectNamedPipe(self._handle, self._overlapped)
+            assert result == 0
+            if ctypes.get_last_error() != ERROR_IO_PENDING:
+                raise ctypes.WinError(ctypes.get_last_error())
+
+        def finish_connection(self, timeout):
+            if not GetOverlappedResultEx(self._handle, self._overlapped,
+                    ctypes.byref(DWORD(0)), timeout*1000, True):
+                raise ctypes.WinError(ctypes.get_last_error())
+
+        def read(self, size, timeout):
+            buf = ctypes.create_string_buffer(size)
+            if not ReadFile(self._handle, ctypes.byref(buf), size, None,
+                    self._overlapped):
+                if ctypes.get_last_error() != ERROR_IO_PENDING:
+                    raise ctypes.WinError(ctypes.get_last_error())
+            read = DWORD(0)
+            if not GetOverlappedResultEx(self._handle, self._overlapped,
+                    ctypes.byref(read), timeout*1000, True):
+                raise ctypes.WinError(ctypes.get_last_error())
+            return buf.raw[0:read.value]
+
+        def close(self):
+            CloseHandle(self._overlapped.hEvent)
+            CloseHandle(self._handle)
+
+
+else:
+    class Pipe(object):
+        def __init__(self, prefix):
+            self.name = os.path.join(prefix, "stub_port_number")
+            os.mkfifo(self.name)
+            self._fd = os.open(self.name, os.O_RDONLY | os.O_NONBLOCK)
+
+        def finish_connection(self, timeout):
+            pass
+
+        def read(self, size, timeout):
+            (readers, _, _) = select.select([self._fd], [], [], timeout)
+            if self._fd not in readers:
+                raise TimeoutError
+            return os.read(self._fd, size)
+
+        def close(self):
+            os.close(self._fd)
 
 
 class TestGdbRemoteConnection(gdbremote_testcase.GdbRemoteTestCaseBase):
@@ -19,7 +135,6 @@ class TestGdbRemoteConnection(gdbremote_testcase.GdbRemoteTestCaseBase):
         self.do_handshake(self.sock)
 
     @skipIfRemote
-    @skipIfWindows
     def test_named_pipe_llgs(self):
         family, type, proto, _, addr = socket.getaddrinfo(
             self.stub_hostname, 0, proto=socket.IPPROTO_TCP)[0]
@@ -28,16 +143,9 @@ class TestGdbRemoteConnection(gdbremote_testcase.GdbRemoteTestCaseBase):
 
         self.addTearDownHook(lambda: self.sock.close())
 
-        named_pipe_path = self.getBuildArtifact("stub_port_number")
+        pipe = Pipe(self.getBuildDir())
 
-        # Create the named pipe.
-        os.mkfifo(named_pipe_path)
-
-        # Open the read side of the pipe in non-blocking mode.  This will
-        # return right away, ready or not.
-        named_pipe_fd = os.open(named_pipe_path, os.O_RDONLY | os.O_NONBLOCK)
-
-        self.addTearDownHook(lambda: os.close(named_pipe_fd))
+        self.addTearDownHook(lambda: pipe.close())
 
         args = self.debug_monitor_extra_args
         if lldb.remote_platform:
@@ -45,19 +153,15 @@ class TestGdbRemoteConnection(gdbremote_testcase.GdbRemoteTestCaseBase):
         else:
             args += ["localhost:0"]
 
-        args += ["--named-pipe", named_pipe_path]
+        args += ["--named-pipe", pipe.name]
 
         server = self.spawnSubprocess(
             self.debug_monitor_exe,
             args,
             install_remote=False)
 
-        (ready_readers, _, _) = select.select(
-            [named_pipe_fd], [], [], self.DEFAULT_TIMEOUT)
-        self.assertIsNotNone(
-            ready_readers,
-            "write side of pipe has not written anything - stub isn't writing to pipe.")
-        port = os.read(named_pipe_fd, 10)
+        pipe.finish_connection(self.DEFAULT_TIMEOUT)
+        port = pipe.read(10, self.DEFAULT_TIMEOUT)
         # Trim null byte, convert to int
         addr = (addr[0], int(port[:-1]))
         self.sock.connect(addr)