first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
__all__ = []
import os
# Force absolute path on Python 2.
__file__ = os.path.abspath(__file__)
adapter_host = None
"""The host on which adapter is running and listening for incoming connections
from the launcher and the servers."""
channel = None
"""DAP message channel to the adapter."""
def connect(host, port):
from debugpy.common import log, messaging, sockets
from debugpy.launcher import handlers
global channel, adapter_host
assert channel is None
assert adapter_host is None
log.info("Connecting to adapter at {0}:{1}", host, port)
sock = sockets.create_client()
sock.connect((host, port))
adapter_host = host
stream = messaging.JsonIOStream.from_socket(sock, "Adapter")
channel = messaging.JsonMessageChannel(stream, handlers=handlers)
channel.start()

View File

@@ -0,0 +1,97 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
__all__ = ["main"]
import locale
import os
import signal
import sys
# WARNING: debugpy and submodules must not be imported on top level in this module,
# and should be imported locally inside main() instead.
# Force absolute path on Python 2.
__file__ = os.path.abspath(__file__)
def main():
from debugpy import launcher
from debugpy.common import log
from debugpy.launcher import debuggee
log.to_file(prefix="debugpy.launcher")
log.describe_environment("debugpy.launcher startup environment:")
if sys.platform == "win32":
# For windows, disable exceptions on Ctrl+C - we want to allow the debuggee
# process to handle these, or not, as it sees fit. If the debuggee exits
# on Ctrl+C, the launcher will also exit, so it doesn't need to observe
# the signal directly.
signal.signal(signal.SIGINT, signal.SIG_IGN)
# Everything before "--" is command line arguments for the launcher itself,
# and everything after "--" is command line arguments for the debuggee.
log.info("sys.argv before parsing: {0}", sys.argv)
sep = sys.argv.index("--")
launcher_argv = sys.argv[1:sep]
sys.argv[:] = [sys.argv[0]] + sys.argv[sep + 1 :]
log.info("sys.argv after patching: {0}", sys.argv)
# The first argument specifies the host/port on which the adapter is waiting
# for launcher to connect. It's either host:port, or just port.
adapter = launcher_argv[0]
host, sep, port = adapter.partition(":")
if not sep:
host = "127.0.0.1"
port = adapter
port = int(port)
launcher.connect(host, port)
launcher.channel.wait()
if debuggee.process is not None:
sys.exit(debuggee.process.returncode)
if __name__ == "__main__":
# debugpy can also be invoked directly rather than via -m. In this case, the first
# entry on sys.path is the one added automatically by Python for the directory
# containing this file. This means that import debugpy will not work, since we need
# the parent directory of debugpy/ to be in sys.path, rather than debugpy/launcher/.
#
# The other issue is that many other absolute imports will break, because they
# will be resolved relative to debugpy/launcher/ - e.g. `import state` will then try
# to import debugpy/launcher/state.py.
#
# To fix both, we need to replace the automatically added entry such that it points
# at parent directory of debugpy/ instead of debugpy/launcher, import debugpy with that
# in sys.path, and then remove the first entry entry altogether, so that it doesn't
# affect any further imports we might do. For example, suppose the user did:
#
# python /foo/bar/debugpy/launcher ...
#
# At the beginning of this script, sys.path will contain "/foo/bar/debugpy/launcher"
# as the first entry. What we want is to replace it with "/foo/bar', then import
# debugpy with that in effect, and then remove the replaced entry before any more
# code runs. The imported debugpy module will remain in sys.modules, and thus all
# future imports of it or its submodules will resolve accordingly.
if "debugpy" not in sys.modules:
# Do not use dirname() to walk up - this can be a relative path, e.g. ".".
sys.path[0] = sys.path[0] + "/../../"
__import__("debugpy")
del sys.path[0]
# Apply OS-global and user-specific locale settings.
try:
locale.setlocale(locale.LC_ALL, "")
except Exception:
# On POSIX, locale is set via environment variables, and this can fail if
# those variables reference a non-existing locale. Ignore and continue using
# the default "C" locale if so.
pass
main()

View File

