1# Copyright (c) Microsoft Corporation. All rights reserved.
2# Licensed under the MIT License. See LICENSE in the project root
3# for license information.
4
5from __future__ import absolute_import, division, print_function, unicode_literals
6
7import atexit
8import contextlib
9import functools
10import inspect
11import io
12import os
13import platform
14import sys
15import threading
16import traceback
17
18import debugpy
19from debugpy.common import compat, fmt, timestamp, util
20
21
22LEVELS = ("debug", "info", "warning", "error")
23"""Logging levels, lowest to highest importance.
24"""
25
26log_dir = os.getenv("DEBUGPY_LOG_DIR")
27"""If not None, debugger logs its activity to a file named debugpy.*-<pid>.log
28in the specified directory, where <pid> is the return value of os.getpid().
29"""
30
31timestamp_format = "09.3f"
32"""Format spec used for timestamps. Can be changed to dial precision up or down.
33"""
34
35_lock = threading.RLock()
36_tls = threading.local()
37_files = {}  # filename -> LogFile
38_levels = set()  # combined for all log files
39
40
41def _update_levels():
42    global _levels
43    _levels = frozenset(level for file in _files.values() for level in file.levels)
44
45
46class LogFile(object):
47    def __init__(self, filename, file, levels=LEVELS, close_file=True):
48        info("Also logging to {0!j}.", filename)
49
50        self.filename = filename
51        self.file = file
52        self.close_file = close_file
53        self._levels = frozenset(levels)
54
55        with _lock:
56            _files[self.filename] = self
57            _update_levels()
58            info(
59                "{0} {1}\n{2} {3} ({4}-bit)\ndebugpy {5}",
60                platform.platform(),
61                platform.machine(),
62                platform.python_implementation(),
63                platform.python_version(),
64                64 if sys.maxsize > 2 ** 32 else 32,
65                debugpy.__version__,
66                _to_files=[self],
67            )
68
69    @property
70    def levels(self):
71        return self._levels
72
73    @levels.setter
74    def levels(self, value):
75        with _lock:
76            self._levels = frozenset(LEVELS if value is all else value)
77            _update_levels()
78
79    def write(self, level, output):
80        if level in self.levels:
81            try:
82                self.file.write(output)
83                self.file.flush()
84            except Exception:
85                pass
86
87    def close(self):
88        with _lock:
89            del _files[self.filename]
90            _update_levels()
91        info("Not logging to {0!j} anymore.", self.filename)
92
93        if self.close_file:
94            try:
95                self.file.close()
96            except Exception:
97                pass
98
99    def __enter__(self):
100        return self
101
102    def __exit__(self, exc_type, exc_val, exc_tb):
103        self.close()
104
105
106class NoLog(object):
107    file = filename = None
108
109    __bool__ = __nonzero__ = lambda self: False
110
111    def close(self):
112        pass
113
114    def __enter__(self):
115        return self
116
117    def __exit__(self, exc_type, exc_val, exc_tb):
118        pass
119
120
121# Used to inject a newline into stderr if logging there, to clean up the output
122# when it's intermixed with regular prints from other sources.
123def newline(level="info"):
124    with _lock:
125        stderr.write(level, "\n")
126
127
128def write(level, text, _to_files=all):
129    assert level in LEVELS
130
131    t = timestamp.current()
132    format_string = "{0}+{1:" + timestamp_format + "}: "
133    prefix = fmt(format_string, level[0].upper(), t)
134
135    text = getattr(_tls, "prefix", "") + text
136    indent = "\n" + (" " * len(prefix))
137    output = indent.join(text.split("\n"))
138    output = prefix + output + "\n\n"
139
140    with _lock:
141        if _to_files is all:
142            _to_files = _files.values()
143        for file in _to_files:
144            file.write(level, output)
145
146    return text
147
148
149def write_format(level, format_string, *args, **kwargs):
150    # Don't spend cycles doing expensive formatting if we don't have to. Errors are
151    # always formatted, so that error() can return the text even if it's not logged.
152    if level != "error" and level not in _levels:
153        return
154
155    try:
156        text = fmt(format_string, *args, **kwargs)
157    except Exception:
158        reraise_exception()
159
160    return write(level, text, kwargs.pop("_to_files", all))
161
162
163debug = functools.partial(write_format, "debug")
164info = functools.partial(write_format, "info")
165warning = functools.partial(write_format, "warning")
166
167
168def error(*args, **kwargs):
169    """Logs an error.
170
171    Returns the output wrapped in AssertionError. Thus, the following::
172
173        raise log.error(...)
174
175    has the same effect as::
176
177        log.error(...)
178        assert False, fmt(...)
179    """
180    return AssertionError(write_format("error", *args, **kwargs))
181
182
183def _exception(format_string="", *args, **kwargs):
184    level = kwargs.pop("level", "error")
185    exc_info = kwargs.pop("exc_info", sys.exc_info())
186
187    if format_string:
188        format_string += "\n\n"
189    format_string += "{exception}\nStack where logged:\n{stack}"
190
191    exception = "".join(traceback.format_exception(*exc_info))
192
193    f = inspect.currentframe()
194    f = f.f_back if f else f  # don't log this frame
195    try:
196        stack = "".join(traceback.format_stack(f))
197    finally:
198        del f  # avoid cycles
199
200    write_format(
201        level, format_string, *args, exception=exception, stack=stack, **kwargs
202    )
203
204
205def swallow_exception(format_string="", *args, **kwargs):
206    """Logs an exception with full traceback.
207
208    If format_string is specified, it is formatted with fmt(*args, **kwargs), and
209    prepended to the exception traceback on a separate line.
210
211    If exc_info is specified, the exception it describes will be logged. Otherwise,
212    sys.exc_info() - i.e. the exception being handled currently - will be logged.
213
214    If level is specified, the exception will be logged as a message of that level.
215    The default is "error".
216    """
217
218    _exception(format_string, *args, **kwargs)
219
220
221def reraise_exception(format_string="", *args, **kwargs):
222    """Like swallow_exception(), but re-raises the current exception after logging it.
223    """
224
225    assert "exc_info" not in kwargs
226    _exception(format_string, *args, **kwargs)
227    raise
228
229
230def to_file(filename=None, prefix=None, levels=LEVELS):
231    """Starts logging all messages at the specified levels to the designated file.
232
233    Either filename or prefix must be specified, but not both.
234
235    If filename is specified, it designates the log file directly.
236
237    If prefix is specified, the log file is automatically created in options.log_dir,
238    with filename computed as prefix + os.getpid(). If log_dir is None, no log file
239    is created, and the function returns immediately.
240
241    If the file with the specified or computed name is already being used as a log
242    file, it is not overwritten, but its levels are updated as specified.
243
244    The function returns an object with a close() method. When the object is closed,
245    logs are not written into that file anymore. Alternatively, the returned object
246    can be used in a with-statement:
247
248        with log.to_file("some.log"):
249            # now also logging to some.log
250        # not logging to some.log anymore
251    """
252
253    assert (filename is not None) ^ (prefix is not None)
254
255    if filename is None:
256        if log_dir is None:
257            return NoLog()
258        try:
259            os.makedirs(log_dir)
260        except OSError:
261            pass
262        filename = fmt("{0}/{1}-{2}.log", log_dir, prefix, os.getpid())
263
264    file = _files.get(filename)
265    if file is None:
266        file = LogFile(filename, io.open(filename, "w", encoding="utf-8"), levels)
267    else:
268        file.levels = levels
269    return file
270
271
272@contextlib.contextmanager
273def prefixed(format_string, *args, **kwargs):
274    """Adds a prefix to all messages logged from the current thread for the duration
275    of the context manager.
276    """
277    prefix = fmt(format_string, *args, **kwargs)
278    old_prefix = getattr(_tls, "prefix", "")
279    _tls.prefix = prefix + old_prefix
280    try:
281        yield
282    finally:
283        _tls.prefix = old_prefix
284
285
286def describe_environment(header):
287    import sysconfig
288    import site  # noqa
289
290    result = [header, "\n\n"]
291
292    def report(*args, **kwargs):
293        result.append(fmt(*args, **kwargs))
294
295    def report_paths(get_paths, label=None):
296        prefix = fmt("    {0}: ", label or get_paths)
297
298        expr = None
299        if not callable(get_paths):
300            expr = get_paths
301            get_paths = lambda: util.evaluate(expr)
302        try:
303            paths = get_paths()
304        except AttributeError:
305            report("{0}<missing>\n", prefix)
306            return
307        except Exception:
308            swallow_exception(
309                "Error evaluating {0}",
310                repr(expr) if expr else compat.srcnameof(get_paths),
311            )
312            return
313
314        if not isinstance(paths, (list, tuple)):
315            paths = [paths]
316
317        for p in sorted(paths):
318            report("{0}{1}", prefix, p)
319            rp = os.path.realpath(p)
320            if p != rp:
321                report("({0})", rp)
322            report("\n")
323
324            prefix = " " * len(prefix)
325
326    report("System paths:\n")
327    report_paths("sys.prefix")
328    report_paths("sys.base_prefix")
329    report_paths("sys.real_prefix")
330    report_paths("site.getsitepackages()")
331    report_paths("site.getusersitepackages()")
332
333    site_packages = [
334        p
335        for p in sys.path
336        if os.path.exists(p)
337        and os.path.basename(p) == compat.filename_str("site-packages")
338    ]
339    report_paths(lambda: site_packages, "sys.path (site-packages)")
340
341    for name in sysconfig.get_path_names():
342        expr = fmt("sysconfig.get_path({0!r})", name)
343        report_paths(expr)
344
345    report_paths("os.__file__")
346    report_paths("threading.__file__")
347
348    result = "".join(result).rstrip("\n")
349    info("{0}", result)
350
351
352stderr = LogFile(
353    "<stderr>",
354    sys.stderr,
355    levels=os.getenv("DEBUGPY_LOG_STDERR", "warning error").split(),
356    close_file=False,
357)
358
359
360@atexit.register
361def _close_files():
362    for file in tuple(_files.values()):
363        file.close()
364
365
366# The following are helper shortcuts for printf debugging. They must never be used
367# in production code.
368
369
370def _repr(value):  # pragma: no cover
371    warning("$REPR {0!r}", value)
372
373
374def _vars(*names):  # pragma: no cover
375    locals = inspect.currentframe().f_back.f_locals
376    if names:
377        locals = {name: locals[name] for name in names if name in locals}
378    warning("$VARS {0!r}", locals)
379
380
381def _stack():  # pragma: no cover
382    stack = "\n".join(traceback.format_stack())
383    warning("$STACK:\n\n{0}", stack)
384