From 85f025e5b33d148808177427eebca4cc14f93079 Mon Sep 17 00:00:00 2001 From: Pavel Labath Date: Mon, 8 Feb 2021 10:28:42 +0100 Subject: [PATCH] [lldb/test] Test lldb-server named pipe functionality on windows 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 --- .../commandline/TestGdbRemoteConnection.py | 138 ++++++++++++++++++--- 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/lldb/test/API/tools/lldb-server/commandline/TestGdbRemoteConnection.py b/lldb/test/API/tools/lldb-server/commandline/TestGdbRemoteConnection.py index 5a7220f..c9799d1 100644 --- a/lldb/test/API/tools/lldb-server/commandline/TestGdbRemoteConnection.py +++ b/lldb/test/API/tools/lldb-server/commandline/TestGdbRemoteConnection.py @@ -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) -- 2.7.4