Skip to content

Commit 481369f

Browse files
committed
[lldb-dap] Support cancel requests and add explicit DAP structures.
This is a refactor of lldb-dap focused on supporting cancel requests and well defined Debug Adapter Protocol (DAP) structures. Existing request handlers today tend to use unstructured `llvm::json::Object`'s to represent requests and replies. The lack of structure makes it hard for us to apply some unfirom handling around requests to better support cancellation. To address this, this change includes a new `Protocol.h` file with a set of well defined POD structures to represent the types in the DAP and includes `toJSON`/`fromJSON` serialization support. Building on these types, this includes a new approach to registering request handlers with these new well defined types. ``` template <typename Args, typename Body> void DAP::RegisterRequest(llvm::StringLiteral command, RequestHandler<Args, Body> handler); ... llvm::Expected<SourceResponseBody> request_source(DAP &dap, const SourceArguments &args) { SourceResponseBody body; ... if (/*some error*/) return llvm::make_error<DAPError>("msg"); return std::move(body); } ... dap.RegisterRequest("source", request_source); ... ``` The main features of this refactor are: * Created a queue of pending protocol messages to allow for preemptive cancellations and active cancellations. * A cleaner separation between the Debug Adapter Protocol types and lldb types that include serialization support toJSON/fromJSON. * Improved error handling using toJSON/fromJSON. * Unified handling of responses and errors. * Unified handling of cancel requests. This change include minimal support to implement the Debug Adapter Protocol 'source' request, 'evaluate' request and the 'exited' event.
1 parent 998b28f commit 481369f

File tree

18 files changed

+2127
-476
lines changed

18 files changed

+2127
-476
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,17 @@ def encode_content(cls, s):
144144
@classmethod
145145
def validate_response(cls, command, response):
146146
if command["command"] != response["command"]:
147-
raise ValueError("command mismatch in response")
147+
raise ValueError(
148+
"command mismatch in response, got '{}', expected '{}', response: {}".format(
149+
command["command"], response["command"], response
150+
)
151+
)
148152
if command["seq"] != response["request_seq"]:
149-
raise ValueError("seq mismatch in response")
153+
raise ValueError(
154+
"seq mismatch in response, got '{}', expected {}, response: {}".format(
155+
command["seq"], response["request_seq"], response
156+
)
157+
)
150158

