src/edbg_sup_trees.erl

%%%-------------------------------------------------------------------
%%% @author Torbjorn Tornkvist <kruskakli@gmail.com>
%%% @copyright (C) 2017, Torbjorn Tornkvist
%%% @doc Explore all supervision trees
%%%
%%% Display all supervisors we have found in our system.
%%%
%%% Each line has a number that can be referenced,
%%% where the `S' mean that the process is a supervisor.
%%%
%%% ```
%%%   (abc@ozzy)1> edbg:suptrees().
%%%   1:S kernel_safe_sup <0.74.0> []
%%%   2:S kernel_sup <0.49.0> [erl_distribution]
%%%  23:S logger_sup <0.75.0> []
%%%  27:S net_sup <0.57.0> []
%%%
%%%  (h)elp e(x)pand [<N>] (s)hrink [<N>]
%%%  (p)rocess info [<N> [<M>]] (b)acktrace [<N> [<M>]]
%%%  (r)efresh (q)uit
%%% '''
%%%
%%% Note the short help text; we will now go through what those commands does.
%%%
%%% First let us expand supervisor 23 to see its children. Note the indentation
%%% and the `G' which mean that the process is a `gen_server' process,
%%% where `W' mean some other worker process.
%%%
%%% ```
%%% suptrees> x 23
%%% 1:S kernel_safe_sup <0.74.0> []
%%% 2:S kernel_sup <0.49.0> [erl_distribution]
%%% 23:S logger_sup <0.75.0> []
%%% 24:G   default <0.79.0> [logger_h_common]
%%% 25:G   logger_proxy <0.77.0> [logger_proxy]
%%% 26:G   logger_handler_watcher <0.76.0> [logger_handler_watcher]
%%% 27:S net_sup <0.57.0> []
%%% '''
%%%
%%% Print the process-info of worker 24 Note the list of linked processes.
%%%
%%% ```
%%%   suptrees> p 24
%%%
%%%   === Process Info: <0.79.0>
%%%   [{registered_name,logger_std_h_default},
%%%   {current_function,{gen_server,loop,7}},
%%%   {initial_call,{proc_lib,init_p,5}},
%%%   {status,waiting},
%%%   {message_queue_len,0},
%%%   {links,[<0.75.0>,<0.80.0>]},
%%%   {dictionary,
%%%        [{'$ancest...snip...
%%% '''
%%%
%%% We can also print the process info of any linked processes.
%%% Let us print the process-info of the second process in the links list.
%%%
%%% ```
%%%   suptrees> p 24 2
%%%
%%%   === Process Info: <0.80.0>
%%%   [{current_function,{logger_std_h,file_ctrl_loop,1}},
%%%   {initial_call,{erlang,apply,2}},
%%%   {status,waiting},
%%%   {message_queue_len,0},
%%%   {links,[<0.79.0>]},
%%%   {diction.....snip...
%%% '''
%%%
%%% We can continue like this...
%%%
%%% ```
%%%   suptrees> p 24 2 1
%%%
%%%   === Process Info: <0.79.0>
%%%   [{registered_name,logger_std_h_default},
%%%   {current_function,{gen_server,loop,7}},
%%%   {initial_call,{proc_lib,init_p,5}},
%%%   {status,waiting},
%%%   {message_queue_len,0},
%%%   {links,[<0.75.0>,<0.80.0>]},
%%%   {dictionary,....snip...
%%% '''
%%%
%%% We can also print the process backtrace in the same way:
%%%
%%% ```
%%%   suptrees> b 24 2
%%%    ...snip...
%%% '''
%%%
%%% We can also setup a monitor for a process:
%%%
%%% ```
%%%   suptrees> m 161 2
%%%
%%%   Monitoring: <0.343.0>
%%%
%%%   ...do stuff, crunch...
%%%
%%%   Monitor got DOWN from: <0.343.0> , Reason: shutdown
%%% '''
%%%
%%% We can print the state of a gen_server.
%%% Let say we have the following:
%%%
%%% ```
%%%   1:S kernel_safe_sup <0.74.0> []
%%%   2:S kernel_sup <0.49.0> [erl_distribution]
%%%   3:S   logger_sup <0.75.0> []
%%%   4:G     default <0.79.0> [logger_h_common]
%%%    ...snip...
%%% '''
%%%
%%% Now print the state of the &lt;0.79.0&gt; gen_server process.
%%%
%%% ```
%%%   suptrees> g 4
%%%
%%%   Process State: <0.79.0>
%%%   #{burst_limit_enable => true,burst_limit_max_count => 500,
%%%   burst_limit_window_time => 1000,burst_msg_count => 0,
%%%    ...snip..
%%% '''
%%%
%%% You can start tracing on a process like this:
%%%
%%% ```
%%%   suptrees> ts 4
%%% '''
%%%
%%% You stop the tracing like this:
%%%
%%% ```
%%%   suptrees> te
%%% '''
%%%
%%% You show the trace output (like with {@link edbg:file/0} ) like this:
%%%
%%% ```
%%% suptrees> tf
%%% '''
%%%
%%% @end
%%% Created : 14 Dec 2022 by Torbjorn Tornkvist <kruskakli@gmail.com>
%%%-------------------------------------------------------------------
-module(edbg_sup_trees).

-export([all_sup_trees/0
        , loop/3
        , start/0
        , supervisors/0
        , supervisors/1
        ]).


-define(SERVER, ?MODULE).

-ifdef(USE_COLORS).
-define(info_msg(Fmt,Args), edbg_color_srv:info_msg(Fmt,Args)).
-define(att_msg(Fmt,Args), edbg_color_srv:att_msg(Fmt,Args)).
-define(warn_msg(Fmt,Args), edbg_color_srv:warn_msg(Fmt,Args)).
-define(err_msg(Fmt,Args), edbg_color_srv:err_msg(Fmt,Args)).
-define(cur_line_msg(Fmt,Args), edbg_color_srv:cur_line_msg(Fmt,Args)).
-define(c_hi(Str), edbg_color_srv:c_hi(Str)).
-define(c_warn(Str), edbg_color_srv:c_warn(Str)).
-define(c_err(Str), edbg_color_srv:c_err(Str)).
-define(help_hi(Str), edbg_color_srv:help_hi(Str)).
-define(edbg_color_srv_init(), edbg_color_srv:init()).
-else.
-define(info_msg(Fmt,Args), io:format(Fmt,Args)).
-define(att_msg(Fmt,Args), io:format(Fmt,Args)).
-define(warn_msg(Fmt,Args), io:format(Fmt,Args)).
-define(err_msg(Fmt,Args), io:format(Fmt,Args)).
-define(cur_line_msg(Fmt,Args), io:format(Fmt,Args)).
-define(c_hi(Str), Str).
-define(c_warn(Str), Str).
-define(c_err(Str), Str).
-define(help_hi(Str), Str).
-define(edbg_color_srv_init(), ok).
-endif.

-record(state,
        {
         sup_trees = [],
         trace_started = false,
         traced_pid
        }).

%% Regarding the 'modules' entry:
%% It is used by the release handler during code replacement to determine
%% which processes are using a certain module. As a rule of thumb,
%% if the child process is a supervisor, gen_server or, gen_statem,
%% this is to be a list with one element [Module], where Module is
%% the callback module. If the child process is an event manager
%% (gen_event) with a dynamic set of callback modules,
%% value dynamic must be used.
-record(sup_tree,
        {n = 0,  % enumeration number
         id,
         pid,
         modules = [],
         children = [],
         expand = false
        }).

-record(worker,
        {n = 0,  % enumeration number
         id,
         pid,
         modules = [],
         is_gen_server = false,
         gen_module
        }).

%% @doc Enter the Supervisor Tree Browser.
start() ->
    Self = self(),
    Prompt = spawn_link(fun() -> prompt(Self) end),
    Prompt ! show,
    ploop(Prompt).

ploop(Prompt) ->
    receive
        {'EXIT', Prompt, _} ->
            true;

        quit ->
            true;

        _ ->
            ?MODULE:ploop(Prompt)
    end.

prompt(Pid) when is_pid(Pid) ->
    process_flag(trap_exit, true),
    State0 = refresh(),
    State = help(show(State0)),
    loop(Pid, prompt(), State).


%% @private
loop(Pid, Prompt, State0) ->
    io:format("~n",[]),
    State =
        case string:tokens(b2l(io:get_line(Prompt)), "\n") of
            ["h"++X]  -> help(X, State0);
            ["x"++X]  -> help(expand(X, State0));
            ["s"++X]  -> shrink_or_show(X, State0);
            ["p"++X]  -> pinfo(X, State0);
            ["g"++X]  -> gen_state(X, State0);
            ["b"++X]  -> backtrace(X, State0);
            ["m"++X]  -> setup_monitor(X, State0);
            ["r"++_]  -> help(show(refresh()));
            ["ts"++X] -> start_trace(X, State0);
            ["te"++_] -> stop_trace(State0);
            ["tf"++_] -> show(show_trace(State0));
            ["q"++_]  -> Pid ! quit, exit(normal);

            _X ->
                ?info_msg("prompt got: ~p~n",[_X]),
                State0
        end,
    ?MODULE:loop(Pid, Prompt, State).


b2l(B) when is_binary(B) ->
     erlang:binary_to_list(B);
b2l(L) when is_list(L) ->
    L.


refresh() ->
    #state{sup_trees = enumerate(all_sup_trees())}.

prompt() ->
    "suptrees> ".


show(State) ->
    show("", State).

show(Chars, State) ->
    display(Chars, State#state.sup_trees, 0),
    State.

display(Chars,
        [#sup_tree{n = N,
                   id = Id,
                   pid = Pid,
                   modules = Modules,
                   expand = false}|Tail],
        Indent) ->
    Pad = indent(Indent),
    io:format("~p:~s ~s~p ~p ~p~n", [N,  ?c_err("S"), Pad, Id, Pid, Modules]),
    display(Chars, Tail, Indent);
display(Chars,
        [#sup_tree{n = N,
                   id = Id,
                   pid = Pid,
                   modules = Modules,
                   children = Children,
                   expand = true}|Tail],
        Indent) ->
    Pad = indent(Indent),
    io:format("~p:~s ~s~p ~p ~p~n", [N,  ?c_err("S"), Pad, Id, Pid, Modules]),
    display(Chars, Children, Indent+2),
    display(Chars, Tail, Indent);
display(Chars, [#worker{n = N,
                        id = Id,
                        pid = Pid,
                        is_gen_server = IsGenSrv,
                        modules = Modules}|Tail], Indent) ->
    Pad = indent(Indent),
    io:format("~p:~s ~s~p ~p ~p~n", [N, ?c_warn(wg(IsGenSrv)), Pad, Id, Pid, Modules]),
    display(Chars, Tail, Indent);
display(_, [], _) ->
    ok.

wg(true = _IsGenSrv) -> "G";
wg(_)                -> "W".


indent(N) ->
    string:copies(" ", N).

help(State) ->
    print_help(),
    State.

help(_, State) ->
    print_help(),
    State.


get_pid(#sup_tree{pid = Pid}) -> Pid;
get_pid(#worker{pid = Pid})   -> Pid;
get_pid(Pid) when is_pid(Pid) -> Pid.


gen_state(Chars, #state{sup_trees = SupTrees} = State) ->
    try
        case parse_ints(Chars) of
            [I] ->
                do(SupTrees, I, fun(X) -> do_pinfo(X, fun p_gen_state/1) end),
                State;
            [I | Ints] ->
                do(SupTrees, I, fun(X) -> do_linfo(Ints, X,  fun p_gen_state/1) end),
                State
        end
    catch
        _:_ ->
            show(State)
    end.

%% Print the State reord of the callback module that
%% the gen_server is holding. Try to pretty-print it
%% if possible.
p_gen_state(Pid) when is_pid(Pid) ->
    try
        State = sys:get_state(Pid, 100),
        UsePpRecord = is_pp_record_available(),
        print_gen_state(Pid, State, UsePpRecord)
    catch
        _:_ ->
            ok
    end;
p_gen_state(_) ->
    ok.

print_gen_state(Pid, State, false = _UsePpRecord) ->
    io:format("~n~s ~p~n~p~n", [?c_warn("Process State:"),Pid, State]);
print_gen_state(Pid, State, true = _UsePpRecord) ->
    try
        {ok, {Mod,_,_}} = get_initial_call(Pid),
        Fname = edbg:find_source(Mod),
        {ok, Defs} = pp_record:read(Fname),
        io:format("~n~s~n", [pp_record:print(State, Defs)])
    catch
        _:_ ->
            print_gen_state(Pid, State, false)
    end.

is_pp_record_available() ->
    case code:ensure_loaded(pp_record) of
        {module, pp_record} ->
            true;
        _ ->
            false
    end.


start_trace(Chars, #state{trace_started = false, sup_trees = SupTrees} = State) ->
    try
        case parse_ints(Chars) of
            [I] ->
                do(SupTrees, I, fun(X) -> do_pinfo(X, fun do_trace/1) end),
                store_traced_pid(State);
            [I | Ints] ->
                do(SupTrees, I, fun(X) -> do_linfo(Ints, X,  fun do_trace/1) end),
                store_traced_pid(State)
        end
    catch
        _:_ ->
            State
    end;
start_trace(_Chars, #state{trace_started = true, traced_pid = Pid} = State) ->
    io:format("~n~s ~s ~p~n",[?c_err("<ERROR>"),"Already tracing on Pid:",Pid]),
    State.

store_traced_pid(State) ->
    case get(traced_pid) of
        Pid when is_pid(Pid) ->
            State#state{trace_started = true, traced_pid = Pid};
        _ ->
            State
    end.

do_trace(Pid) when is_pid(Pid) ->
    edbg:fstart([], [{trace_spec,Pid},
                     dump_output_eager,
                     send_receive,
                     {max_msgs, 1000000}]),
    put(traced_pid, Pid),
    io:format("~n~s ~s ~p~n",[?c_warn("<INFO>"),"Tracing on Pid:",Pid]).


stop_trace(#state{trace_started = true, traced_pid = Pid} = State) ->
    edbg:fstop(),
    io:format("~n~s ~s ~p~n",[?c_warn("<INFO>"),"Stopped tracing on Pid:",Pid]),
    State#state{trace_started = false, traced_pid = undefined};
stop_trace(State) ->
    %% In case suptree was stopped/started and
    %% perhaps even the shells proc.dict was erased;
    Pid = case erase(traced_pid) of
              Pid0 when is_pid(Pid0) -> Pid0;
              _ ->
                  edbg_tracer:get_traced_pid()
          end,
    edbg:fstop(),
    io:format("~n~s ~s ~p~n",[?c_warn("<INFO>"),"Stopped tracing on Pid:",Pid]),
    State#state{trace_started = false, traced_pid = undefined}.

show_trace(State) ->
    edbg:file(),
    State.


setup_monitor(Chars, #state{sup_trees = SupTrees} = State) ->
    try
        case parse_ints(Chars) of
            [I] ->
                do(SupTrees, I, fun(X) -> do_pinfo(X, fun create_monitor/1) end),
                State;
            [I | Ints] ->
                do(SupTrees, I, fun(X) -> do_linfo(Ints, X,  fun create_monitor/1) end),
                State
        end
    catch
        _:_ ->
            show(State)
    end.

create_monitor(Pid) when is_pid(Pid) ->
    F = fun() ->
                ReqId = erlang:monitor(process, Pid),
                io:format("~n~s ~s ~p~n",[?c_warn("<INFO>"),"Monitoring:",Pid]),
                receive
                    {'DOWN', ReqId, process, Pid, ExitReason} ->
                        io:format("~n~s ~s ~p , Reason: ~p~n",
                                  [?c_warn("<INFO>"),"Monitor got DOWN from:",
                                   Pid, ExitReason])
                end
        end,
    erlang:spawn(F);
create_monitor(_) ->
    ok.



pinfo(Chars, #state{sup_trees = SupTrees} = State) ->
    try
        case parse_ints(Chars) of
            [I] ->
                do(SupTrees, I, fun(X) -> do_pinfo(X, fun out_pinfo/1) end),
                State;
            [I | Ints] ->
                do(SupTrees, I, fun(X) -> do_linfo(Ints, X,  fun out_pinfo/1) end),
                State
        end
    catch
        _:_ ->
            show(State)
    end.

backtrace(Chars, #state{sup_trees = SupTrees} = State) ->
    try
        case parse_ints(Chars) of
            [I] ->
                do(SupTrees, I, fun(X) -> do_pinfo(X, fun out_btrace/1) end),
                State;
            [I | Ints] ->
                do(SupTrees, I, fun(X) -> do_linfo(Ints, X,  fun out_btrace/1) end),
                State
        end
    catch
        _:_ ->
            show(State)
    end.




do_pinfo(X, OutFun) ->
    Pid = get_pid(X),
    OutFun(Pid),
    X.

do_linfo([I], X, OutFun) ->
    try
        Pid = get_pid(X),
        {links, Pids} = erlang:process_info(Pid, links),
        LPid = lists:nth(I, Pids),
        OutFun(LPid),
        X
    catch
        _:_ -> X
    end;
do_linfo([H|T], X, OutFun) ->
    try
        Pid = get_pid(X),
        {links, Pids} = erlang:process_info(Pid, links),
        LPid = lists:nth(H, Pids),
        do_linfo(T, LPid, OutFun)
    catch
        _:_ -> X
    end;
do_linfo([], X, _OutFun) ->
    X.

out_pinfo(Pid) when is_pid(Pid) ->
    io:format("~n=== Process Info: ~p~n~p~n", [Pid,erlang:process_info(Pid)]).

out_btrace(Pid) when is_pid(Pid) ->
    io:format("~n=== Process Backtrace: ~p~n", [Pid]),
    c:bt(Pid).

expand(Chars, #state{sup_trees = SupTrees0} = State) ->
    try
        I = parse_int(Chars),
        SupTrees = set_expand(SupTrees0, I, true),
        show(State#state{sup_trees = SupTrees})
    catch
        _:_ ->
            help(show(State))
    end.

shrink_or_show(Chars, #state{sup_trees = SupTrees0} = State) ->
    try
        I = parse_int(Chars),
        SupTrees = set_expand(SupTrees0, I, false),
        help(show(State#state{sup_trees = SupTrees}))
    catch
        _:_ ->
            help(show(State))
    end.


enumerate(SupTrees0) ->
    {SupTrees, _MaxN} = enumerate(SupTrees0, 1),
    SupTrees.

enumerate([#sup_tree{children = Children0} = H | Tail0], N) ->
    {Children, N1} = enumerate(Children0, N+1),
    {Tail, N2} = enumerate(Tail0, N1),
    {[H#sup_tree{n = N, children = Children} | Tail], N2};
enumerate([#worker{} = H | Tail0], N) ->
    {Tail, N1} = enumerate(Tail0, N+1),
    {[H#worker{n = N} | Tail], N1};
enumerate([], N) ->
    {[],N}.





set_expand([#sup_tree{n = N} = H | T], N, Bool) ->
    [H#sup_tree{expand = Bool} | T];
set_expand([#sup_tree{n = N1, children = Children} = H1,
            #sup_tree{n = N2} = H2 | T], I, Bool)
  when N1<I andalso N2>I->
    [H1#sup_tree{children = set_expand(Children, I, Bool)},H2|T];
set_expand([#sup_tree{n = N, children = Children} = H], I, Bool) when N<I ->
    [H#sup_tree{children = set_expand(Children, I, Bool)}];
set_expand([H|T], N, Bool) ->
    [H | set_expand(T, N, Bool)];
set_expand([], _, _) ->
    [].


do([#sup_tree{n = N} = H | T], N, Fun) ->
    [Fun(H) | T];
do([#sup_tree{n = N1, children = Children} = H1,
            #sup_tree{n = N2} = H2 | T], I, Fun)
  when N1<I andalso N2>I->
    [H1#sup_tree{children = do(Children, I, Fun)},H2|T];
do([#sup_tree{n = N, children = Children} = H], I, Fun) when N<I ->
    [H#sup_tree{children = do(Children, I, Fun)}];
do([#worker{n = N} = H | T], N, Fun) ->
    [Fun(H) | T];
do([H|T], N, Fun) ->
    [H | do(T, N, Fun)];
do([], _, _) ->
    [].


parse_int(Chars) ->
    try string:tokens(string:strip(Chars), " ") of
        [X]   -> list_to_integer(X);
        _     -> throw(parse_int)
    catch
        _:_ ->
            throw(parse_int)
    end.

parse_ints(Chars) ->
    try
        [list_to_integer(X) ||
            X <- string:tokens(string:strip(Chars), " ")]
    catch
        _:_ ->
            throw(parse_int)
    end.



print_help() ->
    S1 = " (h)elp e(x)pand [<N>] (s)hrink [<N>] (s)how",
    S2 = " (p)rocess info [<N> ]+ (b)acktrace [<N> ]+",
    S3 = " (m)onitor [<N> ]+ (g)en-state [<N> ]+",
    S4 = " (ts)start-trace [<N> ]+ (te)end-trace (tf)show-trace",
    S5 = " (r)efresh (q)uit",
    S = io_lib:format("~n~s~n~s~n~s~n~s~n~s~n",[S1,S2,S3,S4,S5]),
    ?info_msg(?help_hi(S), []).


%% @private
supervisors() ->
    supervisors(kernel_sup).

%% @private
supervisors(Supervisor) when is_atom(Supervisor) ->
    supervisors(Supervisor, new_sup_tree(Supervisor)).

%% supervisors(user = Supervisor, SupTree) -> % eternal loop?
%%     SupTree;
%% supervisors(standard_error = Supervisor, SupTree) -> % eternal loop?
%%     SupTree;
supervisors(_Supervisor, SupTree) ->
    case supervisor:which_children(SupTree#sup_tree.pid) of
        Children when is_list(Children) ->
            lists:foldl(fun({Id, Pid, Type, Modules}, AccTree)
                              when (Type == supervisor) andalso is_pid(Pid) ->
                                Cs = AccTree#sup_tree.children,
                                AccTree#sup_tree{modules = Modules,
                                                 children =
                                                     Cs ++
                                                     [supervisors(Id,  new_sup_tree(Id, Pid))]};
                           ({Id, Pid, Type, Modules}, AccTree) when Type == worker ->
                                {IsGenSrv, Gmod} = is_gen_server(Pid),
                                Entry = #worker{id = Id, pid = Pid,
                                                is_gen_server = IsGenSrv,
                                                gen_module = Gmod,
                                                modules = Modules},
                                Cs = AccTree#sup_tree.children,
                                AccTree#sup_tree{children = Cs ++ [Entry]};
                           (Entry, AccTree) ->
                                ?warn_msg("Ignoring: ~p~n",[Entry]),
                                AccTree
                        end, SupTree, Children);
        _ ->
            SupTree
    end.

%% Is the given process of type: gen_server, gen_statem or gen_event?
%% We will use this info to enable extration of the State using
%% the sys:get_state/2 function
is_gen_server(Pid) when is_pid(Pid) ->
    case erlang:process_info(Pid, current_function) of
        {current_function, {M , _Fun, _NoOfArgs}}
        when M == gen_server orelse
             M == gen_statem orelse
             M == gen_event ->
            {true, M};
        _ ->
            {false, undefined}
    end;
is_gen_server(_) ->
    {false, undefined}.


%% Try to find all Supervisors in the system.
%% Idea: Go through all registered processes and
%% try to figure out if they are a supervisor.
%% Is there a better way?
%% Can a supervisor not be registered and hence not be included here?
%% @private
all_sup_trees() ->
    Sups = lists:foldr(
             fun(Id,Acc) ->
                     case get_initial_call(whereis(Id)) of
                         {ok, {supervisor,_,_}} ->
                             [Id|Acc];
                         _ ->
                             Acc
                     end
             end, [], lists:sort(erlang:registered())),
    [supervisors(Id) || Id <- Sups].


get_initial_call(Pid) when is_pid(Pid) ->
    case process_info(Pid, dictionary) of
        {dictionary,L} ->
            case lists:keyfind('$initial_call', 1, L) of
                {'$initial_call', Icall} ->
                    {ok, Icall};
                _ ->
                    false
            end;
        _ ->
            false
    end;
get_initial_call(_) ->
    false.



new_sup_tree(SupervisorId) ->
    new_sup_tree(SupervisorId, erlang:whereis(SupervisorId)).

new_sup_tree(SupervisorId, Pid) ->
    #sup_tree{id = SupervisorId,
              pid = Pid}.