Skip to content

Commit c48461d

Browse files
authored
Merge pull request llvm#11790 from swiftlang/jdevlieghere/radar/156111423
[lldb] Correctly detach (rather than kill) when connecting with gdb-remote
2 parents aa7bded + 2612b02 commit c48461d

File tree

8 files changed

+170
-65
lines changed

8 files changed

+170
-65
lines changed

lldb/packages/Python/lldbsuite/test/gdbclientutils.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import threading
55
import socket
66
import traceback
7+
from enum import Enum
78
from lldbsuite.support import seven
9+
from typing import Optional, List, Tuple, Union, Sequence
810

911

1012
def checksum(message):
@@ -74,6 +76,35 @@ def hex_decode_bytes(hex_bytes):
7476
return out
7577

7678

79+
class PacketDirection(Enum):
80+
RECV = "recv"
81+
SEND = "send"
82+
83+
84+
class PacketLog:
85+
def __init__(self):
86+
self._packets: list[tuple[PacketDirection, str]] = []
87+
88+
def add_sent(self, packet: str):
89+
self._packets.append((PacketDirection.SEND, packet))
90+
91+
def add_received(self, packet: str):
92+
self._packets.append((PacketDirection.RECV, packet))
93+
94+
def get_sent(self):
95+
return [
96+
pkt for direction, pkt in self._packets if direction == PacketDirection.SEND
97+
]
98+
99+
def get_received(self):
100+
return [
101+
pkt for direction, pkt in self._packets if direction == PacketDirection.RECV
102+
]
103+
104+
def __iter__(self):
105+
return iter(self._packets)
106+
107+
77108
class MockGDBServerResponder:
78109
"""
79110
A base class for handling client packets and issuing server responses for
@@ -89,21 +120,33 @@ class MockGDBServerResponder:
89120
registerCount = 40
90121
packetLog = None
91122

92-
class RESPONSE_DISCONNECT:
93-
pass
123+
class SpecialResponse(Enum):
124+
RESPONSE_DISCONNECT = 0
125+
RESPONSE_NONE = 1
94126

95-
class RESPONSE_NONE:
96-
pass
127+
RESPONSE_DISCONNECT = SpecialResponse.RESPONSE_DISCONNECT
128+
RESPONSE_NONE = SpecialResponse.RESPONSE_NONE
129+
Response = Union[str, SpecialResponse]
97130

98131
def __init__(self):
99-
self.packetLog = []
132+
self.packetLog = PacketLog()
100133

101-
def respond(self, packet):
134+
def respond(self, packet: str) -> Sequence[Response]:
102135
"""
103136
Return the unframed packet data that the server should issue in response
104137
to the given packet received from the client.
105138
"""
106-
self.packetLog.append(packet)
139+
self.packetLog.add_received(packet)
140+
response = self._respond_impl(packet)
141+
if not isinstance(response, list):
142+
response = [response]
143+
for part in response:
144+
if isinstance(part, self.SpecialResponse):
145+
continue
146+
self.packetLog.add_sent(part)
147+
return response
148+
149+
def _respond_impl(self, packet) -> Union[Response, List[Response]]:
107150
if packet is MockGDBServer.PACKET_INTERRUPT:
108151
return self.interrupt()
109152
if packet == "c":
@@ -649,24 +692,28 @@ def _handlePacket(self, packet):
649692
# adding validation code to make sure the client only sends ACKs
650693
# when it's supposed to.
651694
return
652-
response = ""
695+
response = [""]
653696
# We'll handle the ack stuff here since it's not something any of the
654697
# tests will be concerned about, and it'll get turned off quickly anyway.
655698
if self._shouldSendAck:
656699
self._socket.sendall(seven.bitcast_to_bytes("+"))
657700
if packet == "QStartNoAckMode":
658701
self._shouldSendAck = False
659-
response = "OK"
702+
response = ["OK"]
660703
elif self.responder is not None:
661704
# Delegate everything else to our responder
662705
response = self.responder.respond(packet)
706+
# MockGDBServerResponder no longer returns non-lists but others like
707+
# ReverseTestBase still do
663708
if not isinstance(response, list):
664709
response = [response]
665710
for part in response:
666711
if part is MockGDBServerResponder.RESPONSE_NONE:
667712
continue
668713
if part is MockGDBServerResponder.RESPONSE_DISCONNECT:
669714
raise self.TerminateConnectionException()
715+
# Should have handled the non-str's above
716+
assert isinstance(part, str)
670717
self._sendPacket(part)
671718

672719
PACKET_ACK = object()

lldb/packages/Python/lldbsuite/test/lldbgdbclient.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class GDBRemoteTestBase(TestBase):
1010
Base class for GDB client tests.
1111
1212
This class will setup and start a mock GDB server for the test to use.
13-
It also provides assertPacketLogContains, which simplifies the checking
13+
It also provides assertPacketLogReceived, which simplifies the checking
1414
of packets sent by the client.
1515
"""
1616

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

