Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 56 additions & 9 deletions lldb/packages/Python/lldbsuite/test/gdbclientutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import threading
import socket
import traceback
from enum import Enum
from lldbsuite.support import seven
from typing import Optional, List, Tuple, Union, Sequence


def checksum(message):
Expand Down Expand Up @@ -74,6 +76,35 @@ def hex_decode_bytes(hex_bytes):
return out


class PacketDirection(Enum):
RECV = "recv"
SEND = "send"


class PacketLog:
def __init__(self):
self._packets: list[tuple[PacketDirection, str]] = []

def add_sent(self, packet: str):
self._packets.append((PacketDirection.SEND, packet))

def add_received(self, packet: str):
self._packets.append((PacketDirection.RECV, packet))

def get_sent(self):
return [
pkt for direction, pkt in self._packets if direction == PacketDirection.SEND
]

def get_received(self):
return [
pkt for direction, pkt in self._packets if direction == PacketDirection.RECV
]

def __iter__(self):
return iter(self._packets)


class MockGDBServerResponder:
"""
A base class for handling client packets and issuing server responses for
Expand All @@ -89,21 +120,33 @@ class MockGDBServerResponder:
registerCount = 40
packetLog = None

class RESPONSE_DISCONNECT:
pass
class SpecialResponse(Enum):
RESPONSE_DISCONNECT = 0
RESPONSE_NONE = 1

class RESPONSE_NONE:
pass
RESPONSE_DISCONNECT = SpecialResponse.RESPONSE_DISCONNECT
RESPONSE_NONE = SpecialResponse.RESPONSE_NONE
Response = Union[str, SpecialResponse]

def __init__(self):
self.packetLog = []
self.packetLog = PacketLog()

def respond(self, packet):
def respond(self, packet: str) -> Sequence[Response]:
"""
Return the unframed packet data that the server should issue in response
to the given packet received from the client.
"""
self.packetLog.append(packet)
self.packetLog.add_received(packet)
response = self._respond_impl(packet)
if not isinstance(response, list):
response = [response]
for part in response:
if isinstance(part, self.SpecialResponse):
continue
self.packetLog.add_sent(part)
return response

def _respond_impl(self, packet) -> Union[Response, List[Response]]:
if packet is MockGDBServer.PACKET_INTERRUPT:
return self.interrupt()
if packet == "c":
Expand Down Expand Up @@ -649,24 +692,28 @@ def _handlePacket(self, packet):
# adding validation code to make sure the client only sends ACKs
# when it's supposed to.
return
response = ""
response = [""]
# We'll handle the ack stuff here since it's not something any of the
# tests will be concerned about, and it'll get turned off quickly anyway.
if self._shouldSendAck:
self._socket.sendall(seven.bitcast_to_bytes("+"))
if packet == "QStartNoAckMode":
self._shouldSendAck = False
response = "OK"
response = ["OK"]
elif self.responder is not None:
# Delegate everything else to our responder
response = self.responder.respond(packet)
# MockGDBServerResponder no longer returns non-lists but others like
# ReverseTestBase still do
if not isinstance(response, list):
response = [response]
for part in response:
if part is MockGDBServerResponder.RESPONSE_NONE:
continue
if part is MockGDBServerResponder.RESPONSE_DISCONNECT:
raise self.TerminateConnectionException()
# Should have handled the non-str's above
assert isinstance(part, str)
self._sendPacket(part)

PACKET_ACK = object()
Expand Down
18 changes: 10 additions & 8 deletions lldb/packages/Python/lldbsuite/test/lldbgdbclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class GDBRemoteTestBase(TestBase):
Base class for GDB client tests.

This class will setup and start a mock GDB server for the test to use.
It also provides assertPacketLogContains, which simplifies the checking
It also provides assertPacketLogReceived, which simplifies the checking
of packets sent by the client.
"""

Expand Down Expand Up @@ -60,30 +60,32 @@ def connect(self, target, plugin="gdb-remote"):
self.assertTrue(process, PROCESS_IS_VALID)
return process

