Skip to content

Commit cbaa41b

Browse files
committed
Fix DAP defer_stop_events implementation
DAP requests have a "defer_stop_events" option that is intended to defer the emission of any "stopped" event until after the current request completes. This was needed to handle async continues like "finish &". However, I noticed that sometimes DAP tests can fail, because a stop event does arrive before the response to the "stepOut" request. I've only noticed this when the machine is fairly loaded -- for instance when I'm regression-testing a series, it may occur in some of the tests mid-series. I believe the problem is that the implementation in the "request" function is incorrect -- the flag is set when "request" is invoked, but instead it must be deferred until the request itself is run. That is, the setting must be captured in one of the wrapper functions. Following up on this, Simon pointed out that introducing a delay before sending a request's response will cause test case failures. That is, there's a race here that is normally hidden. Investigation showed that that deferred requests can't force event deferral. This patch implements this; but more testing showed many more race failures. Some of these are due to how the test suite is written. Anyway, in the end I took the radical approach of deferring all events by default. Most DAP requests are asynchronous by nature, so this seemed ok. The only case I found that really required this is pause.exp, where the test (rightly) expects to see a 'continued' event while performing an inferior function call. I went through all events and all requests and tried to convince myself that this patch will cause acceptable behavior in every case. However, it's hard to be completely sure about this approach. Maybe there are cases that do still need an event before the response, but we just don't have tests for them. Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32685 Acked-By: Simon Marchi <[email protected]>
1 parent 568ec5b commit cbaa41b

File tree

5 files changed

+64
-55
lines changed

5 files changed

+64
-55
lines changed

gdb/python/lib/gdb/dap/evaluate.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _repl(command, frame_id):
6969
}
7070

7171