63-
def assertPacketLogContains(self, packets, log=None):
63+
def assertPacketLogReceived(self, packets, log: PacketLog = None):
6464
"""
65-
Assert that the mock server's packet log contains the given packets.
65+
Assert that the mock server's packet log received the given packets.
6666
6767
The packet log includes all packets sent by the client and received
68-
by the server. This fuction makes it easy to verify that the client
68+
by the server. This function makes it easy to verify that the client
6969
sent the expected packets to the server.
7070
7171
The check does not require that the packets be consecutive, but does
7272
require that they are ordered in the log as they ordered in the arg.
7373
"""
7474
if log is None:
75-
log = self.server.responder.packetLog
75+
received = self.server.responder.packetLog.get_received()
76+
else:
77+
received = log.get_received()
7678
i = 0
7779
j = 0
7880

79-
while i < len(packets) and j < len(log):
80-
if log[j] == packets[i]:
81+
while i < len(packets) and j < len(received):
82+
if received[j] == packets[i]:
8183
i += 1
8284
j += 1
8385
if i < len(packets):
8486
self.fail(
8587
"Did not receive: %s\nLast 10 packets:\n\t%s"
86-
% (packets[i], "\n\t".join(log))
88+
% (packets[i], "\n\t".join(received))
8789
)
8890

8991

lldb/source/Target/Process.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3441,6 +3441,7 @@ Status Process::ConnectRemote(llvm::StringRef remote_url) {
34413441
if (state == eStateStopped || state == eStateCrashed) {
34423442
// If we attached and actually have a process on the other end, then
34433443
// this ended up being the equivalent of an attach.
3444+
SetShouldDetach(true);
34443445
CompleteAttach();
34453446

34463447
// This delays passing the stopped event to listeners till
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Test that ConnectRemote sets ShouldDetach flag correctly.
3+
4+
When connecting to a remote process that stops after connection,
5+
the process should be marked for detach (not kill) on destruction.
6+
"""
7+
8+
import lldb
9+
from lldbsuite.test.lldbtest import *
10+
from lldbsuite.test.decorators import *
11+
from lldbsuite.test.gdbclientutils import *
12+
from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
13+
from lldbsuite.test import lldbutil
14+
15+
16+
class TestConnectRemoteDetach(GDBRemoteTestBase):
17+
"""Test that ConnectRemote properly sets ShouldDetach flag."""
18+
19+
class StoppedResponder(MockGDBServerResponder):
20+
"""A responder that returns a stopped process."""
21+
22+
def qfThreadInfo(self):
23+
return "m1"
24+
25+
def qsThreadInfo(self):
26+
return "l"
27+
28+
def qC(self):
29+
return "QC1"
30+
31+
def haltReason(self):
32+
# Return that we're stopped
33+
return "T05thread:1;"
34+
35+
def cont(self):
36+
# Stay stopped
37+
return "T05thread:1;"
38+
39+
def D(self):
40+
# Detach packet: this is what we want to verify gets called.
41+
return "OK"
42+
43+
def k(self):
44+
# Kill packet: this is what we want to verify doesn't get called.
45+
raise RuntimeError("should not receive k(ill) packet")
46+
47+
def test_connect_remote_sets_detach(self):
48+
"""Test that ConnectRemote to a stopped process sets ShouldDetach."""
49+
self.server.responder = self.StoppedResponder()
50+
51+
target = self.createTarget("a.yaml")
52+
process = self.connect(target)
53+
54+
# Wait for the process to be in stopped state after connecting.
55+
# When ConnectRemote connects to a remote process that is stopped,
56+
# it should call SetShouldDetach(true) before CompleteAttach().
57+
lldbutil.expect_state_changes(
58+
self, self.dbg.GetListener(), process, [lldb.eStateStopped]
59+
)
60+
61+
# Now destroy the process. Because ShouldDetach was set to true
62+
# during ConnectRemote, this should send a 'D' (detach) packet
63+
# rather than a 'k' (kill) packet when the process is destroyed.
64+
process.Destroy()
65+
66+
# Verify that the (D)etach packet was sent.
67+
self.assertPacketLogReceived(["D"])

lldb/test/API/functionalities/gdb_remote_client/TestContinue.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def qfThreadInfo(self):
4141
lldbutil.expect_state_changes(
4242
self, self.dbg.GetListener(), process, [lldb.eStateExited]
4343
)
44-
self.assertPacketLogContains(["vCont;C13:401"])
44+
self.assertPacketLogReceived(["vCont;C13:401"])
4545

4646
def test_continue_no_vCont(self):
4747
class MyResponder(self.BaseResponder):
@@ -61,7 +61,7 @@ def other(self, packet):
6161
lldbutil.expect_state_changes(
6262
self, self.dbg.GetListener(), process, [lldb.eStateExited]
6363
)
64-
self.assertPacketLogContains(["Hc401", "C13"])
64+
self.assertPacketLogReceived(["Hc401", "C13"])
6565

6666
def test_continue_multiprocess(self):
6767
class MyResponder(self.BaseResponder):
@@ -74,4 +74,4 @@ class MyResponder(self.BaseResponder):
7474
lldbutil.expect_state_changes(
7575
self, self.dbg.GetListener(), process, [lldb.eStateExited]
7676
)
77-
self.assertPacketLogContains(["vCont;C13:p400.401"])
77+
self.assertPacketLogReceived(["vCont;C13:p400.401"])

0 commit comments

Comments
 (0)