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