defmodule Rexbug.Printing do
@moduledoc """
Provides the print handler and helper functions for writing custom
print handlers for `Rexbug`. You shouldn't need to use it directly
unless you want to implement a print handler yourself.
See `Rexbug.start/2` `print_fun` option for details.
"""
alias Rexbug.Printing
import Rexbug.Printing.Utils
# ===========================================================================
# Helper Structs
# ===========================================================================
defmodule MFA do
@type t :: %__MODULE__{}
defstruct [
:m,
:f,
# either args or arity
:a
]
def from_erl({m, f, a}) do
%__MODULE__{m: m, f: f, a: a}
end
def from_erl(a) when is_atom(a) or is_list(a) do
a
end
def represent(a, _opts) when is_atom(a) or is_list(a) do
"(#{inspect(a)})"
end
def represent(%__MODULE__{m: m, f: f, a: a}, _opts) do
mrep =
case Atom.to_string(m) do
"Elixir." <> rest -> rest
erlang_module -> ":#{erlang_module}"
end
arep =
if is_list(a) do
middle =
a
|> Enum.map(&printing_inspect/1)
|> Enum.join(", ")
"(#{middle})"
else
"/#{a}"
end
"#{mrep}.#{f}#{arep}"
end
end
defmodule Timestamp do
@type t :: %__MODULE__{}
defstruct ~w(hours minutes seconds us)a
def from_erl({h, m, s, us}) do
%__MODULE__{hours: h, minutes: m, seconds: s, us: us}
end
def represent(%__MODULE__{hours: h, minutes: m, seconds: s, us: us}, opts) do
if Keyword.get(opts, :print_msec, false) do
"#{format_int(h)}:#{format_int(m)}:#{format_int(s)}.#{format_int(div(us, 1000), 3)}"
else
"#{format_int(h)}:#{format_int(m)}:#{format_int(s)}"
end
end
defp format_int(i, length \\ 2) do
i
|> Integer.to_string()
|> String.pad_leading(length, "0")
end
end
# ---------------------------------------------------------------------------
# Received message types
# ---------------------------------------------------------------------------
defmodule Call do
@type t :: %__MODULE__{}
defstruct ~w(mfa dump from_pid from_mfa time)a
def represent(%__MODULE__{} = struct, opts) do
ts = Timestamp.represent(struct.time, opts)
pid = printing_inspect(struct.from_pid)
from_mfa = MFA.represent(struct.from_mfa, opts)
mfa = MFA.represent(struct.mfa, opts)
maybe_stack = represent_stack(struct.dump, opts)
"# #{ts} #{pid} #{from_mfa}\n# #{mfa}#{maybe_stack}"
end
defp represent_stack(nil, _opts), do: ""
defp represent_stack("", _opts), do: ""
defp represent_stack(dump, _opts) do
dump
|> Printing.extract_stack()
|> Enum.map(fn fun_rep -> "\n# #{fun_rep}" end)
|> Enum.join("")
end
end
defmodule Return do
@type t :: %__MODULE__{}
defstruct ~w(mfa return_value from_pid from_mfa time)a
def represent(%__MODULE__{} = struct, opts) do
ts = Timestamp.represent(struct.time, opts)
pid = printing_inspect(struct.from_pid)
from_mfa = MFA.represent(struct.from_mfa, opts)
mfa = MFA.represent(struct.mfa, opts)
retn = printing_inspect(struct.return_value)
"# #{ts} #{pid} #{from_mfa}\n# #{mfa} -> #{retn}"
end
end
defmodule Send do
@type t :: %__MODULE__{}
defstruct ~w(msg to_pid to_mfa from_pid from_mfa time)a
def represent(%__MODULE__{} = struct, opts) do
ts = Timestamp.represent(struct.time, opts)
to_pid = printing_inspect(struct.to_pid)
to_mfa = MFA.represent(struct.to_mfa, opts)
from_pid = printing_inspect(struct.from_pid)
from_mfa = MFA.represent(struct.from_mfa, opts)
msg = printing_inspect(struct.msg)
"# #{ts} #{from_pid} #{from_mfa}\n# #{to_pid} #{to_mfa} <<< #{msg}"
end
end
defmodule Receive do
@type t :: %__MODULE__{}
defstruct ~w(msg to_pid to_mfa time)a
def represent(%__MODULE__{} = struct, opts) do
ts = Timestamp.represent(struct.time, opts)
to_pid = printing_inspect(struct.to_pid)
to_mfa = MFA.represent(struct.to_mfa, opts)
msg = printing_inspect(struct.msg)
"# #{ts} #{to_pid} #{to_mfa}\n# <<< #{msg}"
end
end
# ===========================================================================
# Public Functions
# ===========================================================================
@doc """
The default value for the `Rexbug.start/2` `print_fun` option. Prints out
the tracing messages generated by `:redbug` in a nice Elixir format.
"""
@spec print(tuple()) :: :ok
def print(message) do
# this is not a 2 arg function just so that no-one passes it as the
# print_fun to :redbug, the second argument is not an accumulator
print_with_opts(message, [])
end
@doc false
@spec print_with_opts(tuple(), Keyword.t()) :: :ok
def print_with_opts(message, opts) do
IO.puts("\n" <> format(message, opts))
end
@doc false
def format(message, opts \\ []) do
message
|> from_erl()
|> represent(opts)
end
@doc """
Translates the `:redbug` tuples representing the tracing messages to
Elixir structs.
You can use it to implement your own custom `print_fun`.
"""
@spec from_erl(tuple()) ::
Printing.Call.t()
| Printing.Return.t()
| Printing.Send.t()
| Printing.Receive.t()
| term()
def from_erl({:call, {mfa, dump}, from_pid_mfa, time}) do
{from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)
%Call{
mfa: MFA.from_erl(mfa),
dump: dump,
from_pid: from_pid,
from_mfa: MFA.from_erl(from_mfa),
time: Timestamp.from_erl(time)
}
end
def from_erl({:retn, {mfa, retn}, from_pid_mfa, time}) do
{from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)
%Return{
mfa: MFA.from_erl(mfa),
return_value: retn,
from_pid: from_pid,
from_mfa: MFA.from_erl(from_mfa),
time: Timestamp.from_erl(time)
}
end
def from_erl({:send, {msg, to_pid_mfa}, from_pid_mfa, time}) do
{to_pid, to_mfa} = get_pid_mfa(to_pid_mfa)
{from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)
%Send{
msg: msg,
to_pid: to_pid,
to_mfa: MFA.from_erl(to_mfa),
from_pid: from_pid,
from_mfa: MFA.from_erl(from_mfa),
time: Timestamp.from_erl(time)
}
end
def from_erl({:recv, msg, to_pid_mfa, time}) do
{to_pid, to_mfa} = get_pid_mfa(to_pid_mfa)
%Receive{
msg: msg,
to_pid: to_pid,
to_mfa: MFA.from_erl(to_mfa),
time: Timestamp.from_erl(time)
}
end
# fallthrough so that you can use it indiscriminately
def from_erl(message) do
message
end
defp get_pid_mfa({pid, mfa}) do
{pid, mfa}
end
defp get_pid_mfa(other) when is_atom(other) or is_list(other) do
{nil, other}
end
@doc false
def represent(%mod{} = struct, opts) when mod in [Call, Return, Send, Receive] do
mod.represent(struct, opts)
end
@doc """
Extracts the call stack from `t:Rexbug.Printing.Call.t/0` `dump` field.
**NOTE**: The `dump` field will contain call stack information only
if you specify `" :: stack"` in `Rexbug.start/2`'s trace pattern,
it will be nil or empty otherwise.
"""
@spec extract_stack(String.t()) :: [String.t()]
def extract_stack(dump) do
String.split(dump, "\n")
|> Enum.filter(&Regex.match?(~r/Return addr 0x|CP: 0x/, &1))
|> Enum.flat_map(&extract_function/1)
end
# ===========================================================================
# Internal Functions
# ===========================================================================
defp extract_function(line) do
case Regex.run(~r"^.+\((.+):(.+)/(\d+).+\)$", line, capture: :all_but_first) do
[m, f, arity] ->
m = translate_module_from_dump(m)
f = strip_single_quotes(f)
["#{m}.#{f}/#{arity}"]
nil ->
[]
end
end
defp strip_single_quotes(str) do
String.trim(str, "'")
end
defp translate_module_from_dump(module) do
case strip_single_quotes(module) do
"Elixir." <> rest ->
rest
erlang_module ->
":#{erlang_module}"
end
end
end