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