@@ -0,0 +1,248 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
import atexit
import ctypes
import os
import signal
import struct
import subprocess
import sys
import threading
from debugpy import launcher
from debugpy.common import fmt, log, messaging, compat
from debugpy.launcher import output
if sys.platform == "win32":
from debugpy.launcher import winapi
process = None
"""subprocess.Popen instance for the debuggee process."""
job_handle = None
"""On Windows, the handle for the job object to which the debuggee is assigned."""
wait_on_exit_predicates = []
"""List of functions that determine whether to pause after debuggee process exits.
Every function is invoked with exit code as the argument. If any of the functions
returns True, the launcher pauses and waits for user input before exiting.
"""
def describe():
return fmt("Debuggee[PID={0}]", process.pid)
def spawn(process_name, cmdline, env, redirect_output):
log.info(
"Spawning debuggee process:\n\n"
"Command line: {0!r}\n\n"
"Environment variables: {1!r}\n\n",
cmdline,
env,
)
close_fds = set()
try:
if redirect_output:
# subprocess.PIPE behavior can vary substantially depending on Python version
# and platform; using our own pipes keeps it simple, predictable, and fast.
stdout_r, stdout_w = os.pipe()
stderr_r, stderr_w = os.pipe()
close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w}
kwargs = dict(stdout=stdout_w, stderr=stderr_w)
else:
kwargs = {}
if sys.platform != "win32":
def preexec_fn():
try:
# Start the debuggee in a new process group, so that the launcher can
# kill the entire process tree later.
os.setpgrp()
# Make the new process group the foreground group in its session, so
# that it can interact with the terminal. The debuggee will receive
# SIGTTOU when tcsetpgrp() is called, and must ignore it.
old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
try:
tty = os.open("/dev/tty", os.O_RDWR)
try:
os.tcsetpgrp(tty, os.getpgrp())
finally:
os.close(tty)
finally:
signal.signal(signal.SIGTTOU, old_handler)
except Exception:
# Not an error - /dev/tty doesn't work when there's no terminal.
log.swallow_exception(
"Failed to set up process group", level="info"
)
kwargs.update(preexec_fn=preexec_fn)
try:
global process
process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs)
except Exception as exc:
raise messaging.MessageHandlingError(
fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline)
)
log.info("Spawned {0}.", describe())
if sys.platform == "win32":
# Assign the debuggee to a new job object, so that the launcher can kill
# the entire process tree later.
try:
global job_handle
job_handle = winapi.kernel32.CreateJobObjectA(None, None)
job_info = winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
job_info_size = winapi.DWORD(ctypes.sizeof(job_info))
winapi.kernel32.QueryInformationJobObject(
job_handle,
winapi.JobObjectExtendedLimitInformation,
ctypes.pointer(job_info),
job_info_size,
ctypes.pointer(job_info_size),
)
job_info.BasicLimitInformation.LimitFlags |= (
# Ensure that the job will be terminated by the OS once the
# launcher exits, even if it doesn't terminate the job explicitly.
winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
# Allow the debuggee to create its own jobs unrelated to ours.
winapi.JOB_OBJECT_LIMIT_BREAKAWAY_OK
)
winapi.kernel32.SetInformationJobObject(
job_handle,
winapi.JobObjectExtendedLimitInformation,
ctypes.pointer(job_info),
job_info_size,
)
process_handle = winapi.kernel32.OpenProcess(
winapi.PROCESS_TERMINATE | winapi.PROCESS_SET_QUOTA,
False,
process.pid,
)
winapi.kernel32.AssignProcessToJobObject(job_handle, process_handle)
except Exception:
log.swallow_exception("Failed to set up job object", level="warning")
atexit.register(kill)
launcher.channel.send_event(
"process",
{
"startMethod": "launch",
"isLocalProcess": True,
"systemProcessId": process.pid,
"name": process_name,
"pointerSize": struct.calcsize(compat.force_str("P")) * 8,
},
)
if redirect_output:
for category, fd, tee in [
("stdout", stdout_r, sys.stdout),
("stderr", stderr_r, sys.stderr),
]:
output.CaptureOutput(describe(), category, fd, tee)
close_fds.remove(fd)
wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()")
wait_thread.daemon = True
wait_thread.start()
finally:
for fd in close_fds:
try:
os.close(fd)
except Exception:
log.swallow_exception(level="warning")
def kill():
if process is None:
return
try:
if process.poll() is None:
log.info("Killing {0}", describe())
# Clean up the process tree
if sys.platform == "win32":
# On Windows, kill the job object.
winapi.kernel32.TerminateJobObject(job_handle, 0)
else:
# On POSIX, kill the debuggee's process group.
os.killpg(process.pid, signal.SIGKILL)
except Exception:
log.swallow_exception("Failed to kill {0}", describe())
def wait_for_exit():
try:
code = process.wait()
if sys.platform != "win32" and code < 0:
# On POSIX, if the process was terminated by a signal, Popen will use
# a negative returncode to indicate that - but the actual exit code of
# the process is always an unsigned number, and can be determined by
# taking the lowest 8 bits of that negative returncode.
code &= 0xFF
except Exception:
log.swallow_exception("Couldn't determine process exit code")
code = -1
log.info("{0} exited with code {1}", describe(), code)
output.wait_for_remaining_output()
# Determine whether we should wait or not before sending "exited", so that any
# follow-up "terminate" requests don't affect the predicates.
should_wait = any(pred(code) for pred in wait_on_exit_predicates)
try:
launcher.channel.send_event("exited", {"exitCode": code})
except Exception:
pass
if should_wait:
_wait_for_user_input()
try:
launcher.channel.send_event("terminated")
except Exception:
pass
def _wait_for_user_input():
if sys.stdout and sys.stdin and sys.stdin.isatty():
from debugpy.common import log
try:
import msvcrt
except ImportError:
can_getch = False
else:
can_getch = True
if can_getch:
log.debug("msvcrt available - waiting for user input via getch()")
sys.stdout.write("Press any key to continue . . . ")
sys.stdout.flush()
msvcrt.getch()
else:
log.debug("msvcrt not available - waiting for user input via read()")
sys.stdout.write("Press Enter to continue . . . ")
sys.stdout.flush()
sys.stdin.read(1)

