Refactor debugpy API and CLI for clarity and consistency.

This commit is contained in:
Pavel Minaev 2020-02-09 22:43:28 -08:00
parent 7d34a12644
commit 8447a15396
22 changed files with 630 additions and 521 deletions

View file

@ -17,17 +17,23 @@ This debugger implements the Debug Adapter Protocol: [debugProtocol.json](https:
## `debugpy` CLI Usage
### Debugging a script file
To run a script file with debugging enabled, but without waiting for the IDE to attach (i.e. code starts executing immediately):
To run a script file with debugging enabled, but without waiting for the client to attach (i.e. code starts executing immediately):
```console
-m debugpy --host localhost --port 5678 myfile.py
-m debugpy --listen localhost:5678 myfile.py
```
To wait until the IDE attaches before running your code, use the `--wait` switch.
To wait until the client attaches before running your code, use the `--wait-for-client` switch.
```console
-m debugpy --host localhost --port 5678 --wait myfile.py
-m debugpy --listen localhost:5678 --wait-for-client myfile.py
```
The `--host` option specifies the interface on which the debug server is listening for connections. To be able to attach from another machine, make sure that the server is listening on a public interface - using `0.0.0.0` will make it listen on all available interfaces:
The hostname passed to `--listen` specifies the interface on which the debug adapter will be listening for connections from DAP clients. It can be omitted, with only the port number specified:
```console
-m debugpy --host 0.0.0.0 --port 5678 myfile.py
-m debugpy --listen 5678 ...
```
in which case the default interface is 127.0.0.1.
To be able to attach from another machine, make sure that the adapter is listening on a public interface - using `0.0.0.0` will make it listen on all available interfaces:
```console
-m debugpy --listen 0.0.0.0:5678 myfile.py
```
This should only be done on secure networks, since anyone who can connect to the specified port can then execute arbitrary code within the debugged process.
@ -36,100 +42,64 @@ To pass arguments to the script, just specify them after the filename. This work
### Debugging a module
To run a module, use the `-m` switch instead of filename:
```console
-m debugpy --host localhost --port 5678 -m mymodule
-m debugpy --listen localhost:5678 -m mymodule
```
Same as with scripts, command line arguments can be passed to the module by specifying them after the module name. All other debugpy switches work identically in this mode; in particular, `--wait` can be used to block execution until the IDE attaches.
Same as with scripts, command line arguments can be passed to the module by specifying them after the module name. All other debugpy switches work identically in this mode; in particular, `--wait-for-client` can be used to block execution until the client attaches.
### Attaching to a running process by ID
The following command injects the debugger into a process with a given PID that is running Python code. Once the command returns, a debugpy server is running within the process, as if that process was launched via `-m debugpy` itself.
```console
-m debugpy --host localhost --port 5678 --pid 12345
-m debugpy --listen localhost:5678 --pid 12345
```
## `debugpy` Import usage
### Enabling debugging
At the beginning of your script, import debugpy, and call `debugpy.enable_attach()` to start the debug server. The default hostname is `0.0.0.0`, and the default port is 5678; these can be overridden by passing a `(host, port)` tuple as the first argument of `enable_attach()`.
At the beginning of your script, import debugpy, and call `debugpy.listen()` to start the debug adapter, passing a `(host, port)` tuple as the first argument.
```python
import debugpy
debugpy.enable_attach()
debugpy.listen(("localhost", 5678))
...
```
As with the `--listen` command line switch, hostname can be omitted, and defaults to `"127.0.0.1"`:
```python
debugpy.listen(5678)
...
```
### Waiting for the IDE to attach
Use the `debugpy.wait_for_attach()` function to block program execution until the IDE is attached.
### Waiting for the client to attach
Use the `debugpy.wait_for_client()` function to block program execution until the client is attached.
```python
import debugpy
debugpy.enable_attach()
debugpy.wait_for_attach() # blocks execution until IDE is attached
debugpy.listen(5678)
debugpy.wait_for_client() # blocks execution until client is attached
...
```
### `breakpoint()` function
In Python 3.7 and above, `debugpy` supports the standard `breakpoint()` function. Use `debugpy.break_into_debugger()` function for similar behavior and compatibility with older versions of Python (3.6 and below). If the debugger is attached when either of these functions is invoked, it will pause execution on the calling line, as if it had a breakpoint set. If there's no IDE attached, the functions do nothing, and the code continues to execute normally.
In Python 3.7 and above, `debugpy` supports the standard `breakpoint()` function. Use `debugpy.breakpoint()` function for similar behavior and compatibility with older versions of Python. If the debugger is attached when either of these functions is invoked, it will pause execution on the calling line, as if it had a breakpoint set. If there's no client attached, the functions do nothing, and the code continues to execute normally.
```python
import debugpy
debugpy.enable_attach()
debugpy.listen(...)
while True:
...
breakpoint() # or debugpy.break_into_debugger() on <3.7
breakpoint() # or debugpy.breakpoint() on 3.6 and below
...
```
## Custom Protocol arguments
### Launch request arguments
```json5
{
"debugOptions": [
"RedirectOutput", // Whether to redirect stdout and stderr (see pydevd_comm.CMD_REDIRECT_OUTPUT)
"WaitOnNormalExit", // Wait for user input after user code exits normally
"WaitOnAbnormalExit", // Wait for user input after user code exits with error
"Django", // Enables Django Template debugging
"Jinja", // Enables Jinja (Flask) Template debugging
"FixFilePathCase", // See FIX_FILE_PATH_CASE in wrapper.py
"DebugStdLib", // Whether to enable debugging of standard library functions
"StopOnEntry", // Whether to stop at first line of user code
"ShowReturnValue", // Show return values of functions
]
}
```
### Attach request arguments
```json5
{
"debugOptions": [
"RedirectOutput", // Whether to redirect stdout and stderr (see pydevd_comm.CMD_REDIRECT_OUTPUT)
"Django", // Enables Django Template debugging
"Jinja", // Enables Jinja (Flask) Template debugging
"FixFilePathCase", // See FIX_FILE_PATH_CASE in wrapper.py
"DebugStdLib", // Whether to enable debugging of standard library functions
"WindowsClient", // Whether client OS is Windows
"UnixClient", // Whether client OS is Unix
"ShowReturnValue", // Show return values of functions
],
"pathMappings": [
{
"localRoot": "C:\\Project\\src", // Local root (where the IDE is running)
"remoteRoot": "/home/smith/proj" // Remote root (where remote code is running)
},
// Add more path mappings
]
}
```
## Debugger logging
To enable debugger internal logging via CLI, the `--log-dir` switch can be used:
To enable debugger internal logging via CLI, the `--log-to` switch can be used:
```console
-m debugpy --log-dir path/to/logs ...
-m debugpy --log-to path/to/logs ...
```
When using `enable_attach`, the same can be done with `log_dir` argument:
When using the API, the same can be done with `debugpy.log_to()`:
```py
debugpy.enable_attach(log_dir='path/to/logs')
debugpy.log_to('path/to/logs')
debugpy.listen(...)
```
In both cases, the environment variable `DEBUGPY_LOG_DIR` can also be set to the same effect.
When logging is enabled, debugpy will create several log files with names matching `debugpy*.log` in the specified directory, corresponding to different components of the debugger. When subprocess debugging is enabled, separate logs are created for every subprocess.

View file

@ -9,21 +9,25 @@ from __future__ import absolute_import, division, print_function, unicode_litera
https://microsoft.github.io/debug-adapter-protocol/
"""
# debugpy stable public API consists solely of members of this module that are
# enumerated below.
__all__ = [
"__version__",
"attach",
"break_into_debugger",
"breakpoint",
"configure",
"debug_this_thread",
"enable_attach",
"is_attached",
"wait_for_attach",
"tracing",
"is_client_connected",
"listen",
"log_to",
"trace_this_thread",
"wait_for_client",
]
import codecs
import os
from debugpy import _version
from debugpy.common import compat
# Expose debugpy.server API from subpackage, but do not actually import it unless
@ -34,106 +38,142 @@ from debugpy import _version
# than 72 characters per line! - and must be readable when retrieved via help().
def wait_for_attach():
"""If an IDE is connected to the debug server in this process,
returns immediately. Otherwise, blocks until an IDE connects.
def log_to(path):
"""Generate detailed debugpy logs in the specified directory.
While this function is waiting, it can be canceled by calling
wait_for_attach.cancel().
The directory must already exist. Several log files are generated,
one for every process involved in the debug session.
"""
from debugpy.server import api
return api.wait_for_attach()
return api.log_to(path)
def enable_attach(address, log_dir=None, multiprocess=True):
"""Starts a DAP (Debug Adapter Protocol) server in this process,
listening for incoming socket connection from the IDE on the
specified address.
def configure(properties=None, **kwargs):
"""Sets debug configuration properties that cannot be set in the
"attach" request, because they must be applied as early as possible
in the process being debugged.
address must be a (host, port) tuple, as defined by the standard
socket module for the AF_INET address family.
For example, a "launch" configuration with subprocess debugging
disabled can be defined entirely in JSON::
If specified, log_dir must be a path to some existing directory;
the debugger will then create its log files in that directory.
Separate log files are created for every process, to accommodate
scenarios involving multiple processes. All generated log files
have names starting with "debugpy.", and extension ".log".
{
"request": "launch",
"subProcess": false,
...
}
If multiprocess is true, debugpy will also intercept child processes
spawned by this process, inject a debug server into them, and
configure it to attach to the same IDE before the child process
starts running any user code.
But the same cannot be done with "attach", because "subProcess"
must be known at the point debugpy starts tracing execution. Thus,
it is not available in JSON, and must be omitted::
Returns the interface and the port on which the debug server is
{
"request": "attach",
...
}
and set from within the debugged process instead::
debugpy.configure(subProcess=False)
debugpy.listen(...)
Properties to set can be passed either as a single dict argument,
or as separate keyword arguments::
debugpy.configure({"subProcess": False})
"""
pass
def listen(address):
"""Starts a debug adapter debugging this process, that listens for
incoming socket connections from clients on the specified address.
address must be either a (host, port) tuple, as defined by the
standard socket module for the AF_INET address family, or a port
number. If only the port is specified, host is "127.0.0.1".
Returns the interface and the port on which the debug adapter is
actually listening, in the same format as address. This may be
different from address if port was 0 in the latter, in which case
the server will pick some unused ephemeral port to listen on.
the adapter will pick some unused ephemeral port to listen on.
This function does't wait for the IDE to connect to the debug server
that it starts. Use wait_for_attach() to block execution until the
IDE connects.
This function does't wait for a client to connect to the debug
adapter that it starts. Use wait_for_client() to block execution
until the client connects.
"""
from debugpy.server import api
return api.enable_attach(address, log_dir)
return api.listen(address)
def attach(address, log_dir=None, multiprocess=True):
"""Starts a DAP (Debug Adapter Protocol) server in this process,
and connects it to the IDE that is listening for an incoming
connection on a socket with the specified address.
@compat.kwonly
def connect(address, access_token=None):
"""Tells an existing debug adapter instance that is listening on the
specified address to debug this process.
address must be a (host, port) tuple, as defined by the standard
socket module for the AF_INET address family.
address must be either a (host, port) tuple, as defined by the
standard socket module for the AF_INET address family, or a port
number. If only the port is specified, host is "127.0.0.1".
If specified, log_dir must be a path to some existing directory;
the debugger will then create its log files in that directory.
Separate log files are created for every process, to accommodate
scenarios involving multiple processes. All generated log files
have names starting with "debugpy.", and extension ".log".
access_token must be the same value that was passed to the adapter
via the --server-access-token command-line switch.
If multiprocess is true, debugpy will also intercept child processes
spawned by this process, inject a debug server into them, and
configure it to attach to the same IDE before the child process
starts running any user code.
This function doesn't return until connection to the IDE has been
established.
This function does't wait for a client to connect to the debug
adapter that it connects to. Use wait_for_client() to block
execution until the client connects.
"""
from debugpy.server import api
return api.attach(address, log_dir)
return api.connect(address, access_token=access_token)
def is_attached():
"""True if an IDE is connected to the debug server in this process.
def wait_for_client():
"""If there is a client connected to the debug adapter that is
debugging this process, returns immediately. Otherwise, blocks
until a client connects to the adapter.
While this function is waiting, it can be canceled by calling
wait_for_client.cancel() from another thread.
"""
from debugpy.server import api
return api.is_attached()
return api.wait_for_client()
def break_into_debugger():
"""If the IDE is connected, pauses execution of all threads, and
breaks into the debugger with current thread as active.
def is_client_connected():
"""True if a client is connected to the debug adapter that is
debugging this process.
"""
from debugpy.server import api
return api.break_into_debugger()
return api.is_client_connected()
def breakpoint():
"""If a client is connected to the debug adapter that is debugging
this process, pauses execution of all threads, and simulates a
breakpoint being hit at the line following the call.
On Python 3.7 and above, this is the same as builtins.breakpoint().
"""
from debugpy.server import api
return api.breakpoint()
def debug_this_thread():
"""Tells debugpy to start tracing the current thread.
"""Makes the debugger aware of the current thread.
Must be called on any background thread that is started by means
other than the usual Python APIs (i.e. the "threading" module),
for breakpoints to work on that thread.
in order for breakpoints to work on that thread.
"""
from debugpy.server import api
@ -141,26 +181,23 @@ def debug_this_thread():
return api.debug_this_thread()
def tracing(should_trace=None):
"""Enables or disables tracing on this thread. When called without an
argument, returns the current tracing state.
When tracing is disabled, breakpoints will not be hit, but code executes
significantly faster.
If debugger is not attached, this function has no effect.
This function can also be used in a with-statement to automatically save
and then restore the previous tracing setting::
with debugpy.tracing(False):
# Tracing disabled
...
# Tracing restored
Parameters
----------
should_trace : bool, optional
Whether to enable or disable tracing.
def trace_this_thread(should_trace):
"""Tells the debug adapter to enable or disable tracing on the
current thread.
When the thread is traced, the debug adapter can detect breakpoints
being hit, but execution is slower, especially in functions that
have any breakpoints set in them. Disabling tracing when breakpoints
are not anticipated to be hit can improve performance. It can also
be used to skip breakpoints on a particular thread.
Tracing is automatically disabled for all threads when there is no
client connected to the debug adapter.
"""
from debugpy.server import api
return api.tracing(should_trace)
return api.trace_this_thread(should_trace)
__version__ = _version.get_versions()["version"]

View file

@ -51,7 +51,7 @@ import debugpy # noqa
def debugpy_breakpointhook():
debugpy.break_into_debugger()
debugpy.breakpoint()
pydevd.install_breakpointhook(debugpy_breakpointhook)

View file

@ -66,7 +66,7 @@ def main(args):
"error": "Can't listen for IDE connections: " + str(exc)
}
else:
endpoints["ide"] = {"host": ide_host, "port": ide_port}
endpoints["client"] = {"host": ide_host, "port": ide_port}
if args.for_server is not None:
log.info(

View file

@ -304,12 +304,12 @@ class IDE(components.Component):
# connected already, and thus the wait timeout is zero.
#
# If neither is specified, and "waitForAttach" is true, this is attach-by-socket
# with the server expected to connect to the adapter via debugpy.attach(). There
# with the server expected to connect to the adapter via debugpy.connect(). There
# is no PID known in advance, so just wait until the first server connection
# indefinitely, with no timeout.
#
# If neither is specified, and "waitForAttach" is false, this is attach-by-socket
# in which the server has spawned the adapter via debugpy.enable_attach(). There
# in which the server has spawned the adapter via debugpy.listen(). There
# is no PID known to the IDE in advance, but the server connection should be
# either be there already, or the server should be connecting shortly, so there
# must be a timeout.

View file

@ -74,7 +74,7 @@ def spawn_debuggee(session, start_request, sudo, args, console, console_title):
arguments = dict(start_request.arguments)
if not session.no_debug:
_, arguments["port"] = servers.listener.getsockname()
arguments["clientAccessToken"] = adapter.access_token
arguments["adapterAccessToken"] = adapter.access_token
def on_launcher_connected(sock):
listener.close()

View file

@ -99,7 +99,9 @@ if 'debugpy' not in sys.modules:
# Failure to inject is not a fatal error - such a subprocess can
# still be debugged, it just won't support "import debugpy" in user
# code - so don't terminate the session.
log.exception("Failed to inject debugpy into {0}:", self, level="warning")
log.exception(
"Failed to inject debugpy into {0}:", self, level="warning"
)
with _lock:
# The server can disconnect concurrently before we get here, e.g. if
@ -422,14 +424,11 @@ def inject(pid, debugpy_args):
cmdline = [
sys.executable,
compat.filename(os.path.dirname(debugpy.__file__)),
"--client",
"--host",
host,
"--port",
str(port),
"--connect",
host + ":" + str(port),
]
if adapter.access_token is not None:
cmdline += ["--client-access-token", adapter.access_token]
cmdline += ["--adapter-access-token", adapter.access_token]
cmdline += debugpy_args
cmdline += ["--pid", str(pid)]

View file

@ -1,5 +1,5 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# # Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
"""Python 2/3 compatibility helpers.
"""
import functools
import inspect
import itertools
import sys
@ -174,3 +175,35 @@ def srcnameof(obj):
name += ")"
return name
def kwonly(f):
"""Makes all arguments with default values keyword-only.
If the default value is kwonly.required, then the argument must be specified.
"""
arg_names, args_name, kwargs_name, arg_defaults = inspect.getargspec(f)
assert args_name is None and kwargs_name is None
argc = len(arg_names)
pos_argc = argc - len(arg_defaults)
required_names = {
name
for name, val in zip(arg_names[pos_argc:], arg_defaults)
if val is kwonly.required
}
@functools.wraps(f)
def kwonly_f(*args, **kwargs):
if len(args) > pos_argc:
raise TypeError("too many positional arguments")
if not required_names.issubset(kwargs):
missing_names = required_names.difference(kwargs)
missing_names = ", ".join(repr(s) for s in missing_names)
raise TypeError("missing required keyword-only arguments: " + missing_names)
return f(*args, **kwargs)
return kwonly_f
kwonly.required = object()

View file

@ -64,15 +64,12 @@ def launch_request(request):
port = request("port", int)
cmdline += [
compat.filename(os.path.dirname(debugpy.__file__)),
"--client",
"--host",
"127.0.0.1",
"--port",
"--connect",
str(port),
]
client_access_token = request("clientAccessToken", unicode, optional=True)
if client_access_token != ():
cmdline += ["--client-access-token", compat.filename(client_access_token)]
adapter_access_token = request("adapterAccessToken", unicode, optional=True)
if adapter_access_token != ():
cmdline += ["--adapter-access-token", compat.filename(adapter_access_token)]
debugpy_args = request("debugpyArgs", json.array(unicode))
cmdline += debugpy_args

View file

@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import codecs
import contextlib
import json
import os
import pydevd
@ -15,62 +14,112 @@ import threading
import debugpy
from debugpy import adapter
from debugpy.common import compat, log, sockets
from debugpy.server import options
from debugpy.common import compat, fmt, log, sockets
from _pydevd_bundle.pydevd_constants import get_global_debugger
from pydevd_file_utils import get_abs_path_real_path_and_base_from_file
_tls = threading.local()
# TODO: "gevent", if possible.
_config = {"subProcess": True}
# 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)
try:
return pydevd.settrace(*args, **kwargs)
except Exception:
raise
else:
_settrace.called = True
def wait_for_attach():
log.debug("wait_for_attach()")
dbg = get_global_debugger()
if dbg is None:
raise RuntimeError("wait_for_attach() called before enable_attach()")
_settrace.called = False
cancel_event = threading.Event()
debugpy.wait_for_attach.cancel = wait_for_attach.cancel = cancel_event.set
pydevd._wait_for_attach(cancel=cancel_event)
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:")
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, **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(fmt("Unknown property {0!r}", k))
expected_type = type(_config[k])
if type(v) is not expected_type:
raise ValueError(fmt("{0!r} must be a {1}", k, expected_type.__name__))
_config[k] = v
def _starts_debugging(func):
def debug(address, log_dir=None, multiprocess=True):
if log_dir:
log.log_dir = log_dir
def debug(address, **kwargs):
if _settrace.called:
raise RuntimeError("this process already has a debug adapter")
log.to_file(prefix="debugpy.server")
log.describe_environment("debugpy.server debug start environment:")
log.debug("{0}{1!r}", func.__name__, (address, log_dir, multiprocess))
try:
_, port = address
except Exception:
port = address
address = ("127.0.0.1", 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")
if is_attached():
log.info("{0}() ignored - already attached.", func.__name__)
return options.host, options.port
ensure_logging()
log.debug("{0}({1!r}, **{2!r})", func.__name__, address, kwargs)
# Ensure port is int
if address is not options:
host, port = address
options.host, options.port = (host, int(port))
if multiprocess is not options:
options.multiprocess = multiprocess
settrace_kwargs = {
"suspend": False,
"patch_multiprocessing": _config.get("subProcess", True),
}
debugpy_path, _, _ = get_abs_path_real_path_and_base_from_file(debugpy.__file__)
debugpy_path = os.path.dirname(debugpy_path)
start_patterns = (debugpy_path,)
end_patterns = ("debugpy_launcher.py",)
log.info(
"Won't trace filenames starting with: {0!j}\n"
"Won't trace filenames ending with: {1!j}",
start_patterns,
end_patterns,
)
settrace_kwargs["dont_trace_start_patterns"] = (debugpy_path,)
settrace_kwargs["dont_trace_end_patterns"] = ("debugpy_launcher.py",)
try:
return func(start_patterns, end_patterns)
return func(address, settrace_kwargs, **kwargs)
except Exception:
raise log.exception("{0}() failed:", func.__name__, level="info")
@ -78,15 +127,12 @@ def _starts_debugging(func):
@_starts_debugging
def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
def listen(address, settrace_kwargs):
# 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.
import subprocess
if hasattr(enable_attach, "adapter"):
raise AssertionError("enable_attach() can only be called once per process")
server_access_token = compat.force_str(codecs.encode(os.urandom(32), "hex"))
try:
@ -99,21 +145,22 @@ def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
"Waiting for adapter endpoints on {0}:{1}...", endpoints_host, endpoints_port
)
host, port = address
adapter_args = [
sys.executable,
os.path.dirname(adapter.__file__),
"--for-server",
str(endpoints_port),
"--host",
options.host,
host,
"--port",
str(options.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("enable_attach() spawning adapter: {0!j}", adapter_args)
log.info("debugpy.listen() spawning adapter: {0!j}", 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.
@ -127,18 +174,19 @@ def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
# by holding a reference to it in a non-local variable, to avoid triggering
# https://bugs.python.org/issue37380.
try:
enable_attach.adapter = subprocess.Popen(
global _adapter_process
_adapter_process = subprocess.Popen(
adapter_args, close_fds=True, creationflags=creationflags
)
if os.name == "posix":
# It's going to fork again to daemonize, so we need to wait on it to
# clean it up properly.
enable_attach.adapter.wait()
_adapter_process.wait()
else:
# Suppress misleading warning about child process still being alive when
# this process exits (https://bugs.python.org/issue38890).
enable_attach.adapter.returncode = 0
pydevd.add_dont_terminate_child_pid(enable_attach.adapter.pid)
_adapter_process.returncode = 0
pydevd.add_dont_terminate_child_pid(_adapter_process.pid)
except Exception as exc:
log.exception("Error spawning debug adapter:", level="info")
raise RuntimeError("error spawning debug adapter: " + str(exc))
@ -167,66 +215,69 @@ def enable_attach(dont_trace_start_patterns, dont_trace_end_patterns):
raise RuntimeError(str(endpoints["error"]))
try:
host = str(endpoints["server"]["host"])
port = int(endpoints["server"]["port"])
options.port = int(endpoints["ide"]["port"])
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.exception(
"Error parsing adapter endpoints:\n{0!j}\n", endpoints, level="info"
)
raise RuntimeError("error parsing adapter endpoints: " + str(exc))
log.info(
"Adapter is accepting incoming IDE connections on {0}:{1}",
options.host,
options.port,
"Adapter is accepting incoming client connections on {0}:{1}",
client_host,
client_port,
)
_settrace(
host=host,
port=port,
suspend=False,
patch_multiprocessing=options.multiprocess,
host=server_host,
port=server_port,
wait_for_ready_to_run=False,
block_until_connected=True,
dont_trace_start_patterns=dont_trace_start_patterns,
dont_trace_end_patterns=dont_trace_end_patterns,
access_token=server_access_token,
client_access_token=options.client_access_token,
**settrace_kwargs
)
log.info("pydevd is connected to adapter at {0}:{1}", host, port)
return options.host, options.port
log.info("pydevd is connected to adapter at {0}:{1}", server_host, server_port)
return client_host, client_port
@_starts_debugging
def attach(dont_trace_start_patterns, dont_trace_end_patterns):
_settrace(
host=options.host,
port=options.port,
suspend=False,
patch_multiprocessing=options.multiprocess,
dont_trace_start_patterns=dont_trace_start_patterns,
dont_trace_end_patterns=dont_trace_end_patterns,
client_access_token=options.client_access_token,
)
def connect(address, settrace_kwargs, access_token):
host, port = address
_settrace(host=host, port=port, client_access_token=access_token, **settrace_kwargs)
def is_attached():
def wait_for_client():
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()
debugpy.wait_for_client.cancel = wait_for_client.cancel = cancel_event.set
pydevd._wait_for_attach(cancel=cancel_event)
def is_client_connected():
return pydevd._is_attached()
def break_into_debugger():
log.debug("break_into_debugger()")
if not is_attached():
log.info("break_into_debugger() ignored - debugger not 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.
global_debugger = get_global_debugger()
pydb = get_global_debugger()
stop_at_frame = sys._getframe().f_back
while (
stop_at_frame is not None
and global_debugger.get_file_type(stop_at_frame) == global_debugger.PYDEV_FILE
and pydb.get_file_type(stop_at_frame) == pydb.PYDEV_FILE
):
stop_at_frame = stop_at_frame.f_back
@ -240,63 +291,18 @@ def break_into_debugger():
def debug_this_thread():
ensure_logging()
log.debug("debug_this_thread()")
_settrace(suspend=False)
_tls = threading.local()
def trace_this_thread(should_trace):
ensure_logging()
log.debug("trace_this_thread({0!r})", should_trace)
def tracing(should_trace):
pydb = get_global_debugger()
try:
was_tracing = _tls.is_tracing
except AttributeError:
was_tracing = pydb is not None
if should_trace is None:
return was_tracing
# It is possible that IDE attaches after tracing is changed, but before it is
# restored. In this case, we don't really want to restore the original value,
# because it will effectively disable tracing for the just-attached IDE. Doing
# the check outside the function below makes it so that if the original change
# was a no-op because IDE wasn't attached, restore will be no-op as well, even
# if IDE has attached by then.
tid = threading.current_thread().ident
if pydb is None:
log.info("debugpy.tracing() ignored on thread {0} - debugger not attached", tid)
def enable_or_disable(_):
# Always fetch the fresh value, in case it changes before we restore.
_tls.is_tracing = get_global_debugger() is not None
else:
def enable_or_disable(enable):
if enable:
log.info("Enabling tracing on thread {0}", tid)
if should_trace:
pydb.enable_tracing()
else:
log.info("Disabling tracing on thread {0}", tid)
pydb.disable_tracing()
_tls.is_tracing = enable
# Context managers don't do anything unless used in a with-statement - that is,
# even the code up to yield won't run. But we want callers to be able to omit
# with-statement for this function, if they don't want to restore. So, we apply
# the change directly out here in the non-generator context, so that it happens
# immediately - and then return a context manager that is solely for the purpose
# of restoring the original value, which the caller can use or discard.
@contextlib.contextmanager
def restore_tracing():
try:
yield
finally:
enable_or_disable(was_tracing)
enable_or_disable(should_trace)
return restore_tracing()

View file

@ -4,6 +4,8 @@
from __future__ import absolute_import, division, print_function, unicode_literals
"""Script injected into the debuggee process during attach-to-PID."""
import os
@ -11,7 +13,8 @@ __file__ = os.path.abspath(__file__)
_debugpy_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
def attach(host, port, client, log_dir=None, client_access_token=None):
def attach(setup):
log = None
try:
import sys
@ -53,40 +56,38 @@ def attach(host, port, client, log_dir=None, client_access_token=None):
raise
sys.path.insert(0, _debugpy_dir)
try:
import debugpy
# NOTE: Don't do sys.path.remove here it will remove all instances of that path
# and the user may have set that to debugpy path via PYTHONPATH
import debugpy.server
from debugpy.common import log
import pydevd
finally:
assert sys.path[0] == _debugpy_dir
del sys.path[0]
from debugpy.common import log
from debugpy.server import options
import pydevd
py_db = pydevd.get_global_debugger()
if py_db is not None:
py_db.dispose_and_kill_all_pydevd_threads(wait=False)
if log_dir is not None:
log.log_dir = log_dir
options.client = client
options.host = host
options.port = port
options.client_access_token = client_access_token
if setup["log_to"] is not None:
debugpy.log_to(setup["log_to"])
log.info("Configuring injected debugpy: {0!j}", setup)
if options.client:
debugpy.attach((options.host, options.port))
if setup["mode"] == "listen":
debugpy.listen(setup["address"])
elif setup["mode"] == "connect":
debugpy.connect(
setup["address"], access_token=setup["adapter_access_token"]
)
else:
debugpy.enable_attach((options.host, options.port))
from debugpy.common import log
log.info("Debugger successfully injected")
raise AssertionError(repr(setup))
except:
import traceback
traceback.print_exc()
raise log.exception()
if log is not None:
log.exception()
raise
log.info("debugpy injected successfully")

View file

@ -4,7 +4,9 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import json
import os
import re
import runpy
import sys
@ -16,7 +18,7 @@ import pydevd
import debugpy
from debugpy.common import compat, fmt, log
from debugpy.server import options
from debugpy.server import api
TARGET = "<filename> | -m <module> | -c <code> | --pid <pid>"
@ -24,16 +26,30 @@ TARGET = "<filename> | -m <module> | -c <code> | --pid <pid>"
HELP = """debugpy {0}
See https://aka.ms/debugpy for documentation.
Usage: debugpy [--client] --host <address> [--port <port>]
[--wait]
[--no-subprocesses]
[--log-dir <path>] [--log-stderr]
Usage: debugpy --listen [<address>:]<port>
[--config-<name> <value>]...
[--log-to <path>] [--log-to-stderr]
{1}
""".format(
debugpy.__version__, TARGET
)
class Options(object):
mode = None
address = None
log_to = None
log_to_stderr = False
target = None
target_kind = None
wait_for_client = False
adapter_access_token = None
options = Options()
options.config = {"subProcess": True}
def in_range(parser, start, stop):
def parse(s):
n = parser(s)
@ -46,8 +62,6 @@ def in_range(parser, start, stop):
return parse
port = in_range(int, 0, 2 ** 16)
pid = in_range(int, 0, None)
@ -61,28 +75,66 @@ def print_version_and_exit(switch, it):
sys.exit(0)
def set_arg(varname, parser=(lambda x: x), target=options):
def set_arg(varname, parser=(lambda x: x)):
def do(arg, it):
value = parser(next(it))
setattr(target, varname, value)
setattr(options, varname, value)
return do
def set_const(varname, value, target=options):
def set_const(varname, value):
def do(arg, it):
setattr(target, varname, value)
setattr(options, varname, value)
return do
def set_log_stderr():
def set_address(mode):
def do(arg, it):
log.stderr.levels |= set(log.LEVELS)
if options.address is not None:
raise ValueError("--listen and --connect are mutually exclusive")
# It's either host:port, or just port.
value = next(it)
host, sep, port = value.partition(":")
if not sep:
host = "127.0.0.1"
port = value
try:
port = int(port)
except Exception:
port = -1
if not (0 <= port < 2 ** 16):
raise ValueError("invalid port number")
options.mode = mode
options.address = (host, port)
return do
def set_config(arg, it):
prefix = "--configure-"
assert arg.startswith(prefix)
name = arg[len(prefix) :]
value = next(it)
if name not in options.config:
raise ValueError(fmt("unknown property {0!r}", name))
expected_type = type(options.config[name])
try:
if expected_type is bool:
value = {"true": True, "false": False}[value.lower()]
else:
value = expected_type(value)
except Exception:
raise ValueError(fmt("{0!r} must be a {1}", name, expected_type.__name__))
options.config[name] = value
def set_target(kind, parser=(lambda x: x), positional=False):
def do(arg, it):
options.target_kind = kind
@ -93,34 +145,33 @@ def set_target(kind, parser=(lambda x: x), positional=False):
# fmt: off
switches = [
# Switch Placeholder Action Required
# ====== =========== ====== ========
# Switch Placeholder Action
# ====== =========== ======
# Switches that are documented for use by end users.
(("-?", "-h", "--help"), None, print_help_and_exit, False),
(("-V", "--version"), None, print_version_and_exit, False),
("--client", None, set_const("client", True), False),
("--host", "<address>", set_arg("host"), True),
("--port", "<port>", set_arg("port", port), False),
("--wait", None, set_const("wait", True), False),
("--no-subprocesses", None, set_const("multiprocess", False), False),
("--log-dir", "<path>", set_arg("log_dir", target=log), False),
("--log-stderr", None, set_log_stderr(), False),
("-(\?|h|-help)", None, print_help_and_exit),
("-(V|-version)", None, print_version_and_exit),
("--log-to" , "<path>", set_arg("log_to")),
("--log-to-stderr", None, set_const("log_to_stderr", True)),
("--listen", "<address>", set_address("listen")),
("--wait-for-client", None, set_const("wait_for_client", True)),
("--configure-.+", "<value>", set_config),
# Switches that are used internally by the IDE or debugpy itself.
("--client-access-token", "<token>", set_arg("client_access_token"), False),
("--connect", "<address>", set_address("connect")),
("--adapter-access-token", "<token>", set_arg("adapter_access_token")),
# Targets. The "" entry corresponds to positional command line arguments,
# i.e. the ones not preceded by any switch name.
("", "<filename>", set_target("file", positional=True), False),
("-m", "<module>", set_target("module"), False),
("-c", "<code>", set_target("code"), False),
("--pid", "<pid>", set_target("pid", pid), False),
("", "<filename>", set_target("file", positional=True)),
("-m", "<module>", set_target("module")),
("-c", "<code>", set_target("code")),
("--pid", "<pid>", set_target("pid", pid)),
]
# fmt: on
def parse(args, options=options):
def parse(args):
seen = set()
it = (compat.filename(arg) for arg in args)
@ -131,18 +182,16 @@ def parse(args, options=options):
raise ValueError("missing target: " + TARGET)
switch = arg if arg.startswith("-") else ""
for i, (sw, placeholder, action, _) in enumerate(switches):
if not isinstance(sw, tuple):
sw = (sw,)
if switch in sw:
for pattern, placeholder, action in switches:
if re.match("^(" + pattern + ")$", switch):
break
else:
raise ValueError("unrecognized switch " + switch)
if i in seen:
if switch in seen:
raise ValueError("duplicate switch " + switch)
else:
seen.add(i)
seen.add(switch)
try:
action(arg, it)
@ -155,38 +204,42 @@ def parse(args, options=options):
if options.target is not None:
break
for i, (sw, placeholder, _, required) in enumerate(switches):
if not required or i in seen:
continue
if isinstance(sw, tuple):
sw = sw[0]
message = fmt("missing required {0}", sw)
if placeholder is not None:
message += " " + placeholder
raise ValueError(message)
if options.mode is None:
raise ValueError("either --listen or --connect is required")
if options.adapter_access_token is not None and options.mode != "connect":
raise ValueError("--adapter-access-token requires --connect")
if options.target_kind == "pid" and options.wait_for_client:
raise ValueError("--pid does not support --wait-for-client")
if options.target_kind == "pid" and options.wait:
raise ValueError("--pid does not support --wait")
assert options.target is not None
assert options.target_kind is not None
assert options.address is not None
return it
def setup_debug_server(argv_0):
# We need to set up sys.argv[0] before invoking attach() or enable_attach(),
def start_debugging(argv_0):
# We need to set up sys.argv[0] before invoking either listen() or connect(),
# because they use it to report the "process" event. Thus, we can't rely on
# run_path() and run_module() doing that, even though they will eventually.
sys.argv[0] = compat.filename(argv_0)
log.debug("sys.argv after patching: {0!r}", sys.argv)
debug = debugpy.attach if options.client else debugpy.enable_attach
debug(address=options, multiprocess=options)
debugpy.configure(options.config)
if options.wait:
debugpy.wait_for_attach()
if options.mode == "listen":
debugpy.listen(options.address)
elif options.mode == "connect":
debugpy.connect(options.address, access_token=options.adapter_access_token)
else:
raise AssertionError(repr(options.mode))
if options.wait_for_client:
debugpy.wait_for_client()
def run_file():
setup_debug_server(options.target)
start_debugging(options.target)
# run_path has one difference with invoking Python from command-line:
# if the target is a file (rather than a directory), it does not add its
@ -200,6 +253,7 @@ def run_file():
log.describe_environment("Pre-launch environment:")
log.info("Running file {0!j}", options.target)
runpy.run_path(options.target, run_name="__main__")
@ -225,7 +279,7 @@ def run_module():
except Exception:
log.exception("Error determining module path for sys.argv")
setup_debug_server(argv_0)
start_debugging(argv_0)
# On Python 2, module name must be a non-Unicode string, because it ends up
# a part of module's __package__, and Python will refuse to run the module
@ -254,58 +308,58 @@ def run_module():
def run_code():
log.describe_environment("Pre-launch environment:")
log.info("Running code:\n\n{0}", options.target)
# Add current directory to path, like Python itself does for -c.
sys.path.insert(0, "")
code = compile(options.target, "<string>", "exec")
setup_debug_server("-c")
start_debugging("-c")
log.describe_environment("Pre-launch environment:")
log.info("Running code:\n\n{0}", options.target)
eval(code, {})
def attach_to_pid():
log.info("Attaching to process with PID={0}", options.target)
pid = options.target
log.info("Attaching to process with PID={0}", pid)
attach_pid_injected_dirname = os.path.join(
os.path.dirname(debugpy.__file__), "server"
)
assert os.path.exists(attach_pid_injected_dirname)
log_dir = (log.log_dir or "").replace("\\", "/")
encode = lambda s: list(bytearray(s.encode("utf-8"))) if s is not None else None
script_dir = os.path.dirname(debugpy.server.__file__)
assert os.path.exists(script_dir)
script_dir = encode(script_dir)
setup = {
"script": encode(attach_pid_injected_dirname),
"host": encode(options.host),
"port": options.port,
"client": options.client,
"log_dir": encode(log_dir),
"client_access_token": encode(options.client_access_token),
"mode": options.mode,
"address": options.address,
"wait_for_client": options.wait_for_client,
"log_to": options.log_to,
"adapter_access_token": options.adapter_access_token,
}
setup = encode(json.dumps(setup))
python_code = """
import sys;
import codecs;
import json;
import sys;
decode = lambda s: codecs.utf_8_decode(bytearray(s))[0] if s is not None else None;
script_path = decode({script});
sys.path.insert(0, script_path);
script_dir = decode({script_dir});
setup = json.loads(decode({setup}));
sys.path.insert(0, script_dir);
import attach_pid_injected;
sys.path.remove(script_path);
host = decode({host});
log_dir = decode({log_dir}) or None;
client_access_token = decode({client_access_token}) or None;
attach_pid_injected.attach(
port={port},
host=host,
client={client},
log_dir=log_dir,
client_access_token=client_access_token,
)
del sys.path[0];
attach_pid_injected.attach(setup);
"""
python_code = python_code.replace("\r", "").replace("\n", "").format(**setup)
python_code = (
python_code.replace("\r", "")
.replace("\n", "")
.format(script_dir=script_dir, setup=setup)
)
log.info("Code to be injected: \n{0}", python_code.replace(";", ";\n"))
# pydevd restriction on characters in injected code.
@ -343,8 +397,13 @@ def main():
print(HELP + "\nError: " + str(ex), file=sys.stderr)
sys.exit(2)
log.to_file(prefix="debugpy.server")
log.describe_environment("debugpy.server startup environment:")
if options.log_to is not None:
debugpy.log_to(options.log_to)
if options.log_to_stderr:
debugpy.log_to(sys.stderr)
api.ensure_logging()
log.info(
"sys.argv before parsing: {0!r}\n" " after parsing: {1!r}",
original_argv,

View file

@ -222,30 +222,36 @@ def attach_by_socket(
port = config["port"] = attach_by_socket.port
if method == "cli":
args = [os.path.dirname(debugpy.__file__)]
args = [
os.path.dirname(debugpy.__file__),
"--listen",
compat.filename_str(host) + ":" + str(port),
]
if wait:
args += ["--wait"]
args += ["--host", compat.filename_str(host), "--port", str(port)]
args += ["--wait-for-client"]
if log_dir is not None:
args += ["--log-dir", log_dir]
args += ["--log-to", log_dir]
debuggee_setup = None
elif method == "api":
args = []
debuggee_setup = """
import debugpy
debugpy.enable_attach(({host!r}, {port!r}), {args})
if {log_dir!r}:
debugpy.log_to({log_dir!r})
debugpy.listen(({host!r}, {port!r}))
if {wait!r}:
debugpy.wait_for_attach()
debugpy.wait_for_client()
"""
attach_args = "" if log_dir is None else fmt("log_dir={0!r}", log_dir)
debuggee_setup = fmt(debuggee_setup, host=host, port=port, wait=wait, args=attach_args)
debuggee_setup = fmt(
debuggee_setup, host=host, port=port, wait=wait, log_dir=log_dir
)
else:
raise ValueError
args += target.cli(session.spawn_debuggee.env)
session.spawn_debuggee(args, cwd=cwd, setup=debuggee_setup)
if wait:
session.wait_for_enable_attach()
session.wait_for_adapter_socket()
session.connect_to_adapter((host, port))
return session.request_attach()

View file

@ -383,7 +383,7 @@ class Session(object):
for fd in popen_fds.values():
os.close(fd)
def wait_for_enable_attach(self):
def wait_for_adapter_socket(self):
log.info("Waiting for {0} to open the IDE listener socket...", self.adapter_id)
while not self.adapter_endpoints.check():
time.sleep(0.1)

View file

@ -10,6 +10,7 @@ import subprocess
import sys
from debugpy.common import log
from tests.patterns import some
@pytest.fixture
@ -19,7 +20,7 @@ def cli(pyfile):
import os
import pickle
import sys
from debugpy.server import cli, options
from debugpy.server import cli
try:
sys.argv[1:] = cli.parse(sys.argv[1:])
@ -29,15 +30,16 @@ def cli(pyfile):
else:
# We only care about options that correspond to public switches.
options = {
name: getattr(options, name)
name: getattr(cli.options, name)
for name in [
"target_kind",
"address",
"config",
"log_to",
"log_to_stderr",
"mode",
"target",
"host",
"port",
"client",
"wait",
"multiprocess",
"target_kind",
"wait_for_client",
]
}
os.write(1, pickle.dumps([sys.argv[1:], options]))
@ -60,25 +62,27 @@ def cli(pyfile):
@pytest.mark.parametrize("target_kind", ["file", "module", "code"])
@pytest.mark.parametrize("port", ["", "8888"])
@pytest.mark.parametrize("client", ["", "client"])
@pytest.mark.parametrize("wait", ["", "wait"])
@pytest.mark.parametrize("subprocesses", ["", "subprocesses"])
@pytest.mark.parametrize("extra", ["", "extra"])
def test_targets(cli, target_kind, port, client, wait, subprocesses, extra):
args = ["--host", "localhost"]
@pytest.mark.parametrize("mode", ["listen", "connect"])
@pytest.mark.parametrize("address", ["8888", "localhost:8888"])
@pytest.mark.parametrize("wait_for_client", ["", "wait_for_client"])
@pytest.mark.parametrize("script_args", ["", "script_args"])
def test_targets(cli, target_kind, mode, address, wait_for_client, script_args):
expected_options = {
"mode": mode,
"target_kind": target_kind,
"wait_for_client": bool(wait_for_client),
}
if port:
args += ["--port", port]
args = ["--" + mode, address]
if client:
args += ["--client"]
host, sep, port = address.partition(":")
if sep:
expected_options["address"] = (host, int(port))
else:
expected_options["address"] = ("127.0.0.1", int(address))
if wait:
args += ["--wait"]
if not subprocesses:
args += ["--no-subprocesses"]
if wait_for_client:
args += ["--wait-for-client"]
if target_kind == "file":
target = "spam.py"
@ -91,49 +95,55 @@ def test_targets(cli, target_kind, port, client, wait, subprocesses, extra):
args += ["-c", target]
else:
pytest.fail(target_kind)
expected_options["target"] = target
if extra:
extra = [
if script_args:
script_args = [
"ham",
"--client",
"--wait",
"--listen",
"--wait-for-client",
"-y",
"spam",
"--",
"--host",
"--port",
"--connect",
"-c",
"--something",
"-m",
]
args += extra
args += script_args
else:
extra = []
script_args = []
argv, options = cli(args)
assert argv == extra
assert options == {
"target_kind": target_kind,
"target": target,
"host": "localhost",
"port": int(port) if port else 5678,
"wait": bool(wait),
"multiprocess": bool(subprocesses),
"client": bool(client),
}
assert argv == script_args
assert options == some.dict.containing(expected_options)
def test_unsupported_arg(cli):
@pytest.mark.parametrize("value", ["", True, False])
def test_configure_subProcess(cli, value):
args = ["--listen", "8888"]
if value == "":
value = True
else:
args += ["--configure-subProcess", str(value)]
args += ["spam.py"]
_, options = cli(args)
assert options["config"]["subProcess"] == value
def test_unsupported_switch(cli):
with pytest.raises(Exception):
cli(["--port", "8888", "--xyz", "123", "spam.py"])
cli(["--listen", "8888", "--xyz", "123", "spam.py"])
def test_host_required(cli):
def test_unsupported_configure(cli):
with pytest.raises(Exception):
cli(["--port", "8888", "-m", "spam"])
cli(["--connect", "127.0.0.1:8888", "--configure-xyz", "123", "spam.py"])
def test_host_empty(cli):
_, options = cli(["--host", "", "--port", "8888", "spam.py"])
assert options["host"] == ""
def test_address_required(cli):
with pytest.raises(Exception):
cli(["-m", "spam"])

View file

@ -11,11 +11,11 @@ from tests.debug import runners, targets
from tests.patterns import some
@pytest.mark.parametrize("stop_method", ["break_into_debugger", "pause"])
@pytest.mark.parametrize("is_attached", ["is_attached", ""])
@pytest.mark.parametrize("wait_for_attach", ["wait_for_attach", ""])
@pytest.mark.parametrize("stop_method", ["breakpoint", "pause"])
@pytest.mark.parametrize("is_client_connected", ["is_client_connected", ""])
@pytest.mark.parametrize("wait_for_client", ["wait_for_client", ""])
@pytest.mark.parametrize("target", targets.all)
def test_attach_api(pyfile, target, wait_for_attach, is_attached, stop_method):
def test_attach_api(pyfile, target, wait_for_client, is_client_connected, stop_method):
@pyfile
def code_to_debug():
import debuggee
@ -25,25 +25,25 @@ def test_attach_api(pyfile, target, wait_for_attach, is_attached, stop_method):
from debuggee import backchannel, scratchpad
debuggee.setup()
_, host, port, wait_for_attach, is_attached, stop_method = sys.argv
_, host, port, wait_for_client, is_client_connected, stop_method = sys.argv
port = int(port)
debugpy.enable_attach((host, port))
debugpy.listen(address=(host, port))
if wait_for_attach:
backchannel.send("wait_for_attach")
debugpy.wait_for_attach()
if wait_for_client:
backchannel.send("wait_for_client")
debugpy.wait_for_client()
if is_attached:
backchannel.send("is_attached")
while not debugpy.is_attached():
print("looping until is_attached")
if is_client_connected:
backchannel.send("is_client_connected")
while not debugpy.is_client_connected():
print("looping until is_client_connected()")
time.sleep(0.1)
if stop_method == "break_into_debugger":
backchannel.send("break_into_debugger?")
if stop_method == "breakpoint":
backchannel.send("breakpoint?")
assert backchannel.receive() == "proceed"
debugpy.break_into_debugger()
print("break") # @break_into_debugger
debugpy.breakpoint()
print("break") # @breakpoint
else:
scratchpad["paused"] = False
backchannel.send("loop?")
@ -58,25 +58,25 @@ def test_attach_api(pyfile, target, wait_for_attach, is_attached, stop_method):
backchannel = session.open_backchannel()
session.spawn_debuggee(
[code_to_debug, host, port, wait_for_attach, is_attached, stop_method]
[code_to_debug, host, port, wait_for_client, is_client_connected, stop_method]
)
session.wait_for_enable_attach()
session.wait_for_adapter_socket()
session.connect_to_adapter((host, port))
with session.request_attach():
pass
if wait_for_attach:
assert backchannel.receive() == "wait_for_attach"
if wait_for_client:
assert backchannel.receive() == "wait_for_client"
if is_attached:
assert backchannel.receive() == "is_attached"
if is_client_connected:
assert backchannel.receive() == "is_client_connected"
if stop_method == "break_into_debugger":
assert backchannel.receive() == "break_into_debugger?"
if stop_method == "breakpoint":
assert backchannel.receive() == "breakpoint?"
backchannel.send("proceed")
session.wait_for_stop(
expected_frames=[some.dap.frame(code_to_debug, "break_into_debugger")]
expected_frames=[some.dap.frame(code_to_debug, "breakpoint")]
)
elif stop_method == "pause":
assert backchannel.receive() == "loop?"
@ -100,13 +100,13 @@ def test_reattach(pyfile, target, run):
from debuggee import scratchpad
debuggee.setup()
debugpy.break_into_debugger()
debugpy.breakpoint()
object() # @first
scratchpad["exit"] = False
while not scratchpad["exit"]:
time.sleep(0.1)
debugpy.break_into_debugger()
debugpy.breakpoint()
object() # @second
with debug.Session() as session1:

View file

@ -422,7 +422,7 @@ def test_deep_stacks(pyfile, target, run):
@pytest.mark.parametrize("target", targets.all)
@pytest.mark.parametrize("func", ["breakpoint", "debugpy.break_into_debugger"])
@pytest.mark.parametrize("func", ["breakpoint", "debugpy.breakpoint"])
def test_break_api(pyfile, target, run, func):
if func == "breakpoint" and sys.version_info < (3, 7):
pytest.skip("breakpoint() was introduced in Python 3.7")

View file

@ -265,7 +265,7 @@ def test_unicode(pyfile, target, run):
# this needs to do a roundabout way of setting it to avoid parse issues.
globals()["\u16A0"] = 123
debuggee.setup()
debugpy.break_into_debugger()
debugpy.breakpoint()
print("break")
with debug.Session() as session:
@ -520,7 +520,7 @@ def test_set_variable(pyfile, target, run):
debuggee.setup()
a = 1
debugpy.break_into_debugger()
debugpy.breakpoint()
backchannel.send(a)
with debug.Session() as session:

View file

@ -40,7 +40,7 @@ def test_log_dir(pyfile, tmpdir, target, method):
debuggee.setup()
# Depending on the method, attach_by_socket will use either `debugpy --log-dir ...`
# or `enable_attach(log_dir=) ...`.
# or `debugpy.log_to() ...`.
run = runners.attach_by_socket[method].with_options(log_dir=tmpdir.strpath)
with check_logs(tmpdir, run):
with debug.Session() as session:

View file

@ -113,7 +113,7 @@ def test_wait_on_exit(
import debugpy
debuggee.setup()
debugpy.break_into_debugger()
debugpy.breakpoint()
print() # line on which it'll actually break
sys.exit(int(sys.argv[1]))

View file

@ -62,7 +62,7 @@ def test_debugpySystemInfo(pyfile, target, run, expected_system_info):
import debugpy
debuggee.setup()
debugpy.break_into_debugger()
debugpy.breakpoint()
print()
with debug.Session() as session:

View file

@ -17,9 +17,6 @@ def test_tracing(pyfile, target, run):
debuggee.setup()
def func(expected_tracing):
assert debugpy.tracing() == expected_tracing, "inside func({0!r})".format(
expected_tracing
)
print(1) # @inner1
# Test nested change/restore. Going from False to True only works entirely
@ -31,23 +28,17 @@ def test_tracing(pyfile, target, run):
def inner2():
print(2) # @inner2
with debugpy.tracing(not expected_tracing):
assert debugpy.tracing() != expected_tracing, "inside with-statement"
debugpy.trace_this_thread(not expected_tracing)
inner2()
assert debugpy.tracing() == expected_tracing, "after with-statement"
debugpy.trace_this_thread(expected_tracing)
print(3) # @inner3
assert debugpy.tracing(), "before tracing(False)"
debugpy.tracing(False)
assert not debugpy.tracing(), "after tracing(False)"
debugpy.trace_this_thread(False)
print(0) # @outer1
func(False)
debugpy.tracing(True)
assert debugpy.tracing(), "after tracing(True)"
debugpy.trace_this_thread(True)
print(0) # @outer2
func(True)