1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2011-2018. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19
20-module(observer_procinfo).
21
22-behaviour(wx_object).
23
24-export([start/3]).
25
26-export([init/1, handle_event/2, handle_cast/2, terminate/2, code_change/3,
27	 handle_call/3, handle_info/2]).
28
29-include_lib("wx/include/wx.hrl").
30-include("observer_defs.hrl").
31
32-define(REFRESH, 601).
33-define(SELECT_ALL, 603).
34-define(ID_NOTEBOOK, 604).
35
36-record(state, {parent,
37		frame,
38		notebook,
39		pid,
40		pages=[],
41		expand_table,
42		expand_wins=[]
43	       }).
44
45-record(worker, {panel, callback}).
46
47-record(io, {rdata=""}).
48
49start(Process, ParentFrame, Parent) ->
50    wx_object:start_link(?MODULE, [Process, ParentFrame, Parent], []).
51
52%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
53
54init([Pid, ParentFrame, Parent]) ->
55    try
56	Table = ets:new(observer_expand,[set,public]),
57	Title=case observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, registered_name]) of
58		  [] -> io_lib:format("~p",[Pid]);
59		  {registered_name, Registered} -> io_lib:format("~tp (~p)",[Registered, Pid]);
60		  undefined -> throw(process_undefined)
61	      end,
62    Scale = observer_wx:get_scale(),
63	Frame=wxFrame:new(ParentFrame, ?wxID_ANY, [atom_to_list(node(Pid)), $:, Title],
64			  [{style, ?wxDEFAULT_FRAME_STYLE}, {size, {Scale * 850, Scale * 600}}]),
65	MenuBar = wxMenuBar:new(),
66	create_menus(MenuBar),
67	wxFrame:setMenuBar(Frame, MenuBar),
68
69	Notebook = wxNotebook:new(Frame, ?ID_NOTEBOOK, [{style, ?wxBK_DEFAULT}]),
70
71	ProcessPage = init_panel(Notebook, "Process Information", [Pid], fun init_process_page/2),
72	MessagePage = init_panel(Notebook, "Messages", [Pid,Table], fun init_message_page/3),
73	DictPage    = init_panel(Notebook, "Dictionary", [Pid,Table], fun init_dict_page/3),
74	StackPage   = init_panel(Notebook, "Stack Trace", [Pid], fun init_stack_page/2),
75	StatePage   = init_panel(Notebook, "State", [Pid,Table], fun init_state_page/3),
76        Ps = case gen_server:call(observer, log_status) of
77		 true  -> [init_panel(Notebook, "Log", [Pid,Table], fun init_log_page/3)];
78		 false -> []
79	     end,
80
81	wxFrame:connect(Frame, close_window),
82	wxMenu:connect(Frame, command_menu_selected),
83	%% wxNotebook:connect(Notebook, command_notebook_page_changed, [{skip,true}]),
84	wxFrame:show(Frame),
85	{Frame, #state{parent=Parent,
86		       pid=Pid,
87		       frame=Frame,
88		       notebook=Notebook,
89		       pages=[ProcessPage,MessagePage,DictPage,StackPage,StatePage|Ps],
90		       expand_table=Table
91		      }}
92    catch error:{badrpc, _} ->
93	    observer_wx:return_to_localnode(ParentFrame, node(Pid)),
94	    {stop, badrpc};
95	  process_undefined ->
96	    observer_lib:display_info_dialog(ParentFrame,"No such alive process"),
97	    {stop, normal}
98    end.
99
100init_panel(Notebook, Str, FunArgs, Fun) ->
101    Panel  = wxPanel:new(Notebook),
102    Sizer  = wxBoxSizer:new(?wxHORIZONTAL),
103    {Window,Callback} = apply(Fun,[Panel|FunArgs]),
104    wxSizer:add(Sizer, Window, [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]),
105    wxPanel:setSizer(Panel, Sizer),
106    true = wxNotebook:addPage(Notebook, Panel, Str),
107    #worker{panel=Panel, callback=Callback}.
108
109%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%Callbacks%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
110handle_event(#wx{event=#wxClose{type=close_window}}, State) ->
111    {stop, normal, State};
112
113handle_event(#wx{id=?wxID_CLOSE, event=#wxCommand{type=command_menu_selected}}, State) ->
114    {stop, normal, State};
115
116handle_event(#wx{id=?REFRESH}, #state{frame=Frame, pid=Pid, pages=Pages, expand_table=T}=State) ->
117    ets:delete_all_objects(T),
118    try [(W#worker.callback)() || W <- Pages]
119    catch process_undefined ->
120	    wxFrame:setTitle(Frame, io_lib:format("*DEAD* ~p",[Pid]))
121    end,
122    {noreply, State};
123
124handle_event(#wx{obj=MoreEntry,event=#wxMouse{type=left_down},userData={more,More}}, State) ->
125    observer_lib:add_scroll_entries(MoreEntry,More),
126    {noreply, State};
127
128handle_event(#wx{event=#wxMouse{type=left_down}, userData=TargetPid}, State) ->
129    observer ! {open_link, TargetPid},
130    {noreply, State};
131
132handle_event(#wx{obj=Obj, event=#wxMouse{type=enter_window}}, State) ->
133    wxStaticText:setForegroundColour(Obj,{0,0,100,255}),
134    {noreply, State};
135
136handle_event(#wx{obj=Obj, event=#wxMouse{type=leave_window}}, State) ->
137    wxStaticText:setForegroundColour(Obj,?wxBLUE),
138    {noreply, State};
139
140handle_event(#wx{event=#wxHtmlLink{linkInfo=#wxHtmlLinkInfo{href=Href}}},
141	     #state{frame=Frame,expand_table=T,expand_wins=Opened0}=State) ->
142    {Type, Rest} = case Href of
143		       "#Term?"++Keys   -> {cdv_term_cb, Keys};
144		       "#OBSBinary?"++Keys -> {cdv_bin_cb, Keys};
145		       _ -> {other, Href}
146		   end,
147    case Type of
148	other ->
149	    observer ! {open_link, Href},
150	    {noreply, State};
151	Callback ->
152	    [{"key1",Key1},{"key2",Key2},{"key3",Key3}] = uri_string:dissect_query(Rest),
153	    Id = {obs, {T,{list_to_integer(Key1),
154			   list_to_integer(Key2),
155			   list_to_integer(Key3)}}},
156	    Opened =
157		case lists:keyfind(Id,1,Opened0) of
158		    false ->
159			Win = cdv_detail_wx:start_link(Id,[],Frame,Callback,obs),
160			[{Id,Win}|Opened0];
161		    {_,Win} ->
162			wxFrame:raise(Win),
163			Opened0
164		end,
165	    {noreply,State#state{expand_wins=Opened}}
166    end;
167
168handle_event(#wx{event=#wxHtmlLink{linkInfo=#wxHtmlLinkInfo{href=Info}}}, State) ->
169    observer ! {open_link, Info},
170    {noreply, State};
171
172handle_event(Event, _State) ->
173    error({unhandled_event, Event}).
174
175handle_info({get_debug_info, From}, State = #state{notebook=Notebook}) ->
176    From ! {procinfo_debug, Notebook},
177    {noreply, State};
178handle_info(_Info, State) ->
179    %% io:format("~p: ~p, Handle info: ~tp~n", [?MODULE, ?LINE, Info]),
180    {noreply, State}.
181
182handle_call(Call, From, _State) ->
183    error({unhandled_call, Call, From}).
184
185handle_cast({detail_win_closed,Id}, #state{expand_wins=Opened0}=State) ->
186    Opened = lists:keydelete(Id,1,Opened0),
187    {noreply,State#state{expand_wins=Opened}};
188
189handle_cast(Cast, _State) ->
190    error({unhandled_cast, Cast}).
191
192terminate(_Reason, #state{parent=Parent,pid=Pid,frame=Frame,expand_table=T}) ->
193    T=/=undefined andalso ets:delete(T),
194    Parent ! {procinfo_menu_closed, Pid},
195    case Frame of
196	undefined ->  ok;
197	_ -> wxFrame:destroy(Frame)
198    end,
199    ok.
200
201code_change(_, _, State) ->
202    {ok, State}.
203
204%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
205init_process_page(Panel, Pid) ->
206    WSz = observer_wx:try_rpc(node(Pid), erlang, system_info,[wordsize]),
207    Fields0 = process_info_fields(Pid, WSz),
208    {FPanel, _, UpFields} = observer_lib:display_info(Panel, Fields0),
209    {FPanel, fun() ->
210		     Fields = process_info_fields(Pid, WSz),
211		     observer_lib:update_info(UpFields, Fields)
212	     end}.
213
214
215init_message_page(Parent, Pid, Table) ->
216    Win = observer_lib:html_window(Parent),
217    Cs = observer_lib:colors(Parent),
218    Update = fun() ->
219		     case observer_wx:try_rpc(node(Pid), erlang, process_info,
220					      [Pid, messages])
221		     of
222			 {messages, Messages} ->
223			     Html = observer_html_lib:expandable_term("Message Queue", Messages,
224                                                                      Table, Cs),
225			     wxHtmlWindow:setPage(Win, Html);
226			 _ ->
227			     throw(process_undefined)
228		     end
229	     end,
230    Update(),
231    {Win, Update}.
232
233init_dict_page(Parent, Pid, Table) ->
234    Win = observer_lib:html_window(Parent),
235    Cs = observer_lib:colors(Parent),
236    Update = fun() ->
237		     case observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, dictionary])
238		     of
239			 {dictionary,Dict} ->
240			     Html = observer_html_lib:expandable_term("Dictionary", Dict, Table, Cs),
241			     wxHtmlWindow:setPage(Win, Html);
242			 _ ->
243			     throw(process_undefined)
244		     end
245	     end,
246    Update(),
247    {Win, Update}.
248
249init_stack_page(Parent, Pid) ->
250    LCtrl = wxListCtrl:new(Parent, [{style, ?wxLC_REPORT bor ?wxLC_HRULES}]),
251    Li = wxListItem:new(),
252    Scale = observer_wx:get_scale(),
253    wxListItem:setText(Li, "Module:Function/Arg"),
254    wxListCtrl:insertColumn(LCtrl, 0, Li),
255    wxListCtrl:setColumnWidth(LCtrl, 0, Scale * 300),
256    wxListItem:setText(Li, "File:LineNumber"),
257    wxListCtrl:insertColumn(LCtrl, 1, Li),
258    wxListCtrl:setColumnWidth(LCtrl, 1, Scale * 300),
259    wxListItem:destroy(Li),
260    Even = wxSystemSettings:getColour(?wxSYS_COLOUR_LISTBOX),
261    Odd = observer_lib:mix(Even, wxSystemSettings:getColour(?wxSYS_COLOUR_HIGHLIGHT), 0.8),
262    Update = fun() ->
263		     case observer_wx:try_rpc(node(Pid), erlang, process_info,
264					      [Pid, current_stacktrace])
265		     of
266			 {current_stacktrace,RawBt} ->
267			     wxListCtrl:deleteAllItems(LCtrl),
268			     wx:foldl(fun({M, F, A, Info}, Row) ->
269					      _Item = wxListCtrl:insertItem(LCtrl, Row, ""),
270					      ?EVEN(Row) orelse
271						  wxListCtrl:setItemBackgroundColour(LCtrl, Row, Odd),
272					      wxListCtrl:setItem(LCtrl, Row, 0, observer_lib:to_str({M,F,A})),
273					      FileLine = case Info of
274							     [{file,File},{line,Line}] ->
275								 io_lib:format("~ts:~w", [File,Line]);
276							     _ ->
277								 []
278							 end,
279					      wxListCtrl:setItem(LCtrl, Row, 1, FileLine),
280					      Row+1
281				      end, 0, RawBt);
282			 _ ->
283			     throw(process_undefined)
284		     end
285	     end,
286    Resize = fun(#wx{event=#wxSize{size={W,_}}},Ev) ->
287		     wxEvent:skip(Ev),
288		     observer_lib:set_listctrl_col_size(LCtrl, W)
289	     end,
290    wxListCtrl:connect(LCtrl, size, [{callback, Resize}]),
291    Update(),
292    {LCtrl, Update}.
293
294init_state_page(Parent, Pid, Table) ->
295    Win = observer_lib:html_window(Parent),
296    Cs = observer_lib:colors(Parent),
297    Update = fun() ->
298		     StateInfo = fetch_state_info(Pid),
299		     Html = observer_html_lib:expandable_term("ProcState", StateInfo, Table, Cs),
300		     wxHtmlWindow:setPage(Win, Html)
301	     end,
302    Update(),
303    {Win, Update}.
304
305fetch_state_info(Pid) ->
306    %% First, test if sys:get_status/2 have any chance to return an answer
307    case rpc:call(node(Pid), proc_lib, translate_initial_call, [Pid]) of
308	%% Not a gen process
309	{proc_lib,init_p,5} -> [];
310	%% May be a gen process
311	{M, _F, _A} -> fetch_state_info2(Pid, M);
312	_ -> throw(process_undefined)
313    end.
314
315fetch_state_info2(Pid, M) ->
316    %% Get the behavio(u)r
317    I = rpc:call(node(Pid), M, module_info, [attributes]),
318    case lists:keyfind(behaviour, 1, I) of
319	false -> case lists:keyfind(behavior, 1, I) of
320		     false		-> B = undefined;
321		     {behavior, [B]}	-> B
322		 end;
323	{behaviour, [B]} -> B
324    end,
325    %% but not sure that system messages are treated by this process
326    %% so using a rpc with a small timeout in order not to lag the display
327    case rpc:call(node(Pid), sys, get_status, [Pid, 200])
328    of
329	{status, _, {module, _},
330	 [_PDict, _SysState, _Parent, _Dbg,
331	  [Header,{data, First},{data, Second}|_]]} ->
332	    [{"Behaviour", B}, Header] ++ First ++ Second;
333	{status, _, {module, _},
334	 [_PDict, _SysState, _Parent, _Dbg,
335	  [Header,{data, First}, OtherFormat]]} ->
336	    [{"Behaviour", B}, Header] ++ First ++ [{"State",OtherFormat}];
337	{status, _, {module, _},
338	 [_PDict, _SysState, _Parent, _Dbg, OtherFormat]} ->
339	    %% Formatted status ?
340	    case lists:keyfind(format_status, 1, rpc:call(node(Pid), M, module_info, [exports])) of
341		false	-> Opt = {"Format", unknown};
342		_	-> Opt = {"Format", overriden}
343	    end,
344	    [{"Behaviour", B}, Opt, {"State",OtherFormat}];
345	{badrpc,{'EXIT',{timeout, _}}} -> []
346    end.
347
348init_log_page(Parent, Pid, Table) ->
349    Win = observer_lib:html_window(Parent),
350    Cs = observer_lib:colors(Parent),
351    Update = fun() ->
352		     Fd = spawn_link(fun() -> io_server() end),
353		     rpc:call(node(Pid), rb, rescan, [[{start_log, Fd}]]),
354		     rpc:call(node(Pid), rb, grep, [local_pid_str(Pid)]),
355		     Logs = io_get_data(Fd),
356		     %% Replace remote local pid notation to global notation
357		     Pref = global_pid_node_pref(Pid),
358		     ExpPid = re:replace(Logs,"<0\.","<" ++ Pref ++ ".",[global, {return, list}]),
359		     %% Try to keep same look by removing blanks at right of rewritten PID
360		     NbBlanks = length(Pref) - 1,
361		     Re = "(<" ++ Pref ++ "\.[^>]{1,}>)[ ]{"++ integer_to_list(NbBlanks) ++ "}",
362		     Look = re:replace(ExpPid, Re, "\\1", [global, {return, list}]),
363		     Html = observer_html_lib:expandable_term("SaslLog", Look, Table, Cs),
364		     wxHtmlWindow:setPage(Win, Html)
365	     end,
366    Update(),
367    {Win, Update}.
368
369create_menus(MenuBar) ->
370    Menus = [{"File", [#create_menu{id=?wxID_CLOSE, text="Close"}]},
371	     {"View", [#create_menu{id=?REFRESH, text="Refresh\tCtrl-R"}]}],
372    observer_lib:create_menus(Menus, MenuBar, new_window).
373
374process_info_fields(Pid, WSz) ->
375    Struct = [{"Overview",
376	       [{"Initial Call",     initial_call},
377		{"Current Function", current_function},
378		{"Registered Name",  registered_name},
379		{"Status",           status},
380		{"Message Queue Len",message_queue_len},
381		{"Group Leader",     {click, group_leader}},
382		{"Priority",         priority},
383		{"Trap Exit",        trap_exit},
384		{"Reductions",       reductions},
385		{"Binary",           fun(Data) -> stringify_bins(Data) end},
386		{"Last Calls",       last_calls},
387		{"Catch Level",      catchlevel},
388		{"Trace",            trace},
389		{"Suspending",       suspending},
390		{"Sequential Trace Token", sequential_trace_token},
391		{"Error Handler",    error_handler}]},
392	      {scroll_boxes,
393	       [{"Links",            {click, links}},
394		{"Monitors",         {click, filter_monitor_info()}},
395		{"Monitored by",     {click, monitored_by}}]},
396	      {"Memory and Garbage Collection", right,
397	       [{"Memory",           {bytes, memory}},
398		{"Stack and Heaps",  {{words,WSz}, total_heap_size}},
399		{"Heap Size",        {{words,WSz}, heap_size}},
400		{"Stack Size",       {{words,WSz}, stack_size}},
401		{"GC Min Heap Size", {{words,WSz}, get_gc_info(min_heap_size)}},
402		{"GC FullSweep After", get_gc_info(fullsweep_after)}
403	       ]}],
404    case observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, item_list()]) of
405	RawInfo when is_list(RawInfo) ->
406	    observer_lib:fill_info(Struct, RawInfo);
407	_ ->
408	    throw(process_undefined)
409    end.
410
411item_list() ->
412    [ %% backtrace,
413      binary,
414      catchlevel,
415      current_function,
416      %% dictionary,
417      error_handler,
418      garbage_collection,
419      group_leader,
420      heap_size,
421      initial_call,
422      last_calls,
423      links,
424      memory,
425      message_queue_len,
426      %% messages,
427      monitored_by,
428      monitors,
429      priority,
430      reductions,
431      registered_name,
432      sequential_trace_token,
433      stack_size,
434      status,
435      suspending,
436      total_heap_size,
437      trace,
438      trap_exit].
439
440get_gc_info(Arg) ->
441    fun(Data) ->
442	    GC = proplists:get_value(garbage_collection, Data),
443	    proplists:get_value(Arg, GC)
444    end.
445
446filter_monitor_info() ->
447    fun(Data) ->
448	    Ms = proplists:get_value(monitors, Data),
449	    [Id || {_Type, Id} <- Ms] % Type is process or port
450    end.
451
452stringify_bins(Data) ->
453    Bins = proplists:get_value(binary, Data),
454    [lists:flatten(io_lib:format("<< ~s, refc ~w>>", [observer_lib:to_str({bytes,Sz}),Refc]))
455     || {_Ptr, Sz, Refc} <- Bins].
456
457local_pid_str(Pid) ->
458    %% observer can observe remote nodes
459    %% There is no function to get the local
460    %% pid from the remote pid ...
461    %% So grep will fail to find remote pid in remote local log.
462    %% i.e. <4589.42.1> will not be found, but <0.42.1> will
463    %% Let's replace first integer by zero
464    "<0" ++ re:replace(pid_to_list(Pid),"\<([0-9]{1,})","",[{return, list}]).
465
466global_pid_node_pref(Pid) ->
467    %% Global PID node prefix : X of <X.Y.Z>
468    [NodePrefix|_] = string:lexemes(pid_to_list(Pid),"<."),
469    NodePrefix.
470
471io_get_data(Pid) ->
472    Pid ! {self(), get_data_and_close},
473    receive
474	{Pid, data, Data} ->  lists:flatten(Data)
475    end.
476
477io_server() ->
478    io_server(#io{}).
479
480io_server(State) ->
481    receive
482	{io_request, From, ReplyAs, Request} ->
483	    {_, Reply, NewState} =  io_request(Request,State),
484	    From ! {io_reply, ReplyAs, Reply},
485	    io_server(NewState);
486	{Pid, get_data_and_close} ->
487	    Pid ! {self(), data, lists:reverse(State#io.rdata)},
488	    normal;
489	_Unknown ->
490	    io_server(State)
491    end.
492
493io_request({put_chars, _Encoding, Chars}, State = #io{rdata=Data}) ->
494    {ok, ok, State#io{rdata=[Chars|Data]}};
495io_request({put_chars, Encoding, Module, Function, Args}, State) ->
496    try
497	io_request({put_chars, Encoding, apply(Module, Function, Args)}, State)
498    catch _:_ ->
499	    {error, {error, Function}, State}
500    end;
501io_request(_Req, State) ->
502    %% io:format("~p: Unknown req: ~tp ~n",[?LINE, _Req]),
503    {ok, {error, request}, State}.
504