| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- # Copyright (c) Microsoft Corporation. All rights reserved.
- # Licensed under the MIT License. See LICENSE in the project root
- # for license information.
- import codecs
- import os
- import pydevd
- import socket
- import sys
- import threading
- import debugpy
- from debugpy import adapter
- from debugpy.common import json, log, sockets
- from _pydevd_bundle.pydevd_constants import get_global_debugger
- from pydevd_file_utils import absolute_path
- from debugpy.common.util import hide_debugpy_internals
- _tls = threading.local()
- # TODO: "gevent", if possible.
- _config = {
- "qt": "none",
- "subProcess": True,
- "python": sys.executable,
- "pythonEnv": {},
- }
- _config_valid_values = {
- # If property is not listed here, any value is considered valid, so long as
- # its type matches that of the default value in _config.
- "qt": ["auto", "none", "pyside", "pyside2", "pyqt4", "pyqt5"],
- }
- # This must be a global to prevent it from being garbage collected and triggering
- # https://bugs.python.org/issue37380.
- _adapter_process = None
- def _settrace(*args, **kwargs):
- log.debug("pydevd.settrace(*{0!r}, **{1!r})", args, kwargs)
- # The stdin in notification is not acted upon in debugpy, so, disable it.
- kwargs.setdefault("notify_stdin", False)
- try:
- pydevd.settrace(*args, **kwargs)
- except Exception:
- raise
- def ensure_logging():
- """Starts logging to log.log_dir, if it hasn't already been done."""
- if ensure_logging.ensured:
- return
- ensure_logging.ensured = True
- log.to_file(prefix="debugpy.server")
- log.describe_environment("Initial environment:")
- if log.log_dir is not None:
- pydevd.log_to(log.log_dir + "/debugpy.pydevd.log")
- ensure_logging.ensured = False
- def log_to(path):
- if ensure_logging.ensured:
- raise RuntimeError("logging has already begun")
- log.debug("log_to{0!r}", (path,))
- if path is sys.stderr:
- log.stderr.levels |= set(log.LEVELS)
- else:
- log.log_dir = path
- def configure(properties=None, **kwargs):
- ensure_logging()
- log.debug("configure{0!r}", (properties, kwargs))
- if properties is None:
- properties = kwargs
- else:
- properties = dict(properties)
- properties.update(kwargs)
- for k, v in properties.items():
- if k not in _config:
- raise ValueError("Unknown property {0!r}".format(k))
- expected_type = type(_config[k])
- if type(v) is not expected_type:
- raise ValueError("{0!r} must be a {1}".format(k, expected_type.__name__))
- valid_values = _config_valid_values.get(k)
- if (valid_values is not None) and (v not in valid_values):
- raise ValueError("{0!r} must be one of: {1!r}".format(k, valid_values))
- _config[k] = v
- def _starts_debugging(func):
- def debug(address, **kwargs):
- try:
- _, port = address
- except Exception:
- port = address
- localhost = sockets.get_default_localhost()
- address = (localhost, port)
- try:
- port.__index__() # ensure it's int-like
- except Exception:
- raise ValueError("expected port or (host, port)")
- if not (0 <= port < 2**16):
- raise ValueError("invalid port number")
- ensure_logging()
- log.debug("{0}({1!r}, **{2!r})", func.__name__, address, kwargs)
- log.info("Initial debug configuration: {0}", json.repr(_config))
- qt_mode = _config.get("qt", "none")
- if qt_mode != "none":
- pydevd.enable_qt_support(qt_mode)
- settrace_kwargs = {
- "suspend": False,
- "patch_multiprocessing": _config.get("subProcess", True),
- }
- if hide_debugpy_internals():
- debugpy_path = os.path.dirname(absolute_path(debugpy.__file__))
- settrace_kwargs["dont_trace_start_patterns"] = (debugpy_path,)
- settrace_kwargs["dont_trace_end_patterns"] = (str("debugpy_launcher.py"),)
- try:
- return func(address, settrace_kwargs, **kwargs)
- except Exception:
- log.reraise_exception("{0}() failed:", func.__name__, level="info")
- return debug
- @_starts_debugging
- def listen(address, settrace_kwargs, in_process_debug_adapter=False):
- # Errors below are logged with level="info", because the caller might be catching
- # and handling exceptions, and we don't want to spam their stderr unnecessarily.
- if listen.called:
- # Multiple calls to listen() cause the debuggee to hang
- raise RuntimeError("debugpy.listen() has already been called on this process")
- host, port = address
- if in_process_debug_adapter:
- log.info("Listening: pydevd without debugpy adapter: {0}:{1}", host, port)
- settrace_kwargs["patch_multiprocessing"] = False
- _settrace(
- host=host,
- port=port,
- wait_for_ready_to_run=False,
- block_until_connected=False,
- **settrace_kwargs
- )
- return
- import subprocess
- server_access_token = codecs.encode(os.urandom(32), "hex").decode("ascii")
- try:
- localhost = sockets.get_default_localhost()
- endpoints_listener = sockets.create_server(localhost, 0, timeout=30)
- except Exception as exc:
- log.swallow_exception("Can't listen for adapter endpoints:")
- raise RuntimeError("can't listen for adapter endpoints: " + str(exc))
- try:
- endpoints_host, endpoints_port = sockets.get_address(endpoints_listener)
- log.info(
- "Waiting for adapter endpoints on {0}:{1}...",
- endpoints_host,
- endpoints_port,
- )
- host, port = address
- adapter_args = [
- _config.get("python", sys.executable),
- os.path.dirname(adapter.__file__),
- "--for-server",
- str(endpoints_port),
- "--host",
- host,
- "--port",
- str(port),
- "--server-access-token",
- server_access_token,
- ]
- if log.log_dir is not None:
- adapter_args += ["--log-dir", log.log_dir]
- log.info("debugpy.listen() spawning adapter: {0}", json.repr(adapter_args))
- # On Windows, detach the adapter from our console, if any, so that it doesn't
- # receive Ctrl+C from it, and doesn't keep it open once we exit.
- creationflags = 0
- if sys.platform == "win32":
- creationflags |= 0x08000000 # CREATE_NO_WINDOW
- creationflags |= 0x00000200 # CREATE_NEW_PROCESS_GROUP
- # On embedded applications, environment variables might not contain
- # Python environment settings.
- python_env = _config.get("pythonEnv")
- if not bool(python_env):
- python_env = None
- # Adapter will outlive this process, so we shouldn't wait for it. However, we
- # need to ensure that the Popen instance for it doesn't get garbage-collected
- # by holding a reference to it in a non-local variable, to avoid triggering
- # https://bugs.python.org/issue37380.
- try:
- global _adapter_process
- _adapter_process = subprocess.Popen(
- adapter_args,
- close_fds=True,
- creationflags=creationflags,
- env=python_env,
- )
- if os.name == "posix":
- # It's going to fork again to daemonize, so we need to wait on it to
- # clean it up properly.
- _adapter_process.wait()
- else:
- # Suppress misleading warning about child process still being alive when
- # this process exits (https://bugs.python.org/issue38890).
- _adapter_process.returncode = 0
- pydevd.add_dont_terminate_child_pid(_adapter_process.pid)
- except Exception as exc:
- log.swallow_exception("Error spawning debug adapter:", level="info")
- raise RuntimeError("error spawning debug adapter: " + str(exc))
- try:
- sock, _ = endpoints_listener.accept()
- try:
- sock.settimeout(None)
- sock_io = sock.makefile("rb", 0)
- try:
- endpoints = json.loads(sock_io.read().decode("utf-8"))
- finally:
- sock_io.close()
- finally:
- sockets.close_socket(sock)
- except socket.timeout:
- log.swallow_exception(
- "Timed out waiting for adapter to connect:", level="info"
- )
- raise RuntimeError("timed out waiting for adapter to connect")
- except Exception as exc:
- log.swallow_exception("Error retrieving adapter endpoints:", level="info")
- raise RuntimeError("error retrieving adapter endpoints: " + str(exc))
- finally:
- endpoints_listener.close()
- log.info("Endpoints received from adapter: {0}", json.repr(endpoints))
- if "error" in endpoints:
- raise RuntimeError(str(endpoints["error"]))
- try:
- server_host = str(endpoints["server"]["host"])
- server_port = int(endpoints["server"]["port"])
- client_host = str(endpoints["client"]["host"])
- client_port = int(endpoints["client"]["port"])
- except Exception as exc:
- log.swallow_exception(
- "Error parsing adapter endpoints:\n{0}\n",
- json.repr(endpoints),
- level="info",
- )
- raise RuntimeError("error parsing adapter endpoints: " + str(exc))
- log.info(
- "Adapter is accepting incoming client connections on {0}:{1}",
- client_host,
- client_port,
- )
- _settrace(
- host=server_host,
- port=server_port,
- wait_for_ready_to_run=False,
- block_until_connected=True,
- access_token=server_access_token,
- **settrace_kwargs
- )
- log.info("pydevd is connected to adapter at {0}:{1}", server_host, server_port)
- listen.called = True
- return client_host, client_port
- listen.called = False
- @_starts_debugging
- def connect(address, settrace_kwargs, access_token=None, parent_session_pid=None):
- host, port = address
- _settrace(host=host, port=port, client_access_token=access_token, ppid=parent_session_pid or 0, **settrace_kwargs)
- class wait_for_client:
- def __call__(self):
- ensure_logging()
- log.debug("wait_for_client()")
- pydb = get_global_debugger()
- if pydb is None:
- raise RuntimeError("listen() or connect() must be called first")
- cancel_event = threading.Event()
- self.cancel = cancel_event.set
- pydevd._wait_for_attach(cancel=cancel_event)
- @staticmethod
- def cancel():
- raise RuntimeError("wait_for_client() must be called first")
- wait_for_client = wait_for_client()
- def is_client_connected():
- return pydevd._is_attached()
- def breakpoint():
- ensure_logging()
- if not is_client_connected():
- log.info("breakpoint() ignored - debugger not attached")
- return
- log.debug("breakpoint()")
- # Get the first frame in the stack that's not an internal frame.
- pydb = get_global_debugger()
- stop_at_frame = sys._getframe().f_back
- while (
- stop_at_frame is not None
- and pydb.get_file_type(stop_at_frame) == pydb.PYDEV_FILE
- ):
- stop_at_frame = stop_at_frame.f_back
- _settrace(
- suspend=True,
- trace_only_current_thread=True,
- patch_multiprocessing=False,
- stop_at_frame=stop_at_frame,
- )
- stop_at_frame = None
- def debug_this_thread():
- ensure_logging()
- log.debug("debug_this_thread()")
- _settrace(suspend=False)
- def trace_this_thread(should_trace):
- ensure_logging()
- log.debug("trace_this_thread({0!r})", should_trace)
- pydb = get_global_debugger()
- if should_trace:
- pydb.enable_tracing()
- else:
- pydb.disable_tracing()
|