View File

@@ -0,0 +1,161 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
import functools
import os
import sys
import debugpy
from debugpy import launcher
from debugpy.common import compat, json
from debugpy.common.compat import unicode
from debugpy.launcher import debuggee
def launch_request(request):
debug_options = set(request("debugOptions", json.array(unicode)))
# Handling of properties that can also be specified as legacy "debugOptions" flags.
# If property is explicitly set to false, but the flag is in "debugOptions", treat
# it as an error. Returns None if the property wasn't explicitly set either way.
def property_or_debug_option(prop_name, flag_name):
assert prop_name[0].islower() and flag_name[0].isupper()
value = request(prop_name, bool, optional=True)
if value == ():
value = None
if flag_name in debug_options:
if value is False:
raise request.isnt_valid(
'{0!j}:false and "debugOptions":[{1!j}] are mutually exclusive',
prop_name,
flag_name,
)
value = True
return value
python = request("python", json.array(unicode, size=(1,)))
cmdline = list(python)
if not request("noDebug", json.default(False)):
port = request("port", int)
cmdline += [
compat.filename(os.path.dirname(debugpy.__file__)),
"--connect",
launcher.adapter_host + ":" + str(port),
]
if not request("subProcess", True):
cmdline += ["--configure-subProcess", "False"]
qt_mode = request(
"qt",
json.enum(
"none", "auto", "pyside", "pyside2", "pyqt4", "pyqt5", optional=True
),
)
cmdline += ["--configure-qt", qt_mode]
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
# Further arguments can come via two channels: the launcher's own command line, or
# "args" in the request; effective arguments are concatenation of these two in order.
# Arguments for debugpy (such as -m) always come via CLI, but those specified by the
# user via "args" are passed differently by the adapter depending on "argsExpansion".
cmdline += sys.argv[1:]
cmdline += request("args", json.array(unicode))
process_name = request("processName", compat.filename(sys.executable))
env = os.environ.copy()
env_changes = request("env", json.object((unicode, type(None))))
if sys.platform == "win32":
# Environment variables are case-insensitive on Win32, so we need to normalize
# both dicts to make sure that env vars specified in the debug configuration
# overwrite the global env vars correctly. If debug config has entries that
# differ in case only, that's an error.
env = {k.upper(): v for k, v in os.environ.items()}
new_env_changes = {}
for k, v in env_changes.items():
k_upper = k.upper()
if k_upper in new_env_changes:
if new_env_changes[k_upper] == v:
continue
else:
raise request.isnt_valid('Found duplicate in "env": {0}.'.format(k_upper))
new_env_changes[k_upper] = v
env_changes = new_env_changes
if "DEBUGPY_TEST" in env:
# If we're running as part of a debugpy test, make sure that codecov is not
# applied to the debuggee, since it will conflict with pydevd.
env.pop("COV_CORE_SOURCE", None)
env.update(env_changes)
env = {k: v for k, v in env.items() if v is not None}
if request("gevent", False):
env["GEVENT_SUPPORT"] = "True"
console = request(
"console",
json.enum(
"internalConsole", "integratedTerminal", "externalTerminal", optional=True
),
)
redirect_output = property_or_debug_option("redirectOutput", "RedirectOutput")
if redirect_output is None:
# If neither the property nor the option were specified explicitly, choose
# the default depending on console type - "internalConsole" needs it to
# provide any output at all, but it's unnecessary for the terminals.
redirect_output = console == "internalConsole"
if redirect_output:
# sys.stdout buffering must be disabled - otherwise we won't see the output
# at all until the buffer fills up.
env["PYTHONUNBUFFERED"] = "1"
# Force UTF-8 output to minimize data loss due to re-encoding.
env["PYTHONIOENCODING"] = "utf-8"
if property_or_debug_option("waitOnNormalExit", "WaitOnNormalExit"):
if console == "internalConsole":
raise request.isnt_valid(
'"waitOnNormalExit" is not supported for "console":"internalConsole"'
)
debuggee.wait_on_exit_predicates.append(lambda code: code == 0)
if property_or_debug_option("waitOnAbnormalExit", "WaitOnAbnormalExit"):
if console == "internalConsole":
raise request.isnt_valid(
'"waitOnAbnormalExit" is not supported for "console":"internalConsole"'
)
debuggee.wait_on_exit_predicates.append(lambda code: code != 0)
if sys.version_info < (3,):
# Popen() expects command line and environment to be bytes, not Unicode.
# Assume that values are filenames - it's usually either that, or numbers -
# but don't allow encoding to fail if we guessed wrong.
encode = functools.partial(compat.filename_bytes, errors="replace")
cmdline = [encode(s) for s in cmdline]
env = {encode(k): encode(v) for k, v in env.items()}
debuggee.spawn(process_name, cmdline, env, redirect_output)
return {}
def terminate_request(request):
del debuggee.wait_on_exit_predicates[:]
request.respond({})
debuggee.kill()
def disconnect():
del debuggee.wait_on_exit_predicates[:]
debuggee.kill()