151159
def get_modules(self):
152160
module_list = self.request_modules()["body"]["modules"]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
Test lldb-dap cancel request
3+
"""
4+
5+
from lldbsuite.test.decorators import *
6+
from lldbsuite.test.lldbtest import *
7+
import lldbdap_testcase
8+
9+
10+
class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
11+
def send_async_req(self, command: str, arguments={}) -> int:
12+
seq = self.dap_server.sequence
13+
self.dap_server.send_packet(
14+
{
15+
"type": "request",
16+
"command": command,
17+
"arguments": arguments,
18+
}
19+
)
20+
return seq
21+
22+
def async_blocking_request(self, duration: float) -> int:
23+
"""
24+
Sends an evaluate request that will sleep for the specified duration to
25+
block the request handling thread.
26+
"""
27+
return self.send_async_req(
28+
command="evaluate",
29+
arguments={
30+
"expression": "`script import time; time.sleep({})".format(duration),
31+
"context": "repl",
32+
},
33+
)
34+
35+
def async_cancel(self, requestId: int) -> int:
36+
return self.send_async_req(command="cancel", arguments={"requestId": requestId})
37+
38+
def test_pending_request(self):
39+
"""
40+
Tests cancelling a pending request.
41+
"""
42+
program = self.getBuildArtifact("a.out")
43+
self.build_and_launch(program, stopOnEntry=True)
44+
self.continue_to_next_stop()
45+
46+
# Use a relatively short timeout since this is only to ensure the
47+
# following request is queued.
48+
blocking_seq = self.async_blocking_request(duration=1.0)
49+
# Use a longer timeout to ensure we catch if the request was interrupted
50+
# properly.
51+
pending_seq = self.async_blocking_request(duration=self.timeoutval)
52+
cancel_seq = self.async_cancel(requestId=pending_seq)
53+
54+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
55+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
56+
self.assertEqual(blocking_resp["command"], "evaluate")
57+
self.assertEqual(blocking_resp["success"], True)
58+
59+
pending_resp = self.dap_server.recv_packet(filter_type=["response"])
60+
self.assertEqual(pending_resp["request_seq"], pending_seq)
61+
self.assertEqual(pending_resp["command"], "evaluate")
62+
self.assertEqual(pending_resp["success"], False)
63+
self.assertEqual(pending_resp["message"], "cancelled")
64+
65+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
66+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
67+
self.assertEqual(cancel_resp["command"], "cancel")
68+
self.assertEqual(cancel_resp["success"], True)
69+
self.continue_to_exit()
70+
71+
def test_inflight_request(self):
72+
"""
73+
Tests cancelling an inflight request.
74+
"""
75+
program = self.getBuildArtifact("a.out")
76+
self.build_and_launch(program, stopOnEntry=True)
77+
self.continue_to_next_stop()
78+
79+
blocking_seq = self.async_blocking_request(duration=self.timeoutval)
80+
cancel_seq = self.async_cancel(requestId=blocking_seq)
81+
82+
blocking_resp = self.dap_server.recv_packet(filter_type=["response"])
83+
self.assertEqual(blocking_resp["request_seq"], blocking_seq)
84+
self.assertEqual(blocking_resp["command"], "evaluate")
85+
self.assertEqual(blocking_resp["success"], False)
86+
self.assertEqual(blocking_resp["message"], "cancelled")
87+
88+
cancel_resp = self.dap_server.recv_packet(filter_type=["response"])
89+
self.assertEqual(cancel_resp["request_seq"], cancel_seq)
90+
self.assertEqual(cancel_resp["command"], "cancel")
91+
self.assertEqual(cancel_resp["success"], True)
92+
self.continue_to_exit()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#include <stdio.h>
2+
3+
int main(int argc, char const *argv[]) {
4+
printf("Hello world!\n");
5+
return 0;
6+
}

lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import re
6+
import json
67

78
import lldbdap_testcase
89
import dap_server
@@ -13,17 +14,21 @@
1314

1415
class TestDAP_evaluate(lldbdap_testcase.DAPTestCaseBase):
1516
def assertEvaluate(self, expression, regex):
16-
self.assertRegex(
17-
self.dap_server.request_evaluate(expression, context=self.context)["body"][
18-
"result"
19-
],
20-
regex,
17+
resp = self.dap_server.request_evaluate(expression, context=self.context)
18+
self.assertEqual(
19+
resp["success"],
20+
True,
21+
"evaluate with expression = '{}' context = '{}' failed unexpectedly: {}".format(
22+
expression, self.context, json.dumps(resp)
23+
),
2124
)
25+
self.assertRegex(resp["body"]["result"], regex)
2226

2327
def assertEvaluateFailure(self, expression):
24-
self.assertNotIn(
25-
"result",
26-
self.dap_server.request_evaluate(expression, context=self.context)["body"],
28+
self.assertFalse(
29+
self.dap_server.request_evaluate(expression, context=self.context)[
30+
"success"
31+
],
2732
)
2833

2934
def isResultExpandedDescription(self):
@@ -231,5 +236,7 @@ def test_hover_evaluate_expressions(self):
231236

232237
@skipIfWindows
233238
def test_variable_evaluate_expressions(self):
234-
# Tests expression evaluations that are triggered in the variable explorer
235-
self.run_test_evaluate_expressions("variable", enableAutoVariableSummaries=True)
239+
# Tests expression evaluations that are triggered in the variables explorer
240+
self.run_test_evaluate_expressions(
241+
"variables", enableAutoVariableSummaries=True
242+
)

lldb/test/API/tools/lldb-dap/launch/TestDAP_launch.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"""
2-
Test lldb-dap setBreakpoints request
2+
Test lldb-dap launch request
33
"""
44

5-
import dap_server
65
from lldbsuite.test.decorators import *
76
from lldbsuite.test.lldbtest import *
8-
from lldbsuite.test import lldbutil
97
import lldbdap_testcase
10-
import time
118
import os
129
import re
1310

@@ -41,7 +38,9 @@ def test_termination(self):
4138
self.dap_server.request_disconnect()
4239

4340
# Wait until the underlying lldb-dap process dies.
44-
self.dap_server.process.wait(timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval)
41+
self.dap_server.process.wait(
42+
timeout=lldbdap_testcase.DAPTestCaseBase.timeoutval
43+
)
4544

4645
# Check the return code
4746
self.assertEqual(self.dap_server.process.poll(), 0)

lldb/test/API/tools/lldb-dap/optimized/TestDAP_optimized.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ def test_stack_frame_name(self):
2424
)
2525
self.continue_to_breakpoints(breakpoint_ids)
2626
leaf_frame = self.dap_server.get_stackFrame(frameIndex=0)
27-
self.assertTrue(leaf_frame["name"].endswith(" [opt]"))
27+
self.assertRegex(leaf_frame["name"], r"\[.*opt.*\]")
2828
parent_frame = self.dap_server.get_stackFrame(frameIndex=1)
29-
self.assertTrue(parent_frame["name"].endswith(" [opt]"))
29+
self.assertRegex(parent_frame["name"], r"\[.*opt.*\]")
3030

31-
@skipIfAsan # On ASAN builds this test intermittently fails https://github.com/llvm/llvm-project/issues/111061
31+
@skipIfAsan # On ASAN builds this test intermittently fails https://github.com/llvm/llvm-project/issues/111061
3232
@skipIfWindows
3333
def test_optimized_variable(self):
3434
"""Test optimized variable value contains error."""

lldb/test/API/tools/lldb-dap/server/TestDAP_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def start_server(self, connection):
2323

2424
def cleanup():
2525
process.terminate()
26+
dap_server.dump_dap_log(log_file_path)
2627

2728
self.addTearDownHook(cleanup)
2829

@@ -47,6 +48,7 @@ def run_debug_session(self, connection, name):
4748
output = self.get_stdout()
4849
self.assertEqual(output, f"Hello {name}!\r\n")
4950
self.dap_server.request_disconnect()
51+
self.dap_server.terminate()
5052

5153
def test_server_port(self):
5254
"""

lldb/tools/lldb-dap/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ add_lldb_tool(lldb-dap
3232
LLDBUtils.cpp
3333
OutputRedirector.cpp
3434
ProgressEvent.cpp
35+
Protocol.cpp
3536
RunInTerminal.cpp
3637
SourceBreakpoint.cpp
3738
Watchpoint.cpp

0 commit comments

Comments
 (0)