debugger.ex (28319B)
1 defmodule Plug.Debugger do 2 @moduledoc """ 3 A module (**not a plug**) for debugging in development. 4 5 This module is commonly used within a `Plug.Builder` or a `Plug.Router` 6 and it wraps the `call/2` function. 7 8 Notice `Plug.Debugger` *does not* catch errors, as errors should still 9 propagate so that the Elixir process finishes with the proper reason. 10 This module does not perform any logging either, as all logging is done 11 by the web server handler. 12 13 **Note:** If this module is used with `Plug.ErrorHandler`, only one of 14 them will effectively handle errors. For this reason, it is recommended 15 that `Plug.Debugger` is used before `Plug.ErrorHandler` and only in 16 particular environments, like `:dev`. 17 18 In case of an error, the rendered page drops the `content-security-policy` 19 header before rendering the error to ensure that the error is displayed 20 correctly. 21 22 ## Examples 23 24 defmodule MyApp do 25 use Plug.Builder 26 27 if Mix.env == :dev do 28 use Plug.Debugger, otp_app: :my_app 29 end 30 31 plug :boom 32 33 def boom(conn, _) do 34 # Error raised here will be caught and displayed in a debug page 35 # complete with a stacktrace and other helpful info. 36 raise "oops" 37 end 38 end 39 40 ## Options 41 42 * `:otp_app` - the OTP application that is using Plug. This option is used 43 to filter stacktraces that belong only to the given application. 44 * `:style` - custom styles (see below) 45 * `:banner` - the optional MFA (`{module, function, args}`) which receives 46 exception details and returns banner contents to appear at the top of 47 the page. May be any string, including markup. 48 49 ## Custom styles 50 51 You may pass a `:style` option to customize the look of the HTML page. 52 53 use Plug.Debugger, style: 54 [primary: "#c0392b", logo: "data:image/png;base64,..."] 55 56 The following keys are available: 57 58 * `:primary` - primary color 59 * `:accent` - accent color 60 * `:logo` - logo URI, or `nil` to disable 61 62 The `:logo` is preferred to be a base64-encoded data URI so not to make any 63 external requests, though external URLs (eg, `https://...`) are supported. 64 65 ## Custom Banners 66 67 You may pass an MFA (`{module, function, args}`) to be invoked when an 68 error is rendered which provides a custom banner at the top of the 69 debugger page. The function receives the following arguments, with the 70 passed `args` concatenated at the end: 71 72 [conn, status, kind, reason, stacktrace] 73 74 For example, the following `:banner` option: 75 76 use Plug.Debugger, banner: {MyModule, :debug_banner, []} 77 78 would invoke the function: 79 80 MyModule.debug_banner(conn, status, kind, reason, stacktrace) 81 82 ## Links to the text editor 83 84 If a `PLUG_EDITOR` environment variable is set, `Plug.Debugger` will 85 use it to generate links to your text editor. The variable should be 86 set with `__FILE__` and `__LINE__` placeholders which will be correctly 87 replaced. For example (with the [TextMate](http://macromates.com) editor): 88 89 txmt://open/?url=file://__FILE__&line=__LINE__ 90 91 Or, using Visual Studio Code: 92 93 vscode://file/__FILE__:__LINE__ 94 """ 95 96 @already_sent {:plug_conn, :sent} 97 98 @logo "data:image/png;base64,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" 99 100 @default_style %{ 101 primary: "#4e2a8e", 102 accent: "#607080", 103 highlight: "#f0f4fa", 104 red_highlight: "#ffe5e5", 105 line_color: "#eee", 106 text_color: "#203040", 107 logo: @logo, 108 monospace_font: "menlo, consolas, monospace" 109 } 110 111 @salt "plug-debugger-actions" 112 113 import Plug.Conn 114 require Logger 115 116 @doc false 117 defmacro __using__(opts) do 118 quote do 119 @plug_debugger unquote(opts) 120 @before_compile Plug.Debugger 121 end 122 end 123 124 @doc false 125 defmacro __before_compile__(_) do 126 quote location: :keep do 127 defoverridable call: 2 128 129 def call(conn, opts) do 130 try do 131 case conn do 132 %Plug.Conn{path_info: ["__plug__", "debugger", "action"], method: "POST"} -> 133 Plug.Debugger.run_action(conn) 134 135 %Plug.Conn{} -> 136 super(conn, opts) 137 end 138 rescue 139 e in Plug.Conn.WrapperError -> 140 %{conn: conn, kind: kind, reason: reason, stack: stack} = e 141 Plug.Debugger.__catch__(conn, kind, reason, stack, @plug_debugger) 142 catch 143 kind, reason -> 144 Plug.Debugger.__catch__(conn, kind, reason, __STACKTRACE__, @plug_debugger) 145 end 146 end 147 end 148 end 149 150 @doc false 151 def __catch__(conn, kind, reason, stack, opts) do 152 reason = Exception.normalize(kind, reason, stack) 153 status = status(kind, reason) 154 155 receive do 156 @already_sent -> 157 send(self(), @already_sent) 158 log(status, kind, reason, stack) 159 :erlang.raise(kind, reason, stack) 160 after 161 0 -> 162 render(conn, status, kind, reason, stack, opts) 163 log(status, kind, reason, stack) 164 :erlang.raise(kind, reason, stack) 165 end 166 end 167 168 # We don't log status >= 500 because those are treated as errors and logged later. 169 defp log(status, kind, reason, stack) when status < 500, 170 do: Logger.debug(Exception.format(kind, reason, stack)) 171 172 defp log(_status, _kind, _reason, _stack), do: :ok 173 174 ## Rendering 175 176 require EEx 177 178 html_template_path = "lib/plug/templates/debugger.html.eex" 179 EEx.function_from_file(:defp, :template_html, html_template_path, [:assigns]) 180 181 markdown_template_path = "lib/plug/templates/debugger.md.eex" 182 EEx.function_from_file(:defp, :template_markdown, markdown_template_path, [:assigns]) 183 184 # Made public with @doc false for testing. 185 @doc false 186 def render(conn, status, kind, reason, stack, opts) do 187 session = maybe_fetch_session(conn) 188 params = maybe_fetch_query_params(conn) 189 {title, message} = info(kind, reason) 190 191 assigns = [ 192 conn: conn, 193 title: title, 194 formatted: Exception.format(kind, reason, stack), 195 session: session, 196 params: params, 197 frames: frames(:md, stack, opts) 198 ] 199 200 markdown = template_markdown(assigns) 201 202 if accepts_html?(get_req_header(conn, "accept")) do 203 conn = 204 conn 205 |> put_resp_content_type("text/html") 206 |> delete_resp_header("content-security-policy") 207 208 actions = encoded_actions_for_exception(reason, conn) 209 last_path = actions_redirect_path(conn) 210 style = Enum.into(opts[:style] || [], @default_style) 211 banner = banner(conn, status, kind, reason, stack, opts) 212 213 assigns = 214 Keyword.merge(assigns, 215 conn: conn, 216 message: message, 217 markdown: markdown, 218 style: style, 219 banner: banner, 220 actions: actions, 221 frames: frames(:html, stack, opts), 222 last_path: last_path 223 ) 224 225 send_resp(conn, status, template_html(assigns)) 226 else 227 conn = put_resp_content_type(conn, "text/markdown") 228 send_resp(conn, status, markdown) 229 end 230 end 231 232 @doc false 233 def run_action(%Plug.Conn{} = conn) do 234 with %Plug.Conn{body_params: params} <- fetch_body_params(conn), 235 {:ok, {module, function, args}} <- 236 Plug.Crypto.verify(conn.secret_key_base, @salt, params["encoded_handler"]) do 237 apply(module, function, args) 238 239 conn 240 |> Plug.Conn.put_resp_header("location", params["last_path"] || "/") 241 |> send_resp(302, "") 242 |> halt() 243 else 244 _ -> raise "could not run Plug.Debugger action" 245 end 246 end 247 248 @doc false 249 def encoded_actions_for_exception(exception, conn) do 250 exception_implementation = Plug.Exception.impl_for(exception) 251 252 implements_actions? = 253 Code.ensure_loaded?(exception_implementation) && 254 function_exported?(exception_implementation, :actions, 1) 255 256 # TODO: Remove implements_actions? in future Plug versions 257 if implements_actions? && conn.secret_key_base do 258 actions = Plug.Exception.actions(exception) 259 260 Enum.map(actions, fn %{label: label, handler: handler} -> 261 encoded_handler = Plug.Crypto.sign(conn.secret_key_base, @salt, handler) 262 %{label: label, encoded_handler: encoded_handler} 263 end) 264 else 265 [] 266 end 267 end 268 269 defp actions_redirect_path(%Plug.Conn{ 270 method: "GET", 271 request_path: request_path, 272 query_string: query_string 273 }) do 274 case query_string do 275 "" -> request_path 276 query_string -> "#{request_path}?#{query_string}" 277 end 278 end 279 280 defp actions_redirect_path(conn) do 281 case get_req_header(conn, "referer") do 282 [referer] -> referer 283 [] -> "/" 284 end 285 end 286 287 defp accepts_html?(_accept_header = []), do: false 288 289 defp accepts_html?(_accept_header = [header | _]), 290 do: String.contains?(header, ["*/*", "text/*", "text/html"]) 291 292 defp maybe_fetch_session(conn) do 293 if conn.private[:plug_session_fetch] do 294 conn |> fetch_session(conn) |> get_session() 295 end 296 end 297 298 defp maybe_fetch_query_params(conn) do 299 fetch_query_params(conn).params 300 rescue 301 Plug.Conn.InvalidQueryError -> 302 case conn.params do 303 %Plug.Conn.Unfetched{} -> %{} 304 params -> params 305 end 306 end 307 308 @parsers_opts Plug.Parsers.init(parsers: [:urlencoded]) 309 defp fetch_body_params(conn), do: Plug.Parsers.call(conn, @parsers_opts) 310 311 defp status(:error, error), do: Plug.Exception.status(error) 312 defp status(_, _), do: 500 313 314 defp info(:error, error), do: {inspect(error.__struct__), Exception.message(error)} 315 defp info(:throw, thrown), do: {"unhandled throw", inspect(thrown)} 316 defp info(:exit, reason), do: {"unhandled exit", Exception.format_exit(reason)} 317 318 defp frames(renderer, stacktrace, opts) do 319 app = opts[:otp_app] 320 editor = System.get_env("PLUG_EDITOR") 321 322 stacktrace 323 |> Enum.map_reduce(0, &each_frame(&1, &2, renderer, app, editor)) 324 |> elem(0) 325 end 326 327 defp each_frame(entry, index, renderer, root, editor) do 328 {module, info, location, app, fun, arity, args} = get_entry(entry) 329 {file, line} = {to_string(location[:file] || "nofile"), location[:line]} 330 331 doc = module && get_doc(module, fun, arity, app) 332 clauses = module && get_clauses(renderer, module, fun, args) 333 source = get_source(app, module, file) 334 context = get_context(root, app) 335 snippet = get_snippet(source, line) 336 337 {%{ 338 app: app, 339 info: info, 340 file: file, 341 line: line, 342 context: context, 343 snippet: snippet, 344 index: index, 345 doc: doc, 346 clauses: clauses, 347 args: args, 348 link: editor && get_editor(source, line, editor) 349 }, index + 1} 350 end 351 352 # From :elixir_compiler_* 353 defp get_entry({module, :__MODULE__, 0, location}) do 354 {module, inspect(module) <> " (module)", location, get_app(module), nil, nil, nil} 355 end 356 357 # From :elixir_compiler_* 358 defp get_entry({_module, :__MODULE__, 1, location}) do 359 {nil, "(module)", location, nil, nil, nil, nil} 360 end 361 362 # From :elixir_compiler_* 363 defp get_entry({_module, :__FILE__, 1, location}) do 364 {nil, "(file)", location, nil, nil, nil, nil} 365 end 366 367 defp get_entry({module, fun, args, location}) when is_list(args) do 368 arity = length(args) 369 formatted_mfa = Exception.format_mfa(module, fun, arity) 370 {module, formatted_mfa, location, get_app(module), fun, arity, args} 371 end 372 373 defp get_entry({module, fun, arity, location}) do 374 {module, Exception.format_mfa(module, fun, arity), location, get_app(module), fun, arity, nil} 375 end 376 377 defp get_entry({fun, arity, location}) do 378 {nil, Exception.format_fa(fun, arity), location, nil, fun, arity, nil} 379 end 380 381 defp get_app(module) do 382 case :application.get_application(module) do 383 {:ok, app} -> app 384 :undefined -> nil 385 end 386 end 387 388 defp get_doc(module, fun, arity, app) do 389 with true <- has_docs?(module, fun, arity), 390 {:ok, vsn} <- :application.get_key(app, :vsn) do 391 vsn = vsn |> List.to_string() |> String.split("-") |> hd() 392 fun = fun |> Atom.to_string() |> URI.encode() 393 "https://hexdocs.pm/#{app}/#{vsn}/#{inspect(module)}.html##{fun}/#{arity}" 394 else 395 _ -> nil 396 end 397 end 398 399 defp has_docs?(module, name, arity) do 400 case Code.fetch_docs(module) do 401 {:docs_v1, _, _, _, module_doc, _, docs} when module_doc != :hidden -> 402 Enum.any?(docs, has_doc_matcher?(name, arity)) 403 404 _ -> 405 false 406 end 407 end 408 409 defp has_doc_matcher?(name, arity) do 410 &match?( 411 {{kind, ^name, ^arity}, _, _, doc, _} 412 when kind in [:function, :macro] and doc != :hidden and doc != :none, 413 &1 414 ) 415 end 416 417 defp get_clauses(renderer, module, fun, args) do 418 with true <- is_list(args), 419 {:ok, kind, clauses} <- Exception.blame_mfa(module, fun, args) do 420 top_10 = 421 clauses 422 |> Enum.take(10) 423 |> Enum.map(fn {args, guards} -> 424 args = Enum.map_join(args, ", ", &blame_match(renderer, &1)) 425 base = "#{kind} #{fun}(#{args})" 426 Enum.reduce(guards, base, &"#{&2} when #{blame_clause(renderer, &1)}") 427 end) 428 429 {length(top_10), length(clauses), top_10} 430 else 431 _ -> nil 432 end 433 end 434 435 defp blame_match(:html, %{match?: true, node: node}), 436 do: ~s(<i class="green">) <> h(Macro.to_string(node)) <> "</i>" 437 438 defp blame_match(:html, %{match?: false, node: node}), 439 do: ~s(<i class="red">) <> h(Macro.to_string(node)) <> "</i>" 440 441 defp blame_match(_md, %{node: node}), 442 do: h(Macro.to_string(node)) 443 444 defp blame_clause(renderer, {op, _, [left, right]}), 445 do: blame_clause(renderer, left) <> " #{op} " <> blame_clause(renderer, right) 446 447 defp blame_clause(renderer, node), do: blame_match(renderer, node) 448 449 defp get_context(app, app) when app != nil, do: :app 450 defp get_context(_app1, _app2), do: :all 451 452 defp get_source(app, module, file) do 453 cond do 454 File.regular?(file) -> 455 file 456 457 File.regular?("apps/#{app}/#{file}") -> 458 "apps/#{app}/#{file}" 459 460 source = module && Code.ensure_loaded?(module) && module.module_info(:compile)[:source] -> 461 to_string(source) 462 463 true -> 464 file 465 end 466 end 467 468 defp get_editor(file, line, editor) do 469 editor 470 |> :binary.replace("__FILE__", URI.encode(Path.expand(file))) 471 |> :binary.replace("__LINE__", to_string(line)) 472 |> h 473 end 474 475 @radius 5 476 477 defp get_snippet(file, line) do 478 if File.regular?(file) and is_integer(line) do 479 to_discard = max(line - @radius - 1, 0) 480 lines = File.stream!(file) |> Stream.take(line + 5) |> Stream.drop(to_discard) 481 482 {first_five, lines} = Enum.split(lines, line - to_discard - 1) 483 first_five = with_line_number(first_five, to_discard + 1, false) 484 485 {center, last_five} = Enum.split(lines, 1) 486 center = with_line_number(center, line, true) 487 last_five = with_line_number(last_five, line + 1, false) 488 489 first_five ++ center ++ last_five 490 end 491 end 492 493 defp with_line_number(lines, initial, highlight) do 494 lines 495 |> Enum.map_reduce(initial, fn line, acc -> {{acc, line, highlight}, acc + 1} end) 496 |> elem(0) 497 end 498 499 defp banner(conn, status, kind, reason, stack, opts) do 500 case Keyword.fetch(opts, :banner) do 501 {:ok, {mod, func, args}} -> 502 apply(mod, func, [conn, status, kind, reason, stack] ++ args) 503 504 {:ok, other} -> 505 raise ArgumentError, 506 "expected :banner to be an MFA ({module, func, args}), got: #{inspect(other)}" 507 508 :error -> 509 nil 510 end 511 end 512 513 ## Helpers 514 515 defp method(%Plug.Conn{method: method}), do: method 516 517 defp url(%Plug.Conn{scheme: scheme, host: host, port: port} = conn), 518 do: "#{scheme}://#{host}:#{port}#{conn.request_path}" 519 520 defp h(string) do 521 string |> to_string() |> Plug.HTML.html_escape() 522 end 523 end