def assertPacketLogContains(self, packets, log=None):
def assertPacketLogReceived(self, packets, log: PacketLog = None):
"""
Assert that the mock server's packet log contains the given packets.
Assert that the mock server's packet log received the given packets.

The packet log includes all packets sent by the client and received
by the server. This fuction makes it easy to verify that the client
by the server. This function makes it easy to verify that the client
sent the expected packets to the server.

The check does not require that the packets be consecutive, but does
require that they are ordered in the log as they ordered in the arg.
"""
if log is None:
log = self.server.responder.packetLog
received = self.server.responder.packetLog.get_received()
else:
received = log.get_received()
i = 0
j = 0

while i < len(packets) and j < len(log):
if log[j] == packets[i]:
while i < len(packets) and j < len(received):
if received[j] == packets[i]:
i += 1
j += 1
if i < len(packets):
self.fail(
"Did not receive: %s\nLast 10 packets:\n\t%s"
% (packets[i], "\n\t".join(log))
% (packets[i], "\n\t".join(received))
)


Expand Down
1 change: 1 addition & 0 deletions lldb/source/Target/Process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3443,6 +3443,7 @@ Status Process::ConnectRemote(llvm::StringRef remote_url) {
if (state == eStateStopped || state == eStateCrashed) {
// If we attached and actually have a process on the other end, then
// this ended up being the equivalent of an attach.
SetShouldDetach(true);
CompleteAttach();

// This delays passing the stopped event to listeners till
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
Test that ConnectRemote sets ShouldDetach flag correctly.

When connecting to a remote process that stops after connection,
the process should be marked for detach (not kill) on destruction.
"""

import lldb
from lldbsuite.test.lldbtest import *
from lldbsuite.test.decorators import *
from lldbsuite.test.gdbclientutils import *
from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
from lldbsuite.test import lldbutil


class TestConnectRemoteDetach(GDBRemoteTestBase):
"""Test that ConnectRemote properly sets ShouldDetach flag."""

class StoppedResponder(MockGDBServerResponder):
"""A responder that returns a stopped process."""

def qfThreadInfo(self):
return "m1"

def qsThreadInfo(self):
return "l"

def qC(self):
return "QC1"

def haltReason(self):
# Return that we're stopped
return "T05thread:1;"

def cont(self):
# Stay stopped
return "T05thread:1;"

def D(self):
# Detach packet: this is what we want to verify gets called.
return "OK"

def k(self):
# Kill packet: this is what we want to verify doesn't get called.
raise RuntimeError("should not receive k(ill) packet")

def test_connect_remote_sets_detach(self):
"""Test that ConnectRemote to a stopped process sets ShouldDetach."""
self.server.responder = self.StoppedResponder()

target = self.createTarget("a.yaml")
process = self.connect(target)

# Wait for the process to be in stopped state after connecting.
# When ConnectRemote connects to a remote process that is stopped,
# it should call SetShouldDetach(true) before CompleteAttach().
lldbutil.expect_state_changes(
self, self.dbg.GetListener(), process, [lldb.eStateStopped]
)

# Now destroy the process. Because ShouldDetach was set to true
# during ConnectRemote, this should send a 'D' (detach) packet
# rather than a 'k' (kill) packet when the process is destroyed.
process.Destroy()

# Verify that the (D)etach packet was sent.
self.assertPacketLogReceived(["D"])
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def qfThreadInfo(self):
lldbutil.expect_state_changes(
self, self.dbg.GetListener(), process, [lldb.eStateExited]
)
self.assertPacketLogContains(["vCont;C13:401"])
self.assertPacketLogReceived(["vCont;C13:401"])

def test_continue_no_vCont(self):
class MyResponder(self.BaseResponder):
Expand All @@ -61,7 +61,7 @@ def other(self, packet):
lldbutil.expect_state_changes(
self, self.dbg.GetListener(), process, [lldb.eStateExited]
)
self.assertPacketLogContains(["Hc401", "C13"])
self.assertPacketLogReceived(["Hc401", "C13"])

def test_continue_multiprocess(self):
class MyResponder(self.BaseResponder):
Expand All @@ -74,4 +74,4 @@ class MyResponder(self.BaseResponder):
lldbutil.expect_state_changes(
self, self.dbg.GetListener(), process, [lldb.eStateExited]
)
self.assertPacketLogContains(["vCont;C13:p400.401"])
self.assertPacketLogReceived(["vCont;C13:p400.401"])
Loading