]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
Implement the notStopped DAP response
authorTom Tromey <tromey@adacore.com>
Tue, 7 Nov 2023 15:44:44 +0000 (08:44 -0700)
committerTom Tromey <tromey@adacore.com>
Fri, 17 Nov 2023 15:26:03 +0000 (08:26 -0700)
DAP specifies that a request can fail with the "notStopped" message if
the inferior is running but the request requires that it first be
stopped.

This patch implements this for gdb.  Most requests are assumed to
require a stopped inferior, and the exceptions are noted by a new
'request' parameter.

You may notice that the implementation is a bit racy.  I think this is
inherent -- unless the client waits for a stop event before sending a
request, the request may be processed at any time relative to a stop.

https://sourceware.org/bugzilla/show_bug.cgi?id=31037

Reviewed-by: Kévin Le Gouguec <legouguec@adacore.com>
gdb/python/lib/gdb/dap/events.py
gdb/python/lib/gdb/dap/pause.py
gdb/python/lib/gdb/dap/server.py
gdb/testsuite/gdb.dap/pause.exp

index 09214ec3dc81e008bfd9cd02423a7d004428a09f..bfc3f9ee1dcd15e45aa4932a8d3828b7b64a847e 100644 (file)
@@ -21,8 +21,17 @@ from .startup import exec_and_log, in_gdb_thread, log
 from .modules import is_module, make_module
 
 
+# True when the inferior is thought to be running, False otherwise.
+# This may be accessed from any thread, which can be racy.  However,
+# this unimportant because this global is only used for the
+# 'notStopped' response, which itself is inherently racy.
+inferior_running = False
+
+
 @in_gdb_thread
 def _on_exit(event):
+    global inferior_running
+    inferior_running = False
     code = 0
     if hasattr(event, "exit_code"):
         code = event.exit_code
@@ -48,6 +57,8 @@ def thread_event(event, reason):
 
 @in_gdb_thread
 def _new_thread(event):
+    global inferior_running
+    inferior_running = True
     thread_event(event, "started")
 
 
@@ -85,6 +96,8 @@ _suppress_cont = False
 
 @in_gdb_thread
 def _cont(event):
+    global inferior_running
+    inferior_running = True
     global _suppress_cont
     if _suppress_cont:
         log("_suppress_cont case")
@@ -123,6 +136,8 @@ def exec_and_expect_stop(cmd, reason):
 
 @in_gdb_thread
 def _on_stop(event):
+    global inferior_running
+    inferior_running = False
     log("entering _on_stop: " + repr(event))
     global _expected_stop
     obj = {
index d276ab1cb92f23b514a0897f877ad7975a43a984..b7e21452d691ca323bfa64b00b18d45edde162a4 100644 (file)
@@ -17,6 +17,6 @@ from .events import StopKinds, exec_and_expect_stop
 from .server import request
 
 
-@request("pause", response=False)
+@request("pause", response=False, expect_stopped=False)
 def pause(**args):
     exec_and_expect_stop("interrupt -a", StopKinds.PAUSE)
index 4430d2aba5fc88e182cd675fe42d0ea41b1ce852..031bf49f48696e4080409e6977335e8ab6ec3a83 100644 (file)
@@ -13,6 +13,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+import functools
 import inspect
 import json
 import queue
@@ -163,7 +164,28 @@ def send_event(event, body=None):
     _server.send_event(event, body)
 
 
-def request(name: str, *, response: bool = True, on_dap_thread: bool = False):
+# A helper decorator that checks whether the inferior is running.
+def _check_not_running(func):
+    @functools.wraps(func)
+    def check(*args, **kwargs):
+        # Import this as late as possible.  This is done to avoid
+        # circular imports.
+        from .events import inferior_running
+
+        if inferior_running:
+            raise Exception("notStopped")
+        return func(*args, **kwargs)
+
+    return check
+
+
+def request(
+    name: str,
+    *,
+    response: bool = True,
+    on_dap_thread: bool = False,
+    expect_stopped: bool = True
+):
     """A decorator for DAP requests.
 
     This registers the function as the implementation of the DAP
@@ -178,6 +200,11 @@ def request(name: str, *, response: bool = True, on_dap_thread: bool = False):
 
     If ON_DAP_THREAD is True, the function will be invoked in the DAP
     thread.  When ON_DAP_THREAD is True, RESPONSE may not be False.
+
+    If EXPECT_STOPPED is True (the default), then the request will
+    fail with the 'notStopped' reason if it is processed while the
+    inferior is running.  When EXPECT_STOPPED is False, the request
+    will proceed regardless of the inferior's state.
     """
 
     # Validate the parameters.
@@ -217,6 +244,12 @@ def request(name: str, *, response: bool = True, on_dap_thread: bool = False):
 
                 cmd = non_sync_call
 
+        # If needed, check that the inferior is not running.  This
+        # wrapping is done last, so the check is done first, before
+        # trying to dispatch the request to another thread.
+        if expect_stopped:
+            cmd = _check_not_running(cmd)
+
         global _commands
         _commands[name] = cmd
         return cmd
@@ -255,13 +288,13 @@ def initialize(**args):
     return _capabilities.copy()
 
 
-@request("terminate")
+@request("terminate", expect_stopped=False)
 @capability("supportsTerminateRequest")
 def terminate(**args):
     exec_and_log("kill")
 
 
-@request("disconnect", on_dap_thread=True)
+@request("disconnect", on_dap_thread=True, expect_stopped=False)
 @capability("supportTerminateDebuggee")
 def disconnect(*, terminateDebuggee: bool = False, **args):
     if terminateDebuggee:
index 27955d3152646e206a9121f66b97fd0c44e0f31f..558ede982eec03614c1cb411c73d01adb39fe1dd 100644 (file)
@@ -32,6 +32,13 @@ if {[dap_launch $testfile] == ""} {
 dap_check_request_and_response "start inferior" configurationDone
 dap_wait_for_event_and_check "inferior started" thread "body reason" started
 
+set resp [lindex [dap_request_and_response evaluate {o expression [s 23]}] \
+             0]
+gdb_assert {[dict get $resp success] == "false"} \
+    "evaluate failed while inferior executing"
+gdb_assert {[dict get $resp message] == "notStopped"} \
+    "evaluate issued notStopped"
+
 dap_check_request_and_response pause pause \
     {o threadId [i 1]}