1# Copyright 2022-2024 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 heapq 18import inspect 19import json 20import threading 21from contextlib import contextmanager 22 23import gdb 24 25from .io import read_json, start_json_writer 26from .startup import ( 27 DAPException, 28 DAPQueue, 29 LogLevel, 30 exec_and_log, 31 in_dap_thread, 32 in_gdb_thread, 33 log, 34 log_stack, 35 start_thread, 36 thread_log, 37) 38from .typecheck import type_check 39 40# Map capability names to values. 41_capabilities = {} 42 43# Map command names to callables. 44_commands = {} 45 46# The global server. 47_server = None 48 49 50# A subclass of Exception that is used solely for reporting that a 51# request needs the inferior to be stopped, but it is not stopped. 52class NotStoppedException(Exception): 53 pass 54 55 56# This is used to handle cancellation requests. It tracks all the 57# needed state, so that we can cancel both requests that are in flight 58# as well as queued requests. 59class CancellationHandler: 60 def __init__(self): 61 # Methods on this class acquire this lock before proceeding. 62 self.lock = threading.Lock() 63 # The request currently being handled, or None. 64 self.in_flight_dap_thread = None 65 self.in_flight_gdb_thread = None 66 self.reqs = [] 67 68 def starting(self, req): 69 """Call at the start of the given request.""" 70 with self.lock: 71 self.in_flight_dap_thread = req 72 73 def done(self, req): 74 """Indicate that the request is done.""" 75 with self.lock: 76 self.in_flight_dap_thread = None 77 78 def cancel(self, req): 79 """Call to cancel a request. 80 81 If the request has already finished, this is ignored. 82 If the request is in flight, it is interrupted. 83 If the request has not yet been seen, the cancellation is queued.""" 84 with self.lock: 85 if req == self.in_flight_gdb_thread: 86 gdb.interrupt() 87 else: 88 # We don't actually ignore the request here, but in 89 # the 'starting' method. This way we don't have to 90 # track as much state. Also, this implementation has 91 # the weird property that a request can be cancelled 92 # before it is even sent. It didn't seem worthwhile 93 # to try to check for this. 94 heapq.heappush(self.reqs, req) 95 96 @contextmanager 97 def interruptable_region(self, req): 98 """Return a new context manager that sets in_flight_gdb_thread to 99 REQ.""" 100 if req is None: 101 # No request is handled in the region, just execute the region. 102 yield 103 return 104 try: 105 with self.lock: 106 # If the request is cancelled, don't execute the region. 107 while len(self.reqs) > 0 and self.reqs[0] <= req: 108 if heapq.heappop(self.reqs) == req: 109 raise KeyboardInterrupt() 110 # Request is being handled by the gdb thread. 111 self.in_flight_gdb_thread = req 112 # Execute region. This may be interrupted by gdb.interrupt. 113 yield 114 finally: 115 with self.lock: 116 # Request is no longer handled by the gdb thread. 117 self.in_flight_gdb_thread = None 118 119 120class Server: 121 """The DAP server class.""" 122 123 def __init__(self, in_stream, out_stream, child_stream): 124 self.in_stream = in_stream 125 self.out_stream = out_stream 126 self.child_stream = child_stream 127 self.delayed_events = [] 128 # This queue accepts JSON objects that are then sent to the 129 # DAP client. Writing is done in a separate thread to avoid 130 # blocking the read loop. 131 self.write_queue = DAPQueue() 132 # Reading is also done in a separate thread, and a queue of 133 # requests is kept. 134 self.read_queue = DAPQueue() 135 self.done = False 136 self.canceller = CancellationHandler() 137 global _server 138 _server = self 139 140 # Treat PARAMS as a JSON-RPC request and perform its action. 141 # PARAMS is just a dictionary from the JSON. 142 @in_dap_thread 143 def _handle_command(self, params): 144 req = params["seq"] 145 result = { 146 "request_seq": req, 147 "type": "response", 148 "command": params["command"], 149 } 150 try: 151 self.canceller.starting(req) 152 if "arguments" in params: 153 args = params["arguments"] 154 else: 155 args = {} 156 global _commands 157 body = _commands[params["command"]](**args) 158 if body is not None: 159 result["body"] = body 160 result["success"] = True 161 except NotStoppedException: 162 # This is an expected exception, and the result is clearly 163 # visible in the log, so do not log it. 164 result["success"] = False 165 result["message"] = "notStopped" 166 except KeyboardInterrupt: 167 # This can only happen when a request has been canceled. 168 result["success"] = False 169 result["message"] = "cancelled" 170 except DAPException as e: 171 # Don't normally want to see this, as it interferes with 172 # the test suite. 173 log_stack(LogLevel.FULL) 174 result["success"] = False 175 result["message"] = str(e) 176 except BaseException as e: 177 log_stack() 178 result["success"] = False 179 result["message"] = str(e) 180 self.canceller.done(req) 181 return result 182 183 # Read inferior output and sends OutputEvents to the client. It 184 # is run in its own thread. 185 def _read_inferior_output(self): 186 while True: 187 line = self.child_stream.readline() 188 self.send_event( 189 "output", 190 { 191 "category": "stdout", 192 "output": line, 193 }, 194 ) 195 196 # Send OBJ to the client, logging first if needed. 197 def _send_json(self, obj): 198 log("WROTE: <<<" + json.dumps(obj) + ">>>") 199 self.write_queue.put(obj) 200 201 # This is run in a separate thread and simply reads requests from 202 # the client and puts them into a queue. A separate thread is 203 # used so that 'cancel' requests can be handled -- the DAP thread 204 # will normally block, waiting for each request to complete. 205 def _reader_thread(self): 206 while True: 207 cmd = read_json(self.in_stream) 208 if cmd is None: 209 break 210 log("READ: <<<" + json.dumps(cmd) + ">>>") 211 # Be extra paranoid about the form here. If anything is 212 # missing, it will be put in the queue and then an error 213 # issued by ordinary request processing. 214 if ( 215 "command" in cmd 216 and cmd["command"] == "cancel" 217 and "arguments" in cmd 218 # gdb does not implement progress, so there's no need 219 # to check for progressId. 220 and "requestId" in cmd["arguments"] 221 ): 222 self.canceller.cancel(cmd["arguments"]["requestId"]) 223 self.read_queue.put(cmd) 224 # When we hit EOF, signal it with None. 225 self.read_queue.put(None) 226 227 @in_dap_thread 228 def main_loop(self): 229 """The main loop of the DAP server.""" 230 # Before looping, start the thread that writes JSON to the 231 # client, and the thread that reads output from the inferior. 232 start_thread("output reader", self._read_inferior_output) 233 json_writer = start_json_writer(self.out_stream, self.write_queue) 234 start_thread("JSON reader", self._reader_thread) 235 while not self.done: 236 cmd = self.read_queue.get() 237 # A None value here means the reader hit EOF. 238 if cmd is None: 239 break 240 result = self._handle_command(cmd) 241 self._send_json(result) 242 events = self.delayed_events 243 self.delayed_events = [] 244 for event, body in events: 245 self.send_event(event, body) 246 # Got the terminate request. This is handled by the 247 # JSON-writing thread, so that we can ensure that all 248 # responses are flushed to the client before exiting. 249 self.write_queue.put(None) 250 json_writer.join() 251 send_gdb("quit") 252 253 @in_dap_thread 254 def send_event_later(self, event, body=None): 255 """Send a DAP event back to the client, but only after the 256 current request has completed.""" 257 self.delayed_events.append((event, body)) 258 259 # Note that this does not need to be run in any particular thread, 260 # because it just creates an object and writes it to a thread-safe 261 # queue. 262 def send_event(self, event, body=None): 263 """Send an event to the DAP client. 264 EVENT is the name of the event, a string. 265 BODY is the body of the event, an arbitrary object.""" 266 obj = { 267 "type": "event", 268 "event": event, 269 } 270 if body is not None: 271 obj["body"] = body 272 self._send_json(obj) 273 274 def shutdown(self): 275 """Request that the server shut down.""" 276 # Just set a flag. This operation is complicated because we 277 # want to write the result of the request before exiting. See 278 # main_loop. 279 self.done = True 280 281 282def send_event(event, body=None): 283 """Send an event to the DAP client. 284 EVENT is the name of the event, a string. 285 BODY is the body of the event, an arbitrary object.""" 286 global _server 287 _server.send_event(event, body) 288 289 290# A helper decorator that checks whether the inferior is running. 291def _check_not_running(func): 292 @functools.wraps(func) 293 def check(*args, **kwargs): 294 # Import this as late as possible. This is done to avoid 295 # circular imports. 296 from .events import inferior_running 297 298 if inferior_running: 299 raise NotStoppedException() 300 return func(*args, **kwargs) 301 302 return check 303 304 305def request( 306 name: str, 307 *, 308 response: bool = True, 309 on_dap_thread: bool = False, 310 expect_stopped: bool = True 311): 312 """A decorator for DAP requests. 313 314 This registers the function as the implementation of the DAP 315 request NAME. By default, the function is invoked in the gdb 316 thread, and its result is returned as the 'body' of the DAP 317 response. 318 319 Some keyword arguments are provided as well: 320 321 If RESPONSE is False, the result of the function will not be 322 waited for and no 'body' will be in the response. 323 324 If ON_DAP_THREAD is True, the function will be invoked in the DAP 325 thread. When ON_DAP_THREAD is True, RESPONSE may not be False. 326 327 If EXPECT_STOPPED is True (the default), then the request will 328 fail with the 'notStopped' reason if it is processed while the 329 inferior is running. When EXPECT_STOPPED is False, the request 330 will proceed regardless of the inferior's state. 331 """ 332 333 # Validate the parameters. 334 assert not on_dap_thread or response 335 336 def wrap(func): 337 code = func.__code__ 338 # We don't permit requests to have positional arguments. 339 try: 340 assert code.co_posonlyargcount == 0 341 except AttributeError: 342 # Attribute co_posonlyargcount is supported starting python 3.8. 343 pass 344 assert code.co_argcount == 0 345 # A request must have a **args parameter. 346 assert code.co_flags & inspect.CO_VARKEYWORDS 347 348 # Type-check the calls. 349 func = type_check(func) 350 351 # Verify that the function is run on the correct thread. 352 if on_dap_thread: 353 cmd = in_dap_thread(func) 354 else: 355 func = in_gdb_thread(func) 356 357 if response: 358 359 def sync_call(**args): 360 return send_gdb_with_response(lambda: func(**args)) 361 362 cmd = sync_call 363 else: 364 365 def non_sync_call(**args): 366 return send_gdb(lambda: func(**args)) 367 368 cmd = non_sync_call 369 370 # If needed, check that the inferior is not running. This 371 # wrapping is done last, so the check is done first, before 372 # trying to dispatch the request to another thread. 373 if expect_stopped: 374 cmd = _check_not_running(cmd) 375 376 global _commands 377 assert name not in _commands 378 _commands[name] = cmd 379 return cmd 380 381 return wrap 382 383 384def capability(name, value=True): 385 """A decorator that indicates that the wrapper function implements 386 the DAP capability NAME.""" 387 388 def wrap(func): 389 global _capabilities 390 assert name not in _capabilities 391 _capabilities[name] = value 392 return func 393 394 return wrap 395 396 397def client_bool_capability(name): 398 """Return the value of a boolean client capability. 399 400 If the capability was not specified, or did not have boolean type, 401 False is returned.""" 402 global _server 403 if name in _server.config and isinstance(_server.config[name], bool): 404 return _server.config[name] 405 return False 406 407 408@request("initialize", on_dap_thread=True) 409def initialize(**args): 410 global _server, _capabilities 411 _server.config = args 412 _server.send_event_later("initialized") 413 return _capabilities.copy() 414 415 416@request("terminate", expect_stopped=False) 417@capability("supportsTerminateRequest") 418def terminate(**args): 419 exec_and_log("kill") 420 421 422@request("disconnect", on_dap_thread=True, expect_stopped=False) 423@capability("supportTerminateDebuggee") 424def disconnect(*, terminateDebuggee: bool = False, **args): 425 if terminateDebuggee: 426 send_gdb_with_response("kill") 427 _server.shutdown() 428 429 430@request("cancel", on_dap_thread=True, expect_stopped=False) 431@capability("supportsCancelRequest") 432def cancel(**args): 433 # If a 'cancel' request can actually be satisfied, it will be 434 # handled specially in the reader thread. However, in order to 435 # construct a proper response, the request is also added to the 436 # command queue and so ends up here. Additionally, the spec says: 437 # The cancel request may return an error if it could not cancel 438 # an operation but a client should refrain from presenting this 439 # error to end users. 440 # ... which gdb takes to mean that it is fine for all cancel 441 # requests to report success. 442 return None 443 444 445class Invoker(object): 446 """A simple class that can invoke a gdb command.""" 447 448 def __init__(self, cmd): 449 self.cmd = cmd 450 451 # This is invoked in the gdb thread to run the command. 452 @in_gdb_thread 453 def __call__(self): 454 exec_and_log(self.cmd) 455 456 457class Cancellable(object): 458 459 def __init__(self, fn, result_q=None): 460 self.fn = fn 461 self.result_q = result_q 462 with _server.canceller.lock: 463 self.req = _server.canceller.in_flight_dap_thread 464 465 # This is invoked in the gdb thread to run self.fn. 466 @in_gdb_thread 467 def __call__(self): 468 try: 469 with _server.canceller.interruptable_region(self.req): 470 val = self.fn() 471 if self.result_q is not None: 472 self.result_q.put(val) 473 except (Exception, KeyboardInterrupt) as e: 474 if self.result_q is not None: 475 # Pass result or exception to caller. 476 self.result_q.put(e) 477 elif isinstance(e, KeyboardInterrupt): 478 # Fn was cancelled. 479 pass 480 else: 481 # Exception happened. Ignore and log it. 482 err_string = "%s, %s" % (e, type(e)) 483 thread_log("caught exception: " + err_string) 484 log_stack() 485 486 487def send_gdb(cmd): 488 """Send CMD to the gdb thread. 489 CMD can be either a function or a string. 490 If it is a string, it is passed to gdb.execute.""" 491 if isinstance(cmd, str): 492 cmd = Invoker(cmd) 493 494 # Post the event and don't wait for the result. 495 gdb.post_event(Cancellable(cmd)) 496 497 498def send_gdb_with_response(fn): 499 """Send FN to the gdb thread and return its result. 500 If FN is a string, it is passed to gdb.execute and None is 501 returned as the result. 502 If FN throws an exception, this function will throw the 503 same exception in the calling thread. 504 """ 505 if isinstance(fn, str): 506 fn = Invoker(fn) 507 508 # Post the event and wait for the result in result_q. 509 result_q = DAPQueue() 510 gdb.post_event(Cancellable(fn, result_q)) 511 val = result_q.get() 512 513 if isinstance(val, (Exception, KeyboardInterrupt)): 514 raise val 515 return val 516