72-
@request("evaluate")
72+
@request("evaluate", defer_events=False)
7373
@capability("supportsEvaluateForHovers")
7474
@capability("supportsValueFormattingOptions")
7575
def eval_request(
@@ -110,7 +110,7 @@ def variables(
110110

111111

112112
@capability("supportsSetExpression")
113-
@request("setExpression")
113+
@request("setExpression", defer_events=False)
114114
def set_expression(
115115
*, expression: str, value: str, frameId: Optional[int] = None, format=None, **args
116116
):
@@ -126,7 +126,7 @@ def set_expression(
126126

127127

128128
@capability("supportsSetVariable")
129-
@request("setVariable")
129+
@request("setVariable", defer_events=False)
130130
def set_variable(
131131
*, variablesReference: int, name: str, value: str, format=None, **args
132132
):

gdb/python/lib/gdb/dap/events.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from .modules import is_module, make_module
1919
from .scopes import set_finish_value
20-
from .server import send_event, send_event_maybe_later
20+
from .server import send_event
2121
from .startup import exec_and_log, in_gdb_thread, log
2222

2323
# True when the inferior is thought to be running, False otherwise.
@@ -240,7 +240,7 @@ def _on_stop(event):
240240
else:
241241
obj["reason"] = stop_reason_map[event.details["reason"]]
242242
_expected_pause = False
243-
send_event_maybe_later("stopped", obj)
243+
send_event("stopped", obj)
244244

245245

246246
# This keeps a bit of state between the start of an inferior call and

gdb/python/lib/gdb/dap/next.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import gdb
1717

1818
from .events import exec_and_expect_stop
19-
from .server import capability, request, send_gdb, send_gdb_with_response
19+
from .server import capability, request
2020
from .startup import in_gdb_thread
2121
from .state import set_thread
2222

@@ -73,19 +73,14 @@ def step_in(
7373
exec_and_expect_stop(cmd)
7474

7575

76-
@request("stepOut", defer_stop_events=True)
76+
@request("stepOut")
7777
def step_out(*, threadId: int, singleThread: bool = False, **args):
7878
_handle_thread_step(threadId, singleThread, True)
7979
exec_and_expect_stop("finish &", propagate_exception=True)
8080

8181

82-
# This is a server-side request because it is funny: it wants to
83-
# 'continue' but also return a result, which precludes using
84-
# response=False. Using 'continue &' would mostly work ok, but this
85-
# yields races when a stop occurs before the response is sent back to
86-
# the client.
87-
@request("continue", on_dap_thread=True)
82+
@request("continue")
8883
def continue_request(*, threadId: int, singleThread: bool = False, **args):
89-
locked = send_gdb_with_response(lambda: _handle_thread_step(threadId, singleThread))
90-
send_gdb(lambda: exec_and_expect_stop("continue"))
84+
locked = _handle_thread_step(threadId, singleThread)
85+
exec_and_expect_stop("continue &")
9186
return {"allThreadsContinued": not locked}

gdb/python/lib/gdb/dap/server.py

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ def set_request(self, req, result):
7373
self._req = req
7474
self._result = result
7575

76+
@in_dap_thread
77+
def defer_events(self):
78+
"""Return True if events should be deferred during execution.
79+
80+
This may be overridden by subclasses."""
81+
return True
82+
7683
@in_dap_thread
7784
def invoke(self):
7885
"""Implement the deferred request.
@@ -95,7 +102,10 @@ def reschedule(self):
95102
96103
"""
97104
with _server.canceller.current_request(self._req):
105+
if self.defer_events():
106+
_server.set_defer_events()
98107
_server.invoke_request(self._req, self._result, self.invoke)
108+
_server.emit_pending_events()
99109

100110

101111
# A subclass of Exception that is used solely for reporting that a
@@ -230,7 +240,7 @@ def __init__(self, in_stream, out_stream, child_stream):
230240
self._out_stream = out_stream
231241
self._child_stream = child_stream
232242
self._delayed_fns_lock = threading.Lock()
233-
self.defer_stop_events = False
243+
self._defer_events = False
234244
self._delayed_fns = []
235245
# This queue accepts JSON objects that are then sent to the
236246
# DAP client. Writing is done in a separate thread to avoid
@@ -316,7 +326,7 @@ def fn():
316326
def _read_inferior_output(self):
317327
while True:
318328
line = self._child_stream.readline()
319-
self.send_event(
329+
self.send_event_maybe_later(
320330
"output",
321331
{
322332
"category": "stdout",
@@ -355,6 +365,17 @@ def _reader_thread(self):
355365
# When we hit EOF, signal it with None.
356366
self._read_queue.put(None)
357367

368+
@in_dap_thread
369+
def emit_pending_events(self):
370+
"""Emit any pending events."""
371+
fns = None
372+
with self._delayed_fns_lock:
373+
fns = self._delayed_fns
374+
self._delayed_fns = []
375+
self._defer_events = False
376+
for fn in fns:
377+
fn()
378+
358379
@in_dap_thread
359380
def main_loop(self):
360381
"""The main loop of the DAP server."""
@@ -371,13 +392,7 @@ def main_loop(self):
371392
req = cmd["seq"]
372393
with self.canceller.current_request(req):
373394
self._handle_command(cmd)
374-
fns = None
375-
with self._delayed_fns_lock:
376-
fns = self._delayed_fns
377-
self._delayed_fns = []
378-
self.defer_stop_events = False
379-
for fn in fns:
380-
fn()
395+
self.emit_pending_events()
381396
# Got the terminate request. This is handled by the
382397
# JSON-writing thread, so that we can ensure that all
383398
# responses are flushed to the client before exiting.
@@ -386,13 +401,13 @@ def main_loop(self):
386401
send_gdb("quit")
387402

388403
@in_dap_thread
389-
def send_event_later(self, event, body=None):
390-
"""Send a DAP event back to the client, but only after the
391-
current request has completed."""
404+
def set_defer_events(self):
405+
"""Defer any events until the current request has completed."""
392406
with self._delayed_fns_lock:
393-
self._delayed_fns.append(lambda: self.send_event(event, body))
407+
self._defer_events = True
394408

395-
@in_gdb_thread
409+
# Note that this does not need to be run in any particular thread,
410+
# because it uses locks for thread-safety.
396411
def send_event_maybe_later(self, event, body=None):
397412
"""Send a DAP event back to the client, but if a request is in-flight
398413
within the dap thread and that request is configured to delay the event,
@@ -401,10 +416,10 @@ def send_event_maybe_later(self, event, body=None):
401416
with self.canceller.lock:
402417
if self.canceller.in_flight_dap_thread:
403418
with self._delayed_fns_lock:
404-
if self.defer_stop_events:
405-
self._delayed_fns.append(lambda: self.send_event(event, body))
419+
if self._defer_events:
420+
self._delayed_fns.append(lambda: self._send_event(event, body))
406421
return
407-
self.send_event(event, body)
422+
self._send_event(event, body)
408423

409424
@in_dap_thread
410425
def call_function_later(self, fn):
@@ -415,7 +430,7 @@ def call_function_later(self, fn):
415430
# Note that this does not need to be run in any particular thread,
416431
# because it just creates an object and writes it to a thread-safe
417432
# queue.
418-
def send_event(self, event, body=None):
433+
def _send_event(self, event, body=None):
419434
"""Send an event to the DAP client.
420435
EVENT is the name of the event, a string.
421436
BODY is the body of the event, an arbitrary object."""
@@ -439,14 +454,6 @@ def send_event(event, body=None):
439454
"""Send an event to the DAP client.
440455
EVENT is the name of the event, a string.
441456
BODY is the body of the event, an arbitrary object."""
442-
_server.send_event(event, body)
443-
444-
445-
def send_event_maybe_later(event, body=None):
446-
"""Send a DAP event back to the client, but if a request is in-flight
447-
within the dap thread and that request is configured to delay the event,
448-
wait until the response has been sent until the event is sent back to
449-
the client."""
450457
_server.send_event_maybe_later(event, body)
451458

452459

@@ -476,7 +483,7 @@ def request(
476483
response: bool = True,
477484
on_dap_thread: bool = False,
478485
expect_stopped: bool = True,
479-
defer_stop_events: bool = False
486+
defer_events: bool = True
480487
):
481488
"""A decorator for DAP requests.
482489
@@ -498,9 +505,9 @@ def request(
498505
inferior is running. When EXPECT_STOPPED is False, the request
499506
will proceed regardless of the inferior's state.
500507
501-
If DEFER_STOP_EVENTS is True, then make sure any stop events sent
502-
during the request processing are not sent to the client until the
503-
response has been sent.
508+
If DEFER_EVENTS is True, then make sure any events sent during the
509+
request processing are not sent to the client until the response
510+
has been sent.
504511
"""
505512

506513
# Validate the parameters.
@@ -523,26 +530,33 @@ def wrap(func):
523530

524531
# Verify that the function is run on the correct thread.
525532
if on_dap_thread:
526-
cmd = in_dap_thread(func)
533+
check_cmd = in_dap_thread(func)
527534
else:
528535
func = in_gdb_thread(func)
529536

530537
if response:
531-
if defer_stop_events:
532-
if _server is not None:
533-
with _server.delayed_events_lock:
534-
_server.defer_stop_events = True
535538

536539
def sync_call(**args):
537540
return send_gdb_with_response(lambda: func(**args))
538541

539-
cmd = sync_call
542+
check_cmd = sync_call
540543
else:
541544

542545
def non_sync_call(**args):
543546
return send_gdb(lambda: func(**args))
544547

545-
cmd = non_sync_call
548+
check_cmd = non_sync_call
549+
550+
if defer_events:
551+
552+
def deferring(**args):
553+
_server.set_defer_events()
554+
return check_cmd(**args)
555+
556+
cmd = deferring
557+
558+
else:
559+
cmd = check_cmd
546560

547561
# If needed, check that the inferior is not running. This
548562
# wrapping is done last, so the check is done first, before
@@ -582,7 +596,7 @@ def client_bool_capability(name, default=False):
582596
@request("initialize", on_dap_thread=True)
583597
def initialize(**args):
584598
_server.config = args
585-
_server.send_event_later("initialized")
599+
_server.send_event_maybe_later("initialized")
586600
global _lines_start_at_1
587601
_lines_start_at_1 = client_bool_capability("linesStartAt1", True)
588602
global _columns_start_at_1

gdb/testsuite/gdb.dap/attach.exp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ set attach_id [dap_attach $testpid $binfile]
3333

3434
dap_check_request_and_response "configurationDone" configurationDone
3535

36+
dap_check_response "attach response" attach $attach_id
37+
3638
dap_wait_for_event_and_check "stopped" stopped \
3739
"body reason" attach
3840

39-
dap_check_response "attach response" attach $attach_id
40-
4141
dap_shutdown true
4242

4343
kill_wait_spawned_process $test_spawn_id

0 commit comments

Comments
 (0)