1 # Copyright (c) Microsoft Corporation. All rights reserved. 2 # Licensed under the MIT License. See LICENSE in the project root 3 # for license information. 4 5 from __future__ import annotations 6 7 import atexit 8 import os 9 import sys 10 11 import debugpy 12 from debugpy import adapter, common, launcher 13 from debugpy.common import json, log, messaging, sockets 14 from debugpy.adapter import clients, components, launchers, servers, sessions 15 16 17 class Client(components.Component): 18 """Handles the client side of a debug session.""" 19 20 message_handler = components.Component.message_handler 21 22 known_subprocesses: set[servers.Connection] 23 """Server connections to subprocesses that this client has been made aware of. 24 """ 25 26 class Capabilities(components.Capabilities): 27 PROPERTIES = { 28 "supportsVariableType": False, 29 "supportsVariablePaging": False, 30 "supportsRunInTerminalRequest": False, 31 "supportsMemoryReferences": False, 32 "supportsArgsCanBeInterpretedByShell": False, 33 "supportsStartDebuggingRequest": False, 34 } 35 36 class Expectations(components.Capabilities): 37 PROPERTIES = { 38 "locale": "en-US", 39 "linesStartAt1": True, 40 "columnsStartAt1": True, 41 "pathFormat": json.enum("path", optional=True), # we don't support "uri" 42 } 43 44 def __init__(self, sock): 45 if sock == "stdio": 46 log.info("Connecting to client over stdio...", self) 47 self.using_stdio = True 48 stream = messaging.JsonIOStream.from_stdio() 49 # Make sure that nothing else tries to interfere with the stdio streams 50 # that are going to be used for DAP communication from now on. 51 sys.stdin = stdin = open(os.devnull, "r") 52 atexit.register(stdin.close) 53 sys.stdout = stdout = open(os.devnull, "w") 54 atexit.register(stdout.close) 55 else: 56 self.using_stdio = False 57 stream = messaging.JsonIOStream.from_socket(sock) 58 59 with sessions.Session() as session: 60 super().__init__(session, stream) 61 62 self.client_id = None 63 """ID of the connecting client. This can be 'test' while running tests.""" 64 65 self.has_started = False 66 """Whether the "launch" or "attach" request was received from the client, and 67 fully handled. 68 """ 69 70 self.start_request = None 71 """The "launch" or "attach" request as received from the client. 72 """ 73 74 self.restart_requested = False 75 """Whether the client requested the debug adapter to be automatically 76 restarted via "restart":true in the start request. 77 """ 78 79 self._initialize_request = None 80 """The "initialize" request as received from the client, to propagate to the 81 server later.""" 82 83 self._deferred_events = [] 84 """Deferred events from the launcher and the server that must be propagated 85 only if and when the "launch" or "attach" response is sent. 86 """ 87 88 self._forward_terminate_request = False 89 90 self.known_subprocesses = set() 91 92 session.client = self 93 session.register() 94 95 # For the transition period, send the telemetry events with both old and new 96 # name. The old one should be removed once the new one lights up. 97 self.channel.send_event( 98 "output", 99 { 100 "category": "telemetry", 101 "output": "ptvsd", 102 "data": {"packageVersion": debugpy.__version__}, 103 }, 104 ) 105 self.channel.send_event( 106 "output", 107 { 108 "category": "telemetry", 109 "output": "debugpy", 110 "data": {"packageVersion": debugpy.__version__}, 111 }, 112 ) 113 sessions.report_sockets() 114 115 def propagate_after_start(self, event): 116 # pydevd starts sending events as soon as we connect, but the client doesn't 117 # expect to see any until it receives the response to "launch" or "attach" 118 # request. If client is not ready yet, save the event instead of propagating 119 # it immediately. 120 if self._deferred_events is not None: 121 self._deferred_events.append(event) 122 log.debug("Propagation deferred.") 123 else: 124 self.client.channel.propagate(event) 125 126 def _propagate_deferred_events(self): 127 log.debug("Propagating deferred events to {0}...", self.client) 128 for event in self._deferred_events: 129 log.debug("Propagating deferred {0}", event.describe()) 130 self.client.channel.propagate(event) 131 log.info("All deferred events propagated to {0}.", self.client) 132 self._deferred_events = None 133 134 # Generic event handler. There are no specific handlers for client events, because 135 # there are no events from the client in DAP - but we propagate them if we can, in 136 # case some events appear in future protocol versions. 137 @message_handler 138 def event(self, event): 139 if self.server: 140 self.server.channel.propagate(event) 141 142 # Generic request handler, used if there's no specific handler below. 143 @message_handler 144 def request(self, request): 145 return self.server.channel.delegate(request) 146 147 @message_handler 148 def initialize_request(self, request): 149 if self._initialize_request is not None: 150 raise request.isnt_valid("Session is already initialized") 151 152 self.client_id = request("clientID", "") 153 self.capabilities = self.Capabilities(self, request) 154 self.expectations = self.Expectations(self, request) 155 self._initialize_request = request 156 157 exception_breakpoint_filters = [ 158 { 159 "filter": "raised", 160 "label": "Raised Exceptions", 161 "default": False, 162 "description": "Break whenever any exception is raised.", 163 }, 164 { 165 "filter": "uncaught", 166 "label": "Uncaught Exceptions", 167 "default": True, 168 "description": "Break when the process is exiting due to unhandled exception.", 169 }, 170 { 171 "filter": "userUnhandled", 172 "label": "User Uncaught Exceptions", 173 "default": False, 174 "description": "Break when exception escapes into library code.", 175 }, 176 ] 177 178 return { 179 "supportsCompletionsRequest": True, 180 "supportsConditionalBreakpoints": True, 181 "supportsConfigurationDoneRequest": True, 182 "supportsDebuggerProperties": True, 183 "supportsDelayedStackTraceLoading": True, 184 "supportsEvaluateForHovers": True, 185 "supportsExceptionInfoRequest": True, 186 "supportsExceptionOptions": True, 187 "supportsFunctionBreakpoints": True, 188 "supportsHitConditionalBreakpoints": True, 189 "supportsLogPoints": True, 190 "supportsModulesRequest": True, 191 "supportsSetExpression": True, 192 "supportsSetVariable": True, 193 "supportsValueFormattingOptions": True, 194 "supportsTerminateDebuggee": True, 195 "supportsTerminateRequest": True, 196 "supportsGotoTargetsRequest": True, 197 "supportsClipboardContext": True, 198 "exceptionBreakpointFilters": exception_breakpoint_filters, 199 "supportsStepInTargetsRequest": True, 200 } 201 202 # Common code for "launch" and "attach" request handlers. 203 # 204 # See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 205 # for the sequence of request and events necessary to orchestrate the start. 206 def _start_message_handler(f): 207 @components.Component.message_handler 208 def handle(self, request): 209 assert request.is_request("launch", "attach") 210 if self._initialize_request is None: 211 raise request.isnt_valid("Session is not initialized yet") 212 if self.launcher or self.server: 213 raise request.isnt_valid("Session is already started") 214 215 self.session.no_debug = request("noDebug", json.default(False)) 216 if self.session.no_debug: 217 servers.dont_wait_for_first_connection() 218 219 self.session.debug_options = debug_options = set( 220 request("debugOptions", json.array(str)) 221 ) 222 223 f(self, request) 224 if request.response is not None: 225 return 226 227 if self.server: 228 self.server.initialize(self._initialize_request) 229 self._initialize_request = None 230 231 arguments = request.arguments 232 if self.launcher: 233 redirecting = arguments.get("console") == "internalConsole" 234 if "RedirectOutput" in debug_options: 235 # The launcher is doing output redirection, so we don't need the 236 # server to do it, as well. 237 arguments = dict(arguments) 238 arguments["debugOptions"] = list( 239 debug_options - {"RedirectOutput"} 240 ) 241 redirecting = True 242 243 if arguments.get("redirectOutput"): 244 arguments = dict(arguments) 245 del arguments["redirectOutput"] 246 redirecting = True 247 248 arguments["isOutputRedirected"] = redirecting 249 250 # pydevd doesn't send "initialized", and responds to the start request 251 # immediately, without waiting for "configurationDone". If it changes 252 # to conform to the DAP spec, we'll need to defer waiting for response. 253 try: 254 self.server.channel.request(request.command, arguments) 255 except messaging.NoMoreMessages: 256 # Server closed connection before we could receive the response to 257 # "attach" or "launch" - this can happen when debuggee exits shortly 258 # after starting. It's not an error, but we can't do anything useful 259 # here at this point, either, so just bail out. 260 request.respond({}) 261 self.session.finalize( 262 "{0} disconnected before responding to {1}".format( 263 self.server, 264 json.repr(request.command), 265 ) 266 ) 267 return 268 except messaging.MessageHandlingError as exc: 269 exc.propagate(request) 270 271 if self.session.no_debug: 272 self.start_request = request 273 self.has_started = True 274 request.respond({}) 275 self._propagate_deferred_events() 276 return 277 278 # Let the client know that it can begin configuring the adapter. 279 self.channel.send_event("initialized") 280 281 self.start_request = request 282 return messaging.NO_RESPONSE # will respond on "configurationDone" 283 284 return handle 285 286 @_start_message_handler 287 def launch_request(self, request): 288 from debugpy.adapter import launchers 289 290 if self.session.id != 1 or len(servers.connections()): 291 raise request.cant_handle('"attach" expected') 292 293 debug_options = set(request("debugOptions", json.array(str))) 294 295 # Handling of properties that can also be specified as legacy "debugOptions" flags. 296 # If property is explicitly set to false, but the flag is in "debugOptions", treat 297 # it as an error. Returns None if the property wasn't explicitly set either way. 298 def property_or_debug_option(prop_name, flag_name): 299 assert prop_name[0].islower() and flag_name[0].isupper() 300 301 value = request(prop_name, bool, optional=True) 302 if value == (): 303 value = None 304 305 if flag_name in debug_options: 306 if value is False: 307 raise request.isnt_valid( 308 '{0}:false and "debugOptions":[{1}] are mutually exclusive', 309 json.repr(prop_name), 310 json.repr(flag_name), 311 ) 312 value = True 313 314 return value 315 316 # "pythonPath" is a deprecated legacy spelling. If "python" is missing, then try 317 # the alternative. But if both are missing, the error message should say "python". 318 python_key = "python" 319 if python_key in request: 320 if "pythonPath" in request: 321 raise request.isnt_valid( 322 '"pythonPath" is not valid if "python" is specified' 323 ) 324 elif "pythonPath" in request: 325 python_key = "pythonPath" 326 python = request(python_key, json.array(str, vectorize=True, size=(0,))) 327 if not len(python): 328 python = [sys.executable] 329 330 python += request("pythonArgs", json.array(str, size=(0,))) 331 request.arguments["pythonArgs"] = python[1:] 332 request.arguments["python"] = python 333 334 launcher_python = request("debugLauncherPython", str, optional=True) 335 if launcher_python == (): 336 launcher_python = python[0] 337 338 program = module = code = () 339 if "program" in request: 340 program = request("program", str) 341 args = [program] 342 request.arguments["processName"] = program 343 if "module" in request: 344 module = request("module", str) 345 args = ["-m", module] 346 request.arguments["processName"] = module 347 if "code" in request: 348 code = request("code", json.array(str, vectorize=True, size=(1,))) 349 args = ["-c", "\n".join(code)] 350 request.arguments["processName"] = "-c" 351 352 num_targets = len([x for x in (program, module, code) if x != ()]) 353 if num_targets == 0: 354 raise request.isnt_valid( 355 'either "program", "module", or "code" must be specified' 356 ) 357 elif num_targets != 1: 358 raise request.isnt_valid( 359 '"program", "module", and "code" are mutually exclusive' 360 ) 361 362 console = request( 363 "console", 364 json.enum( 365 "internalConsole", 366 "integratedTerminal", 367 "externalTerminal", 368 optional=True, 369 ), 370 ) 371 console_title = request("consoleTitle", json.default("Python Debug Console")) 372 373 # Propagate "args" via CLI so that shell expansion can be applied if requested. 374 target_args = request("args", json.array(str, vectorize=True)) 375 args += target_args 376 377 # If "args" was a single string rather than an array, shell expansion must be applied. 378 shell_expand_args = len(target_args) > 0 and isinstance( 379 request.arguments["args"], str 380 ) 381 if shell_expand_args: 382 if not self.capabilities["supportsArgsCanBeInterpretedByShell"]: 383 raise request.isnt_valid( 384 'Shell expansion in "args" is not supported by the client' 385 ) 386 if console == "internalConsole": 387 raise request.isnt_valid( 388 'Shell expansion in "args" is not available for "console":"internalConsole"' 389 ) 390 391 cwd = request("cwd", str, optional=True) 392 if cwd == (): 393 # If it's not specified, but we're launching a file rather than a module, 394 # and the specified path has a directory in it, use that. 395 cwd = None if program == () else (os.path.dirname(program) or None) 396 397 sudo = bool(property_or_debug_option("sudo", "Sudo")) 398 if sudo and sys.platform == "win32": 399 raise request.cant_handle('"sudo":true is not supported on Windows.') 400 401 on_terminate = request("onTerminate", str, optional=True) 402 403 if on_terminate: 404 self._forward_terminate_request = on_terminate == "KeyboardInterrupt" 405 406 launcher_path = request("debugLauncherPath", os.path.dirname(launcher.__file__)) 407 adapter_host = request("debugAdapterHost", "127.0.0.1") 408 409 try: 410 servers.serve(adapter_host) 411 except Exception as exc: 412 raise request.cant_handle( 413 "{0} couldn't create listener socket for servers: {1}", 414 self.session, 415 exc, 416 ) 417 418 launchers.spawn_debuggee( 419 self.session, 420 request, 421 [launcher_python], 422 launcher_path, 423 adapter_host, 424 args, 425 shell_expand_args, 426 cwd, 427 console, 428 console_title, 429 sudo, 430 ) 431 432 @_start_message_handler 433 def attach_request(self, request): 434 if self.session.no_debug: 435 raise request.isnt_valid('"noDebug" is not supported for "attach"') 436 437 host = request("host", str, optional=True) 438 port = request("port", int, optional=True) 439 listen = request("listen", dict, optional=True) 440 connect = request("connect", dict, optional=True) 441 pid = request("processId", (int, str), optional=True) 442 sub_pid = request("subProcessId", int, optional=True) 443 on_terminate = request("onTerminate", bool, optional=True) 444 445 if on_terminate: 446 self._forward_terminate_request = on_terminate == "KeyboardInterrupt" 447 448 if host != () or port != (): 449 if listen != (): 450 raise request.isnt_valid( 451 '"listen" and "host"/"port" are mutually exclusive' 452 ) 453 if connect != (): 454 raise request.isnt_valid( 455 '"connect" and "host"/"port" are mutually exclusive' 456 ) 457 if listen != (): 458 if connect != (): 459 raise request.isnt_valid( 460 '"listen" and "connect" are mutually exclusive' 461 ) 462 if pid != (): 463 raise request.isnt_valid( 464 '"listen" and "processId" are mutually exclusive' 465 ) 466 if sub_pid != (): 467 raise request.isnt_valid( 468 '"listen" and "subProcessId" are mutually exclusive' 469 ) 470 if pid != () and sub_pid != (): 471 raise request.isnt_valid( 472 '"processId" and "subProcessId" are mutually exclusive' 473 ) 474 475 if listen != (): 476 if servers.is_serving(): 477 raise request.isnt_valid( 478 'Multiple concurrent "listen" sessions are not supported' 479 ) 480 host = listen("host", "127.0.0.1") 481 port = listen("port", int) 482 adapter.access_token = None 483 self.restart_requested = request("restart", False) 484 host, port = servers.serve(host, port) 485 else: 486 if not servers.is_serving(): 487 servers.serve() 488 host, port = servers.listener.getsockname() 489 490 # There are four distinct possibilities here. 491 # 492 # If "processId" is specified, this is attach-by-PID. We need to inject the 493 # debug server into the designated process, and then wait until it connects 494 # back to us. Since the injected server can crash, there must be a timeout. 495 # 496 # If "subProcessId" is specified, this is attach to a known subprocess, likely 497 # in response to a "debugpyAttach" event. If so, the debug server should be 498 # connected already, and thus the wait timeout is zero. 499 # 500 # If "listen" is specified, this is attach-by-socket with the server expected 501 # to connect to the adapter via debugpy.connect(). There is no PID known in 502 # advance, so just wait until the first server connection indefinitely, with 503 # no timeout. 504 # 505 # If "connect" is specified, this is attach-by-socket in which the server has 506 # spawned the adapter via debugpy.listen(). There is no PID known to the client 507 # in advance, but the server connection should be either be there already, or 508 # the server should be connecting shortly, so there must be a timeout. 509 # 510 # In the last two cases, if there's more than one server connection already, 511 # this is a multiprocess re-attach. The client doesn't know the PID, so we just 512 # connect it to the oldest server connection that we have - in most cases, it 513 # will be the one for the root debuggee process, but if it has exited already, 514 # it will be some subprocess. 515 if pid != (): 516 if not isinstance(pid, int): 517 try: 518 pid = int(pid) 519 except Exception: 520 raise request.isnt_valid('"processId" must be parseable as int') 521 debugpy_args = request("debugpyArgs", json.array(str)) 522 523 def on_output(category, output): 524 self.channel.send_event( 525 "output", 526 { 527 "category": category, 528 "output": output, 529 }, 530 ) 531 532 try: 533 servers.inject(pid, debugpy_args, on_output) 534 except Exception as e: 535 log.swallow_exception() 536 self.session.finalize( 537 "Error when trying to attach to PID:\n%s" % (str(e),) 538 ) 539 return 540 541 timeout = common.PROCESS_SPAWN_TIMEOUT 542 pred = lambda conn: conn.pid == pid 543 else: 544 if sub_pid == (): 545 pred = lambda conn: True 546 timeout = common.PROCESS_SPAWN_TIMEOUT if listen == () else None 547 else: 548 pred = lambda conn: conn.pid == sub_pid 549 timeout = 0 550 551 self.channel.send_event("debugpyWaitingForServer", {"host": host, "port": port}) 552 conn = servers.wait_for_connection(self.session, pred, timeout) 553 if conn is None: 554 if sub_pid != (): 555 # If we can't find a matching subprocess, it's not always an error - 556 # it might have already exited, or didn't even get a chance to connect. 557 # To prevent the client from complaining, pretend that the "attach" 558 # request was successful, but that the session terminated immediately. 559 request.respond({}) 560 self.session.finalize( 561 'No known subprocess with "subProcessId":{0}'.format(sub_pid) 562 ) 563 return 564 565 raise request.cant_handle( 566 ( 567 "Timed out waiting for debug server to connect." 568 if timeout 569 else "There is no debug server connected to this adapter." 570 ), 571 sub_pid, 572 ) 573 574 try: 575 conn.attach_to_session(self.session) 576 except ValueError: 577 request.cant_handle("{0} is already being debugged.", conn) 578 579 @message_handler 580 def configurationDone_request(self, request): 581 if self.start_request is None or self.has_started: 582 request.cant_handle( 583 '"configurationDone" is only allowed during handling of a "launch" ' 584 'or an "attach" request' 585 ) 586 587 try: 588 self.has_started = True 589 try: 590 result = self.server.channel.delegate(request) 591 except messaging.NoMoreMessages: 592 # Server closed connection before we could receive the response to 593 # "configurationDone" - this can happen when debuggee exits shortly 594 # after starting. It's not an error, but we can't do anything useful 595 # here at this point, either, so just bail out. 596 request.respond({}) 597 self.start_request.respond({}) 598 self.session.finalize( 599 "{0} disconnected before responding to {1}".format( 600 self.server, 601 json.repr(request.command), 602 ) 603 ) 604 return 605 else: 606 request.respond(result) 607 except messaging.MessageHandlingError as exc: 608 self.start_request.cant_handle(str(exc)) 609 finally: 610 if self.start_request.response is None: 611 self.start_request.respond({}) 612 self._propagate_deferred_events() 613 614 # Notify the client of any child processes of the debuggee that aren't already 615 # being debugged. 616 for conn in servers.connections(): 617 if conn.server is None and conn.ppid == self.session.pid: 618 self.notify_of_subprocess(conn) 619 620 @message_handler 621 def evaluate_request(self, request): 622 propagated_request = self.server.channel.propagate(request) 623 624 def handle_response(response): 625 request.respond(response.body) 626 627 propagated_request.on_response(handle_response) 628 629 return messaging.NO_RESPONSE 630 631 @message_handler 632 def pause_request(self, request): 633 request.arguments["threadId"] = "*" 634 return self.server.channel.delegate(request) 635 636 @message_handler 637 def continue_request(self, request): 638 request.arguments["threadId"] = "*" 639 640 try: 641 return self.server.channel.delegate(request) 642 except messaging.NoMoreMessages: 643 # pydevd can sometimes allow the debuggee to exit before the queued 644 # "continue" response gets sent. Thus, a failed "continue" response 645 # indicating that the server disconnected should be treated as success. 646 return {"allThreadsContinued": True} 647 648 @message_handler 649 def debugpySystemInfo_request(self, request): 650 result = {"debugpy": {"version": debugpy.__version__}} 651 if self.server: 652 try: 653 pydevd_info = self.server.channel.request("pydevdSystemInfo") 654 except Exception: 655 # If the server has already disconnected, or couldn't handle it, 656 # report what we've got. 657 pass 658 else: 659 result.update(pydevd_info) 660 return result 661 662 @message_handler 663 def terminate_request(self, request): 664 # If user specifically requests to terminate, it means that they don't want 665 # debug session auto-restart kicking in. 666 self.restart_requested = False 667 668 if self._forward_terminate_request: 669 # According to the spec, terminate should try to do a gracefull shutdown. 670 # We do this in the server by interrupting the main thread with a Ctrl+C. 671 # To force the kill a subsequent request would do a disconnect. 672 # 673 # We only do this if the onTerminate option is set though (the default 674 # is a hard-kill for the process and subprocesses). 675 return self.server.channel.delegate(request) 676 677 self.session.finalize('client requested "terminate"', terminate_debuggee=True) 678 return {} 679 680 @message_handler 681 def disconnect_request(self, request): 682 # If user specifically requests to disconnect, it means that they don't want 683 # debug session auto-restart kicking in. 684 self.restart_requested = False 685 686 terminate_debuggee = request("terminateDebuggee", bool, optional=True) 687 if terminate_debuggee == (): 688 terminate_debuggee = None 689 self.session.finalize('client requested "disconnect"', terminate_debuggee) 690 request.respond({}) 691 692 if self.using_stdio: 693 # There's no way for the client to reconnect to this adapter once it disconnects 694 # from this session, so close any remaining server connections. 695 servers.stop_serving() 696 log.info("{0} disconnected from stdio; closing remaining server connections.", self) 697 for conn in servers.connections(): 698 try: 699 conn.channel.close() 700 except Exception: 701 log.swallow_exception() 702 703 # Close the client channel since we disconnected from the client. 704 try: 705 self.channel.close() 706 except Exception: 707 log.swallow_exception(level="warning") 708 709 def disconnect(self): 710 super().disconnect() 711 712 def report_sockets(self): 713 sockets = [ 714 { 715 "host": host, 716 "port": port, 717 "internal": listener is not clients.listener, 718 } 719 for listener in [clients.listener, launchers.listener, servers.listener] 720 if listener is not None 721 for (host, port) in [listener.getsockname()] 722 ] 723 self.channel.send_event( 724 "debugpySockets", 725 { 726 "sockets": sockets 727 }, 728 ) 729 730 def notify_of_subprocess(self, conn): 731 log.info("{1} is a subprocess of {0}.", self, conn) 732 with self.session: 733 if self.start_request is None or conn in self.known_subprocesses: 734 return 735 if "processId" in self.start_request.arguments: 736 log.warning( 737 "Not reporting subprocess for {0}, because the parent process " 738 'was attached to using "processId" rather than "port".', 739 self.session, 740 ) 741 return 742 743 log.info("Notifying {0} about {1}.", self, conn) 744 body = dict(self.start_request.arguments) 745 self.known_subprocesses.add(conn) 746 self.session.notify_changed() 747 748 for key in "processId", "listen", "preLaunchTask", "postDebugTask", "request", "restart": 749 body.pop(key, None) 750 751 body["name"] = "Subprocess {0}".format(conn.pid) 752 body["subProcessId"] = conn.pid 753 754 for key in "args", "processName", "pythonArgs": 755 body.pop(key, None) 756 757 host = body.pop("host", None) 758 port = body.pop("port", None) 759 if "connect" not in body: 760 body["connect"] = {} 761 if "host" not in body["connect"]: 762 body["connect"]["host"] = host if host is not None else "127.0.0.1" 763 if "port" not in body["connect"]: 764 if port is None: 765 _, port = listener.getsockname() 766 body["connect"]["port"] = port 767 768 if self.capabilities["supportsStartDebuggingRequest"]: 769 self.channel.request("startDebugging", { 770 "request": "attach", 771 "configuration": body, 772 }) 773 else: 774 body["request"] = "attach" 775 self.channel.send_event("debugpyAttach", body) 776 777 778 def serve(host, port): 779 global listener 780 listener = sockets.serve("Client", Client, host, port) 781 sessions.report_sockets() 782 return listener.getsockname() 783 784 785 def stop_serving(): 786 global listener 787 if listener is not None: 788 try: 789 listener.close() 790 except Exception: 791 log.swallow_exception(level="warning") 792 listener = None 793 sessions.report_sockets() 794