1%%% @author zhongwen <zhongwencool@gmail.com>
2-module(observer_cli_application).
3
4-include("observer_cli.hrl").
5
6%% API
7-export([start/1]).
8-export([clean/1]).
9
10%% API
11-define(LAST_LINE,
12    "refresh: ~wms q(quit) Positive Number(set refresh interval time ms) F/B(forward/back) Current pages is ~w"
13).
14
15%% @doc List application info
16
17-spec start(ViewOpts) -> no_return when ViewOpts :: view_opts().
18start(#view_opts{app = App, auto_row = AutoRow} = ViewOpts) ->
19    Pid = spawn_link(fun() ->
20        ?output(?CLEAR),
21        render_worker(App, AutoRow)
22    end),
23    manager(Pid, ViewOpts).
24
25-spec clean(list()) -> ok.
26clean(Pids) -> observer_cli_lib:exit_processes(Pids).
27
28%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
29%%% Private
30%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
31manager(Pid, Opts = #view_opts{app = App = #app{cur_page = CurPage}}) ->
32    case observer_cli_lib:parse_cmd(Opts, ?MODULE, [Pid]) of
33        quit ->
34            erlang:unlink(Pid),
35            erlang:send(Pid, quit),
36            quit;
37        {func, proc_count, message_queue_len} ->
38            clean([Pid]),
39            start(Opts#view_opts{app = App#app{type = {message_queue_len, 4}}});
40        {func, proc_count, reductions} ->
41            clean([Pid]),
42            start(Opts#view_opts{app = App#app{type = {reductions, 3}}});
43        {func, proc_count, memory} ->
44            clean([Pid]),
45            start(Opts#view_opts{app = App#app{type = {memory, 2}}});
46        pause_or_resume ->
47            clean([Pid]),
48            start(Opts#view_opts{app = App#app{type = {proc_count, 1}}});
49        {new_interval, NewInterval} ->
50            clean([Pid]),
51            start(Opts#view_opts{app = App#app{interval = NewInterval}});
52        page_down_top_n ->
53            NewPage = max(CurPage + 1, 1),
54            clean([Pid]),
55            start(Opts#view_opts{app = App#app{cur_page = NewPage}});
56        page_up_top_n ->
57            NewPage = max(CurPage - 1, 1),
58            clean([Pid]),
59            start(Opts#view_opts{app = App#app{cur_page = NewPage}});
60        _ ->
61            manager(Pid, Opts)
62    end.
63
64render_worker(App, AutoRow) ->
65    #app{type = Type, interval = Interval, cur_page = CurPage} = App,
66    TerminalRow = observer_cli_lib:get_terminal_rows(AutoRow),
67    Rows = erlang:max(TerminalRow - 5, 0),
68    Text = "Interval: " ++ integer_to_list(Interval) ++ "ms",
69    Menu = observer_cli_lib:render_menu(app, Text),
70    Info = render_app_info(Rows, CurPage, Type),
71    LastText = io_lib:format(?LAST_LINE, [Interval, CurPage]),
72    LastLine = observer_cli_lib:render_last_line(LastText),
73    ?output([?CURSOR_TOP, Menu, Info, LastLine]),
74    erlang:send_after(Interval, self(), redraw),
75    receive
76        quit -> quit;
77        redraw -> render_worker(App, AutoRow)
78    end.
79
80render_app_info(Row, CurPage, {Type, N}) ->
81    List = [
82        begin
83            {0, {element(N, I), S}, [App, C, M, R, Q, S, V]}
84        end
85        || {App, I = {C, M, R, Q, S, V}} <- maps:to_list(app_info())
86    ],
87    {StartPos, SortList} = observer_cli_lib:sublist(List, Row, CurPage),
88    InitColor = [
89        {memory, ?GRAY_BG},
90        {proc_count, ?GRAY_BG},
91        {reductions, ?GRAY_BG},
92        {message_queue_len, ?GRAY_BG}
93    ],
94    [
95        {_, MemColor},
96        {_, ProcessColor},
97        {_, RedColor},
98        {_, MsgQColor}
99    ] = lists:keyreplace(Type, 1, InitColor, {Type, ?RED_BG}),
100    Title = ?render([
101        ?UNDERLINE,
102        ?W2(?GRAY_BG, "Id", 3),
103        ?UNDERLINE,
104        ?W2(?GRAY_BG, "App", 31),
105        ?UNDERLINE,
106        ?W2(ProcessColor, "ProcessCount(p)", 20),
107        ?UNDERLINE,
108        ?W2(MemColor, "Memory(m)", 20),
109        ?UNDERLINE,
110        ?W2(RedColor, "Reductions(r)", 17),
111        ?UNDERLINE,
112        ?W2(MsgQColor, "MsgQ(mq)", 10),
113        ?UNDERLINE,
114        ?W2(?GRAY_BG, "Status", 12),
115        ?UNDERLINE,
116        ?W2(?GRAY_BG, "version", 16)
117    ]),
118    {_, View} = lists:foldl(
119        fun({_, _, Item}, {Pos, Acc}) ->
120            [App, C, R, M, Q, S, V] = Item,
121            {Pos + 1, [
122                ?render([
123                    ?W(Pos, 2),
124                    ?W(App, 29),
125                    ?W(C, 18),
126                    ?W({byte, M}, 18),
127                    ?W(R, 15),
128                    ?W(Q, 8),
129                    ?W(S, 10),
130                    ?W(V, 15)
131                ])
132                | Acc
133            ]}
134        end,
135        {StartPos, []},
136        SortList
137    ),
138    [Title | lists:reverse(View)].
139
140app_info() ->
141    Info = application:info(),
142    AllApps = app_status(Info),
143    Leaders = leader_info(Info),
144    app_info(AllApps, Leaders, erlang:processes(), self()).
145
146app_info(AllApps, _Leaders, [], _Self) ->
147    AllApps;
148app_info(AllApps, Leaders, [Self | Process], Self) ->
149    app_info(AllApps, Leaders, Process, Self);
150app_info(AllApps, Leaders, [Pid | Process], Self) ->
151    case erlang:process_info(Pid, [group_leader, memory, reductions, message_queue_len]) of
152        undefined ->
153            app_info(AllApps, Leaders, Process, Self);
154        Prop ->
155            [
156                {group_leader, Group},
157                {memory, Memory},
158                {reductions, Reds},
159                {message_queue_len, MsgQ}
160            ] = Prop,
161            NewAllApps =
162                case maps:find(Group, Leaders) of
163                    error ->
164                        {ok, {C1, M1, R1, Q1, S1, V1}} = maps:find(unknown, AllApps),
165                        NewInfo = {C1 + 1, M1 + Memory, R1 + Reds, Q1 + MsgQ, S1, V1},
166                        maps:put(unknown, NewInfo, AllApps);
167                    {ok, App} ->
168                        {ok, {C, M, R, Q, S, V}} = maps:find(App, AllApps),
169                        maps:put(App, {C + 1, M + Memory, R + Reds, Q + MsgQ, S, V}, AllApps)
170                end,
171            app_info(NewAllApps, Leaders, Process, Self)
172    end.
173
174leader_info(Info) ->
175    {running, Running} = lists:keyfind(running, 1, Info),
176    leader_info(Running, #{}).
177
178leader_info([{App, Sup} | Running], Acc) when is_pid(Sup) ->
179    NewAcc =
180        case erlang:process_info(Sup, group_leader) of
181            undefined ->
182                Acc;
183            {group_leader, Pid} ->
184                Acc#{Pid => App}
185        end,
186    leader_info(Running, NewAcc);
187leader_info([_ | Running], Acc) ->
188    leader_info(Running, Acc);
189leader_info([], Acc) ->
190    Acc.
191
192app_status(Info) ->
193    {loaded, Loaded} = lists:keyfind(loaded, 1, Info),
194    {loading, Loading} = lists:keyfind(loading, 1, Info),
195    {started, Started} = lists:keyfind(started, 1, Info),
196    {start_p_false, StartPFalse} = lists:keyfind(start_p_false, 1, Info),
197    {starting, Starting} = lists:keyfind(starting, 1, Info),
198    R0 = #{unknown => {0, 0, 0, 0, "Unknown", "unknown"}},
199    R1 = lists:foldl(
200        fun({App, _From}, Acc) ->
201            Acc#{App => {0, 0, 0, 0, "Loading", "unknown"}}
202        end,
203        R0,
204        Loading
205    ),
206    R2 = lists:foldl(
207        fun({App, _Desc, Version}, Acc) ->
208            Acc#{App => {0, 0, 0, 0, "Loaded", Version}}
209        end,
210        R1,
211        Loaded
212    ),
213    R3 = lists:foldl(
214        fun({App, _RestartType, _Type, _From}, Acc) ->
215            Version = get_version(App, Acc),
216            Acc#{App => {0, 0, 0, 0, "Starting", Version}}
217        end,
218        R2,
219        Starting
220    ),
221    R4 = lists:foldl(
222        fun({App, _RestartType}, Acc) ->
223            Version = get_version(App, Acc),
224            Acc#{App => {0, 0, 0, 0, "Started", Version}}
225        end,
226        R3,
227        Started
228    ),
229    lists:foldl(
230        fun({App, _RestartType, _Type, _From}, Acc) ->
231            Version = get_version(App, Acc),
232            Acc#{App => {0, 0, 0, 0, "StartPFlase", Version}}
233        end,
234        R4,
235        StartPFalse
236    ).
237
238get_version(App, Maps) ->
239    case maps:find(App, Maps) of
240        {ok, {_, _, _, _, _, V}} -> V;
241        _ -> "unknown"
242    end.
243