Current section
Files
Jump to
Current section
Files
lib/ghostbuster.ex
defmodule Ghostbuster do
@moduledoc """
Module provides functions to search for ghost processes in runtime.
Ghost processes don't have ancesstors and do nothing,
they always in waiting state and they live until system restart
"""
@initial_call_info_keys [:initial_call, :dictionary, :current_function]
@unlinked_info_keys [:links, :monitors]
@doc """
`get_top_init_calls/0` returns sorted list of initial call functions with number of processes
## Example:
iex> Ghostbuster.get_top_init_calls()
[
{{:application_master, :init, 4}, 5},
{{:application_master, :start_it, 4}, 5},
{{:supervisor, Supervisor.Default, 1}, 4},
{{:gen_event, :init_it, 6}, 3},
{{:erts_dirty_process_signal_handler, :start, 0}, 3}
]
"""
@spec get_top_init_calls() :: list()
def get_top_init_calls do
Process.list()
|> get_top_init_calls()
end
@doc """
`get_top_init_calls/1` returns sorted list of initial call functions with number of processes
for given list of processes
## Example:
iex> Ghostbuster.get_top_init_calls(Process.list())
[
{{:application_master, :init, 4}, 5},
{{:application_master, :start_it, 4}, 5},
{{:supervisor, Supervisor.Default, 1}, 4},
{{:gen_event, :init_it, 6}, 3},
{{:erts_dirty_process_signal_handler, :start, 0}, 3}
]
"""
@spec get_top_init_calls(list(pid)) :: list()
def get_top_init_calls(process_list) do
process_list
|> Enum.map(&get_info(&1, @initial_call_info_keys))
|> Enum.filter(&is_alive?/1)
|> Enum.reduce(%{}, &count_initial_calls/2)
|> Enum.sort(fn {_k1, c1}, {_k2, c2} -> c1 > c2 end)
|> Enum.take(5)
end
@doc """
`get_pids_by_init_call/1` returns list of pids by given {module, function, args} tuple
## Example:
iex> Ghostbuster.get_pids_by_init_call({:application_master, :init, 4})
[:erlang.list_to_pid('<0.45.0>')]
"""
@spec get_pids_by_init_call({atom, atom, integer}) :: list(pid)
def get_pids_by_init_call(function) do
Process.list()
|> get_pids_by_init_call(function)
end
@doc """
`get_pids_by_init_call/2` returns list of pids by given {module, function, args} tuple
for given list of processes
## Example:
iex> Ghostbuster.get_pids_by_init_call(Process.list(), {:application_master, :init, 4})
[:erlang.list_to_pid('<0.45.0>')]
"""
@spec get_pids_by_init_call(list(pid), {atom, atom, integer}) :: list(pid)
def get_pids_by_init_call(process_list, function) do
process_list
|> Enum.map(&get_info(&1, @initial_call_info_keys))
|> Enum.filter(&is_alive?/1)
|> Enum.filter(&initial_call_eq?(&1, function))
|> Enum.map(& &1[:pid])
end
@doc """
`get_unlinked_pids/0` returns list of pids which don't have links or monitors
## Example:
iex> Ghostbuster.get_unlinked_pids()
[:erlang.list_to_pid('<0.45.0>')]
"""
@spec get_unlinked_pids() :: list(pid)
def get_unlinked_pids do
Process.list()
|> get_unlinked_pids()
end
@doc """
`get_unlinked_pids/1` returns list of pids which don't have links or monitors
for given list of processes
## Example:
iex> Ghostbuster.get_unlinked_pids(Process.list())
[:erlang.list_to_pid('<0.45.0>')]
"""
@spec get_unlinked_pids(list(pid)) :: list(pid)
def get_unlinked_pids(process_list) do
process_list
|> Enum.map(&get_info(&1, @unlinked_info_keys))
|> Enum.filter(&is_alive?/1)
|> Enum.filter(&no_links?/1)
|> Enum.map(& &1[:pid])
end
@doc """
`get_init_call/1` returns {module, function, args} tuple for given pid
## Example:
iex> '<0.45.0>' |> :erlang.list_to_pid() |> Ghostbuster.get_init_call()
{:application_master, :init, 4}
"""
@spec get_init_call(pid) :: tuple
def get_init_call(pid) do
pid
|> get_info(@initial_call_info_keys)
|> get_initial_call()
end
defp get_info(pid, keys) do
pid
|> Process.info(keys)
# in case if process died before the info request Proces.info/2 would return nil value
# and this can happen because there is short time gap between calling Process.list/0 and Process.info/2
# so for that reason we don't want to break the whole pipeline with error, instead we can set
# default key "died" and filter those processes later
|> Kernel.||(died: true)
|> put_in([:pid], pid)
end
defp is_alive?(pid: _, died: true), do: false
defp is_alive?(_), do: true
defp count_initial_calls(info, acc) do
ic = get_initial_call(info)
Map.update(acc, ic, 1, &(&1 + 1))
end
defp get_initial_call(pid: _, initial_call: ic, dictionary: dict, current_function: fun) do
get_initial_call(ic, dict, fun)
end
defp get_initial_call({:proc_lib, :init_p, _} = call, [], _) do
call
end
defp get_initial_call({:proc_lib, :init_p, _}, dict, _) do
dict[:"$initial_call"]
end
defp get_initial_call({:erlang, :apply, _} = call, _, fun) when fun in [nil, "", []] do
call
end
defp get_initial_call({:erlang, :apply, _}, _, fun) do
fun
end
defp get_initial_call(call, _, _), do: call
defp initial_call_eq?(
[pid: _pid, initial_call: ic, dictionary: dict, current_function: fun],
function
) do
get_initial_call(ic, dict, fun) == function
end
defp no_links?(pid: _, links: [], monitors: []), do: true
defp no_links?(_), do: false
end