%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2000-2018. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %%---------------------------------------------------------------------- %% Purpose: Displays details of a trace event %%---------------------------------------------------------------------- -module(et_wx_contents_viewer). -behaviour(wx_object). %% 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, handle_event/2]). -include("../include/et.hrl"). -include("et_internal.hrl"). -include_lib("wx/include/wx.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: Frame object frame, % GUI: Frame object panel, % GUI: Panel object width, % GUI: Window width height, editor, menu_data, % GUI: Window height wx_debug, % GUI: WX debug level trap_exit}). % trap_exit process flag %%%---------------------------------------------------------------------- %%% Client side %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% start_link(Options) -> {ok, ContentsPid} | {error, Reason} %% %% Start a 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} -> try WxRef = wx_object:start_link(?MODULE, [S], []), Pid = wx_object:get_pid(WxRef), if S#state.parent_pid =/= self() -> unlink(Pid); true -> ok end, {ok, Pid} catch error:Reason:Stacktrace -> {error, {'EXIT', Reason, Stacktrace}} 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, wx_debug = 0, trap_exit = true}. 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); ParentPid =:= undefined -> parse_opt(T, S#state{parent_pid = ParentPid}); {viewer_pid, ViewerPid} when is_pid(ViewerPid) -> parse_opt(T, S#state{viewer_pid = ViewerPid}); {wx_debug, Level} -> parse_opt(T, S#state{wx_debug = Level}); {trap_exit, Bool} when Bool =:= true; Bool =:= false-> parse_opt(T, S#state{trap_exit = Bool}); {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) when is_pid(ContentsPid) -> Type = process, MonitorRef = erlang:monitor(Type, ContentsPid), ContentsPid ! {stop, self()}, receive {'DOWN', MonitorRef, Type, ContentsPid, shutdown} -> ok; {'DOWN', MonitorRef, Type, ContentsPid, Reason} -> {error, Reason} end. %% call(Frame, Request) -> %% wx_object:call(Frame, 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, S#state.trap_exit), case S#state.parent_pid of undefined -> ok; ParentPid -> link(ParentPid) end, wx:debug(S#state.wx_debug), S2 = create_window(S), {S2#state.frame, 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(Request, From, S) -> ok = error_logger:format("~p(~p): handle_call(~tp, ~tp, ~tp)~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(~tp, ~tp)~n", [?MODULE, self(), Msg, S]), {noreply, S}. %%---------------------------------------------------------------------- %% Func: handle_event/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_event(#wx{id = Id, event = #wxCommand{type = command_menu_selected}}, S) -> case proplists:get_value(Id, S#state.menu_data) of undefined -> ok; Data when is_record(Data, filter) -> F = Data, ChildState = S#state{active_filter = F#filter.name}, _ = wx_object:start_link(?MODULE, [ChildState], []), ok; {hide, Actors} -> send_viewer_event(S, {delete_actors, Actors}); {show, Actors} -> send_viewer_event(S, {insert_actors, Actors}); {mode, Mode} -> send_viewer_event(S, {mode, Mode}); Nyi -> ok = error_logger:format("~p: click ~p ignored (nyi)~n", [?MODULE, Nyi]) end, case Id of ?wxID_EXIT -> wxFrame:destroy(S#state.frame), opt_unlink(S#state.parent_pid), {stop, shutdown, S}; ?wxID_SAVE -> Event = S#state.event, TimeStamp = case S#state.event_order of trace_ts -> Event#event.trace_ts; event_ts -> Event#event.event_ts end, FileName = lists:flatten(["et_contents_viewer_", now_to_string(TimeStamp), ".txt"]), Style = ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT, Msg = "Select a file to save events to", case select_file(S#state.frame, Msg, filename:absname(FileName), Style) of {ok, FileName2} -> EventString = event_to_string(Event, S#state.event_order), Bin = unicode:characters_to_binary(EventString), ok = file:write_file(FileName2, Bin); cancel -> ok end, {noreply, S}; ?wxID_PRINT -> Html = wxHtmlEasyPrinting:new([{parentWindow, S#state.win}]), Text = "
" ++ wxTextCtrl:getValue(S#state.editor) ++ "
", wxHtmlEasyPrinting:previewText(Html, Text), {noreply, S}; _ -> {noreply, S} end; handle_event(#wx{event = #wxKey{rawCode = KeyCode}}, S) -> case KeyCode of $c -> wxFrame:destroy(S#state.frame), opt_unlink(S#state.parent_pid), {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}, _ = wx_object:start_link(?MODULE, [ChildState], []), ok; false -> ok end, {noreply, S}; Int when is_integer(Int), Int > $0, Int =< $9 -> case catch lists:nth(Int-$0, S#state.filters) of F when is_record(F, filter) -> ChildState = S#state{active_filter = F#filter.name}, _ = wx_object:start_link(?MODULE, [ChildState], []), ok; {'EXIT', _} -> ok end, {noreply, S}; _ -> io:format("~p: ignored: ~p~n", [?MODULE, KeyCode]), {noreply, S} end; handle_event(#wx{event = #wxClose{}}, S) -> opt_unlink(S#state.parent_pid), {stop, shutdown, S}; handle_event(#wx{event = #wxSize{size = {W, H}}}, S) -> S2 = S#state{width = W, height = H}, {noreply, S2}; handle_event(Wx = #wx{}, S) -> io:format("~p got an unexpected event: ~tp\n", [self(), Wx]), {noreply, S}. %%---------------------------------------------------------------------- %% Func: handle_info/2 %% Returns: {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} (terminate/2 is called) %%---------------------------------------------------------------------- handle_info({stop, _From}, S) -> wxFrame:destroy(S#state.frame), opt_unlink(S#state.parent_pid), {stop, shutdown, S}; handle_info({'EXIT', Pid, Reason}, S) -> if Pid =:= S#state.parent_pid -> wxFrame:destroy(S#state.frame), opt_unlink(S#state.parent_pid), {stop, Reason, S}; true -> {noreply, S} end; handle_info(Info, S) -> ok = error_logger:format("~p(~p): handle_info(~tp, ~tp)~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 %%%---------------------------------------------------------------------- opt_unlink(Pid) -> if Pid =:= undefined -> ignore; true -> unlink(Pid) end. create_window(S) -> H = S#state.height, W = S#state.width, Name = S#state.active_filter, Title = lists:concat([?MODULE, " (filter: ", Name, ")"]), WinOpt = [{size, {W,H}}], Frame = wxFrame:new(wx:null(), ?wxID_ANY, Title, WinOpt), _ = wxFrame:createStatusBar(Frame), Panel = wxPanel:new(Frame, []), Bar = wxMenuBar:new(), _ = wxFrame:setMenuBar(Frame,Bar), create_file_menu(Bar), Editor = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, 0 bor ?wxDEFAULT bor ?wxTE_RICH2 %% Needed on Windows bor ?wxTE_MULTILINE bor ?wxTE_READONLY bor ?wxTE_DONTWRAP}]), Font = wxFont:new(10, ?wxFONTFAMILY_TELETYPE, ?wxNORMAL, ?wxNORMAL,[]), TextAttr = wxTextAttr:new(?wxBLACK, [{font, Font}]), _ = wxTextCtrl:setDefaultStyle(Editor, TextAttr), Sizer = wxBoxSizer:new(?wxHORIZONTAL), _ = wxSizer:add(Sizer, Editor, [{flag, ?wxEXPAND}, {proportion, 1}]), FilteredEvent = config_editor(Editor, S), S2 = S#state{win = Frame, panel = Panel, filtered_event = FilteredEvent}, HideData = create_hide_menu(Bar, S2), SearchData = create_search_menu(Bar, S2), FilterData = create_filter_menu(Bar, S#state.filters), _ = wxFrame:connect(Frame, command_menu_selected, []), _ = wxFrame:connect(Frame, key_up), _ = wxFrame:connect(Frame, close_window, [{skip,true}]), _ = wxFrame:setFocus(Frame), _ = wxPanel:setSizer(Panel, Sizer), _ = wxSizer:fit(Sizer, Panel), _ = wxFrame:show(Frame), S2#state{menu_data = HideData++SearchData++FilterData, editor = Editor, frame = Frame}. menuitem(Menu, Id, Text, UserData) -> Item = wxMenu:append(Menu, Id, Text), {wxMenuItem:getId(Item), UserData}. create_file_menu(Bar) -> Menu = wxMenu:new([]), _ = wxMenu:append(Menu, ?wxID_SAVE, "Save"), _ = wxMenu:append(Menu, ?wxID_PRINT,"Print"), _ = wxMenu:appendSeparator(Menu), _ = wxMenu:append(Menu, ?wxID_EXIT, "Close"), _ = wxMenuBar:append(Bar, Menu, "File"), ok. create_filter_menu(Bar, Filters) -> Menu = wxMenu:new([]), _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Select Filter"), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), Item = fun(F, {N,Acc}) when F#filter.name =:= ?DEFAULT_FILTER_NAME-> Label = lists:concat([pad_string(F#filter.name, 20, $\ , right), "(0)"]), MenuItem = menuitem(Menu, ?wxID_ANY, Label, F), {N + 1, [MenuItem|Acc]}; (F, {N, Acc}) -> Name = F#filter.name, Label = lists:concat([pad_string(Name, 20, $\ , right), "(", N, ")"]), MenuItem = menuitem(Menu, ?wxID_ANY, Label, F), {N + 1, [MenuItem|Acc]} end, Filters2 = lists:keysort(#filter.name, Filters), {_,MenuData} = lists:foldl(Item, {1, []}, Filters2), _ = wxMenuBar:append(Bar, Menu, "Filters"), MenuData. create_hide_menu(Bar, S) -> Menu = wxMenu:new([]), E = S#state.filtered_event, From = E#event.from, To = E#event.to, MenuData = if S#state.viewer_pid =:= undefined -> ignore; From =:= To -> _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Hide actor in Viewer "), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), Hide = menuitem(Menu, ?wxID_ANY, "From=To (f|t|b)", {hide, [From]}), _ = wxMenu:appendSeparator(Menu), _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Show actor in Viewer "), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), Show = menuitem(Menu, ?wxID_ANY, "From=To (F|T|B)", {show, [From]}), [Show,Hide]; true -> _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Hide actor in Viewer "), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), Hide = [menuitem(Menu, ?wxID_ANY, "From (f)", {hide, [From]}), menuitem(Menu, ?wxID_ANY, "To (t)", {hide, [To]}), menuitem(Menu, ?wxID_ANY, "Both (b)", {hide, [From, To]})], _ = wxMenu:appendSeparator(Menu), _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Show actor in Viewer "), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), Show = [menuitem(Menu, ?wxID_ANY, "From (F)", {show, [From]}), menuitem(Menu, ?wxID_ANY, "To (T)", {show, [To]}), menuitem(Menu, ?wxID_ANY, "Both (B)", {show, [From, To]})], Show++Hide end, _ = wxMenuBar:append(Bar, Menu, "Hide"), MenuData. create_search_menu(Bar, S) -> Menu = wxMenu:new([]), E = S#state.filtered_event, From = E#event.from, To = E#event.to, _ = wxMenuItem:enable(wxMenu:append(Menu, ?wxID_ANY, "Search in Viewer "), [{enable, false}]), _ = wxMenu:appendSeparator(Menu), MenuData = if S#state.viewer_pid =:= undefined -> [menuitem(Menu, ?wxID_ANY, "Abort search. Display all (a)", {mode, all})]; From =:= To -> Key = et_collector:make_key(S#state.event_order, E), ModeS = {search_actors, forward, Key, [From]}, ModeR = {search_actors, reverse, Key, [From]}, [menuitem(Menu, ?wxID_ANY, "Forward from this event (s)", {mode, ModeS}), menuitem(Menu, ?wxID_ANY, "Reverse from this event (r)", {mode, ModeR}), menuitem(Menu, ?wxID_ANY, "Abort search. Display all (a)", {mode, all})]; 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]}, [menuitem(Menu, ?wxID_ANY, "Forward from this event (s)", {mode, ModeS}), menuitem(Menu, ?wxID_ANY, "Reverse from this event (r)", {mode, ModeR}), menuitem(Menu, ?wxID_ANY, "Abort search. Display all (a)", {mode, all})] end, _ = wxMenuBar:append(Bar, Menu, "Search"), MenuData. 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), wxTextCtrl:appendText(Editor, String), Event. %%%---------------------------------------------------------------------- %%% String handling %%%---------------------------------------------------------------------- term_to_string(Term) -> case catch io_lib:format("~ts", [Term]) of {'EXIT', _} -> io_lib:format("~tp", [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, "-", pad_string(Mo, 2, $0, left), "-", pad_string(D, 2, $0, left), "T", pad_string(H, 2, $0, left), ":", pad_string(Mi, 2, $0, left), ":", pad_string(S, 2, $0, left), ".", 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(Int, MinLen, Char, Dir) when is_integer(Int) -> pad_string(integer_to_list(Int), MinLen, Char, Dir); pad_string(Atom, MinLen, Char, Dir) when is_atom(Atom) -> pad_string(atom_to_list(Atom), MinLen, Char, Dir); pad_string(String, MinLen, Char, Dir) when is_integer(MinLen), MinLen >= 0 -> Len = string:length(String), case {Len >= MinLen, Dir} of {true, _} -> String; {false, right} -> String ++ lists:duplicate(MinLen - Len, Char); {false, left} -> lists:duplicate(MinLen - Len, Char) ++ String end. send_viewer_event(S, Event) -> case S#state.viewer_pid of ViewerPid when is_pid(ViewerPid) -> ViewerPid ! {et, Event}, ok; undefined -> ok end. select_file(Frame, Message, DefaultFile, Style) -> Dialog = wxFileDialog:new(Frame, [{message, Message}, {defaultDir, filename:dirname(DefaultFile)}, {defaultFile, filename:basename(DefaultFile)}, {style, Style}]), Choice = case wxMessageDialog:showModal(Dialog) of ?wxID_CANCEL -> cancel; ?wxID_OK -> {ok, wxFileDialog:getPath(Dialog)} end, wxFileDialog:destroy(Dialog), Choice.