Current section
Files
Jump to
Current section
Files
lib/tracer/handler_agent.ex
defmodule Tracer.HandlerAgent do
@moduledoc """
HandlerAgent takes care of starting and stopping traces in the
NUT (node under test), as well as watching over the event handler
as it processes events.
"""
alias __MODULE__
alias Tracer.PidHandler
import Tracer.Macros
@default_max_tracing_time 30_000
defstruct node: nil,
handler_pid: nil,
timer_ref: nil,
max_tracing_time: @default_max_tracing_time,
pid_handler_opts: [],
start_trace_cmds: [],
stop_trace_cmds: []
def start(opts \\ []) do
initial_state = process_opts(%HandlerAgent{}, opts)
pid = spawn_in_target(initial_state)
send pid, :start
pid
end
def stop(pid) do
send pid, :stop
end
defp process_opts(state, opts) do
state
|> Map.put(:node, Keyword.get(opts, :node, nil))
|> Map.put(:start_trace_cmds, Keyword.get(opts, :start_trace_cmds, []))
|> Map.put(:stop_trace_cmds, Keyword.get(opts, :stop_trace_cmds, []))
|> assign_to(state)
state = if Keyword.get(opts, :max_tracing_time) != nil do
put_in(state.max_tracing_time, Keyword.get(opts, :max_tracing_time))
else
state
end
state = if Keyword.get(opts, :max_message_count) != nil do
put_in(state.pid_handler_opts,
[{:max_message_count, Keyword.get(opts, :max_message_count)}
| state.pid_handler_opts])
else
state
end
state = if Keyword.get(opts, :max_queue_size) != nil do
put_in(state.pid_handler_opts,
[{:max_queue_size, Keyword.get(opts, :max_queue_size)}
| state.pid_handler_opts])
else
state
end
event_callback =
if Keyword.get(opts, :forward_pid) != nil do
{:event_callback, {&__MODULE__.forwarding_handler_callback/2,
Keyword.get(opts, :forward_pid)}}
else
{:event_callback, &__MODULE__.discard_handler_callback/1}
end
if Keyword.get(opts, :event_callback) != nil do
put_in(state.pid_handler_opts,
[{:event_callback, Keyword.get(opts, :event_callback)}
| state.pid_handler_opts])
else
put_in(state.pid_handler_opts,
[event_callback | state.pid_handler_opts])
end
end
defp spawn_in_target(state) do
if state.node != nil do
[__MODULE__, Tracer.PidHandler] |> Enum.each(fn mod ->
ensure_loaded_remote(state.node, mod)
end)
Node.spawn_link(state.node, fn -> process_loop(state) end)
else
spawn_link(fn -> process_loop(state) end)
end
end
defp process_loop(state) do
receive do
:start ->
Process.flag(:trap_exit, true)
state
|> start_handler()
|> stop_tracing()
|> start_tracing()
|> start_timer()
|> process_loop()
{:timeout, _timeref, _} ->
stop_tracing_and_handler(state)
exit({:done_tracing, :tracing_timeout, state.max_tracing_time})
:stop ->
stop_tracing_and_handler(state)
exit({:done_tracing, :stop_command})
{:EXIT, _, :normal} -> # we should be dead by the time this is sent
exit(:normal)
{:EXIT, _, {:message_queue_size, len}} ->
stop_tracing(state)
exit({:done_tracing, :message_queue_size, len})
{:EXIT, _, {:max_message_count, count}} ->
stop_tracing(state)
exit({:done_tracing, :max_message_count, count})
:restart_timer ->
state
|> cancel_timer()
|> start_timer()
|> process_loop()
# testing helpers
{:get_handler_pid, sender_pid} ->
send sender_pid, {:handler_pid, state.handler_pid}
process_loop(state)
{:get_pid_handler_opts, sender_pid} ->
send sender_pid, {:pid_handler_opts, state.pid_handler_opts}
process_loop(state)
_ignore -> process_loop(state)
end
end
defp start_timer(state) do
ref = :erlang.start_timer(state.max_tracing_time, self(), [])
put_in(state.timer_ref, ref)
end
defp cancel_timer(state) do
:erlang.cancel_timer(state.timer_ref, [])
put_in(state.timer_ref, nil)
end
defp stop_tracing_and_handler(state) do
state
|> stop_tracing()
|> stop_handler()
end
defp start_handler(state) do
handler_pid = PidHandler.start(state.pid_handler_opts)
put_in(state.handler_pid, handler_pid)
end
defp stop_handler(state) do
state
|> Map.get(:handler_pid)
|> PidHandler.stop()
put_in(state.handler_pid, nil)
end
def start_tracing(state) do
# TODO store the number of matches, so that it can be send back to admin
# process
trace_fun = &:erlang.trace/3
state.start_trace_cmds
|> Enum.each(fn
[{:fun, ^trace_fun} | args] ->
bare_args = Enum.map(args, fn
# inject tracer option
{:flag_list, flags} -> [{:tracer, state.handler_pid} | flags]
{_other, arg} -> arg
end)
# IO.puts("#{inspect trace_fun} args: #{inspect bare_args}")
_res = apply(trace_fun, bare_args)
# IO.puts("#{inspect trace_fun} args: #{inspect bare_args}" <>
# " = #{inspect res}")
[{:fun, fun} | args] ->
bare_args = Enum.map(args, &(elem(&1, 1)))
_res = apply(fun, bare_args)
# IO.puts("#{inspect fun} args: #{inspect bare_args} = #{inspect res}")
end)
state
end
def stop_tracing(state) do
state.stop_trace_cmds
|> Enum.each(fn [{:fun, fun} | args] ->
bare_args = Enum.map(args, &(elem(&1, 1)))
apply(fun, bare_args)
end)
state
end
# Handler Callbacks
def discard_handler_callback(_event) do
:ok
end
def forwarding_handler_callback(event, pid) do
send pid, event
{:ok, pid}
end
# Remote Loading Helpers
# credit: based on redbug https://github.com/massemanet/redbug
defp ensure_loaded_remote(node, mod) do
case :rpc.call(node, mod, :module_info, [:compile]) do
{:badrpc, {:EXIT, {:undef, _}}} ->
# module was not found
load_remote(node, mod)
ensure_loaded_remote(node, mod)
:ok
{:badrpc , _} -> :ok
info when is_list(info) ->
case {get_ts(info), get_ts(mod.module_info(:compile))} do
{:interpreted, _} -> :ok
{target, host} when target < host -> # old code on target
load_remote(node, mod)
ensure_loaded_remote(node, mod)
_ -> :ok
end
end
end
defp load_remote(node, mod) do
{mod, bin, fun} = :code.get_object_code(mod)
{:module, _mod} = :rpc.call(node, :code, :load_binary, [mod, fun, bin])
end
defp get_ts([]), do: :interpreted
defp get_ts([{:time, time} | _]), do: time
defp get_ts([_ | rest]), do: get_ts(rest)
end