440 lines
13 KiB
Python
440 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import itertools
|
|
import linecache
|
|
import os
|
|
import re
|
|
import sys
|
|
import sysconfig
|
|
import traceback
|
|
import typing as t
|
|
|
|
from markupsafe import escape
|
|
|
|
from ..utils import cached_property
|
|
from .console import Console
|
|
|
|
HEADER = """\
|
|
<!doctype html>
|
|
<html lang=en>
|
|
<head>
|
|
<title>%(title)s // Werkzeug Debugger</title>
|
|
<link rel="stylesheet" href="?__debugger__=yes&cmd=resource&f=style.css">
|
|
<link rel="shortcut icon"
|
|
href="?__debugger__=yes&cmd=resource&f=console.png">
|
|
<script src="?__debugger__=yes&cmd=resource&f=debugger.js"></script>
|
|
<script>
|
|
var CONSOLE_MODE = %(console)s,
|
|
EVALEX = %(evalex)s,
|
|
EVALEX_TRUSTED = %(evalex_trusted)s,
|
|
SECRET = "%(secret)s";
|
|
</script>
|
|
</head>
|
|
<body style="background-color: #fff">
|
|
<div class="debugger">
|
|
"""
|
|
|
|
FOOTER = """\
|
|
<div class="footer">
|
|
Brought to you by <strong class="arthur">DON'T PANIC</strong>, your
|
|
friendly Werkzeug powered traceback interpreter.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pin-prompt">
|
|
<div class="inner">
|
|
<h3>Console Locked</h3>
|
|
<p>
|
|
The console is locked and needs to be unlocked by entering the PIN.
|
|
You can find the PIN printed out on the standard output of your
|
|
shell that runs the server.
|
|
<form>
|
|
<p>PIN:
|
|
<input type=text name=pin size=14>
|
|
<input type=submit name=btn value="Confirm Pin">
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
PAGE_HTML = (
|
|
HEADER
|
|
+ """\
|
|
<h1>%(exception_type)s</h1>
|
|
<div class="detail">
|
|
<p class="errormsg">%(exception)s</p>
|
|
</div>
|
|
<h2 class="traceback">Traceback <em>(most recent call last)</em></h2>
|
|
%(summary)s
|
|
<div class="plain">
|
|
<p>
|
|
This is the Copy/Paste friendly version of the traceback.
|
|
</p>
|
|
<textarea cols="50" rows="10" name="code" readonly>%(plaintext)s</textarea>
|
|
</div>
|
|
<div class="explanation">
|
|
The debugger caught an exception in your WSGI application. You can now
|
|
look at the traceback which led to the error. <span class="nojavascript">
|
|
If you enable JavaScript you can also use additional features such as code
|
|
execution (if the evalex feature is enabled), automatic pasting of the
|
|
exceptions and much more.</span>
|
|
</div>
|
|
"""
|
|
+ FOOTER
|
|
+ """
|
|
<!--
|
|
|
|
%(plaintext_cs)s
|
|
|
|
-->
|
|
"""
|
|
)
|
|
|
|
CONSOLE_HTML = (
|
|
HEADER
|
|
+ """\
|
|
<h1>Interactive Console</h1>
|
|
<div class="explanation">
|
|
In this console you can execute Python expressions in the context of the
|
|
application. The initial namespace was created by the debugger automatically.
|
|
</div>
|
|
<div class="console"><div class="inner">The Console requires JavaScript.</div></div>
|
|
"""
|
|
+ FOOTER
|
|
)
|
|
|
|
SUMMARY_HTML = """\
|
|
<div class="%(classes)s">
|
|
%(title)s
|
|
<ul>%(frames)s</ul>
|
|
%(description)s
|
|
</div>
|
|
"""
|
|
|
|
FRAME_HTML = """\
|
|
<div class="frame" id="frame-%(id)d">
|
|
<h4>File <cite class="filename">"%(filename)s"</cite>,
|
|
line <em class="line">%(lineno)s</em>,
|
|
in <code class="function">%(function_name)s</code></h4>
|
|
<div class="source %(library)s">%(lines)s</div>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def _process_traceback(
|
|
exc: BaseException,
|
|
te: traceback.TracebackException | None = None,
|
|
*,
|
|
skip: int = 0,
|
|
hide: bool = True,
|
|
) -> traceback.TracebackException:
|
|
if te is None:
|
|
te = traceback.TracebackException.from_exception(exc, lookup_lines=False)
|
|
|
|
# Get the frames the same way StackSummary.extract did, in order
|
|
# to match each frame with the FrameSummary to augment.
|
|
frame_gen = traceback.walk_tb(exc.__traceback__)
|
|
limit = getattr(sys, "tracebacklimit", None)
|
|
|
|
if limit is not None:
|
|
if limit < 0:
|
|
limit = 0
|
|
|
|
frame_gen = itertools.islice(frame_gen, limit)
|
|
|
|
if skip:
|
|
frame_gen = itertools.islice(frame_gen, skip, None)
|
|
del te.stack[:skip]
|
|
|
|
new_stack: list[DebugFrameSummary] = []
|
|
hidden = False
|
|
|
|
# Match each frame with the FrameSummary that was generated.
|
|
# Hide frames using Paste's __traceback_hide__ rules. Replace
|
|
# all visible FrameSummary with DebugFrameSummary.
|
|
for (f, _), fs in zip(frame_gen, te.stack):
|
|
if hide:
|
|
hide_value = f.f_locals.get("__traceback_hide__", False)
|
|
|
|
if hide_value in {"before", "before_and_this"}:
|
|
new_stack = []
|
|
hidden = False
|
|
|
|
if hide_value == "before_and_this":
|
|
continue
|
|
elif hide_value in {"reset", "reset_and_this"}:
|
|
hidden = False
|
|
|
|
if hide_value == "reset_and_this":
|
|
continue
|
|
elif hide_value in {"after", "after_and_this"}:
|
|
hidden = True
|
|
|
|
if hide_value == "after_and_this":
|
|
continue
|
|
elif hide_value or hidden:
|
|
continue
|
|
|
|
frame_args: dict[str, t.Any] = {
|
|
"filename": fs.filename,
|
|
"lineno": fs.lineno,
|
|
"name": fs.name,
|
|
"locals": f.f_locals,
|
|
"globals": f.f_globals,
|
|
}
|
|
|
|
if hasattr(fs, "colno"):
|
|
frame_args["colno"] = fs.colno
|
|
frame_args["end_colno"] = fs.end_colno # type: ignore[attr-defined]
|
|
|
|
new_stack.append(DebugFrameSummary(**frame_args))
|
|
|
|
# The codeop module is used to compile code from the interactive
|
|
# debugger. Hide any codeop frames from the bottom of the traceback.
|
|
while new_stack:
|
|
module = new_stack[0].global_ns.get("__name__")
|
|
|
|
if module is None:
|
|
module = new_stack[0].local_ns.get("__name__")
|
|
|
|
if module == "codeop":
|
|
del new_stack[0]
|
|
else:
|
|
break
|
|
|
|
te.stack[:] = new_stack
|
|
|
|
if te.__context__:
|
|
context_exc = t.cast(BaseException, exc.__context__)
|
|
te.__context__ = _process_traceback(context_exc, te.__context__, hide=hide)
|
|
|
|
if te.__cause__:
|
|
cause_exc = t.cast(BaseException, exc.__cause__)
|
|
te.__cause__ = _process_traceback(cause_exc, te.__cause__, hide=hide)
|
|
|
|
return te
|
|
|
|
|
|
class DebugTraceback:
|
|
__slots__ = ("_te", "_cache_all_tracebacks", "_cache_all_frames")
|
|
|
|
def __init__(
|
|
self,
|
|
exc: BaseException,
|
|
te: traceback.TracebackException | None = None,
|
|
*,
|
|
skip: int = 0,
|
|
hide: bool = True,
|
|
) -> None:
|
|
self._te = _process_traceback(exc, te, skip=skip, hide=hide)
|
|
|
|
def __str__(self) -> str:
|
|
return f"<{type(self).__name__} {self._te}>"
|
|
|
|
@cached_property
|
|
def all_tracebacks(
|
|
self,
|
|
) -> list[tuple[str | None, traceback.TracebackException]]:
|
|
out = []
|
|
current = self._te
|
|
|
|
while current is not None:
|
|
if current.__cause__ is not None:
|
|
chained_msg = (
|
|
"The above exception was the direct cause of the"
|
|
" following exception"
|
|
)
|
|
chained_exc = current.__cause__
|
|
elif current.__context__ is not None and not current.__suppress_context__:
|
|
chained_msg = (
|
|
"During handling of the above exception, another"
|
|
" exception occurred"
|
|
)
|
|
chained_exc = current.__context__
|
|
else:
|
|
chained_msg = None
|
|
chained_exc = None
|
|
|
|
out.append((chained_msg, current))
|
|
current = chained_exc
|
|
|
|
return out
|
|
|
|
@cached_property
|
|
def all_frames(self) -> list[DebugFrameSummary]:
|
|
return [
|
|
f # type: ignore[misc]
|
|
for _, te in self.all_tracebacks
|
|
for f in te.stack
|
|
]
|
|
|
|
def render_traceback_text(self) -> str:
|
|
return "".join(self._te.format())
|
|
|
|
def render_traceback_html(self, include_title: bool = True) -> str:
|
|
library_frames = [f.is_library for f in self.all_frames]
|
|
mark_library = 0 < sum(library_frames) < len(library_frames)
|
|
rows = []
|
|
|
|
if not library_frames:
|
|
classes = "traceback noframe-traceback"
|
|
else:
|
|
classes = "traceback"
|
|
|
|
for msg, current in reversed(self.all_tracebacks):
|
|
row_parts = []
|
|
|
|
if msg is not None:
|
|
row_parts.append(f'<li><div class="exc-divider">{msg}:</div>')
|
|
|
|
for frame in current.stack:
|
|
frame = t.cast(DebugFrameSummary, frame)
|
|
info = f' title="{escape(frame.info)}"' if frame.info else ""
|
|
row_parts.append(f"<li{info}>{frame.render_html(mark_library)}")
|
|
|
|
rows.append("\n".join(row_parts))
|
|
|
|
is_syntax_error = issubclass(self._te.exc_type, SyntaxError)
|
|
|
|
if include_title:
|
|
if is_syntax_error:
|
|
title = "Syntax Error"
|
|
else:
|
|
title = "Traceback <em>(most recent call last)</em>:"
|
|
else:
|
|
title = ""
|
|
|
|
exc_full = escape("".join(self._te.format_exception_only()))
|
|
|
|
if is_syntax_error:
|
|
description = f"<pre class=syntaxerror>{exc_full}</pre>"
|
|
else:
|
|
description = f"<blockquote>{exc_full}</blockquote>"
|
|
|
|
return SUMMARY_HTML % {
|
|
"classes": classes,
|
|
"title": f"<h3>{title}</h3>",
|
|
"frames": "\n".join(rows),
|
|
"description": description,
|
|
}
|
|
|
|
def render_debugger_html(
|
|
self, evalex: bool, secret: str, evalex_trusted: bool
|
|
) -> str:
|
|
exc_lines = list(self._te.format_exception_only())
|
|
plaintext = "".join(self._te.format())
|
|
return PAGE_HTML % {
|
|
"evalex": "true" if evalex else "false",
|
|
"evalex_trusted": "true" if evalex_trusted else "false",
|
|
"console": "false",
|
|
"title": escape(exc_lines[0]),
|
|
"exception": escape("".join(exc_lines)),
|
|
"exception_type": escape(self._te.exc_type.__name__),
|
|
"summary": self.render_traceback_html(include_title=False),
|
|
"plaintext": escape(plaintext),
|
|
"plaintext_cs": re.sub("-{2,}", "-", plaintext),
|
|
"secret": secret,
|
|
}
|
|
|
|
|
|
class DebugFrameSummary(traceback.FrameSummary):
|
|
"""A :class:`traceback.FrameSummary` that can evaluate code in the
|
|
frame's namespace.
|
|
"""
|
|
|
|
__slots__ = (
|
|
"local_ns",
|
|
"global_ns",
|
|
"_cache_info",
|
|
"_cache_is_library",
|
|
"_cache_console",
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
locals: dict[str, t.Any],
|
|
globals: dict[str, t.Any],
|
|
**kwargs: t.Any,
|
|
) -> None:
|
|
super().__init__(locals=None, **kwargs)
|
|
self.local_ns = locals
|
|
self.global_ns = globals
|
|
|
|
@cached_property
|
|
def info(self) -> str | None:
|
|
return self.local_ns.get("__traceback_info__")
|
|
|
|
@cached_property
|
|
def is_library(self) -> bool:
|
|
return any(
|
|
self.filename.startswith((path, os.path.realpath(path)))
|
|
for path in sysconfig.get_paths().values()
|
|
)
|
|
|
|
@cached_property
|
|
def console(self) -> Console:
|
|
return Console(self.global_ns, self.local_ns)
|
|
|
|
def eval(self, code: str) -> t.Any:
|
|
return self.console.eval(code)
|
|
|
|
def render_html(self, mark_library: bool) -> str:
|
|
context = 5
|
|
lines = linecache.getlines(self.filename)
|
|
line_idx = self.lineno - 1 # type: ignore[operator]
|
|
start_idx = max(0, line_idx - context)
|
|
stop_idx = min(len(lines), line_idx + context + 1)
|
|
rendered_lines = []
|
|
|
|
def render_line(line: str, cls: str) -> None:
|
|
line = line.expandtabs().rstrip()
|
|
stripped_line = line.strip()
|
|
prefix = len(line) - len(stripped_line)
|
|
colno = getattr(self, "colno", 0)
|
|
end_colno = getattr(self, "end_colno", 0)
|
|
|
|
if cls == "current" and colno and end_colno:
|
|
arrow = (
|
|
f'\n<span class="ws">{" " * prefix}</span>'
|
|
f'{" " * (colno - prefix)}{"^" * (end_colno - colno)}'
|
|
)
|
|
else:
|
|
arrow = ""
|
|
|
|
rendered_lines.append(
|
|
f'<pre class="line {cls}"><span class="ws">{" " * prefix}</span>'
|
|
f"{escape(stripped_line) if stripped_line else ' '}"
|
|
f"{arrow if arrow else ''}</pre>"
|
|
)
|
|
|
|
if lines:
|
|
for line in lines[start_idx:line_idx]:
|
|
render_line(line, "before")
|
|
|
|
render_line(lines[line_idx], "current")
|
|
|
|
for line in lines[line_idx + 1 : stop_idx]:
|
|
render_line(line, "after")
|
|
|
|
return FRAME_HTML % {
|
|
"id": id(self),
|
|
"filename": escape(self.filename),
|
|
"lineno": self.lineno,
|
|
"function_name": escape(self.name),
|
|
"lines": "\n".join(rendered_lines),
|
|
"library": "library" if mark_library and self.is_library else "",
|
|
}
|
|
|
|
|
|
def render_console_html(secret: str, evalex_trusted: bool) -> str:
|
|
return CONSOLE_HTML % {
|
|
"evalex": "true",
|
|
"evalex_trusted": "true" if evalex_trusted else "false",
|
|
"console": "true",
|
|
"title": "Console",
|
|
"secret": secret,
|
|
}
|