590 lines
65 KiB
Elixir
590 lines
65 KiB
Elixir
defmodule Plug.Debugger do
|
|
@moduledoc """
|
|
A module (**not a plug**) for debugging in development.
|
|
|
|
This module is commonly used within a `Plug.Builder` or a `Plug.Router`
|
|
and it wraps the `call/2` function.
|
|
|
|
Notice `Plug.Debugger` *does not* catch errors, as errors should still
|
|
propagate so that the Elixir process finishes with the proper reason.
|
|
This module does not perform any logging either, as all logging is done
|
|
by the web server handler.
|
|
|
|
**Note:** If this module is used with `Plug.ErrorHandler`, only one of
|
|
them will effectively handle errors. For this reason, it is recommended
|
|
that `Plug.Debugger` is used before `Plug.ErrorHandler` and only in
|
|
particular environments, like `:dev`.
|
|
|
|
In case of an error, the rendered page drops the `content-security-policy`
|
|
header before rendering the error to ensure that the error is displayed
|
|
correctly.
|
|
|
|
## Examples
|
|
|
|
defmodule MyApp do
|
|
use Plug.Builder
|
|
|
|
if Mix.env == :dev do
|
|
use Plug.Debugger, otp_app: :my_app
|
|
end
|
|
|
|
plug :boom
|
|
|
|
def boom(conn, _) do
|
|
# Error raised here will be caught and displayed in a debug page
|
|
# complete with a stacktrace and other helpful info.
|
|
raise "oops"
|
|
end
|
|
end
|
|
|
|
## Options
|
|
|
|
* `:otp_app` - the OTP application that is using Plug. This option is used
|
|
to filter stacktraces that belong only to the given application.
|
|
* `:style` - custom styles (see below)
|
|
* `:banner` - the optional MFA (`{module, function, args}`) which receives
|
|
exception details and returns banner contents to appear at the top of
|
|
the page. May be any string, including markup.
|
|
|
|
## Custom styles
|
|
|
|
You may pass a `:style` option to customize the look of the HTML page.
|
|
|
|
use Plug.Debugger, style:
|
|
[primary: "#c0392b", logo: "data:image/png;base64,..."]
|
|
|
|
The following keys are available:
|
|
|
|
* `:primary` - primary color
|
|
* `:accent` - accent color
|
|
* `:logo` - logo URI, or `nil` to disable
|
|
|
|
The `:logo` is preferred to be a base64-encoded data URI so not to make any
|
|
external requests, though external URLs (eg, `https://...`) are supported.
|
|
|
|
## Custom Banners
|
|
|
|
You may pass an MFA (`{module, function, args}`) to be invoked when an
|
|
error is rendered which provides a custom banner at the top of the
|
|
debugger page. The function receives the following arguments, with the
|
|
passed `args` concatenated at the end:
|
|
|
|
[conn, status, kind, reason, stacktrace]
|
|
|
|
For example, the following `:banner` option:
|
|
|
|
use Plug.Debugger, banner: {MyModule, :debug_banner, []}
|
|
|
|
would invoke the function:
|
|
|
|
MyModule.debug_banner(conn, status, kind, reason, stacktrace)
|
|
|
|
## Links to the text editor
|
|
|
|
If a `PLUG_EDITOR` environment variable is set, `Plug.Debugger` will
|
|
use it to generate links to your text editor. The variable should be
|
|
set with `__FILE__` and `__LINE__` placeholders which will be correctly
|
|
replaced. For example (with the [TextMate](http://macromates.com) editor):
|
|
|
|
txmt://open/?url=file://__FILE__&line=__LINE__
|
|
|
|
Or, using Visual Studio Code:
|
|
|
|
vscode://file/__FILE__:__LINE__
|
|
|
|
You can also use `__RELATIVEFILE__` if your project path is different from
|
|
the running application. This is useful when working with Docker containers.
|
|
|
|
vscode://file//path/to/your/project/__RELATIVEFILE__:__LINE__
|
|
"""
|
|
|
|
@already_sent {:plug_conn, :sent}
|
|
|
|
@default_style %{
|
|
primary: "#4e2a8e",
|
|
accent: "#607080",
|
|
highlight: "#f0f4fa",
|
|
red_highlight: "#ffe5e5",
|
|
line_color: "#eee",
|
|
text_color: "#203040",
|
|
logo:
|
|
"data:image/png;base64,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",
|
|
monospace_font: "menlo, consolas, monospace",
|
|
background: "white",
|
|
top_background: "#f9f9fa"
|
|
}
|
|
|
|
@default_dark_style %{
|
|
primary: "#9d86d2",
|
|
accent: "#9aa5b1",
|
|
highlight: "#2d333b",
|
|
red_highlight: "#5c2626",
|
|
line_color: "#404040",
|
|
text_color: "#e5e5e5",
|
|
background: "#1c1c1c",
|
|
top_background: "#2a2a2a",
|
|
logo:
|
|
"data:image/png;base64,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"
|
|
}
|
|
|
|
@salt "plug-debugger-actions"
|
|
|
|
import Plug.Conn
|
|
require Logger
|
|
|
|
@doc false
|
|
defmacro __using__(opts) do
|
|
quote do
|
|
@plug_debugger unquote(opts)
|
|
@before_compile Plug.Debugger
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
defmacro __before_compile__(_) do
|
|
quote location: :keep do
|
|
defoverridable call: 2
|
|
|
|
def call(conn, opts) do
|
|
try do
|
|
case conn do
|
|
%Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} ->
|
|
Plug.Debugger.run_action(conn)
|
|
|
|
%Plug.Conn{} ->
|
|
super(conn, opts)
|
|
end
|
|
rescue
|
|
e in Plug.Conn.WrapperError ->
|
|
%{conn: conn, kind: kind, reason: reason, stack: stack} = e
|
|
Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger)
|
|
catch
|
|
kind, reason ->
|
|
Plug.Debugger.__catch__(conn, kind, reason, __STACKTRACE__, @plug_debugger)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def __catch__(conn, kind, reason, stack, opts) do
|
|
reason = Exception.normalize(kind, reason, stack)
|
|
status = status(kind, reason)
|
|
|
|
receive do
|
|
@already_sent ->
|
|
send(self(), @already_sent)
|
|
log(status, kind, reason, stack)
|
|
:erlang.raise(kind, reason, stack)
|
|
after
|
|
0 ->
|
|
render(conn, status, kind, reason, stack, opts)
|
|
log(status, kind, reason, stack)
|
|
:erlang.raise(kind, reason, stack)
|
|
end
|
|
end
|
|
|
|
# We don't log status >= 500 because those are treated as errors and logged later.
|
|
defp log(status, kind, reason, stack) when status < 500,
|
|
do: Logger.debug(Exception.format(kind, reason, stack))
|
|
|
|
defp log(_status, _kind, _reason, _stack), do: :ok
|
|
|
|
## Rendering
|
|
|
|
require EEx
|
|
|
|
html_template_path = "lib/plug/templates/debugger.html.eex"
|
|
EEx.function_from_file(:defp, :template_html, html_template_path, [:assigns])
|
|
|
|
markdown_template_path = "lib/plug/templates/debugger.md.eex"
|
|
EEx.function_from_file(:defp, :template_markdown, markdown_template_path, [:assigns])
|
|
|
|
# Made public with @doc false for testing.
|
|
@doc false
|
|
def render(conn, status, kind, reason, stack, opts) do
|
|
session = maybe_fetch_session(conn)
|
|
params = maybe_fetch_query_params(conn)
|
|
{title, message} = info(kind, reason)
|
|
html? = accepts_html?(get_req_header(conn, "accept"))
|
|
|
|
assigns = [
|
|
conn: conn,
|
|
title: title,
|
|
formatted: Exception.format(kind, reason, stack),
|
|
session: session,
|
|
params: params,
|
|
full_version: not html?
|
|
]
|
|
|
|
markdown = template_markdown(assigns)
|
|
|
|
if html? do
|
|
conn =
|
|
conn
|
|
|> put_resp_content_type("text/html")
|
|
|> delete_resp_header("content-security-policy")
|
|
|
|
actions = encoded_actions_for_exception(reason, conn)
|
|
last_path = actions_redirect_path(conn)
|
|
style = Enum.into(opts[:style] || [], @default_style)
|
|
style = maybe_merge_dark_styles(style, @default_dark_style)
|
|
banner = banner(conn, status, kind, reason, stack, opts)
|
|
|
|
assigns =
|
|
Keyword.merge(assigns,
|
|
conn: conn,
|
|
message: maybe_autolink(message),
|
|
markdown: h(markdown),
|
|
style: style,
|
|
banner: banner,
|
|
actions: actions,
|
|
frames: frames(:html, stack, opts),
|
|
last_path: last_path
|
|
)
|
|
|
|
send_resp(conn, status, template_html(assigns))
|
|
else
|
|
conn = put_resp_content_type(conn, "text/markdown")
|
|
send_resp(conn, status, markdown)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def run_action(%Plug.Conn{} = conn) do
|
|
with %Plug.Conn{body_params: params} = conn <- fetch_body_params(conn),
|
|
{:ok, {module, function, args}} <-
|
|
Plug.Crypto.verify(conn.secret_key_base, @salt, params["encoded_handler"]) do
|
|
apply(module, function, args)
|
|
|
|
conn
|
|
|> Plug.Conn.put_resp_header("location", params["last_path"] || "/")
|
|
|> send_resp(302, "")
|
|
|> halt()
|
|
else
|
|
_ -> raise "could not run Plug.Debugger action"
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def encoded_actions_for_exception(exception, conn) do
|
|
if conn.secret_key_base do
|
|
actions = Plug.Exception.actions(exception)
|
|
|
|
Enum.map(actions, fn %{label: label, handler: handler} ->
|
|
encoded_handler = Plug.Crypto.sign(conn.secret_key_base, @salt, handler)
|
|
%{label: label, encoded_handler: encoded_handler}
|
|
end)
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
defp actions_redirect_path(%Plug.Conn{
|
|
method: "GET",
|
|
request_path: request_path,
|
|
query_string: query_string
|
|
}) do
|
|
case query_string do
|
|
"" -> request_path
|
|
query_string -> "#{request_path}?#{query_string}"
|
|
end
|
|
end
|
|
|
|
defp actions_redirect_path(conn) do
|
|
case get_req_header(conn, "referer") do
|
|
[referer] -> referer
|
|
[] -> "/"
|
|
end
|
|
end
|
|
|
|
defp accepts_html?(_accept_header = []), do: false
|
|
|
|
defp accepts_html?(_accept_header = [header | _]),
|
|
do: String.contains?(header, ["*/*", "text/*", "text/html"])
|
|
|
|
defp maybe_fetch_session(conn) do
|
|
if conn.private[:plug_session_fetch] do
|
|
conn |> fetch_session(conn) |> get_session()
|
|
end
|
|
end
|
|
|
|
defp maybe_fetch_query_params(conn) do
|
|
fetch_query_params(conn).params
|
|
rescue
|
|
Plug.Conn.InvalidQueryError ->
|
|
case conn.params do
|
|
%Plug.Conn.Unfetched{} -> %{}
|
|
params -> params
|
|
end
|
|
end
|
|
|
|
@parsers_opts Plug.Parsers.init(parsers: [:urlencoded])
|
|
defp fetch_body_params(conn), do: Plug.Parsers.call(conn, @parsers_opts)
|
|
|
|
defp status(:error, error), do: Plug.Exception.status(error)
|
|
defp status(_, _), do: 500
|
|
|
|
defp info(:error, error), do: {inspect(error.__struct__), Exception.message(error)}
|
|
defp info(:throw, thrown), do: {"unhandled throw", inspect(thrown)}
|
|
defp info(:exit, reason), do: {"unhandled exit", Exception.format_exit(reason)}
|
|
|
|
defp frames(renderer, stacktrace, opts) do
|
|
app = opts[:otp_app]
|
|
editor = System.get_env("PLUG_EDITOR")
|
|
|
|
stacktrace
|
|
|> Enum.map_reduce(0, &each_frame(&1, &2, renderer, app, editor))
|
|
|> elem(0)
|
|
end
|
|
|
|
defp each_frame(entry, index, renderer, root, editor) do
|
|
{module, info, location, app, fun, arity, args} = get_entry(entry)
|
|
{file, line} = {to_string(location[:file] || "nofile"), location[:line]}
|
|
|
|
doc = module && get_doc(module, fun, arity, app)
|
|
clauses = module && get_clauses(renderer, module, fun, args)
|
|
source = get_source(app, module, file)
|
|
context = get_context(root, app)
|
|
snippet = get_snippet(source, line)
|
|
|
|
{%{
|
|
app: app,
|
|
info: info,
|
|
file: file,
|
|
line: line,
|
|
context: context,
|
|
snippet: snippet,
|
|
index: index,
|
|
doc: doc,
|
|
clauses: clauses,
|
|
args: args,
|
|
link: editor && get_editor(source, file, line, editor)
|
|
}, index + 1}
|
|
end
|
|
|
|
# From :elixir_compiler_*
|
|
defp get_entry({module, :__MODULE__, 0, location}) do
|
|
{module, inspect(module) <> " (module)", location, get_app(module), nil, nil, nil}
|
|
end
|
|
|
|
# From :elixir_compiler_*
|
|
defp get_entry({_module, :__MODULE__, 1, location}) do
|
|
{nil, "(module)", location, nil, nil, nil, nil}
|
|
end
|
|
|
|
# From :elixir_compiler_*
|
|
defp get_entry({_module, :__FILE__, 1, location}) do
|
|
{nil, "(file)", location, nil, nil, nil, nil}
|
|
end
|
|
|
|
defp get_entry({module, fun, args, location}) when is_list(args) do
|
|
arity = length(args)
|
|
formatted_mfa = Exception.format_mfa(module, fun, arity)
|
|
{module, formatted_mfa, location, get_app(module), fun, arity, args}
|
|
end
|
|
|
|
defp get_entry({module, fun, arity, location}) do
|
|
{module, Exception.format_mfa(module, fun, arity), location, get_app(module), fun, arity, nil}
|
|
end
|
|
|
|
defp get_entry({fun, arity, location}) do
|
|
{nil, Exception.format_fa(fun, arity), location, nil, fun, arity, nil}
|
|
end
|
|
|
|
defp get_app(module) do
|
|
case :application.get_application(module) do
|
|
{:ok, app} -> app
|
|
:undefined -> nil
|
|
end
|
|
end
|
|
|
|
defp get_doc(module, fun, arity, app) do
|
|
with true <- has_docs?(module, fun, arity),
|
|
{:ok, vsn} <- :application.get_key(app, :vsn) do
|
|
vsn = vsn |> List.to_string() |> String.split("-") |> hd()
|
|
fun = fun |> Atom.to_string() |> URI.encode()
|
|
"https://hexdocs.pm/#{app}/#{vsn}/#{inspect(module)}.html##{fun}/#{arity}"
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp has_docs?(module, name, arity) do
|
|
case Code.fetch_docs(module) do
|
|
{:docs_v1, _, _, _, module_doc, _, docs} when module_doc != :hidden ->
|
|
Enum.any?(docs, has_doc_matcher?(name, arity))
|
|
|
|
_ ->
|
|
false
|
|
end
|
|
end
|
|
|
|
defp has_doc_matcher?(name, arity) do
|
|
&match?(
|
|
{{kind, ^name, ^arity}, _, _, doc, _}
|
|
when kind in [:function, :macro] and doc != :hidden and doc != :none,
|
|
&1
|
|
)
|
|
end
|
|
|
|
defp get_clauses(renderer, module, fun, args) do
|
|
with true <- is_list(args),
|
|
{:ok, kind, clauses} <- Exception.blame_mfa(module, fun, args) do
|
|
top_10 =
|
|
clauses
|
|
|> Enum.take(10)
|
|
|> Enum.map(fn {args, guards} ->
|
|
args = Enum.map_join(args, ", ", &blame_match(renderer, &1))
|
|
base = "#{kind} #{fun}(#{args})"
|
|
Enum.reduce(guards, base, &"#{&2} when #{blame_clause(renderer, &1)}")
|
|
end)
|
|
|
|
{length(top_10), length(clauses), top_10}
|
|
else
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp blame_match(:html, %{match?: true, node: node}),
|
|
do: ~s(<i class="green">) <> h(Macro.to_string(node)) <> "</i>"
|
|
|
|
defp blame_match(:html, %{match?: false, node: node}),
|
|
do: ~s(<i class="red">) <> h(Macro.to_string(node)) <> "</i>"
|
|
|
|
defp blame_match(_md, %{node: node}),
|
|
do: h(Macro.to_string(node))
|
|
|
|
defp blame_clause(renderer, {op, _, [left, right]}),
|
|
do: blame_clause(renderer, left) <> " #{op} " <> blame_clause(renderer, right)
|
|
|
|
defp blame_clause(renderer, node), do: blame_match(renderer, node)
|
|
|
|
defp get_context(app, app) when app != nil, do: :app
|
|
defp get_context(_app1, _app2), do: :all
|
|
|
|
defp get_source(app, module, file) do
|
|
cond do
|
|
File.regular?(file) ->
|
|
file
|
|
|
|
File.regular?("apps/#{app}/#{file}") ->
|
|
"apps/#{app}/#{file}"
|
|
|
|
source = module && Code.ensure_loaded?(module) && module.module_info(:compile)[:source] ->
|
|
to_string(source)
|
|
|
|
true ->
|
|
file
|
|
end
|
|
end
|
|
|
|
defp get_editor(source, file, line, editor) do
|
|
editor
|
|
|> :binary.replace("__FILE__", URI.encode(Path.expand(source)))
|
|
|> :binary.replace("__RELATIVEFILE__", URI.encode(file))
|
|
|> :binary.replace("__LINE__", to_string(line))
|
|
|> h
|
|
end
|
|
|
|
@radius 5
|
|
|
|
defp get_snippet(file, line) do
|
|
if File.regular?(file) and is_integer(line) do
|
|
to_discard = max(line - @radius - 1, 0)
|
|
lines = File.stream!(file) |> Stream.take(line + 5) |> Stream.drop(to_discard)
|
|
|
|
{first_five, lines} = Enum.split(lines, line - to_discard - 1)
|
|
first_five = with_line_number(first_five, to_discard + 1, false)
|
|
|
|
{center, last_five} = Enum.split(lines, 1)
|
|
center = with_line_number(center, line, true)
|
|
last_five = with_line_number(last_five, line + 1, false)
|
|
|
|
first_five ++ center ++ last_five
|
|
end
|
|
end
|
|
|
|
defp with_line_number(lines, initial, highlight) do
|
|
lines
|
|
|> Enum.map_reduce(initial, fn line, acc -> {{acc, line, highlight}, acc + 1} end)
|
|
|> elem(0)
|
|
end
|
|
|
|
defp banner(conn, status, kind, reason, stack, opts) do
|
|
case Keyword.fetch(opts, :banner) do
|
|
{:ok, {mod, func, args}} ->
|
|
apply(mod, func, [conn, status, kind, reason, stack] ++ args)
|
|
|
|
{:ok, other} ->
|
|
raise ArgumentError,
|
|
"expected :banner to be an MFA ({module, func, args}), got: #{inspect(other)}"
|
|
|
|
:error ->
|
|
nil
|
|
end
|
|
end
|
|
|
|
## Helpers
|
|
|
|
defp method(%Plug.Conn{method: method}), do: method
|
|
|
|
defp url(%Plug.Conn{scheme: scheme, host: host, port: port} = conn),
|
|
do: "#{scheme}://#{host}:#{port}#{conn.request_path}"
|
|
|
|
defp h(string) do
|
|
string |> to_string() |> Plug.HTML.html_escape()
|
|
end
|
|
|
|
defp maybe_merge_dark_styles(style, default_dark_style) when style != @default_style do
|
|
if Map.has_key?(style, :dark) do
|
|
# only merge default dark if the user also passed the dark key
|
|
Map.update!(style, :dark, fn existing ->
|
|
Map.merge(default_dark_style, Map.new(existing))
|
|
end)
|
|
else
|
|
style
|
|
end
|
|
end
|
|
|
|
defp maybe_merge_dark_styles(style, default_dark_style) do
|
|
Map.put(style, :dark, default_dark_style)
|
|
end
|
|
|
|
defp maybe_autolink(message) do
|
|
splitted =
|
|
Regex.split(~r/`[A-Z][A-Za-z0-9_.]+\.[a-z][A-Za-z0-9_!?]*\/\d+`/, message,
|
|
include_captures: true,
|
|
trim: true
|
|
)
|
|
|
|
Enum.map(splitted, &maybe_format_function_reference/1)
|
|
|> IO.iodata_to_binary()
|
|
end
|
|
|
|
defp maybe_format_function_reference("`" <> reference = text) do
|
|
reference = String.trim_trailing(reference, "`")
|
|
|
|
with {:ok, m, f, a} <- get_mfa(reference),
|
|
url when is_binary(url) <- get_doc(m, f, a, Application.get_application(m)) do
|
|
~s[<a href="#{url}" target="_blank">`#{h(reference)}`</a>]
|
|
else
|
|
_ -> h(text)
|
|
end
|
|
end
|
|
|
|
defp maybe_format_function_reference(text), do: h(text)
|
|
|
|
def get_mfa(capture) do
|
|
[function_path, arity] = String.split(capture, "/")
|
|
{arity, ""} = Integer.parse(arity)
|
|
parts = String.split(function_path, ".")
|
|
{function_str, parts} = List.pop_at(parts, -1)
|
|
module = Module.safe_concat(parts)
|
|
function = String.to_existing_atom(function_str)
|
|
{:ok, module, function, arity}
|
|
rescue
|
|
_ -> :error
|
|
end
|
|
end
|