|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
_config = { |
|
"qt": "none", |
|
"subProcess": True, |
|
"python": sys.executable, |
|
"pythonEnv": {}, |
|
} |
|
|
|
_config_valid_values = { |
|
|
|
|
|
"qt": ["auto", "none", "pyside", "pyside2", "pyqt4", "pyqt5"], |
|
} |
|
|
|
|
|
|
|
_adapter_process = None |
|
|
|
|
|
def _settrace(*args, **kwargs): |
|
log.debug("pydevd.settrace(*{0!r}, **{1!r})", args, kwargs) |
|
|
|
kwargs.setdefault("notify_stdin", False) |
|
try: |
|
return pydevd.settrace(*args, **kwargs) |
|
except Exception: |
|
raise |
|
else: |
|
_settrace.called = True |
|
|
|
|
|
_settrace.called = False |
|
|
|
|
|
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): |
|
if _settrace.called: |
|
raise RuntimeError("debug adapter is already running") |
|
|
|
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): |
|
if _settrace.called: |
|
raise RuntimeError("this process already has a debug adapter") |
|
|
|
try: |
|
_, port = address |
|
except Exception: |
|
port = address |
|
address = ("127.0.0.1", port) |
|
try: |
|
port.__index__() |
|
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): |
|
|
|
|
|
|
|
if in_process_debug_adapter: |
|
host, port = address |
|
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: |
|
endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=10) |
|
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 = endpoints_listener.getsockname() |
|
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)) |
|
|
|
|
|
|
|
creationflags = 0 |
|
if sys.platform == "win32": |
|
creationflags |= 0x08000000 |
|
creationflags |= 0x00000200 |
|
|
|
|
|
|
|
python_env = _config.get("pythonEnv") |
|
if not bool(python_env): |
|
python_env = None |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
global _adapter_process |
|
_adapter_process = subprocess.Popen( |
|
adapter_args, close_fds=True, creationflags=creationflags, env=python_env |
|
) |
|
if os.name == "posix": |
|
|
|
|
|
_adapter_process.wait() |
|
else: |
|
|
|
|
|
_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) |
|
return client_host, client_port |
|
|
|
|
|
@_starts_debugging |
|
def connect(address, settrace_kwargs, access_token=None): |
|
host, port = address |
|
_settrace(host=host, port=port, client_access_token=access_token, **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()") |
|
|
|
|
|
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() |
|
|