Handle DAP supportsVariableType capability
[binutils-gdb.git] / gdb / python / lib / gdb / dap / server.py
1 # Copyright 2022-2023 Free Software Foundation, Inc.
2
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 3 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
16 import json
17 import queue
18 import sys
19
20 from .io import start_json_writer, read_json
21 from .startup import (
22 in_dap_thread,
23 start_thread,
24 log,
25 log_stack,
26 send_gdb_with_response,
27 )
28 from .typecheck import type_check
29
30
31 # Map capability names to values.
32 _capabilities = {}
33
34 # Map command names to callables.
35 _commands = {}
36
37 # The global server.
38 _server = None
39
40
41 class Server:
42 """The DAP server class."""
43
44 def __init__(self, in_stream, out_stream, child_stream):
45 self.in_stream = in_stream
46 self.out_stream = out_stream
47 self.child_stream = child_stream
48 self.delayed_events = []
49 # This queue accepts JSON objects that are then sent to the
50 # DAP client. Writing is done in a separate thread to avoid
51 # blocking the read loop.
52 if sys.version_info[0] == 3 and sys.version_info[1] <= 6:
53 self.write_queue = queue.Queue()
54 else:
55 self.write_queue = queue.SimpleQueue()
56 self.done = False
57 global _server
58 _server = self
59
60 # Treat PARAMS as a JSON-RPC request and perform its action.
61 # PARAMS is just a dictionary from the JSON.
62 @in_dap_thread
63 def _handle_command(self, params):
64 # We don't handle 'cancel' for now.
65 result = {
66 "request_seq": params["seq"],
67 "type": "response",
68 "command": params["command"],
69 }
70 try:
71 if "arguments" in params:
72 args = params["arguments"]
73 else:
74 args = {}
75 global _commands
76 body = _commands[params["command"]](**args)
77 if body is not None:
78 result["body"] = body
79 result["success"] = True
80 except BaseException as e:
81 log_stack()
82 result["success"] = False
83 result["message"] = str(e)
84 return result
85
86 # Read inferior output and sends OutputEvents to the client. It
87 # is run in its own thread.
88 def _read_inferior_output(self):
89 while True:
90 line = self.child_stream.readline()
91 self.send_event(
92 "output",
93 {
94 "category": "stdout",
95 "output": line,
96 },
97 )
98
99 # Send OBJ to the client, logging first if needed.
100 def _send_json(self, obj):
101 log("WROTE: <<<" + json.dumps(obj) + ">>>")
102 self.write_queue.put(obj)
103
104 @in_dap_thread
105 def main_loop(self):
106 """The main loop of the DAP server."""
107 # Before looping, start the thread that writes JSON to the
108 # client, and the thread that reads output from the inferior.
109 start_thread("output reader", self._read_inferior_output)
110 start_json_writer(self.out_stream, self.write_queue)
111 while not self.done:
112 cmd = read_json(self.in_stream)
113 log("READ: <<<" + json.dumps(cmd) + ">>>")
114 result = self._handle_command(cmd)
115 self._send_json(result)
116 events = self.delayed_events
117 self.delayed_events = []
118 for event, body in events:
119 self.send_event(event, body)
120 # Got the terminate request. This is handled by the
121 # JSON-writing thread, so that we can ensure that all
122 # responses are flushed to the client before exiting.
123 self.write_queue.put(None)
124
125 @in_dap_thread
126 def send_event_later(self, event, body=None):
127 """Send a DAP event back to the client, but only after the
128 current request has completed."""
129 self.delayed_events.append((event, body))
130
131 # Note that this does not need to be run in any particular thread,
132 # because it just creates an object and writes it to a thread-safe
133 # queue.
134 def send_event(self, event, body=None):
135 """Send an event to the DAP client.
136 EVENT is the name of the event, a string.
137 BODY is the body of the event, an arbitrary object."""
138 obj = {
139 "type": "event",
140 "event": event,
141 }
142 if body is not None:
143 obj["body"] = body
144 self._send_json(obj)
145
146 def shutdown(self):
147 """Request that the server shut down."""
148 # Just set a flag. This operation is complicated because we
149 # want to write the result of the request before exiting. See
150 # main_loop.
151 self.done = True
152
153
154 def send_event(event, body):
155 """Send an event to the DAP client.
156 EVENT is the name of the event, a string.
157 BODY is the body of the event, an arbitrary object."""
158 global _server
159 _server.send_event(event, body)
160
161
162 def request(name):
163 """A decorator that indicates that the wrapper function implements
164 the DAP request NAME."""
165
166 def wrap(func):
167 global _commands
168 # All requests must run in the DAP thread.
169 # Also type-check the calls.
170 func = in_dap_thread(type_check(func))
171 _commands[name] = func
172 return func
173
174 return wrap
175
176
177 def capability(name, value=True):
178 """A decorator that indicates that the wrapper function implements
179 the DAP capability NAME."""
180
181 def wrap(func):
182 global _capabilities
183 _capabilities[name] = value
184 return func
185
186 return wrap
187
188
189 def client_bool_capability(name):
190 """Return the value of a boolean client capability.
191
192 If the capability was not specified, or did not have boolean type,
193 False is returned."""
194 global _server
195 if name in _server.config and isinstance(_server.config[name], bool):
196 return _server.config[name]
197 return False
198
199
200 @request("initialize")
201 def initialize(**args):
202 global _server, _capabilities
203 _server.config = args
204 _server.send_event_later("initialized")
205 return _capabilities.copy()
206
207
208 @request("terminate")
209 @capability("supportsTerminateRequest")
210 def terminate(**args):
211 # We can ignore the result here, because we only really need to
212 # synchronize.
213 send_gdb_with_response("kill")
214
215
216 @request("disconnect")
217 @capability("supportTerminateDebuggee")
218 def disconnect(*, terminateDebuggee: bool = False, **args):
219 if terminateDebuggee:
220 terminate()
221 _server.shutdown()