Format: text/x-python
⬇ Download File (11.9 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 codecs
   6  import os
   7  import pydevd
   8  import socket
   9  import sys
  10  import threading
  11  
  12  import debugpy
  13  from debugpy import adapter
  14  from debugpy.common import json, log, sockets
  15  from _pydevd_bundle.pydevd_constants import get_global_debugger
  16  from pydevd_file_utils import absolute_path
  17  from debugpy.common.util import hide_debugpy_internals
  18  
  19  _tls = threading.local()
  20  
  21  # TODO: "gevent", if possible.
  22  _config = {
  23      "qt": "none",
  24      "subProcess": True,
  25      "python": sys.executable,
  26      "pythonEnv": {},
  27  }
  28  
  29  _config_valid_values = {
  30      # If property is not listed here, any value is considered valid, so long as
  31      # its type matches that of the default value in _config.
  32      "qt": ["auto", "none", "pyside", "pyside2", "pyqt4", "pyqt5"],
  33  }
  34  
  35  # This must be a global to prevent it from being garbage collected and triggering
  36  # https://bugs.python.org/issue37380.
  37  _adapter_process = None
  38  
  39  
  40  def _settrace(*args, **kwargs):
  41      log.debug("pydevd.settrace(*{0!r}, **{1!r})", args, kwargs)
  42      # The stdin in notification is not acted upon in debugpy, so, disable it.
  43      kwargs.setdefault("notify_stdin", False)
  44      try:
  45          pydevd.settrace(*args, **kwargs)
  46      except Exception:
  47          raise
  48  
  49  
  50  def ensure_logging():
  51      """Starts logging to log.log_dir, if it hasn't already been done."""
  52      if ensure_logging.ensured:
  53          return
  54      ensure_logging.ensured = True
  55      log.to_file(prefix="debugpy.server")
  56      log.describe_environment("Initial environment:")
  57      if log.log_dir is not None:
  58          pydevd.log_to(log.log_dir + "/debugpy.pydevd.log")
  59  
  60  
  61  ensure_logging.ensured = False
  62  
  63  
  64  def log_to(path):
  65      if ensure_logging.ensured:
  66          raise RuntimeError("logging has already begun")
  67  
  68      log.debug("log_to{0!r}", (path,))
  69      if path is sys.stderr:
  70          log.stderr.levels |= set(log.LEVELS)
  71      else:
  72          log.log_dir = path
  73  
  74  
  75  def configure(properties=None, **kwargs):
  76      ensure_logging()
  77      log.debug("configure{0!r}", (properties, kwargs))
  78  
  79      if properties is None:
  80          properties = kwargs
  81      else:
  82          properties = dict(properties)
  83          properties.update(kwargs)
  84  
  85      for k, v in properties.items():
  86          if k not in _config:
  87              raise ValueError("Unknown property {0!r}".format(k))
  88          expected_type = type(_config[k])
  89          if type(v) is not expected_type:
  90              raise ValueError("{0!r} must be a {1}".format(k, expected_type.__name__))
  91          valid_values = _config_valid_values.get(k)
  92          if (valid_values is not None) and (v not in valid_values):
  93              raise ValueError("{0!r} must be one of: {1!r}".format(k, valid_values))
  94          _config[k] = v
  95  
  96  
  97  def _starts_debugging(func):
  98      def debug(address, **kwargs):
  99          try:
 100              _, port = address
 101          except Exception:
 102              port = address
 103              address = ("127.0.0.1", port)
 104          try:
 105              port.__index__()  # ensure it's int-like
 106          except Exception:
 107              raise ValueError("expected port or (host, port)")
 108          if not (0 <= port < 2**16):
 109              raise ValueError("invalid port number")
 110  
 111          ensure_logging()
 112          log.debug("{0}({1!r}, **{2!r})", func.__name__, address, kwargs)
 113          log.info("Initial debug configuration: {0}", json.repr(_config))
 114  
 115          qt_mode = _config.get("qt", "none")
 116          if qt_mode != "none":
 117              pydevd.enable_qt_support(qt_mode)
 118  
 119          settrace_kwargs = {
 120              "suspend": False,
 121              "patch_multiprocessing": _config.get("subProcess", True),
 122          }
 123  
 124          if hide_debugpy_internals():
 125              debugpy_path = os.path.dirname(absolute_path(debugpy.__file__))
 126              settrace_kwargs["dont_trace_start_patterns"] = (debugpy_path,)
 127              settrace_kwargs["dont_trace_end_patterns"] = (str("debugpy_launcher.py"),)
 128  
 129          try:
 130              return func(address, settrace_kwargs, **kwargs)
 131          except Exception:
 132              log.reraise_exception("{0}() failed:", func.__name__, level="info")
 133  
 134      return debug
 135  
 136  
 137  @_starts_debugging
 138  def listen(address, settrace_kwargs, in_process_debug_adapter=False):
 139      # Errors below are logged with level="info", because the caller might be catching
 140      # and handling exceptions, and we don't want to spam their stderr unnecessarily.
 141  
 142      if listen.called:
 143          # Multiple calls to listen() cause the debuggee to hang
 144          raise RuntimeError("debugpy.listen() has already been called on this process")
 145  
 146      if in_process_debug_adapter:
 147          host, port = address
 148          log.info("Listening: pydevd without debugpy adapter: {0}:{1}", host, port)
 149          settrace_kwargs["patch_multiprocessing"] = False
 150          _settrace(
 151              host=host,
 152              port=port,
 153              wait_for_ready_to_run=False,
 154              block_until_connected=False,
 155              **settrace_kwargs
 156          )
 157          return
 158  
 159      import subprocess
 160  
 161      server_access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")
 162  
 163      try:
 164          endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=30)
 165      except Exception as exc:
 166          log.swallow_exception("Can't listen for adapter endpoints:")
 167          raise RuntimeError("can't listen for adapter endpoints: " + str(exc))
 168  
 169      try:
 170          endpoints_host, endpoints_port = endpoints_listener.getsockname()
 171          log.info(
 172              "Waiting for adapter endpoints on {0}:{1}...",
 173              endpoints_host,
 174              endpoints_port,
 175          )
 176  
 177          host, port = address
 178          adapter_args = [
 179              _config.get("python", sys.executable),
 180              os.path.dirname(adapter.__file__),
 181              "--for-server",
 182              str(endpoints_port),
 183              "--host",
 184              host,
 185              "--port",
 186              str(port),
 187              "--server-access-token",
 188              server_access_token,
 189          ]
 190          if log.log_dir is not None:
 191              adapter_args += ["--log-dir", log.log_dir]
 192          log.info("debugpy.listen() spawning adapter: {0}", json.repr(adapter_args))
 193  
 194          # On Windows, detach the adapter from our console, if any, so that it doesn't
 195          # receive Ctrl+C from it, and doesn't keep it open once we exit.
 196          creationflags = 0
 197          if sys.platform == "win32":
 198              creationflags |= 0x08000000  # CREATE_NO_WINDOW
 199              creationflags |= 0x00000200  # CREATE_NEW_PROCESS_GROUP
 200  
 201          # On embedded applications, environment variables might not contain
 202          # Python environment settings.
 203          python_env = _config.get("pythonEnv")
 204          if not bool(python_env):
 205              python_env = None
 206  
 207          # Adapter will outlive this process, so we shouldn't wait for it. However, we
 208          # need to ensure that the Popen instance for it doesn't get garbage-collected
 209          # by holding a reference to it in a non-local variable, to avoid triggering
 210          # https://bugs.python.org/issue37380.
 211          try:
 212              global _adapter_process
 213              _adapter_process = subprocess.Popen(
 214                  adapter_args,
 215                  close_fds=True,
 216                  creationflags=creationflags,
 217                  env=python_env,
 218              )
 219              if os.name == "posix":
 220                  # It's going to fork again to daemonize, so we need to wait on it to
 221                  # clean it up properly.
 222                  _adapter_process.wait()
 223              else:
 224                  # Suppress misleading warning about child process still being alive when
 225                  # this process exits (https://bugs.python.org/issue38890).
 226                  _adapter_process.returncode = 0
 227                  pydevd.add_dont_terminate_child_pid(_adapter_process.pid)
 228          except Exception as exc:
 229              log.swallow_exception("Error spawning debug adapter:", level="info")
 230              raise RuntimeError("error spawning debug adapter: " + str(exc))
 231  
 232          try:
 233              sock, _ = endpoints_listener.accept()
 234              try:
 235                  sock.settimeout(None)
 236                  sock_io = sock.makefile("rb", 0)
 237                  try:
 238                      endpoints = json.loads(sock_io.read().decode("utf-8"))
 239                  finally:
 240                      sock_io.close()
 241              finally:
 242                  sockets.close_socket(sock)
 243          except socket.timeout:
 244              log.swallow_exception(
 245                  "Timed out waiting for adapter to connect:", level="info"
 246              )
 247              raise RuntimeError("timed out waiting for adapter to connect")
 248          except Exception as exc:
 249              log.swallow_exception("Error retrieving adapter endpoints:", level="info")
 250              raise RuntimeError("error retrieving adapter endpoints: " + str(exc))
 251  
 252      finally:
 253          endpoints_listener.close()
 254  
 255      log.info("Endpoints received from adapter: {0}", json.repr(endpoints))
 256  
 257      if "error" in endpoints:
 258          raise RuntimeError(str(endpoints["error"]))
 259  
 260      try:
 261          server_host = str(endpoints["server"]["host"])
 262          server_port = int(endpoints["server"]["port"])
 263          client_host = str(endpoints["client"]["host"])
 264          client_port = int(endpoints["client"]["port"])
 265      except Exception as exc:
 266          log.swallow_exception(
 267              "Error parsing adapter endpoints:\n{0}\n",
 268              json.repr(endpoints),
 269              level="info",
 270          )
 271          raise RuntimeError("error parsing adapter endpoints: " + str(exc))
 272      log.info(
 273          "Adapter is accepting incoming client connections on {0}:{1}",
 274          client_host,
 275          client_port,
 276      )
 277  
 278      _settrace(
 279          host=server_host,
 280          port=server_port,
 281          wait_for_ready_to_run=False,
 282          block_until_connected=True,
 283          access_token=server_access_token,
 284          **settrace_kwargs
 285      )
 286      log.info("pydevd is connected to adapter at {0}:{1}", server_host, server_port)
 287      listen.called = True
 288      return client_host, client_port
 289  
 290  listen.called = False
 291  
 292  
 293  @_starts_debugging
 294  def connect(address, settrace_kwargs, access_token=None):
 295      host, port = address
 296      _settrace(host=host, port=port, client_access_token=access_token, **settrace_kwargs)
 297  
 298  
 299  class wait_for_client:
 300      def __call__(self):
 301          ensure_logging()
 302          log.debug("wait_for_client()")
 303  
 304          pydb = get_global_debugger()
 305          if pydb is None:
 306              raise RuntimeError("listen() or connect() must be called first")
 307  
 308          cancel_event = threading.Event()
 309          self.cancel = cancel_event.set
 310          pydevd._wait_for_attach(cancel=cancel_event)
 311  
 312      @staticmethod
 313      def cancel():
 314          raise RuntimeError("wait_for_client() must be called first")
 315  
 316  
 317  wait_for_client = wait_for_client()
 318  
 319  
 320  def is_client_connected():
 321      return pydevd._is_attached()
 322  
 323  
 324  def breakpoint():
 325      ensure_logging()
 326      if not is_client_connected():
 327          log.info("breakpoint() ignored - debugger not attached")
 328          return
 329      log.debug("breakpoint()")
 330  
 331      # Get the first frame in the stack that's not an internal frame.
 332      pydb = get_global_debugger()
 333      stop_at_frame = sys._getframe().f_back
 334      while (
 335          stop_at_frame is not None
 336          and pydb.get_file_type(stop_at_frame) == pydb.PYDEV_FILE
 337      ):
 338          stop_at_frame = stop_at_frame.f_back
 339  
 340      _settrace(
 341          suspend=True,
 342          trace_only_current_thread=True,
 343          patch_multiprocessing=False,
 344          stop_at_frame=stop_at_frame,
 345      )
 346      stop_at_frame = None
 347  
 348  
 349  def debug_this_thread():
 350      ensure_logging()
 351      log.debug("debug_this_thread()")
 352  
 353      _settrace(suspend=False)
 354  
 355  
 356  def trace_this_thread(should_trace):
 357      ensure_logging()
 358      log.debug("trace_this_thread({0!r})", should_trace)
 359  
 360      pydb = get_global_debugger()
 361      if should_trace:
 362          pydb.enable_tracing()
 363      else:
 364          pydb.disable_tracing()
 365