View File

@@ -0,0 +1,116 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
import codecs
import os
import sys
import threading
from debugpy import launcher
from debugpy.common import log
class CaptureOutput(object):
"""Captures output from the specified file descriptor, and tees it into another
file descriptor while generating DAP "output" events for it.
"""
instances = {}
"""Keys are output categories, values are CaptureOutput instances."""
def __init__(self, whose, category, fd, stream):
assert category not in self.instances
self.instances[category] = self
log.info("Capturing {0} of {1}.", category, whose)
self.category = category
self._whose = whose
self._fd = fd
self._decoder = codecs.getincrementaldecoder("utf-8")(errors="surrogateescape")
if stream is None:
# Can happen if running under pythonw.exe.
self._stream = None
else:
self._stream = stream if sys.version_info < (3,) else stream.buffer
encoding = stream.encoding
if encoding is None or encoding == "cp65001":
encoding = "utf-8"
try:
self._encode = codecs.getencoder(encoding)
except Exception:
log.swallow_exception(
"Unsupported {0} encoding {1!r}; falling back to UTF-8.",
category,
encoding,
level="warning",
)
self._encode = codecs.getencoder("utf-8")
self._worker_thread = threading.Thread(target=self._worker, name=category)
self._worker_thread.start()
def __del__(self):
fd = self._fd
if fd is not None:
try:
os.close(fd)
except Exception:
pass
def _worker(self):
while self._fd is not None:
try:
s = os.read(self._fd, 0x1000)
except Exception:
break
if not len(s):
break
self._process_chunk(s)
# Flush any remaining data in the incremental decoder.
self._process_chunk(b"", final=True)
def _process_chunk(self, s, final=False):
s = self._decoder.decode(s, final=final)
if len(s) == 0:
return
try:
launcher.channel.send_event(
"output", {"category": self.category, "output": s.replace("\r\n", "\n")}
)
except Exception:
pass # channel to adapter is already closed
if self._stream is None:
return
s, _ = self._encode(s, "surrogateescape")
size = len(s)
i = 0
while i < size:
# On Python 2, all writes are full writes, and write() returns None.
# On Python 3, writes can be partial, and write() returns the count.
written = self._stream.write(s[i:])
self._stream.flush()
if written is None: # full write
break
elif written == 0:
# This means that the output stream was closed from the other end.
# Do the same to the debuggee, so that it knows as well.
os.close(self._fd)
self._fd = None
break
i += written
def wait_for_remaining_output():
"""Waits for all remaining output to be captured and propagated.
"""
for category, instance in CaptureOutput.instances.items():
log.info("Waiting for remaining {0} of {1}.", category, instance._whose)
instance._worker_thread.join()

