Add base vars and sudo check

This commit is contained in:
Benjamin Zimmerman 2022-12-13 14:20:23 +00:00
parent c151fd6910
commit 054f5ad80c
8733 changed files with 137813 additions and 15 deletions

View file

@ -0,0 +1,32 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
__all__ = []
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,91 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
__all__ = ["main"]
import locale
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.
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,249 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import atexit
import ctypes
import os
import signal
import struct
import subprocess
import sys
import threading
from debugpy import launcher
from debugpy.common import log, messaging
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 f"Debuggee[PID={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(
"Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}".format(
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("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,152 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import os
import sys
import debugpy
from debugpy import launcher
from debugpy.common import json
from debugpy.launcher import debuggee
def launch_request(request):
debug_options = set(request("debugOptions", json.array(str)))
# 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}:false and "debugOptions":[{1}] are mutually exclusive',
json.repr(prop_name),
json.repr(flag_name),
)
value = True
return value
python = request("python", json.array(str, size=(1,)))
cmdline = list(python)
if not request("noDebug", json.default(False)):
# see https://github.com/microsoft/debugpy/issues/861
if sys.version_info[:2] >= (3, 11):
cmdline += ["-X", "frozen_modules=off"]
port = request("port", int)
cmdline += [
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", str, optional=True)
if adapter_access_token != ():
cmdline += ["--adapter-access-token", adapter_access_token]
debugpy_args = request("debugpyArgs", json.array(str))
cmdline += debugpy_args
# Use the copy of arguments that was propagated via the command line rather than
# "args" in the request itself, to allow for shell expansion.
cmdline += sys.argv[1:]
process_name = request("processName", sys.executable)
env = os.environ.copy()
env_changes = request("env", json.object((str, 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)
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,113 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import codecs
import os
import 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.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")
else:
log.info("Using encoding {0!r} for {1}", encoding, category)
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
try:
s, _ = self._encode(s, "surrogateescape")
size = len(s)
i = 0
while i < size:
written = self._stream.write(s[i:])
self._stream.flush()
if 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
except Exception:
log.swallow_exception("Error printing {0!r} to {1}", s, self.category)
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,104 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
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)