|
|
|
|
|
|
|
|
|
import atexit |
|
import contextlib |
|
import functools |
|
import inspect |
|
import io |
|
import os |
|
import platform |
|
import sys |
|
import threading |
|
import traceback |
|
|
|
import debugpy |
|
from debugpy.common import json, timestamp, util |
|
|
|
|
|
LEVELS = ("debug", "info", "warning", "error") |
|
"""Logging levels, lowest to highest importance. |
|
""" |
|
|
|
log_dir = os.getenv("DEBUGPY_LOG_DIR") |
|
"""If not None, debugger logs its activity to a file named debugpy.*-<pid>.log |
|
in the specified directory, where <pid> is the return value of os.getpid(). |
|
""" |
|
|
|
timestamp_format = "09.3f" |
|
"""Format spec used for timestamps. Can be changed to dial precision up or down. |
|
""" |
|
|
|
_lock = threading.RLock() |
|
_tls = threading.local() |
|
_files = {} |
|
_levels = set() |
|
|
|
|
|
def _update_levels(): |
|
global _levels |
|
_levels = frozenset(level for file in _files.values() for level in file.levels) |
|
|
|
|
|
class LogFile(object): |
|
def __init__(self, filename, file, levels=LEVELS, close_file=True): |
|
info("Also logging to {0}.", json.repr(filename)) |
|
self.filename = filename |
|
self.file = file |
|
self.close_file = close_file |
|
self._levels = frozenset(levels) |
|
|
|
with _lock: |
|
_files[self.filename] = self |
|
_update_levels() |
|
info( |
|
"{0} {1}\n{2} {3} ({4}-bit)\ndebugpy {5}", |
|
platform.platform(), |
|
platform.machine(), |
|
platform.python_implementation(), |
|
platform.python_version(), |
|
64 if sys.maxsize > 2**32 else 32, |
|
debugpy.__version__, |
|
_to_files=[self], |
|
) |
|
|
|
@property |
|
def levels(self): |
|
return self._levels |
|
|
|
@levels.setter |
|
def levels(self, value): |
|
with _lock: |
|
self._levels = frozenset(LEVELS if value is all else value) |
|
_update_levels() |
|
|
|
def write(self, level, output): |
|
if level in self.levels: |
|
try: |
|
self.file.write(output) |
|
self.file.flush() |
|
except Exception: |
|
pass |
|
|
|
def close(self): |
|
with _lock: |
|
del _files[self.filename] |
|
_update_levels() |
|
info("Not logging to {0} anymore.", json.repr(self.filename)) |
|
|
|
if self.close_file: |
|
try: |
|
self.file.close() |
|
except Exception: |
|
pass |
|
|
|
def __enter__(self): |
|
return self |
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb): |
|
self.close() |
|
|
|
|
|
class NoLog(object): |
|
file = filename = None |
|
|
|
__bool__ = __nonzero__ = lambda self: False |
|
|
|
def close(self): |
|
pass |
|
|
|
def __enter__(self): |
|
return self |
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb): |
|
pass |
|
|
|
|
|
|
|
|
|
def newline(level="info"): |
|
with _lock: |
|
stderr.write(level, "\n") |
|
|
|
|
|
def write(level, text, _to_files=all): |
|
assert level in LEVELS |
|
|
|
t = timestamp.current() |
|
format_string = "{0}+{1:" + timestamp_format + "}: " |
|
prefix = format_string.format(level[0].upper(), t) |
|
|
|
text = getattr(_tls, "prefix", "") + text |
|
indent = "\n" + (" " * len(prefix)) |
|
output = indent.join(text.split("\n")) |
|
output = prefix + output + "\n\n" |
|
|
|
with _lock: |
|
if _to_files is all: |
|
_to_files = _files.values() |
|
for file in _to_files: |
|
file.write(level, output) |
|
|
|
return text |
|
|
|
|
|
def write_format(level, format_string, *args, **kwargs): |
|
|
|
|
|
if level != "error" and level not in _levels: |
|
return |
|
|
|
try: |
|
text = format_string.format(*args, **kwargs) |
|
except Exception: |
|
reraise_exception() |
|
|
|
return write(level, text, kwargs.pop("_to_files", all)) |
|
|
|
|
|
debug = functools.partial(write_format, "debug") |
|
info = functools.partial(write_format, "info") |
|
warning = functools.partial(write_format, "warning") |
|
|
|
|
|
def error(*args, **kwargs): |
|
"""Logs an error. |
|
|
|
Returns the output wrapped in AssertionError. Thus, the following:: |
|
|
|
raise log.error(s, ...) |
|
|
|
has the same effect as:: |
|
|
|
log.error(...) |
|
assert False, (s.format(...)) |
|
""" |
|
return AssertionError(write_format("error", *args, **kwargs)) |
|
|
|
|
|
def _exception(format_string="", *args, **kwargs): |
|
level = kwargs.pop("level", "error") |
|
exc_info = kwargs.pop("exc_info", sys.exc_info()) |
|
|
|
if format_string: |
|
format_string += "\n\n" |
|
format_string += "{exception}\nStack where logged:\n{stack}" |
|
|
|
exception = "".join(traceback.format_exception(*exc_info)) |
|
|
|
f = inspect.currentframe() |
|
f = f.f_back if f else f |
|
try: |
|
stack = "".join(traceback.format_stack(f)) |
|
finally: |
|
del f |
|
|
|
write_format( |
|
level, format_string, *args, exception=exception, stack=stack, **kwargs |
|
) |
|
|
|
|
|
def swallow_exception(format_string="", *args, **kwargs): |
|
"""Logs an exception with full traceback. |
|
|
|
If format_string is specified, it is formatted with format(*args, **kwargs), and |
|
prepended to the exception traceback on a separate line. |
|
|
|
If exc_info is specified, the exception it describes will be logged. Otherwise, |
|
sys.exc_info() - i.e. the exception being handled currently - will be logged. |
|
|
|
If level is specified, the exception will be logged as a message of that level. |
|
The default is "error". |
|
""" |
|
|
|
_exception(format_string, *args, **kwargs) |
|
|
|
|
|
def reraise_exception(format_string="", *args, **kwargs): |
|
"""Like swallow_exception(), but re-raises the current exception after logging it.""" |
|
|
|
assert "exc_info" not in kwargs |
|
_exception(format_string, *args, **kwargs) |
|
raise |
|
|
|
|
|
def to_file(filename=None, prefix=None, levels=LEVELS): |
|
"""Starts logging all messages at the specified levels to the designated file. |
|
|
|
Either filename or prefix must be specified, but not both. |
|
|
|
If filename is specified, it designates the log file directly. |
|
|
|
If prefix is specified, the log file is automatically created in options.log_dir, |
|
with filename computed as prefix + os.getpid(). If log_dir is None, no log file |
|
is created, and the function returns immediately. |
|
|
|
If the file with the specified or computed name is already being used as a log |
|
file, it is not overwritten, but its levels are updated as specified. |
|
|
|
The function returns an object with a close() method. When the object is closed, |
|
logs are not written into that file anymore. Alternatively, the returned object |
|
can be used in a with-statement: |
|
|
|
with log.to_file("some.log"): |
|
# now also logging to some.log |
|
# not logging to some.log anymore |
|
""" |
|
|
|
assert (filename is not None) ^ (prefix is not None) |
|
|
|
if filename is None: |
|
if log_dir is None: |
|
return NoLog() |
|
try: |
|
os.makedirs(log_dir) |
|
except OSError: |
|
pass |
|
filename = f"{log_dir}/{prefix}-{os.getpid()}.log" |
|
|
|
file = _files.get(filename) |
|
if file is None: |
|
file = LogFile(filename, io.open(filename, "w", encoding="utf-8"), levels) |
|
else: |
|
file.levels = levels |
|
return file |
|
|
|
|
|
@contextlib.contextmanager |
|
def prefixed(format_string, *args, **kwargs): |
|
"""Adds a prefix to all messages logged from the current thread for the duration |
|
of the context manager. |
|
""" |
|
prefix = format_string.format(*args, **kwargs) |
|
old_prefix = getattr(_tls, "prefix", "") |
|
_tls.prefix = prefix + old_prefix |
|
try: |
|
yield |
|
finally: |
|
_tls.prefix = old_prefix |
|
|
|
|
|
def get_environment_description(header): |
|
import sysconfig |
|
import site |
|
|
|
result = [header, "\n\n"] |
|
|
|
def report(s, *args, **kwargs): |
|
result.append(s.format(*args, **kwargs)) |
|
|
|
def report_paths(get_paths, label=None): |
|
prefix = f" {label or get_paths}: " |
|
|
|
expr = None |
|
if not callable(get_paths): |
|
expr = get_paths |
|
get_paths = lambda: util.evaluate(expr) |
|
try: |
|
paths = get_paths() |
|
except AttributeError: |
|
report("{0}<missing>\n", prefix) |
|
return |
|
except Exception: |
|
swallow_exception( |
|
"Error evaluating {0}", |
|
repr(expr) if expr else util.srcnameof(get_paths), |
|
level="info", |
|
) |
|
return |
|
|
|
if not isinstance(paths, (list, tuple)): |
|
paths = [paths] |
|
|
|
for p in sorted(paths): |
|
report("{0}{1}", prefix, p) |
|
if p is not None: |
|
rp = os.path.realpath(p) |
|
if p != rp: |
|
report("({0})", rp) |
|
report("\n") |
|
|
|
prefix = " " * len(prefix) |
|
|
|
report("System paths:\n") |
|
report_paths("sys.executable") |
|
report_paths("sys.prefix") |
|
report_paths("sys.base_prefix") |
|
report_paths("sys.real_prefix") |
|
report_paths("site.getsitepackages()") |
|
report_paths("site.getusersitepackages()") |
|
|
|
site_packages = [ |
|
p |
|
for p in sys.path |
|
if os.path.exists(p) and os.path.basename(p) == "site-packages" |
|
] |
|
report_paths(lambda: site_packages, "sys.path (site-packages)") |
|
|
|
for name in sysconfig.get_path_names(): |
|
expr = "sysconfig.get_path({0!r})".format(name) |
|
report_paths(expr) |
|
|
|
report_paths("os.__file__") |
|
report_paths("threading.__file__") |
|
report_paths("debugpy.__file__") |
|
report("\n") |
|
|
|
importlib_metadata = None |
|
try: |
|
import importlib_metadata |
|
except ImportError: |
|
try: |
|
from importlib import metadata as importlib_metadata |
|
except ImportError: |
|
pass |
|
if importlib_metadata is None: |
|
report("Cannot enumerate installed packages - missing importlib_metadata.") |
|
else: |
|
report("Installed packages:\n") |
|
try: |
|
for pkg in importlib_metadata.distributions(): |
|
report(" {0}=={1}\n", pkg.name, pkg.version) |
|
except Exception: |
|
swallow_exception( |
|
"Error while enumerating installed packages.", level="info" |
|
) |
|
|
|
return "".join(result).rstrip("\n") |
|
|
|
|
|
def describe_environment(header): |
|
info("{0}", get_environment_description(header)) |
|
|
|
|
|
stderr = LogFile( |
|
"<stderr>", |
|
sys.stderr, |
|
levels=os.getenv("DEBUGPY_LOG_STDERR", "warning error").split(), |
|
close_file=False, |
|
) |
|
|
|
|
|
@atexit.register |
|
def _close_files(): |
|
for file in tuple(_files.values()): |
|
file.close() |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _repr(value): |
|
warning("$REPR {0!r}", value) |
|
|
|
|
|
def _vars(*names): |
|
locals = inspect.currentframe().f_back.f_locals |
|
if names: |
|
locals = {name: locals[name] for name in names if name in locals} |
|
warning("$VARS {0!r}", locals) |
|
|
|
|
|
def _stack(): |
|
stack = "\n".join(traceback.format_stack()) |
|
warning("$STACK:\n\n{0}", stack) |
|
|
|
|
|
def _threads(): |
|
output = "\n".join([str(t) for t in threading.enumerate()]) |
|
warning("$THREADS:\n\n{0}", output) |
|
|