View File

@@ -0,0 +1,106 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from __future__ import absolute_import, division, print_function, unicode_literals
import ctypes
from ctypes.wintypes import BOOL, DWORD, HANDLE, LARGE_INTEGER, LPCSTR, UINT
from debugpy.common import log
JOBOBJECTCLASS = ctypes.c_int
LPDWORD = ctypes.POINTER(DWORD)
LPVOID = ctypes.c_void_p
SIZE_T = ctypes.c_size_t
ULONGLONG = ctypes.c_ulonglong
class IO_COUNTERS(ctypes.Structure):
_fields_ = [
("ReadOperationCount", ULONGLONG),
("WriteOperationCount", ULONGLONG),
("OtherOperationCount", ULONGLONG),
("ReadTransferCount", ULONGLONG),
("WriteTransferCount", ULONGLONG),
("OtherTransferCount", ULONGLONG),
]
class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
_fields_ = [
("PerProcessUserTimeLimit", LARGE_INTEGER),
("PerJobUserTimeLimit", LARGE_INTEGER),
("LimitFlags", DWORD),
("MinimumWorkingSetSize", SIZE_T),
("MaximumWorkingSetSize", SIZE_T),
("ActiveProcessLimit", DWORD),
("Affinity", SIZE_T),
("PriorityClass", DWORD),
("SchedulingClass", DWORD),
]
class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
_fields_ = [
("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION),
("IoInfo", IO_COUNTERS),
("ProcessMemoryLimit", SIZE_T),
("JobMemoryLimit", SIZE_T),
("PeakProcessMemoryUsed", SIZE_T),
("PeakJobMemoryUsed", SIZE_T),
]
JobObjectExtendedLimitInformation = JOBOBJECTCLASS(9)
JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
PROCESS_TERMINATE = 0x0001
PROCESS_SET_QUOTA = 0x0100
def _errcheck(is_error_result=(lambda result: not result)):
def impl(result, func, args):
if is_error_result(result):
log.debug("{0} returned {1}", func.__name__, result)
raise ctypes.WinError()
else:
return result
return impl
kernel32 = ctypes.windll.kernel32
kernel32.AssignProcessToJobObject.errcheck = _errcheck()
kernel32.AssignProcessToJobObject.restype = BOOL
kernel32.AssignProcessToJobObject.argtypes = (HANDLE, HANDLE)
kernel32.CreateJobObjectA.errcheck = _errcheck(lambda result: result == 0)
kernel32.CreateJobObjectA.restype = HANDLE
kernel32.CreateJobObjectA.argtypes = (LPVOID, LPCSTR)
kernel32.OpenProcess.errcheck = _errcheck(lambda result: result == 0)
kernel32.OpenProcess.restype = HANDLE
kernel32.OpenProcess.argtypes = (DWORD, BOOL, DWORD)
kernel32.QueryInformationJobObject.errcheck = _errcheck()
kernel32.QueryInformationJobObject.restype = BOOL
kernel32.QueryInformationJobObject.argtypes = (
HANDLE,
JOBOBJECTCLASS,
LPVOID,
DWORD,
LPDWORD,
)
kernel32.SetInformationJobObject.errcheck = _errcheck()
kernel32.SetInformationJobObject.restype = BOOL
kernel32.SetInformationJobObject.argtypes = (HANDLE, JOBOBJECTCLASS, LPVOID, DWORD)
kernel32.TerminateJobObject.errcheck = _errcheck()
kernel32.TerminateJobObject.restype = BOOL
kernel32.TerminateJobObject.argtypes = (HANDLE, UINT)