%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2000-2012. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% %% %%---------------------------------------------------------------------- %% Purpose: Displays details of a trace event %%---------------------------------------------------------------------- -module(et_gs_contents_viewer). -compile([{nowarn_deprecated_function,{gs,config,2}}, {nowarn_deprecated_function,{gs,destroy,1}}, {nowarn_deprecated_function,{gs,editor,2}}, {nowarn_deprecated_function,{gs,frame,2}}, {nowarn_deprecated_function,{gs,menu,2}}, {nowarn_deprecated_function,{gs,menubar,2}}, {nowarn_deprecated_function,{gs,menubutton,2}}, {nowarn_deprecated_function,{gs,menuitem,2}}, {nowarn_deprecated_function,{gs,menuitem,3}}, {nowarn_deprecated_function,{gs,start,0}}, {nowarn_deprecated_function,{gs,window,2}}]). -behaviour(gen_server). %% External exports -export([start_link/1, stop/1]). %% gen_server callbacks -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2]). -include("../include/et.hrl"). -include("et_internal.hrl"). -record(state, {parent_pid, % Pid of parent process viewer_pid, % Pid of viewer process event_order, % Field to be used as primary key event, % The original event filtered_event, % Event processed by active filter active_filter, % Name of the active filter filters, % List of possible filters win, % GUI: Window object packer, % GUI: Packer object width, % GUI: Window width height}). % GUI: Window height %%%---------------------------------------------------------------------- %%% Client side %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% start_link(Options) -> {ok, ContentsPid} | {error, Reason} %% %% Start an viewer for the event contents as window in GS %% %% Options = [option()] %% %% option() = %% %% {parent_pid, pid()} | % Pid of parent process %% {viewer_pid, pid()} | % Pid of viewer process %% {event_order, event_order()} | % Field to be used as primary key %% {active_filter, atom()} | % Name of the active filter %% {filter, atom(), fun()} % A named filter fun %% %% event_order() = 'trace_ts' | 'event_ts' %% ContentsPid = pid() %% Reason = term() %%---------------------------------------------------------------------- start_link(Options) -> case parse_opt(Options, default_state()) of {ok, S} -> case gen_server:start_link(?MODULE, [S], []) of {ok, ContentsPid} when S#state.parent_pid =/= self() -> unlink(ContentsPid), {ok, ContentsPid}; Other -> Other end; {error, Reason} -> {error, Reason} end. default_state() -> #state{parent_pid = self(), viewer_pid = undefined, active_filter = ?DEFAULT_FILTER_NAME, filters = [?DEFAULT_FILTER], width = 600, height = 300}. parse_opt([], S) -> Name = S#state.active_filter, Filters = S#state.filters, if S#state.event =:= undefined -> {error, {badarg, no_event}}; is_atom(Name) -> case lists:keysearch(Name, #filter.name, Filters) of {value, F} when is_record(F, filter) -> {ok, S#state{active_filter = Name}}; false -> {error, {badarg, {no_such_filter, Name, Filters}}} end end; parse_opt([H | T], S) -> case H of {parent_pid, ParentPid} when is_pid(ParentPid) -> parse_opt(T, S#state{parent_pid = ParentPid}); {viewer_pid, ViewerPid} when is_pid(ViewerPid) -> parse_opt(T, S#state{viewer_pid = ViewerPid}); {event_order, trace_ts} -> parse_opt(T, S#state{event_order = trace_ts}); {event_order, event_ts} -> parse_opt(T, S#state{event_order = event_ts}); {event, Event} when is_record(Event, event) -> parse_opt(T, S#state{event = Event}); {active_filter, Name} when is_atom(Name) -> parse_opt(T, S#state{active_filter = Name}); F when is_record(F, filter), is_atom(F#filter.name), is_function(F#filter.function) -> Filters = lists:keydelete(F#filter.name, #filter.name, S#state.filters), Filters2 = lists:keysort(#filter.name, [F | Filters]), parse_opt(T, S#state{filters = Filters2}); {width, Width} when is_integer(Width), Width > 0 -> parse_opt(T, S#state{width = Width}); {height, Height} when is_integer(Height), Height > 0 -> parse_opt(T, S#state{height = Height}); Bad -> {error, {bad_option, Bad}} end; parse_opt(BadList, _S) -> {error, {bad_option_list, BadList}}. %%---------------------------------------------------------------------- %% stop(ContentsPid) -> ok %% %% Stops a contents viewer process %% %% ContentsPid = pid() %%---------------------------------------------------------------------- stop(ContentsPid) -> unlink(ContentsPid), call(ContentsPid, stop). call(ContentsPid, Request) -> gen_server:call(ContentsPid, Request, infinity). %%%---------------------------------------------------------------------- %%% Callback functions from gen_server %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %%---------------------------------------------------------------------- init([S]) when is_record(S, state) -> process_flag(trap_exit, true), S2 = create_window(S), {ok, S2}. %%---------------------------------------------------------------------- %% Func: handle_call/3 %% Returns: {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | (terminate/2 is called) %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_call(stop, _From, S) -> unlink(S#state.parent_pid), {stop, shutdown, ok, S}; handle_call(Request, From, S) -> ok = error_logger:format("~p(~p): handle_call(~p, ~p, ~p)~n", [?MODULE, self(), Request, From, S]), Reply = {error, {bad_request, Request}}, {reply, Reply, S}. %%---------------------------------------------------------------------- %% Func: handle_cast/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_cast(Msg, S) -> ok = error_logger:format("~p(~p): handle_cast(~p, ~p)~n", [?MODULE, self(), Msg, S]), {noreply, S}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info({gs, Button, click, Data, _Other}, S) -> case Button of close -> gs:destroy(S#state.win), {stop, normal, S}; save -> Event = S#state.event, Bin = list_to_binary(event_to_string(Event, S#state.event_order)), TimeStamp = case S#state.event_order of trace_ts -> Event#event.trace_ts; event_ts -> Event#event.event_ts end, FileName = ["et_contents_viewer_", now_to_string(TimeStamp), ".save"], file:write_file(lists:flatten(FileName), Bin), {noreply, S}; _PopupMenuItem when is_record(Data, filter) -> F = Data, ChildState= S#state{active_filter = F#filter.name}, case gen_server:start_link(?MODULE, [ChildState], []) of {ok, Pid} when S#state.parent_pid =/= self() -> unlink(Pid), {noreply, S}; _ -> {noreply, S} end; {hide, Actors} -> send_viewer_event(S, {delete_actors, Actors}), {noreply, S}; {show, Actors} -> send_viewer_event(S, {insert_actors, Actors}), {noreply, S}; {mode, Mode} -> send_viewer_event(S, {mode, Mode}), {noreply, S}; Nyi -> ok = error_logger:format("~p: click ~p ignored (nyi)~n", [?MODULE, Nyi]), {noreply, S} end; handle_info({gs, _Obj, destroy,_, _}, S) -> unlink(S#state.parent_pid), gs:destroy(S#state.win), {stop, normal, S}; handle_info({gs, _Obj, keypress, _, [KeySym, _Keycode, _Shift, _Control | _]}, S) -> case KeySym of 'c' -> gs:destroy(S#state.win), {stop, normal, S}; 'f' -> E = S#state.filtered_event, From = E#event.from, send_viewer_event(S, {delete_actors, [From]}), {noreply, S}; 't' -> E = S#state.filtered_event, To = E#event.to, send_viewer_event(S, {delete_actors, [To]}), {noreply, S}; 'b' -> E = S#state.filtered_event, From = E#event.from, To = E#event.to, send_viewer_event(S, {delete_actors, [From, To]}), {noreply, S}; 'F' -> E = S#state.filtered_event, From = E#event.from, send_viewer_event(S, {insert_actors, [From]}), {noreply, S}; 'T' -> E = S#state.filtered_event, To = E#event.to, send_viewer_event(S, {insert_actors, [To]}), {noreply, S}; 'B' -> E = S#state.filtered_event, From = E#event.from, To = E#event.to, send_viewer_event(S, {insert_actors, [From, To]}), {noreply, S}; 's' -> E = S#state.filtered_event, From = E#event.from, To = E#event.to, First = et_collector:make_key(S#state.event_order, E), Mode = {search_actors, forward, First, [From, To]}, send_viewer_event(S, {mode, Mode}), {noreply, S}; 'r' -> E = S#state.filtered_event, From = E#event.from, To = E#event.to, First = et_collector:make_key(S#state.event_order, E), Mode = {search_actors, reverse, First, [From, To]}, send_viewer_event(S, {mode, Mode}), {noreply, S}; 'a' -> send_viewer_event(S, {mode, all}), {noreply, S}; 0 -> case lists:keysearch(?DEFAULT_FILTER_NAME, #filter.name, S#state.filters) of {value, F} when is_record(F, filter) -> ChildState= S#state{active_filter = F#filter.name}, case gen_server:start_link(?MODULE, [ChildState], []) of {ok, Pid} when S#state.parent_pid =/= self() -> unlink(Pid); _ -> ignore end; false -> ignore end, {noreply, S}; Int when is_integer(Int), Int > 0, Int =< 9 -> case catch lists:nth(Int, S#state.filters) of F when is_record(F, filter) -> ChildState= S#state{active_filter = F#filter.name}, case gen_server:start_link(?MODULE, [ChildState], []) of {ok, Pid} when S#state.parent_pid =/= self() -> unlink(Pid); _ -> ignore end; {'EXIT', _} -> ignore end, {noreply, S}; 'Shift_L' -> {noreply, S}; 'Shift_R' -> {noreply, S}; 'Caps_Lock' -> {noreply, S}; _ -> io:format("~p: ignored: ~p~n", [?MODULE, KeySym]), {noreply, S} end; handle_info({gs, _Obj, configure, [], [W, H | _]}, S) -> gs:config(S#state.packer, [{width, W},{height, H}]), S2 = S#state{width = W, height = H}, {noreply, S2}; handle_info({'EXIT', Pid, Reason}, S) -> if Pid =:= S#state.parent_pid -> unlink(Pid), {stop, Reason, S}; true -> {noreply, S} end; handle_info(Info, S) -> ok = error_logger:format("~p(~p): handle_info(~p, ~p)~n", [?MODULE, self(), Info, S]), {noreply, S}. %%---------------------------------------------------------------------- %% Func: terminate/2 %% Purpose: Shutdown the server %% Returns: any (ignored by gen_server) %%---------------------------------------------------------------------- terminate(_Reason, _S) -> ignore. %%---------------------------------------------------------------------- %% Func: code_change/3 %% Purpose: Convert process state when code is changed %% Returns: {ok, NewState} %%---------------------------------------------------------------------- code_change(_OldVsn, S, _Extra) -> {ok, S}. %%%---------------------------------------------------------------------- %%% Handle graphics %%%---------------------------------------------------------------------- create_window(S) -> H = S#state.height, W = S#state.width, Name = S#state.active_filter, Title = lists:concat([?MODULE, " (filter: ", Name, ")"]), WinOpt = [{title, Title}, {configure, true}, {width, W}, {height, H}], GS = gs:start(), Win = gs:window(GS, WinOpt), Bar = gs:menubar(Win, []), create_file_menu(Bar), PackerOpt = [{packer_x, [{stretch, 1}]}, {packer_y, [{stretch, 1}, {fixed, 25}]}, {x, 0}, {y, 25}], Packer = gs:frame(Win, PackerOpt), EditorOpt = [{pack_xy, {1, 1}}, {vscroll, right}, {hscroll, bottom}, {wrap, none}, {bg, lightblue}, {font, {courier, 12}}], Editor = gs:editor(Packer, EditorOpt), FilteredEvent = config_editor(Editor, S), S2 = S#state{win = Win, packer = Packer, filtered_event = FilteredEvent}, create_hide_menu(Bar, S2), create_search_menu(Bar, S2), create_filter_menu(Bar, S#state.filters), gs:config(Packer, [{width, W}, {height, H}]), gs:config(Win, [{map,true}, {keypress, true}]), S2. create_file_menu(Bar) -> Button = gs:menubutton(Bar, [{label, {text, "File"}}]), Menu = gs:menu(Button, []), gs:menuitem(close, Menu, [{label, {text,"Close (c)"}}]), gs:menuitem(save, Menu, [{label, {text,"Save"}}]). create_filter_menu(Bar, Filters) -> Button = gs:menubutton(Bar, [{label, {text, "Filters"}}]), Menu = gs:menu(Button, []), gs:menuitem(Menu, [{label, {text, "Select Filter"}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), Item = fun(F, N) when F#filter.name =:= ?DEFAULT_FILTER_NAME-> Label = lists:concat([pad_string(F#filter.name, 20), "(0)"]), gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), N + 1; (F, N) -> Name = F#filter.name, Label = lists:concat([pad_string(Name, 20), "(", N, ")"]), gs:menuitem(Menu, [{label, {text, Label}}, {data, F}]), N + 1 end, Filters2 = lists:keysort(#filter.name, Filters), lists:foldl(Item, 1, Filters2), Menu. create_hide_menu(Bar, S) -> Button = gs:menubutton(Bar, [{label, {text, "Hide"}}]), Menu = gs:menu(Button, []), E = S#state.filtered_event, From = E#event.from, To = E#event.to, if S#state.viewer_pid =:= undefined -> ignore; From =:= To -> gs:menuitem(Menu, [{label, {text, "Hide actor in Viewer "}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem({hide, [From]}, Menu, [{label, {text,"From=To (f|t|b)"}}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem(Menu, [{label, {text, "Show actor in Viewer "}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem({show, [From]}, Menu, [{label, {text,"From=To (F|T|B)"}}]); true -> gs:menuitem(Menu, [{label, {text, "Hide actor in Viewer "}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem({hide, [From]}, Menu, [{label, {text,"From (f)"}}]), gs:menuitem({hide, [To]}, Menu, [{label, {text,"To (t)"}}]), gs:menuitem({hide, [From, To]}, Menu, [{label, {text,"Both (b)"}}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem(Menu, [{label, {text, "Show actor in Viewer "}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), gs:menuitem({show, [From]}, Menu, [{label, {text,"From (F)"}}]), gs:menuitem({show, [To]}, Menu, [{label, {text,"To (T)"}}]), gs:menuitem({show, [From, To]}, Menu, [{label, {text,"Both (B)"}}]) end. create_search_menu(Bar, S) -> Button = gs:menubutton(Bar, [{label, {text, "Search"}}]), Menu = gs:menu(Button, []), E = S#state.filtered_event, From = E#event.from, To = E#event.to, gs:menuitem(Menu, [{label, {text, "Search in Viewer "}}, {bg, lightblue}, {enable, false}]), gs:menuitem(Menu, [{itemtype, separator}]), if S#state.viewer_pid =:= undefined -> S; From =:= To -> Key = et_collector:make_key(S#state.event_order, E), ModeS = {search_actors, forward, Key, [From]}, ModeR = {search_actors, reverse, Key, [From]}, gs:menuitem({mode, ModeS}, Menu, [{label, {text,"Forward from this event (s)"}}]), gs:menuitem({mode, ModeR}, Menu, [{label, {text,"Reverse from this event (r)"}}]); true -> Key = et_collector:make_key(S#state.event_order, E), ModeS = {search_actors, forward, Key, [From, To]}, ModeR = {search_actors, reverse, Key, [From, To]}, gs:menuitem({mode, ModeS}, Menu, [{label, {text,"Forward from this event (s)"}}]), gs:menuitem({mode, ModeR}, Menu, [{label, {text,"Reverse from this event (r)"}}]) end, gs:menuitem({mode, all}, Menu, [{label, {text,"Abort search. Display all (a)"}}]). config_editor(Editor, S) -> Event = S#state.event, Name = S#state.active_filter, {value, F} = lists:keysearch(Name, #filter.name, S#state.filters), FilterFun = F#filter.function, case catch FilterFun(Event) of true -> do_config_editor(Editor, Event, lightblue, S#state.event_order); {true, Event2} when is_record(Event2, event) -> do_config_editor(Editor, Event2, lightblue, S#state.event_order); false -> do_config_editor(Editor, Event, red, S#state.event_order); Bad -> Contents = {bad_filter, Name, Bad}, BadEvent = Event#event{contents = Contents}, do_config_editor(Editor, BadEvent, red, S#state.event_order) end. do_config_editor(Editor, Event, Colour, TsKey) -> String = event_to_string(Event, TsKey), gs:config(Editor, {insert, {'end', String}}), gs:config(Editor, {enable, false}), gs:config(Editor, {bg, Colour}), Event. %%%---------------------------------------------------------------------- %%% String handling %%%---------------------------------------------------------------------- term_to_string(Term) -> case catch io_lib:format("~s", [Term]) of {'EXIT', _} -> io_lib:format("~p", [Term]); GoodString -> GoodString end. now_to_string({Mega, Sec, Micro} = Now) when is_integer(Mega), is_integer(Sec), is_integer(Micro) -> {{Y, Mo, D}, {H, Mi, S}} = calendar:now_to_universal_time(Now), lists:concat([Y, "-", Mo, "-", D, " ", H, ".", Mi, ".", S, ".", Micro]); now_to_string(Other) -> term_to_string(Other). event_to_string(Event, TsKey) -> ReportedTs = Event#event.trace_ts, ParsedTs = Event#event.event_ts, Deep = ["DETAIL LEVEL: ", term_to_string(Event#event.detail_level), "\nLABEL: ", term_to_string(Event#event.label), case Event#event.from =:= Event#event.to of true -> ["\nACTOR: ", term_to_string(Event#event.from)]; false -> ["\nFROM: ", term_to_string(Event#event.from), "\nTO: ", term_to_string(Event#event.to)] end, case ReportedTs =:= ParsedTs of true -> ["\nPARSED: ", now_to_string(ParsedTs)]; false -> case TsKey of trace_ts -> ["\nTRACE_TS: ", now_to_string(ReportedTs), "\nEVENT_TS: ", now_to_string(ParsedTs)]; event_ts -> ["\nEVENT_TS: ", now_to_string(ParsedTs), "\nTRACE_TS: ", now_to_string(ReportedTs)] end end, "\nCONTENTS:\n\n", term_to_string(Event#event.contents)], lists:flatten(Deep). pad_string(Atom, MinLen) when is_atom(Atom) -> pad_string(atom_to_list(Atom), MinLen); pad_string(String, MinLen) when is_integer(MinLen), MinLen >= 0 -> Len = length(String), case Len >= MinLen of true -> String; false -> String ++ lists:duplicate(MinLen - Len, $ ) end. send_viewer_event(S, Event) -> case S#state.viewer_pid of ViewerPid when is_pid(ViewerPid) -> ViewerPid ! {et, Event}; undefined -> ignore end.