GDB (xrefs)
Loading...
Searching...
No Matches
server.py
Go to the documentation of this file.
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
16import functools
17import inspect
18import json
19import queue
20import sys
21
22from .io import start_json_writer, read_json
23from .startup import (
24 exec_and_log,
25 in_dap_thread,
26 in_gdb_thread,
27 send_gdb,
28 send_gdb_with_response,
29 start_thread,
30 log,
31 log_stack,
32)
33from .typecheck import type_check
34
35
36# Map capability names to values.
37_capabilities = {}
38
39# Map command names to callables.
40_commands = {}
41
42# The global server.
43_server = None
44
45
46class Server:
47 """The DAP server class."""
48
49 def __init__(self, in_stream, out_stream, child_stream):
50 self.in_stream = in_stream
51 self.out_stream = out_stream
52 self.child_stream = child_stream
53 self.delayed_events = []
54 # This queue accepts JSON objects that are then sent to the
55 # DAP client. Writing is done in a separate thread to avoid
56 # blocking the read loop.
57 if sys.version_info[0] == 3 and sys.version_info[1] <= 6:
58 self.write_queue = queue.Queue()
59 else:
60 self.write_queue = queue.SimpleQueue()
61 self.done = False
62 global _server
63 _server = self
64
65 # Treat PARAMS as a JSON-RPC request and perform its action.
66 # PARAMS is just a dictionary from the JSON.
67 @in_dap_thread
68 def _handle_command(self, params):
69 # We don't handle 'cancel' for now.
70 result = {
71 "request_seq": params["seq"],
72 "type": "response",
73 "command": params["command"],
74 }
75 try:
76 if "arguments" in params:
77 args = params["arguments"]
78 else:
79 args = {}
80 global _commands
81 body = _commands[params["command"]](**args)
82 if body is not None:
83 result["body"] = body
84 result["success"] = True
85 except BaseException as e:
86 log_stack()
87 result["success"] = False
88 result["message"] = str(e)
89 return result
90
91 # Read inferior output and sends OutputEvents to the client. It
92 # is run in its own thread.
94 while True:
95 line = self.child_stream.readline()
97 "output",
98 {
99 "category": "stdout",
100 "output": line,
101 },
102 )
103
104 # Send OBJ to the client, logging first if needed.
105 def _send_json(self, obj):
106 log("WROTE: <<<" + json.dumps(obj) + ">>>")
107 self.write_queue.put(obj)
108
109 @in_dap_thread
110 def main_loop(self):
111 """The main loop of the DAP server."""
112 # Before looping, start the thread that writes JSON to the
113 # client, and the thread that reads output from the inferior.
115 start_json_writer(self.out_stream, self.write_queue)
116 while not self.done:
117 cmd = read_json(self.in_stream)
118 log("READ: <<<" + json.dumps(cmd) + ">>>")
119 result = self._handle_command_handle_command(cmd)
120 self._send_json_send_json(result)
121 events = self.delayed_events
122 self.delayed_events = []
123 for event, body in events:
124 self.send_eventsend_event(event, body)
125 # Got the terminate request. This is handled by the
126 # JSON-writing thread, so that we can ensure that all
127 # responses are flushed to the client before exiting.
128 self.write_queue.put(None)
129
130 @in_dap_thread
131 def send_event_later(self, event, body=None):
132 """Send a DAP event back to the client, but only after the
133 current request has completed."""
134 self.delayed_events.append((event, body))
135
136 # Note that this does not need to be run in any particular thread,
137 # because it just creates an object and writes it to a thread-safe
138 # queue.
139 def send_event(self, event, body=None):
140 """Send an event to the DAP client.
141 EVENT is the name of the event, a string.
142 BODY is the body of the event, an arbitrary object."""
143 obj = {
144 "type": "event",
145 "event": event,
146 }
147 if body is not None:
148 obj["body"] = body
149 self._send_json_send_json(obj)
150
151 def shutdown(self):
152 """Request that the server shut down."""
153 # Just set a flag. This operation is complicated because we
154 # want to write the result of the request before exiting. See
155 # main_loop.
156 self.done = True
157
158
159def send_event(event, body=None):
160 """Send an event to the DAP client.
161 EVENT is the name of the event, a string.
162 BODY is the body of the event, an arbitrary object."""
163 global _server
164 _server.send_event(event, body)
165
166
167# A helper decorator that checks whether the inferior is running.
168def _check_not_running(func):
169 @functools.wraps(func)
170 def check(*args, **kwargs):
171 # Import this as late as possible. This is done to avoid
172 # circular imports.
173 from .events import inferior_running
174
175 if inferior_running:
176 raise Exception("notStopped")
177 return func(*args, **kwargs)
178
179 return check
180
181
182def request(
183 name: str,
184 *,
185 response: bool = True,
186 on_dap_thread: bool = False,
187 expect_stopped: bool = True
188):
189 """A decorator for DAP requests.
190
191 This registers the function as the implementation of the DAP
192 request NAME. By default, the function is invoked in the gdb
193 thread, and its result is returned as the 'body' of the DAP
194 response.
195
196 Some keyword arguments are provided as well:
197
198 If RESPONSE is False, the result of the function will not be
199 waited for and no 'body' will be in the response.
200
201 If ON_DAP_THREAD is True, the function will be invoked in the DAP
202 thread. When ON_DAP_THREAD is True, RESPONSE may not be False.
203
204 If EXPECT_STOPPED is True (the default), then the request will
205 fail with the 'notStopped' reason if it is processed while the
206 inferior is running. When EXPECT_STOPPED is False, the request
207 will proceed regardless of the inferior's state.
208 """
209
210 # Validate the parameters.
211 assert not on_dap_thread or response
212
213 def wrap(func):
214 code = func.__code__
215 # We don't permit requests to have positional arguments.
216 try:
217 assert code.co_posonlyargcount == 0
218 except AttributeError:
219 # Attribute co_posonlyargcount is supported starting python 3.8.
220 pass
221 assert code.co_argcount == 0
222 # A request must have a **args parameter.
223 assert code.co_flags & inspect.CO_VARKEYWORDS
224
225 # Type-check the calls.
226 func = type_check(func)
227
228 # Verify that the function is run on the correct thread.
229 if on_dap_thread:
230 cmd = in_dap_thread(func)
231 else:
232 func = in_gdb_thread(func)
233
234 if response:
235
236 def sync_call(**args):
237 return send_gdb_with_response(lambda: func(**args))
238
239 cmd = sync_call
240 else:
241
242 def non_sync_call(**args):
243 return send_gdb(lambda: func(**args))
244
245 cmd = non_sync_call
246
247 # If needed, check that the inferior is not running. This
248 # wrapping is done last, so the check is done first, before
249 # trying to dispatch the request to another thread.
250 if expect_stopped:
251 cmd = _check_not_running(cmd)
252
253 global _commands
254 _commands[name] = cmd
255 return cmd
256
257 return wrap
258
259
260def capability(name, value=True):
261 """A decorator that indicates that the wrapper function implements
262 the DAP capability NAME."""
263
264 def wrap(func):
265 global _capabilities
266 _capabilities[name] = value
267 return func
268
269 return wrap
270
271
272def client_bool_capability(name):
273 """Return the value of a boolean client capability.
274
275 If the capability was not specified, or did not have boolean type,
276 False is returned."""
277 global _server
278 if name in _server.config and isinstance(_server.config[name], bool):
279 return _server.config[name]
280 return False
281
282
283@request("initialize", on_dap_thread=True)
284def initialize(**args):
285 global _server, _capabilities
286 _server.config = args
287 _server.send_event_later("initialized")
288 return _capabilities.copy()
289
290
291@request("terminate", expect_stopped=False)
292@capability("supportsTerminateRequest")
293def terminate(**args):
294 exec_and_log("kill")
295
296
297@request("disconnect", on_dap_thread=True, expect_stopped=False)
298@capability("supportTerminateDebuggee")
299def disconnect(*, terminateDebuggee: bool = False, **args):
300 if terminateDebuggee:
301 send_gdb_with_response("kill")
302 _server.shutdown()
send_event(self, event, body=None)
Definition server.py:139
_handle_command(self, params)
Definition server.py:68
_send_json(self, obj)
Definition server.py:105
_read_inferior_output(self)
Definition server.py:93
__init__(self, in_stream, out_stream, child_stream)
Definition server.py:49
send_event_later(self, event, body=None)
Definition server.py:131
terminate(**args)
Definition server.py:293
capability(name, value=True)
Definition server.py:260
request(str name, *bool response=True, bool on_dap_thread=False, bool expect_stopped=True)
Definition server.py:188
client_bool_capability(name)
Definition server.py:272
disconnect(*bool terminateDebuggee=False, **args)
Definition server.py:299
_check_not_running(func)
Definition server.py:168
initialize(**args)
Definition server.py:284
void(* func)(remote_target *remote, char *)
static void check(BOOL ok, const char *file, int line)