Format: text/x-python
⬇ Download File (11.3 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  import itertools
   6  import os
   7  import signal
   8  import threading
   9  import time
  10  
  11  from debugpy import common
  12  from debugpy.common import log, util
  13  from debugpy.adapter import components, launchers, servers
  14  
  15  
  16  _lock = threading.RLock()
  17  _sessions = set()
  18  _sessions_changed = threading.Event()
  19  
  20  
  21  class Session(util.Observable):
  22      """A debug session involving a client, an adapter, a launcher, and a debug server.
  23  
  24      The client and the adapter are always present, and at least one of launcher and debug
  25      server is present, depending on the scenario.
  26      """
  27  
  28      _counter = itertools.count(1)
  29  
  30      def __init__(self):
  31          from debugpy.adapter import clients
  32  
  33          super().__init__()
  34  
  35          self.lock = threading.RLock()
  36          self.id = next(self._counter)
  37          self._changed_condition = threading.Condition(self.lock)
  38  
  39          self.client = components.missing(self, clients.Client)
  40          """The client component. Always present."""
  41  
  42          self.launcher = components.missing(self, launchers.Launcher)
  43          """The launcher componet. Always present in "launch" sessions, and never
  44          present in "attach" sessions.
  45          """
  46  
  47          self.server = components.missing(self, servers.Server)
  48          """The debug server component. Always present, unless this is a "launch"
  49          session with "noDebug".
  50          """
  51  
  52          self.no_debug = None
  53          """Whether this is a "noDebug" session."""
  54  
  55          self.pid = None
  56          """Process ID of the debuggee process."""
  57  
  58          self.debug_options = {}
  59          """Debug options as specified by "launch" or "attach" request."""
  60  
  61          self.is_finalizing = False
  62          """Whether finalize() has been invoked."""
  63  
  64          self.observers += [lambda *_: self.notify_changed()]
  65  
  66      def __str__(self):
  67          return f"Session[{self.id}]"
  68  
  69      def __enter__(self):
  70          """Lock the session for exclusive access."""
  71          self.lock.acquire()
  72          return self
  73  
  74      def __exit__(self, exc_type, exc_value, exc_tb):
  75          """Unlock the session."""
  76          self.lock.release()
  77  
  78      def register(self):
  79          with _lock:
  80              _sessions.add(self)
  81              _sessions_changed.set()
  82  
  83      def notify_changed(self):
  84          with self:
  85              self._changed_condition.notify_all()
  86  
  87          # A session is considered ended once all components disconnect, and there
  88          # are no further incoming messages from anything to handle.
  89          components = self.client, self.launcher, self.server
  90          if all(not com or not com.is_connected for com in components):
  91              with _lock:
  92                  if self in _sessions:
  93                      log.info("{0} has ended.", self)
  94                      _sessions.remove(self)
  95                      _sessions_changed.set()
  96  
  97      def wait_for(self, predicate, timeout=None):
  98          """Waits until predicate() becomes true.
  99  
 100          The predicate is invoked with the session locked. If satisfied, the method
 101          returns immediately. Otherwise, the lock is released (even if it was held
 102          at entry), and the method blocks waiting for some attribute of either self,
 103          self.client, self.server, or self.launcher to change. On every change, session
 104          is re-locked and predicate is re-evaluated, until it is satisfied.
 105  
 106          While the session is unlocked, message handlers for components other than
 107          the one that is waiting can run, but message handlers for that one are still
 108          blocked.
 109  
 110          If timeout is not None, the method will unblock and return after that many
 111          seconds regardless of whether the predicate was satisfied. The method returns
 112          False if it timed out, and True otherwise.
 113          """
 114  
 115          def wait_for_timeout():
 116              time.sleep(timeout)
 117              wait_for_timeout.timed_out = True
 118              self.notify_changed()
 119  
 120          wait_for_timeout.timed_out = False
 121          if timeout is not None:
 122              thread = threading.Thread(
 123                  target=wait_for_timeout, name="Session.wait_for() timeout"
 124              )
 125              thread.daemon = True
 126              thread.start()
 127  
 128          with self:
 129              while not predicate():
 130                  if wait_for_timeout.timed_out:
 131                      return False
 132                  self._changed_condition.wait()
 133              return True
 134  
 135      def finalize(self, why, terminate_debuggee=None):
 136          """Finalizes the debug session.
 137  
 138          If the server is present, sends "disconnect" request with "terminateDebuggee"
 139          set as specified request to it; waits for it to disconnect, allowing any
 140          remaining messages from it to be handled; and closes the server channel.
 141  
 142          If the launcher is present, sends "terminate" request to it, regardless of the
 143          value of terminate; waits for it to disconnect, allowing any remaining messages
 144          from it to be handled; and closes the launcher channel.
 145  
 146          If the client is present, sends "terminated" event to it.
 147  
 148          If terminate_debuggee=None, it is treated as True if the session has a Launcher
 149          component, and False otherwise.
 150          """
 151  
 152          if self.is_finalizing:
 153              return
 154          self.is_finalizing = True
 155          log.info("{0}; finalizing {1}.", why, self)
 156  
 157          if terminate_debuggee is None:
 158              terminate_debuggee = bool(self.launcher)
 159  
 160          try:
 161              self._finalize(why, terminate_debuggee)
 162          except Exception:
 163              # Finalization should never fail, and if it does, the session is in an
 164              # indeterminate and likely unrecoverable state, so just fail fast.
 165              log.swallow_exception("Fatal error while finalizing {0}", self)
 166              os._exit(1)
 167  
 168          log.info("{0} finalized.", self)
 169  
 170      def _finalize(self, why, terminate_debuggee):
 171          # If the client started a session, and then disconnected before issuing "launch"
 172          # or "attach", the main thread will be blocked waiting for the first server
 173          # connection to come in - unblock it, so that we can exit.
 174          servers.dont_wait_for_first_connection()
 175  
 176          if self.server:
 177              if self.server.is_connected:
 178                  if terminate_debuggee and self.launcher and self.launcher.is_connected:
 179                      # If we were specifically asked to terminate the debuggee, and we
 180                      # can ask the launcher to kill it, do so instead of disconnecting
 181                      # from the server to prevent debuggee from running any more code.
 182                      self.launcher.terminate_debuggee()
 183                  else:
 184                      # Otherwise, let the server handle it the best it can.
 185                      try:
 186                          self.server.channel.request(
 187                              "disconnect", {"terminateDebuggee": terminate_debuggee}
 188                          )
 189                      except Exception:
 190                          pass
 191              self.server.detach_from_session()
 192  
 193          if self.launcher and self.launcher.is_connected:
 194              # If there was a server, we just disconnected from it above, which should
 195              # cause the debuggee process to exit, unless it is being replaced in situ -
 196              # so let's wait for that first.
 197              if self.server and not self.server.connection.process_replaced:
 198                  log.info('{0} waiting for "exited" event...', self)
 199                  if not self.wait_for(
 200                      lambda: self.launcher.exit_code is not None,
 201                      timeout=common.PROCESS_EXIT_TIMEOUT,
 202                  ):
 203                      log.warning('{0} timed out waiting for "exited" event.', self)
 204  
 205              # Terminate the debuggee process if it's still alive for any reason -
 206              # whether it's because there was no server to handle graceful shutdown,
 207              # or because the server couldn't handle it for some reason - unless the
 208              # process is being replaced in situ.
 209              if not (self.server and self.server.connection.process_replaced):
 210                  self.launcher.terminate_debuggee()
 211  
 212              # Wait until the launcher message queue fully drains. There is no timeout
 213              # here, because the final "terminated" event will only come after reading
 214              # user input in wait-on-exit scenarios. In addition, if the process was
 215              # replaced in situ, the launcher might still have more output to capture
 216              # from its replacement.
 217              log.info("{0} waiting for {1} to disconnect...", self, self.launcher)
 218              self.wait_for(lambda: not self.launcher.is_connected)
 219  
 220              try:
 221                  self.launcher.channel.close()
 222              except Exception:
 223                  log.swallow_exception()
 224  
 225          if self.client:
 226              if self.client.is_connected:
 227                  # Tell the client that debugging is over, but don't close the channel until it
 228                  # tells us to, via the "disconnect" request.
 229                  body = {}
 230                  if self.client.restart_requested:
 231                      body["restart"] = True
 232                  try:
 233                      self.client.channel.send_event("terminated", body)
 234                  except Exception:
 235                      pass
 236  
 237              if (
 238                  self.client.start_request is not None
 239                  and self.client.start_request.command == "launch"
 240                  and not (self.server and self.server.connection.process_replaced)
 241              ):
 242                  servers.stop_serving()
 243                  log.info(
 244                      '"launch" session ended - killing remaining debuggee processes.'
 245                  )
 246  
 247                  pids_killed = set()
 248                  if self.launcher and self.launcher.pid is not None:
 249                      # Already killed above.
 250                      pids_killed.add(self.launcher.pid)
 251  
 252                  while True:
 253                      conns = [
 254                          conn
 255                          for conn in servers.connections()
 256                          if conn.pid not in pids_killed
 257                      ]
 258                      if not len(conns):
 259                          break
 260                      for conn in conns:
 261                          log.info("Killing {0}", conn)
 262                          try:
 263                              os.kill(conn.pid, signal.SIGTERM)
 264                          except Exception:
 265                              log.swallow_exception("Failed to kill {0}", conn)
 266                          pids_killed.add(conn.pid)
 267  
 268  
 269  def get(pid):
 270      with _lock:
 271          return next((session for session in _sessions if session.pid == pid), None)
 272  
 273  
 274  def wait_until_ended():
 275      """Blocks until all sessions have ended.
 276  
 277      A session ends when all components that it manages disconnect from it.
 278      """
 279      while True:
 280          with _lock:
 281              if not len(_sessions):
 282                  return
 283              _sessions_changed.clear()
 284          _sessions_changed.wait()
 285  
 286  
 287  def report_sockets():
 288      if not _sessions:
 289          return
 290      session = sorted(_sessions, key=lambda session: session.id)[0]
 291      client = session.client
 292      if client is not None:
 293          client.report_sockets()
 294