Format: text/x-python
⬇ Download File (31.6 KB)
   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