aboutsummaryrefslogtreecommitdiffstats
path: root/lib/observer
diff options
context:
space:
mode:
authorDan Gudmundsson <[email protected]>2011-11-19 08:46:53 +0100
committerDan Gudmundsson <[email protected]>2011-11-19 08:46:53 +0100
commitf9ab9f01694473e2843f3f1526ef60129a2f123b (patch)
treea106ad6c8d473a3527507c730f2f42f1c0a78b78 /lib/observer
parentde1dc9682e8c6c56e03d5deb20019ff98d0b569a (diff)
parent39730743916b300eb3e229c4e6e8a2987487d797 (diff)
downloadotp-f9ab9f01694473e2843f3f1526ef60129a2f123b.tar.gz
otp-f9ab9f01694473e2843f3f1526ef60129a2f123b.tar.bz2
otp-f9ab9f01694473e2843f3f1526ef60129a2f123b.zip
Merge branch 'dgud/observer/gui/OTP-4779'
* dgud/observer/gui/OTP-4779: [observer] Implemented basic tracing functionality [observer] Work around wxWidgets windows bug [observer] Rework tracing part of the gui [observer] Fix portability bugs [observer] Improve process_info window [observer] Misc cleanup and fixes [observer] Clean up code [observer] Add a [d]ets viewer, tv replacement [observer] Started on a wx gui
Diffstat (limited to 'lib/observer')
-rw-r--r--lib/observer/priv/erlang_observer.pngbin0 -> 4351 bytes
-rw-r--r--lib/observer/src/Makefile23
-rw-r--r--lib/observer/src/observer.erl25
-rw-r--r--lib/observer/src/observer_defs.hrl62
-rw-r--r--lib/observer/src/observer_lib.erl297
-rw-r--r--lib/observer/src/observer_pro_wx.erl608
-rw-r--r--lib/observer/src/observer_procinfo.erl274
-rw-r--r--lib/observer/src/observer_sys_wx.erl223
-rw-r--r--lib/observer/src/observer_trace_wx.erl628
-rw-r--r--lib/observer/src/observer_traceoptions_wx.erl572
-rw-r--r--lib/observer/src/observer_tv.hrl34
-rw-r--r--lib/observer/src/observer_tv_table.erl801
-rw-r--r--lib/observer/src/observer_tv_wx.erl475
-rw-r--r--lib/observer/src/observer_wx.erl537
-rw-r--r--lib/observer/src/ttb.erl44
15 files changed, 4581 insertions, 22 deletions
diff --git a/lib/observer/priv/erlang_observer.png b/lib/observer/priv/erlang_observer.png
new file mode 100644
index 0000000000..78e70461b1
--- /dev/null
+++ b/lib/observer/priv/erlang_observer.png
Binary files differ
diff --git a/lib/observer/src/Makefile b/lib/observer/src/Makefile
index 3875b62101..95954d8587 100644
--- a/lib/observer/src/Makefile
+++ b/lib/observer/src/Makefile
@@ -1,19 +1,19 @@
#
# %CopyrightBegin%
-#
+#
# Copyright Ericsson AB 2002-2011. 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%
#
include $(ERL_TOP)/make/target.mk
@@ -41,8 +41,19 @@ MODULES= \
etop_gui \
etop_tr \
etop_txt \
+ observer \
+ observer_lib \
+ observer_wx \
+ observer_pro_wx \
+ observer_procinfo \
+ observer_sys_wx \
+ observer_trace_wx \
+ observer_traceoptions_wx \
+ observer_tv_table \
+ observer_tv_wx \
ttb \
ttb_et
+
HRL_FILES= \
../include/etop.hrl
INTERNAL_HRL_FILES= \
@@ -54,7 +65,7 @@ EXAMPLE_FILES= multitrace.erl
TARGET_FILES= $(MODULES:%=$(EBIN)/%.$(EMULATOR)) $(APP_TARGET) $(APPUP_TARGET)
PRIVDIR= ../priv
-WEBTOOLFILES= $(PRIVDIR)/crashdump_viewer.tool
+WEBTOOLFILES= $(PRIVDIR)/crashdump_viewer.tool $(PRIVDIR)/erlang_observer.png
BINDIR= $(PRIVDIR)/bin
ifeq ($(findstring win32,$(TARGET)),win32)
WIN32_EXECUTABLES= $(BINDIR)/etop.bat $(BINDIR)/getop.bat $(BINDIR)/cdv.bat
@@ -109,7 +120,7 @@ docs:
# ----------------------------------------------------
# Release Target
-# ----------------------------------------------------
+# ----------------------------------------------------
include $(ERL_TOP)/make/otp_release_targets.mk
release_spec: opt
diff --git a/lib/observer/src/observer.erl b/lib/observer/src/observer.erl
new file mode 100644
index 0000000000..098100e8ee
--- /dev/null
+++ b/lib/observer/src/observer.erl
@@ -0,0 +1,25 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer).
+
+-export([start/0]).
+
+
+start() ->
+ observer_wx:start().
diff --git a/lib/observer/src/observer_defs.hrl b/lib/observer/src/observer_defs.hrl
new file mode 100644
index 0000000000..d83a1e2fa5
--- /dev/null
+++ b/lib/observer/src/observer_defs.hrl
@@ -0,0 +1,62 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-record(trace_options, {send = false,
+ treceive = false,
+ functions = false,
+ events = false,
+ on_1st_spawn = false,
+ on_all_spawn = false,
+ on_1st_link = false,
+ on_all_link = false,
+ main_window = true}).
+
+-record(match_spec, {name = "",
+ term = [],
+ str = [],
+ func = ""}).
+
+-record(tpattern, {m, fa, ms}).
+
+-record(traced_func, {func_name, %atom
+ arity, %integer
+ match_spec = #match_spec{}}).
+
+-record(on_spawn, {checkbox, all_spawn, first_spawn}).
+
+-record(on_link, {checkbox, all_link, first_link}).
+
+-record(pid, {window, traced}).
+
+-record(create_menu,
+ {id,
+ text,
+ type = append,
+ check = false
+ }).
+
+-record(attrs, {even, odd, deleted, changed, searched}).
+-define(EVEN(Row), ((Row rem 2) =:= 0)).
+-define(BG_EVEN, {230,230,250}).
+-define(BG_ODD, {255,255,255}).
+-define(BG_DELETED, {100,100,100}).
+-define(FG_DELETED, {240,30,30}).
+-define(BG_SEARCHED,{235,215,90}).
+-define(BG_CHANGED, {230,230,250}).
+
+-define(LCTRL_WDECR, 4). %% Remove some pixels in column width to avoid creating unnecessary scrollbar
diff --git a/lib/observer/src/observer_lib.erl b/lib/observer/src/observer_lib.erl
new file mode 100644
index 0000000000..90c270e977
--- /dev/null
+++ b/lib/observer/src/observer_lib.erl
@@ -0,0 +1,297 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer_lib).
+
+-export([get_wx_parent/1,
+ display_info_dialog/1,
+ interval_dialog/4, start_timer/1, stop_timer/1,
+ display_info/2, fill_info/2, update_info/2, to_str/1,
+ create_menus/3, create_menu_item/3,
+ create_attrs/0
+ ]).
+
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+
+get_wx_parent(Window) ->
+ Parent = wxWindow:getParent(Window),
+ case wx:is_null(Parent) of
+ true -> Window;
+ false -> get_wx_parent(Parent)
+ end.
+
+interval_dialog(Parent0, {Timer, Value}, Min, Max) ->
+ Parent = get_wx_parent(Parent0),
+ Dialog = wxDialog:new(Parent, ?wxID_ANY, "Update Interval",
+ [{style, ?wxDEFAULT_DIALOG_STYLE bor
+ ?wxRESIZE_BORDER}]),
+ Panel = wxPanel:new(Dialog),
+ Check = wxCheckBox:new(Panel, ?wxID_ANY, "Periodical refresh"),
+ wxCheckBox:setValue(Check, Timer /= false),
+ Style = ?wxSL_HORIZONTAL bor ?wxSL_AUTOTICKS bor ?wxSL_LABELS,
+ Slider = wxSlider:new(Panel, ?wxID_ANY, Value, Min, Max,
+ [{style, Style}, {size, {200, -1}}]),
+ wxWindow:enable(Slider, [{enable, Timer /= false}]),
+ InnerSizer = wxBoxSizer:new(?wxVERTICAL),
+ Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
+ Flags = [{flag, ?wxEXPAND bor ?wxALL}, {border, 2}],
+ wxSizer:add(InnerSizer, Check, Flags),
+ wxSizer:add(InnerSizer, Slider, Flags),
+ wxPanel:setSizer(Panel, InnerSizer),
+ TopSizer = wxBoxSizer:new(?wxVERTICAL),
+ wxSizer:add(TopSizer, Panel, [{flag, ?wxEXPAND bor ?wxALL}, {border, 5}]),
+ wxSizer:add(TopSizer, Buttons, [{flag, ?wxEXPAND}]),
+ wxWindow:setSizerAndFit(Dialog, TopSizer),
+ wxSizer:setSizeHints(TopSizer, Dialog),
+ wxCheckBox:connect(Check, command_checkbox_clicked,
+ [{callback, fun(#wx{event=#wxCommand{commandInt=Enable0}},_) ->
+ Enable = Enable0 > 0,
+ wxWindow:enable(Slider, [{enable, Enable}])
+ end}]),
+ Res = case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ Enabled = wxCheckBox:isChecked(Check),
+ setup_timer(Enabled, {Timer, wxSlider:getValue(Slider)});
+ ?wxID_CANCEL ->
+ {Timer, Value}
+ end,
+ wxDialog:destroy(Dialog),
+ Res.
+
+stop_timer(Timer = {false, _}) -> Timer;
+stop_timer(Timer = {true, _}) -> Timer;
+stop_timer(Timer = {_, Intv}) ->
+ setup_timer(false, Timer),
+ {true, Intv}.
+start_timer(Intv) when is_integer(Intv) ->
+ setup_timer(true, {true, Intv});
+start_timer(Timer) ->
+ setup_timer(true, Timer).
+
+setup_timer(false, {Timer, Value})
+ when is_boolean(Timer) ->
+ {false, Value};
+setup_timer(true, {false, Value}) ->
+ {ok, Timer} = timer:send_interval(Value * 1000, refresh_interval),
+ {Timer, Value};
+setup_timer(Bool, {Timer, Old}) ->
+ timer:cancel(Timer),
+ setup_timer(Bool, {false, Old}).
+
+display_info_dialog(Str) ->
+ Dlg = wxMessageDialog:new(wx:null(), Str),
+ wxMessageDialog:showModal(Dlg),
+ wxMessageDialog:destroy(Dlg),
+ ok.
+
+%% display_info(Parent, [{Title, [{Label, Info}]}]) -> {Panel, Sizer, InfoFieldsToUpdate}
+display_info(Frame, Info) ->
+ Panel = wxPanel:new(Frame),
+ wxWindow:setBackgroundColour(Panel, {255,255,255}),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ wxSizer:addSpacer(Sizer, 5),
+ Add = fun(BoxInfo) ->
+ {Box, InfoFs} = create_box(Panel, BoxInfo),
+ wxSizer:add(Sizer, Box, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}]),
+ wxSizer:addSpacer(Sizer, 5),
+ InfoFs
+ end,
+ InfoFs = [Add(I) || I <- Info],
+ wxWindow:setSizerAndFit(Panel, Sizer),
+ {Panel, Sizer, InfoFs}.
+
+fill_info([{Str, Key}|Rest], Data) when is_atom(Key); is_function(Key) ->
+ [{Str, get_value(Key, Data)} | fill_info(Rest, Data)];
+fill_info([{Str, {Format, Key}}|Rest], Data)
+ when is_atom(Key); is_function(Key), is_atom(Format) ->
+ [{Str, {Format, get_value(Key,Data)}} | fill_info(Rest, Data)];
+fill_info([{Str,SubStructure}|Rest], Data) when is_list(SubStructure) ->
+ [{Str, fill_info(SubStructure, Data)}|fill_info(Rest,Data)];
+fill_info([{Str,Attrib,SubStructure}|Rest], Data) ->
+ [{Str, Attrib, fill_info(SubStructure, Data)}|fill_info(Rest,Data)];
+fill_info([], _) -> [].
+
+get_value(Key, Data) when is_atom(Key) ->
+ proplists:get_value(Key,Data);
+get_value(Fun, Data) when is_function(Fun) ->
+ Fun(Data).
+
+update_info([Fields|Fs], [{_Header, SubStructure}| Rest]) ->
+ update_info2(Fields, SubStructure),
+ update_info(Fs, Rest);
+update_info([Fields|Fs], [{_Header, _Attrib, SubStructure}| Rest]) ->
+ update_info2(Fields, SubStructure),
+ update_info(Fs, Rest);
+update_info([], []) ->
+ ok.
+
+update_info2([Field|Fs], [{_Str, Value}|Rest]) ->
+ wxStaticText:setLabel(Field, to_str(Value)),
+ update_info2(Fs, Rest);
+update_info2([], []) -> ok.
+
+
+to_str(Value) when is_atom(Value) ->
+ atom_to_list(Value);
+to_str({bytes, B}) ->
+ KB = B div 1024,
+ MB = KB div 1024,
+ if
+ MB > 10 -> integer_to_list(MB) ++ " mB";
+ KB > 0 -> integer_to_list(KB) ++ " kB";
+ true -> integer_to_list(B) ++ " B "
+ end;
+to_str({time_ms, MS}) ->
+ S = MS div 1000,
+ Min = S div 60,
+ Hours = Min div 60,
+ Days = Hours div 24,
+ if
+ Days > 0 -> integer_to_list(Days) ++ " Days";
+ Hours > 0 -> integer_to_list(Hours) ++ " Hours";
+ Min > 0 -> integer_to_list(Min) ++ " Mins";
+ true -> integer_to_list(S) ++ " Secs"
+ end;
+
+to_str({func, {F,A}}) when is_atom(F), is_integer(A) ->
+ lists:concat([F, "/", A]);
+to_str({func, {F,'_'}}) when is_atom(F) ->
+ atom_to_list(F);
+to_str({A, B}) when is_atom(A), is_atom(B) ->
+ lists:concat([A, ":", B]);
+to_str({M,F,A}) when is_atom(M), is_atom(F), is_integer(A) ->
+ lists:concat([M, ":", F, "/", A]);
+to_str(Value) when is_list(Value) ->
+ case lists:all(fun(X) -> is_integer(X) end, Value) of
+ true -> Value;
+ false ->
+ lists:foldl(fun(X, Acc) ->
+ to_str(X) ++ " " ++ Acc end,
+ "", Value)
+ end;
+to_str(Port) when is_port(Port) ->
+ erlang:port_to_list(Port);
+to_str(Pid) when is_pid(Pid) ->
+ pid_to_list(Pid);
+to_str(No) when is_integer(No) ->
+ integer_to_list(No);
+to_str(Term) ->
+ io_lib:format("~w", [Term]).
+
+create_menus(Menus, MenuBar, Type) ->
+ Add = fun({Tag, Ms}, Index) ->
+ create_menu(Tag, Ms, Index, MenuBar, Type)
+ end,
+ [{First, _}|_] = Menus,
+ OnMac = os:type() =:= {unix, darwin},
+ Index = if Type =:= default -> 0;
+ First =:= "File" -> 0;
+ OnMac -> 0;
+ true -> 1
+ end,
+ wx:foldl(Add, Index, Menus),
+ ok.
+
+create_menu("File", MenuItems, Index, MenuBar, Type) ->
+ OnMac = os:type() =:= {unix, darwin},
+ if OnMac, Type =:= default ->
+ Index;
+ not OnMac, Type =:= plugin ->
+ MenuId = wxMenuBar:findMenu(MenuBar, "File"),
+ Menu = wxMenuBar:getMenu(MenuBar, MenuId),
+ lists:foldl(fun(Record, N) ->
+ create_menu_item(Record, Menu, N)
+ end, 0, MenuItems),
+ Index + 1;
+ true ->
+ Menu = wxMenu:new(),
+ lists:foldl(fun(Record, N) ->
+ create_menu_item(Record, Menu, N)
+ end, 0, MenuItems),
+ wxMenuBar:insert(MenuBar, Index, Menu, "File"),
+ Index+1
+ end;
+create_menu(Name, MenuItems, Index, MenuBar, _Type) ->
+ Menu = wxMenu:new(),
+ lists:foldl(fun(Record, N) ->
+ create_menu_item(Record, Menu, N)
+ end, 0, MenuItems),
+ wxMenuBar:insert(MenuBar, Index, Menu, Name),
+ Index+1.
+
+create_menu_item(#create_menu{id = ?wxID_HELP=Id}, Menu, Index) ->
+ wxMenu:insert(Menu, Index, Id),
+ Index+1;
+create_menu_item(#create_menu{id = Id, text = Text, type = Type, check = Check}, Menu, Index) ->
+ case Type of
+ append ->
+ wxMenu:insert(Menu, Index, Id, [{text, Text}]);
+ check ->
+ wxMenu:insertCheckItem(Menu, Index, Id, Text),
+ wxMenu:check(Menu, Id, Check);
+ radio ->
+ wxMenu:insertRadioItem(Menu, Index, Id, Text),
+ wxMenu:check(Menu, Id, Check);
+ separator ->
+ wxMenu:insertSeparator(Menu, Index)
+ end,
+ Index+1;
+create_menu_item(separator, Menu, Index) ->
+ wxMenu:insertSeparator(Menu, Index),
+ Index+1.
+
+create_attrs() ->
+ Font = wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT),
+ Text = case wxSystemSettings:getColour(?wxSYS_COLOUR_LISTBOXTEXT) of
+ {255,255,255,_} -> {10,10,10}; %% Is white on Mac for some reason
+ Color -> Color
+ end,
+ #attrs{even = wxListItemAttr:new(Text, ?BG_EVEN, Font),
+ odd = wxListItemAttr:new(Text, ?BG_ODD, Font),
+ deleted = wxListItemAttr:new(?FG_DELETED, ?BG_DELETED, Font),
+ changed = wxListItemAttr:new(Text, ?BG_CHANGED, Font),
+ searched = wxListItemAttr:new(Text, ?BG_SEARCHED, Font)
+ }.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+get_box_info({Title, List}) when is_list(List) -> {Title, ?wxALIGN_LEFT, List};
+get_box_info({Title, left, List}) -> {Title, ?wxALIGN_LEFT, List};
+get_box_info({Title, right, List}) -> {Title, ?wxALIGN_RIGHT, List}.
+
+create_box(Panel, Data) ->
+ {Title, Align, Info} = get_box_info(Data),
+ Box = wxStaticBoxSizer:new(?wxHORIZONTAL, Panel, [{label, Title}]),
+ Left = wxBoxSizer:new(?wxVERTICAL),
+ Right = wxBoxSizer:new(?wxVERTICAL),
+ Expand = [{flag, ?wxEXPAND}],
+ ExpAlign = [{flag, Align}],
+ AddRow = fun({Desc, Value}) ->
+ wxSizer:add(Left, wxStaticText:new(Panel, ?wxID_ANY, Desc ++ ":"), Expand),
+ Field = wxStaticText:new(Panel, ?wxID_ANY, to_str(Value)),
+ wxSizer:add(Right, Field, ExpAlign),
+ Field
+ end,
+ InfoFields = [AddRow(Entry) || Entry <- Info],
+ wxSizer:add(Box, Left),
+ wxSizer:addSpacer(Box, 10),
+ wxSizer:add(Box, Right),
+ wxSizer:addSpacer(Box, 30),
+ {Box, InfoFields}.
diff --git a/lib/observer/src/observer_pro_wx.erl b/lib/observer/src/observer_pro_wx.erl
new file mode 100644
index 0000000000..cfc1c0665f
--- /dev/null
+++ b/lib/observer/src/observer_pro_wx.erl
@@ -0,0 +1,608 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+-module(observer_pro_wx).
+
+-behaviour(wx_object).
+
+-export([start_link/2]).
+
+%% wx_object callbacks
+-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
+ handle_event/2, handle_cast/2]).
+
+-include_lib("wx/include/wx.hrl").
+-include("../include/etop.hrl").
+-include("observer_defs.hrl").
+-include("etop_defs.hrl").
+
+%% Defines
+-define(COL_PID, 0).
+-define(COL_NAME, 1).
+-define(COL_TIME, 2).
+-define(COL_REDS, 3).
+-define(COL_MEM, 4).
+-define(COL_MSG, 5).
+-define(COL_FUN, 6).
+
+-define(ID_KILL, 201).
+-define(ID_PROC, 202).
+-define(ID_REFRESH, 203).
+-define(ID_REFRESH_INTERVAL, 204).
+-define(ID_DUMP_TO_FILE, 205).
+-define(ID_TRACEMENU, 206).
+-define(ID_TRACE_ALL_MENU, 207).
+-define(ID_TRACE_NEW_MENU, 208).
+-define(ID_ACCUMULATE, 209).
+
+%% Records
+
+-record(sort,
+ {
+ sort_key=?COL_REDS,
+ sort_incr=false
+ }).
+
+-record(holder, {parent,
+ info,
+ sort=#sort{},
+ accum=[],
+ attrs,
+ node,
+ backend_pid
+ }).
+
+-record(state, {parent,
+ grid,
+ panel,
+ popup_menu,
+ parent_notebook,
+ timer,
+ procinfo_menu_pids=[],
+ sel={[], []},
+ holder}).
+
+start_link(Notebook, Parent) ->
+ wx_object:start_link(?MODULE, [Notebook, Parent], []).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+init([Notebook, Parent]) ->
+ Attrs = observer_lib:create_attrs(),
+ Self = self(),
+ Holder = spawn_link(fun() -> init_table_holder(Self, Attrs) end),
+ {ProPanel, State} = setup(Notebook, Parent, Holder),
+ {ProPanel, State#state{holder=Holder}}.
+
+setup(Notebook, Parent, Holder) ->
+ ProPanel = wxPanel:new(Notebook, []),
+
+ Grid = create_list_box(ProPanel, Holder),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ wxSizer:add(Sizer, Grid, [{flag, ?wxEXPAND bor ?wxALL},
+ {proportion, 1},
+ {border,4}]),
+
+ wxWindow:setSizer(ProPanel, Sizer),
+
+ Popup = create_popup_menu(ProPanel),
+
+ State = #state{parent=Parent,
+ grid=Grid,
+ panel=ProPanel,
+ popup_menu=Popup,
+ parent_notebook=Notebook,
+ holder=Holder,
+ timer={false, 10}
+ },
+ {ProPanel, State}.
+
+
+%% UI-creation
+
+create_pro_menu(Parent, Holder) ->
+ MenuEntries = [{"File",
+ [#create_menu{id=?ID_DUMP_TO_FILE, text="Dump to file"}]},
+ {"View",
+ [#create_menu{id=?ID_ACCUMULATE, text="Accumulate",
+ type=check,
+ check=call(Holder, {get_accum, self()})},
+ separator,
+ #create_menu{id=?ID_REFRESH, text="Refresh\tCtrl-R"},
+ #create_menu{id=?ID_REFRESH_INTERVAL, text="Refresh Interval"}]},
+ {"Trace",
+ [#create_menu{id=?ID_TRACEMENU, text="Trace selected processes"},
+ #create_menu{id=?ID_TRACE_NEW_MENU, text="Trace new processes"}
+ %% , #create_menu{id=?ID_TRACE_ALL_MENU, text="Trace all processes"}
+ ]}
+ ],
+ observer_wx:create_menus(Parent, MenuEntries).
+
+create_popup_menu(ParentFrame) ->
+ MiniFrame = wxMiniFrame:new(ParentFrame, ?wxID_ANY, "Options", [{style, ?wxFRAME_FLOAT_ON_PARENT}]),
+ Panel = wxPanel:new(MiniFrame),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ TraceBtn = wxButton:new(Panel, ?ID_TRACEMENU, [{label, "Trace selected"},
+ {style, ?wxNO_BORDER}]),
+ ProcBtn = wxButton:new(Panel, ?ID_PROC, [{label, "Process info"},
+ {style, ?wxNO_BORDER}]),
+ KillBtn = wxButton:new(Panel, ?ID_KILL, [{label, "Kill process"},
+ {style, ?wxNO_BORDER}]),
+
+ wxButton:connect(TraceBtn, command_button_clicked),
+ wxButton:connect(ProcBtn, command_button_clicked),
+ wxButton:connect(KillBtn, command_button_clicked),
+ wxSizer:add(Sizer, TraceBtn, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxSizer:add(Sizer, ProcBtn, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxSizer:add(Sizer, KillBtn, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxPanel:setSizer(Panel, Sizer),
+ wxSizer:setSizeHints(Sizer, MiniFrame),
+ MiniFrame.
+
+create_list_box(Panel, Holder) ->
+ Style = ?wxLC_REPORT bor ?wxLC_VIRTUAL bor ?wxLC_HRULES,
+ ListCtrl = wxListCtrl:new(Panel, [{style, Style},
+ {onGetItemText,
+ fun(_, Row, Col) ->
+ call(Holder, {get_row, self(), Row, Col})
+ end},
+ {onGetItemAttr,
+ fun(_, Item) ->
+ call(Holder, {get_attr, self(), Item})
+ end}
+ ]),
+ Li = wxListItem:new(),
+ AddListEntry = fun({Name, Align, DefSize}, Col) ->
+ wxListItem:setText(Li, Name),
+ wxListItem:setAlign(Li, Align),
+ wxListCtrl:insertColumn(ListCtrl, Col, Li),
+ wxListCtrl:setColumnWidth(ListCtrl, Col, DefSize),
+ Col + 1
+ end,
+ ListItems = [{"Pid", ?wxLIST_FORMAT_CENTRE, 120},
+ {"Name or Initial Func", ?wxLIST_FORMAT_LEFT, 200},
+ {"Time", ?wxLIST_FORMAT_CENTRE, 50},
+ {"Reds", ?wxLIST_FORMAT_RIGHT, 100},
+ {"Memory", ?wxLIST_FORMAT_RIGHT, 100},
+ {"MsgQ", ?wxLIST_FORMAT_RIGHT, 50},
+ {"Current Function", ?wxLIST_FORMAT_LEFT, 200}],
+ lists:foldl(AddListEntry, 0, ListItems),
+ wxListItem:destroy(Li),
+
+ wxListCtrl:setItemCount(ListCtrl, 1),
+ wxListCtrl:connect(ListCtrl, size, [{skip, true}]),
+ wxListCtrl:connect(ListCtrl, command_list_item_activated),
+ wxListCtrl:connect(ListCtrl, command_list_item_right_click),
+ wxListCtrl:connect(ListCtrl, command_list_col_click),
+ %% Use focused instead of selected, selected doesn't generate events
+ %% for all multiple selections on Linux
+ wxListCtrl:connect(ListCtrl, command_list_item_focused),
+ ListCtrl.
+
+dump_to_file(Parent, FileName, Holder) ->
+ case file:open(FileName, [write]) of
+ {ok, Fd} ->
+ %% Holder closes the file when it's done
+ Holder ! {dump, Fd};
+ {error, Reason} ->
+ FailMsg = file:format_error(Reason),
+ MD = wxMessageDialog:new(Parent, FailMsg),
+ wxDialog:showModal(MD),
+ wxDialog:destroy(MD)
+ end.
+
+start_procinfo(undefined, _Frame, Opened) ->
+ Opened;
+start_procinfo(Pid, Frame, Opened) ->
+ case lists:member(Pid, Opened) of
+ true ->
+ Opened;
+ false ->
+ observer_procinfo:start(Pid, Frame, self()),
+ [Pid | Opened]
+ end.
+
+call(Holder, What) ->
+ Ref = erlang:monitor(process, Holder),
+ Holder ! What,
+ receive
+ {'DOWN', Ref, _, _, _} -> "";
+ {Holder, Res} ->
+ erlang:demonitor(Ref),
+ Res
+ after 2000 ->
+ io:format("Hanging call ~p~n",[What]),
+ ""
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%% Callbacks %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_info({holder_updated, Count}, State0=#state{grid=Grid}) ->
+ State = update_selection(State0),
+
+ wxListCtrl:setItemCount(Grid, Count),
+ wxListCtrl:refreshItems(Grid, 0, Count-1),
+
+ {noreply, State};
+
+handle_info(refresh_interval, #state{holder=Holder}=State) ->
+ Holder ! refresh,
+ {noreply, State};
+
+handle_info({procinfo_menu_closed, Pid},
+ #state{procinfo_menu_pids=Opened}=State) ->
+ NewPids = lists:delete(Pid, Opened),
+ {noreply, State#state{procinfo_menu_pids=NewPids}};
+
+handle_info({active, Node},
+ #state{holder=Holder, timer=Timer, parent=Parent}=State) ->
+ create_pro_menu(Parent, Holder),
+ Holder ! {change_node, Node},
+ {noreply, State#state{timer=observer_lib:start_timer(Timer)}};
+
+handle_info(not_active, #state{timer=Timer0}=State) ->
+ Timer = observer_lib:stop_timer(Timer0),
+ {noreply, State#state{timer=Timer}};
+
+handle_info({node, Node}, #state{holder=Holder}=State) ->
+ Holder ! {change_node, Node},
+ {noreply, State};
+
+handle_info(Info, State) ->
+ io:format("~p:~p, Unexpected info: ~p~n", [?MODULE, ?LINE, Info]),
+ {noreply, State}.
+
+terminate(_Reason, #state{holder=Holder}) ->
+ Holder ! stop,
+ etop:stop(),
+ ok.
+
+code_change(_, _, State) ->
+ {stop, not_yet_implemented, State}.
+
+
+handle_call(Msg, _From, State) ->
+ io:format("~p:~p: Unhandled call ~p~n",[?MODULE, ?LINE, Msg]),
+ {reply, ok, State}.
+
+
+handle_cast(Msg, State) ->
+ io:format("~p:~p: Unhandled cast ~p~n", [?MODULE, ?LINE, Msg]),
+ {noreply, State}.
+
+%%%%%%%%%%%%%%%%%%%%LOOP%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_event(#wx{id=?ID_DUMP_TO_FILE}, #state{panel=Panel, holder=Holder}=State) ->
+ FD = wxFileDialog:new(Panel,
+ [{style,?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}]),
+ case wxFileDialog:showModal(FD) of
+ ?wxID_OK ->
+ Path = wxFileDialog:getPath(FD),
+ wxDialog:destroy(FD),
+ dump_to_file(Panel, Path, Holder);
+ _ ->
+ wxDialog:destroy(FD)
+ end,
+ {noreply, State};
+
+handle_event(#wx{id=?ID_ACCUMULATE,
+ event=#wxCommand{type=command_menu_selected, commandInt=CmdInt}},
+ #state{holder=Holder}=State) ->
+ Holder ! {accum, CmdInt =:= 1},
+ {noreply, State};
+
+handle_event(#wx{id=?ID_REFRESH, event=#wxCommand{type=command_menu_selected}},
+ #state{holder=Holder}=State) ->
+ Holder ! refresh,
+ {noreply, State};
+
+handle_event(#wx{id=?ID_REFRESH_INTERVAL},
+ #state{panel=Panel, timer=Timer0}=State) ->
+ Timer = observer_lib:interval_dialog(Panel, Timer0, 1, 5*60),
+ {noreply, State#state{timer=Timer}};
+
+handle_event(#wx{id=?ID_KILL},
+ #state{popup_menu=Pop,sel={[_|Ids], [ToKill|Pids]}}=State) ->
+ wxWindow:show(Pop, [{show, false}]),
+ exit(ToKill, kill),
+ {noreply, State#state{sel={Ids,Pids}}};
+
+
+handle_event(#wx{id = ?ID_PROC},
+ #state{panel=Panel,
+ popup_menu=Pop,
+ sel={_, [Pid|_]},
+ procinfo_menu_pids=Opened}=State) ->
+ wxWindow:show(Pop, [{show, false}]),
+ Opened2 = start_procinfo(Pid, Panel, Opened),
+ {noreply, State#state{procinfo_menu_pids=Opened2}};
+
+handle_event(#wx{id = ?ID_TRACEMENU},
+ #state{popup_menu=Pop, sel={_, Pids}, panel=Panel}=State) ->
+ wxWindow:show(Pop, [{show, false}]),
+ case Pids of
+ [] ->
+ observer_wx:create_txt_dialog(Panel, "No selected processes", "Tracer", ?wxICON_EXCLAMATION),
+ {noreply, State};
+ Pids ->
+ observer_trace_wx:add_processes(observer_wx:get_tracer(), Pids),
+ {noreply, State}
+ end;
+
+handle_event(#wx{id=?ID_TRACE_NEW_MENU, event=#wxCommand{type=command_menu_selected}}, State) ->
+ observer_trace_wx:add_processes(observer_wx:get_tracer(), [new]),
+ {noreply, State};
+
+handle_event(#wx{event=#wxSize{size={W,_}}},
+ #state{grid=Grid}=State) ->
+ wx:batch(fun() ->
+ Cols = wxListCtrl:getColumnCount(Grid),
+ Last = lists:foldl(fun(I, Last) ->
+ Last - wxListCtrl:getColumnWidth(Grid, I)
+ end, W-Cols*3-?LCTRL_WDECR, lists:seq(0, Cols - 2)),
+ Size = max(200, Last),
+ %% io:format("Width ~p ~p => ~p~n",[W, Last, Size]),
+ wxListCtrl:setColumnWidth(Grid, Cols-1, Size)
+ end),
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_item_right_click,
+ itemIndex=Row}},
+ #state{popup_menu=Popup,
+ holder=Holder}=State) ->
+
+ case call(Holder, {get_row, self(), Row, pid}) of
+ {error, undefined} ->
+ wxWindow:show(Popup, [{show, false}]),
+ undefined;
+ {ok, _} ->
+ wxWindow:move(Popup, wx_misc:getMousePosition()),
+ wxWindow:show(Popup)
+ end,
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_item_focused,
+ itemIndex=Row}},
+ #state{grid=Grid,popup_menu=Pop,holder=Holder} = State) ->
+ case Row >= 0 of
+ true ->
+ wxWindow:show(Pop, [{show, false}]),
+ SelIds = [Row|lists:delete(Row, get_selected_items(Grid))],
+ Pids = call(Holder, {get_pids, self(), SelIds}),
+ %% io:format("Focused ~p -> ~p~n",[State#state.sel, {SelIds, Pids}]),
+ {noreply, State#state{sel={SelIds, Pids}}};
+ false ->
+ %% io:format("Focused -1~n",[]),
+ {noreply, State}
+ end;
+
+handle_event(#wx{event=#wxList{type=command_list_col_click, col=Col}},
+ #state{holder=Holder}=State) ->
+ Holder ! {change_sort, Col},
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_item_activated}},
+ #state{panel=Panel, procinfo_menu_pids=Opened,
+ sel={_, [Pid|_]}}=State)
+ when Pid =/= undefined ->
+ Opened2 = start_procinfo(Pid, Panel, Opened),
+ {noreply, State#state{procinfo_menu_pids=Opened2}};
+
+handle_event(Event, State) ->
+ io:format("~p:~p: handle event ~p\n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+update_selection(State=#state{holder=Holder, grid=Grid,
+ sel={SelIds0, SelPids0}}) ->
+ Sel = {SelIds,_SelPids} = call(Holder, {get_rows_from_pids, self(), SelPids0}),
+ set_focus(SelIds0, SelIds, Grid),
+ case SelIds =:= SelIds0 of
+ true -> ok;
+ false ->
+ wx:batch(fun() ->
+ [wxListCtrl:setItemState(Grid, I, 0, ?wxLIST_STATE_SELECTED) ||
+ I <- SelIds0],
+ [wxListCtrl:setItemState(Grid, I, 16#FFFF, ?wxLIST_STATE_SELECTED) ||
+ I <- SelIds]
+ end)
+ end,
+ %%io:format("Update ~p -> ~p~n",[{SelIds0, SelPids0}, Sel]),
+ State#state{sel=Sel}.
+
+get_selected_items(Grid) ->
+ get_selected_items(Grid, -1, []).
+
+get_selected_items(Grid, Index, ItemAcc) ->
+ Item = wxListCtrl:getNextItem(Grid, Index, [{geometry, ?wxLIST_NEXT_ALL},
+ {state, ?wxLIST_STATE_SELECTED}]),
+ case Item of
+ -1 ->
+ lists:reverse(ItemAcc);
+ _ ->
+ get_selected_items(Grid, Item, [Item | ItemAcc])
+ end.
+
+set_focus([], [], _Grid) -> ok;
+set_focus([Same|_], [Same|_], _Grid) -> ok;
+set_focus([], [New|_], Grid) ->
+ wxListCtrl:setItemState(Grid, New, 16#FFFF, ?wxLIST_STATE_FOCUSED);
+set_focus([Old|_], [], Grid) ->
+ wxListCtrl:setItemState(Grid, Old, 0, ?wxLIST_STATE_FOCUSED);
+set_focus([Old|_], [New|_], Grid) ->
+ wxListCtrl:setItemState(Grid, Old, 0, ?wxLIST_STATE_FOCUSED),
+ wxListCtrl:setItemState(Grid, New, 16#FFFF, ?wxLIST_STATE_FOCUSED).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%TABLE HOLDER%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+init_table_holder(Parent, Attrs) ->
+ Backend = spawn_link(node(), observer_backend,etop_collect,[self()]),
+ table_holder(#holder{parent=Parent,
+ info=#etop_info{procinfo=[]},
+ node=node(),
+ backend_pid=Backend,
+ attrs=Attrs
+ }).
+
+table_holder(#holder{info=#etop_info{procinfo=Info}, attrs=Attrs,
+ node=Node, backend_pid=Backend}=S0) ->
+ receive
+ {get_row, From, Row, Col} ->
+ get_row(From, Row, Col, Info),
+ table_holder(S0);
+ {get_attr, From, Row} ->
+ get_attr(From, Row, Attrs),
+ table_holder(S0);
+ {Backend, EtopInfo=#etop_info{}} ->
+ State = handle_update(EtopInfo, S0),
+ table_holder(State#holder{backend_pid=undefined});
+ refresh when is_pid(Backend)->
+ table_holder(S0); %% Already updating
+ refresh ->
+ Pid = spawn_link(Node,observer_backend,etop_collect,[self()]),
+ table_holder(S0#holder{backend_pid=Pid});
+ {change_sort, Col} ->
+ State = change_sort(Col, S0),
+ table_holder(State);
+ {get_pids, From, Indices} ->
+ get_pids(From, Indices, Info),
+ table_holder(S0);
+ {get_rows_from_pids, From, Pids} ->
+ get_rows_from_pids(From, Pids, Info),
+ table_holder(S0);
+
+ {get_node, From} ->
+ From ! {self(), Node},
+ table_holder(S0);
+ {change_node, NewNode} ->
+ case Node == NewNode of
+ true ->
+ table_holder(S0);
+ false ->
+ self() ! refresh,
+ table_holder(S0#holder{node=NewNode})
+ end;
+ {accum, Bool} ->
+ table_holder(change_accum(Bool,S0));
+ {get_accum, From} ->
+ From ! {self(), S0#holder.accum == true},
+ table_holder(S0);
+ {dump, Fd} ->
+ etop_txt:do_update(Fd, S0#holder.info, #opts{node=Node}),
+ file:close(Fd),
+ table_holder(S0);
+ stop ->
+ ok;
+ What ->
+ io:format("Table holder got ~p~n",[What]),
+ table_holder(S0)
+ end.
+
+change_sort(Col, S0=#holder{parent=Parent, info=EI=#etop_info{procinfo=Data}, sort=Sort0}) ->
+ {Sort, ProcInfo}=sort(Col, Sort0, Data),
+ Parent ! {holder_updated, length(Data)},
+ S0#holder{info=EI#etop_info{procinfo=ProcInfo}, sort=Sort}.
+
+change_accum(true, S0) ->
+ S0#holder{accum=true};
+change_accum(false, S0=#holder{info=#etop_info{procinfo=Info}}) ->
+ self() ! refresh,
+ S0#holder{accum=lists:sort(Info)}.
+
+handle_update(EI=#etop_info{procinfo=ProcInfo0},
+ S0=#holder{parent=Parent, sort=Sort=#sort{sort_key=KeyField}}) ->
+ {ProcInfo1, S1} = accum(ProcInfo0, S0),
+ {_SO, ProcInfo} = sort(KeyField, Sort#sort{sort_key=undefined}, ProcInfo1),
+ Parent ! {holder_updated, length(ProcInfo)},
+ S1#holder{info=EI#etop_info{procinfo=ProcInfo}}.
+
+accum(ProcInfo, State=#holder{accum=true}) ->
+ {ProcInfo, State};
+accum(ProcInfo0, State=#holder{accum=Previous}) ->
+ ProcInfo = lists:sort(ProcInfo0),
+ {accum2(ProcInfo,Previous,[]), State#holder{accum=ProcInfo}}.
+
+accum2([PI=#etop_proc_info{pid=Pid, reds=Reds, runtime=RT}|PIs],
+ [#etop_proc_info{pid=Pid, reds=OldReds, runtime=OldRT}|Old], Acc) ->
+ accum2(PIs, Old, [PI#etop_proc_info{reds=Reds-OldReds, runtime=RT-OldRT}|Acc]);
+accum2(PIs=[#etop_proc_info{pid=Pid}|_], [#etop_proc_info{pid=OldPid}|Old], Acc)
+ when Pid > OldPid ->
+ accum2(PIs, Old, Acc);
+accum2([PI|PIs], Old, Acc) ->
+ accum2(PIs, Old, [PI|Acc]);
+accum2([], _, Acc) -> Acc.
+
+sort(Col, Opt=#sort{sort_key=Col, sort_incr=Bool}, Table) ->
+ {Opt#sort{sort_incr=not Bool}, lists:reverse(Table)};
+sort(Col, S=#sort{sort_incr=true}, Table) ->
+ {S#sort{sort_key=Col}, lists:keysort(col_to_element(Col), Table)};
+sort(Col, S=#sort{sort_incr=false}, Table) ->
+ {S#sort{sort_key=Col}, lists:reverse(lists:keysort(col_to_element(Col), Table))}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+get_procinfo_data(Col, Info) ->
+ element(col_to_element(Col), Info).
+col_to_element(?COL_PID) -> #etop_proc_info.pid;
+col_to_element(?COL_NAME) -> #etop_proc_info.name;
+col_to_element(?COL_MEM) -> #etop_proc_info.mem;
+col_to_element(?COL_TIME) -> #etop_proc_info.runtime;
+col_to_element(?COL_REDS) -> #etop_proc_info.reds;
+col_to_element(?COL_FUN) -> #etop_proc_info.cf;
+col_to_element(?COL_MSG) -> #etop_proc_info.mq.
+
+get_pids(From, Indices, ProcInfo) ->
+ Processes = [(lists:nth(I+1, ProcInfo))#etop_proc_info.pid || I <- Indices],
+ From ! {self(), Processes}.
+
+get_row(From, Row, pid, Info) ->
+ Pid = case Row =:= -1 of
+ true -> {error, undefined};
+ false -> {ok, get_procinfo_data(?COL_PID, lists:nth(Row+1, Info))}
+ end,
+ From ! {self(), Pid};
+get_row(From, Row, Col, Info) ->
+ Data = case Row+1 > length(Info) of
+ true ->
+ "";
+ false ->
+ ProcInfo = lists:nth(Row+1, Info),
+ get_procinfo_data(Col, ProcInfo)
+ end,
+ From ! {self(), observer_lib:to_str(Data)}.
+
+get_rows_from_pids(From, Pids0, Info) ->
+ Res = lists:foldl(fun(Pid, Data = {Ids, Pids}) ->
+ case index(Pid, Info, 0) of
+ false -> Data;
+ Index -> {[Index|Ids], [Pid|Pids]}
+ end
+ end, {[],[]}, Pids0),
+ From ! {self(), Res}.
+
+get_attr(From, Row, Attrs) ->
+ Attribute = case Row rem 2 =:= 0 of
+ true -> Attrs#attrs.even;
+ false -> Attrs#attrs.odd
+ end,
+ From ! {self(), Attribute}.
+
+index(Pid, [#etop_proc_info{pid=Pid}|_], Index) -> Index;
+index(Pid, [_|PI], Index) -> index(Pid, PI, Index+1);
+index(_, _, _) -> false.
diff --git a/lib/observer/src/observer_procinfo.erl b/lib/observer/src/observer_procinfo.erl
new file mode 100644
index 0000000000..127599a39e
--- /dev/null
+++ b/lib/observer/src/observer_procinfo.erl
@@ -0,0 +1,274 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer_procinfo).
+
+-behaviour(wx_object).
+
+-export([start/3]).
+
+-export([init/1, handle_event/2, handle_cast/2, terminate/2, code_change/3,
+ handle_call/3, handle_info/2]).
+
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+
+-define(REFRESH, 601).
+-define(SELECT_ALL, 603).
+-define(ID_NOTEBOOK, 604).
+
+-record(state, {parent,
+ frame,
+ pid,
+ pages=[]
+ }).
+
+-record(worker, {panel, callback}).
+
+start(Process, ParentFrame, Parent) ->
+ wx_object:start(?MODULE, [Process, ParentFrame, Parent], []).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+init([Pid, ParentFrame, Parent]) ->
+ try
+ Title=case observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, registered_name]) of
+ [] -> io_lib:format("~p",[Pid]);
+ {registered_name, Registered} -> atom_to_list(Registered)
+ end,
+ Frame=wxFrame:new(ParentFrame, ?wxID_ANY, [atom_to_list(node(Pid)), $:, Title],
+ [{style, ?wxDEFAULT_FRAME_STYLE}, {size, {800,700}}]),
+ MenuBar = wxMenuBar:new(),
+ create_menus(MenuBar),
+ wxFrame:setMenuBar(Frame, MenuBar),
+
+ Notebook = wxNotebook:new(Frame, ?ID_NOTEBOOK, [{style, ?wxBK_DEFAULT}]),
+
+ ProcessPage = init_panel(Notebook, "Process Information", Pid, fun init_process_page/2),
+ MessagePage = init_panel(Notebook, "Messages", Pid, fun init_message_page/2),
+ DictPage = init_panel(Notebook, "Dictionary", Pid, fun init_dict_page/2),
+ StackPage = init_panel(Notebook, "Stack Trace", Pid, fun init_stack_page/2),
+
+ wxFrame:connect(Frame, close_window),
+ wxMenu:connect(Frame, command_menu_selected),
+ %% wxNotebook:connect(Notebook, command_notebook_page_changed, [{skip,true}]),
+ wxFrame:show(Frame),
+ {Frame, #state{parent=Parent,
+ pid=Pid,
+ frame=Frame,
+ pages=[ProcessPage,MessagePage,DictPage,StackPage]
+ }}
+ catch error:{badrpc, _} ->
+ observer_wx:return_to_localnode(ParentFrame, node(Pid)),
+ {stop, badrpc, #state{parent=Parent, pid=Pid}};
+ Error:Reason ->
+ io:format("~p:~p: ~p ~p~n ~p~n",
+ [?MODULE, ?LINE, Error, Reason, erlang:get_stacktrace()])
+ end.
+
+init_panel(Notebook, Str, Pid, Fun) ->
+ Panel = wxPanel:new(Notebook),
+ Sizer = wxBoxSizer:new(?wxHORIZONTAL),
+ {Window,Callback} = Fun(Panel, Pid),
+ wxSizer:add(Sizer, Window, [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]),
+ wxPanel:setSizer(Panel, Sizer),
+ true = wxNotebook:addPage(Notebook, Panel, Str),
+ #worker{panel=Panel, callback=Callback}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%Callbacks%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+handle_event(#wx{event=#wxClose{type=close_window}}, State) ->
+ {stop, shutdown, State};
+
+handle_event(#wx{id=?wxID_CLOSE, event=#wxCommand{type=command_menu_selected}}, State) ->
+ {stop, shutdown, State};
+
+handle_event(#wx{id=?REFRESH}, #state{pages=Pages}=State) ->
+ [(W#worker.callback)() || W <- Pages],
+ {noreply, State};
+
+handle_event(Event, State) ->
+ io:format("~p: ~p, Handle event: ~p~n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+handle_info(Info, State) ->
+ io:format("~p: ~p, Handle info: ~p~n", [?MODULE, ?LINE, Info]),
+ {noreply, State}.
+
+handle_call(Call, _From, State) ->
+ io:format("~p ~p: Got call ~p~n",[?MODULE, ?LINE, Call]),
+ {reply, ok, State}.
+
+handle_cast(Cast, State) ->
+ io:format("~p ~p: Got cast ~p~n", [?MODULE, ?LINE, Cast]),
+ {noreply, State}.
+
+terminate(_Reason, #state{parent=Parent,pid=Pid,frame=Frame}) ->
+ Parent ! {procinfo_menu_closed, Pid},
+ case Frame of
+ undefined -> ok;
+ _ -> wxFrame:destroy(Frame)
+ end,
+ ok.
+
+code_change(_, _, State) ->
+ {stop, not_yet_implemented, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+init_process_page(Panel, Pid) ->
+ Fields = process_info_fields(Pid),
+ {FPanel, _, UpFields} = observer_lib:display_info(Panel, Fields),
+ {FPanel, fun() -> observer_lib:update_info(UpFields, process_info_fields(Pid)) end}.
+
+init_text_page(Parent) ->
+ Style = ?wxTE_MULTILINE bor ?wxTE_RICH2 bor ?wxTE_READONLY,
+ Text = wxTextCtrl:new(Parent, ?wxID_ANY, [{style, Style}]),
+ Font = observer_wx:get_attrib({font, fixed}),
+ Attr = wxTextAttr:new(?wxBLACK, [{font, Font}]),
+ true = wxTextCtrl:setDefaultStyle(Text, Attr),
+ wxTextAttr:destroy(Attr),
+ Text.
+
+init_message_page(Parent, Pid) ->
+ Text = init_text_page(Parent),
+ Format = fun(Message, Number) ->
+ {io_lib:format("~-4.w ~p~n", [Number, Message]),
+ Number+1}
+ end,
+ Update = fun() ->
+ {messages,RawMessages} =
+ observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, messages]),
+ {Messages,_} = lists:mapfoldl(Format, 1, RawMessages),
+ Last = wxTextCtrl:getLastPosition(Text),
+ wxTextCtrl:remove(Text, 0, Last),
+ case Messages =:= [] of
+ true ->
+ wxTextCtrl:writeText(Text, "No messages");
+ false ->
+ wxTextCtrl:writeText(Text, Messages)
+ end
+ end,
+ Update(),
+ {Text, Update}.
+
+init_dict_page(Parent, Pid) ->
+ Text = init_text_page(Parent),
+ Update = fun() ->
+ {dictionary,RawDict} =
+ observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, dictionary]),
+ Dict = [io_lib:format("~-20.w ~p~n", [K, V]) || {K, V} <- RawDict],
+ Last = wxTextCtrl:getLastPosition(Text),
+ wxTextCtrl:remove(Text, 0, Last),
+ wxTextCtrl:writeText(Text, Dict)
+ end,
+ Update(),
+ {Text, Update}.
+
+init_stack_page(Parent, Pid) ->
+ Text = init_text_page(Parent),
+ Format = fun({Mod, Fun, Arg, Info}) ->
+ Str = io_lib:format("~w:~w/~w", [Mod,Fun,Arg]),
+ case Info of
+ [{file,File},{line,Line}] ->
+ io_lib:format("~-45.s ~s:~w~n", [Str,File,Line]);
+ _ ->
+ [Str,$\n]
+ end
+ end,
+ Update = fun() ->
+ {current_stacktrace,RawBt} =
+ observer_wx:try_rpc(node(Pid), erlang, process_info,
+ [Pid, current_stacktrace]),
+ Last = wxTextCtrl:getLastPosition(Text),
+ wxTextCtrl:remove(Text, 0, Last),
+ [wxTextCtrl:writeText(Text, Format(Entry)) || Entry <- RawBt]
+ end,
+ Update(),
+ {Text, Update}.
+
+create_menus(MenuBar) ->
+ Menus = [{"File", [#create_menu{id=?wxID_CLOSE, text="Close"}]},
+ {"View", [#create_menu{id=?REFRESH, text="Refresh\tCtrl-R"}]}],
+ observer_lib:create_menus(Menus, MenuBar, new_window).
+
+process_info_fields(Pid) ->
+ RawInfo = observer_wx:try_rpc(node(Pid), erlang, process_info, [Pid, item_list()]),
+ Struct = [{"Overview",
+ [{"Initial Call", initial_call},
+ {"Current Function", current_function},
+ {"Registered Name", registered_name},
+ {"Status", status},
+ {"Message Queue Len",message_queue_len},
+ {"Priority", priority},
+ {"Trap Exit", trap_exit},
+ {"Reductions", reductions},
+ {"Binary", binary},
+ {"Last Calls", last_calls},
+ {"Catch Level", catchlevel},
+ {"Trace", trace},
+ {"Suspending", suspending},
+ {"Sequential Trace Token", sequential_trace_token},
+ {"Error Handler", error_handler}]},
+ {"Connections",
+ [{"Group Leader", group_leader},
+ {"Links", links},
+ {"Monitors", monitors},
+ {"Monitored by", monitored_by}]},
+ {"Memory and Garbage Collection", right,
+ [{"Memory", {bytes, memory}},
+ {"Stack and Heaps", {bytes, total_heap_size}},
+ {"Heap Size", {bytes, heap_size}},
+ {"Stack Size", {bytes, stack_size}},
+ {"GC Min Heap Size", {bytes, get_gc_info(min_heap_size)}},
+ {"GC FullSweep After", get_gc_info(fullsweep_after)}
+ ]}],
+ observer_lib:fill_info(Struct, RawInfo).
+
+item_list() ->
+ [ %% backtrace,
+ binary,
+ catchlevel,
+ current_function,
+ %% dictionary,
+ error_handler,
+ garbage_collection,
+ group_leader,
+ heap_size,
+ initial_call,
+ last_calls,
+ links,
+ memory,
+ message_queue_len,
+ %% messages,
+ monitored_by,
+ monitors,
+ priority,
+ reductions,
+ registered_name,
+ sequential_trace_token,
+ stack_size,
+ status,
+ suspending,
+ total_heap_size,
+ trace,
+ trap_exit].
+
+get_gc_info(Arg) ->
+ fun(Data) ->
+ GC = proplists:get_value(garbage_collection, Data),
+ proplists:get_value(Arg, GC)
+ end.
diff --git a/lib/observer/src/observer_sys_wx.erl b/lib/observer/src/observer_sys_wx.erl
new file mode 100644
index 0000000000..ddedcf3829
--- /dev/null
+++ b/lib/observer/src/observer_sys_wx.erl
@@ -0,0 +1,223 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+-module(observer_sys_wx).
+
+-behaviour(wx_object).
+
+-export([start_link/2]).
+%% wx_object callbacks
+-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
+ handle_event/2, handle_cast/2]).
+
+-export([sys_info/0]).
+
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+
+-define(ID_REFRESH, 101).
+-define(ID_REFRESH_INTERVAL, 102).
+
+%% Records
+-record(sys_wx_state,
+ {parent,
+ node,
+ parent_notebook,
+ panel, sizer,
+ menubar,
+ fields,
+ timer}).
+
+start_link(Notebook, Parent) ->
+ wx_object:start_link(?MODULE, [Notebook, Parent], []).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+init([Notebook, Parent]) ->
+ SysInfo = sys_info(),
+ {Info, Stat} = info_fields(),
+ Panel = wxPanel:new(Notebook),
+ Sizer = wxBoxSizer:new(?wxHORIZONTAL),
+ {FPanel0, _FSizer0, Fields0} =
+ observer_lib:display_info(Panel, observer_lib:fill_info(Info, SysInfo)),
+ {FPanel1, _FSizer1, Fields1} =
+ observer_lib:display_info(Panel, observer_lib:fill_info(Stat, SysInfo)),
+ wxSizer:add(Sizer, FPanel0, [{flag, ?wxEXPAND bor ?wxTOP bor ?wxBOTTOM bor ?wxLEFT},
+ {proportion, 1}, {border, 5}]),
+ wxSizer:add(Sizer, FPanel1, [{flag, ?wxEXPAND bor ?wxTOP bor ?wxBOTTOM bor ?wxRIGHT},
+ {proportion, 1}, {border, 5}]),
+ wxPanel:setSizer(Panel, Sizer),
+ Timer = observer_lib:start_timer(10),
+ {Panel, #sys_wx_state{parent=Parent,
+ parent_notebook=Notebook,
+ panel=Panel, sizer=Sizer,
+ timer=Timer, fields=Fields0 ++ Fields1}}.
+
+create_sys_menu(Parent) ->
+ View = {"View", [#create_menu{id = ?ID_REFRESH, text = "Refresh\tCtrl-R"},
+ #create_menu{id = ?ID_REFRESH_INTERVAL, text = "Refresh interval"}]},
+ observer_wx:create_menus(Parent, [View]).
+
+update_syspage(#sys_wx_state{node = Node, fields=Fields, sizer=Sizer}) ->
+ SysInfo = observer_wx:try_rpc(Node, ?MODULE, sys_info, []),
+ {Info, Stat} = info_fields(),
+ observer_lib:update_info(Fields, observer_lib:fill_info(Info, SysInfo) ++
+ observer_lib:fill_info(Stat, SysInfo)),
+ wxSizer:layout(Sizer).
+
+info_fields() ->
+ Info = [{"System and Architecture",
+ [{"System Version", otp_release},
+ {"Erts Version", version},
+ {"Compiled for", system_architecture},
+ {"Emulator Wordsize", wordsize_external},
+ {"Process Wordsize", wordsize_internal},
+ {"Smp Support", smp_support},
+ {"Thread Support", threads},
+ {"Async thread pool size", thread_pool_size}
+ ]},
+ {"CPU's and Threads",
+ [{"System Logical CPU's", logical_processors},
+ {"Erlang Logical CPU's", logical_processors_online},
+ {"Used Logical CPU's", logical_processors_available}
+ ]}
+ ],
+ Stat = [{"Memory Usage", right,
+ [{"Total", total},
+ {"Processes", processes},
+ {"Atoms", atom},
+ {"Binaries", binary},
+ {"Code", code},
+ {"Ets", ets}
+ ]},
+ {"Statistics", right,
+ [{"Up time", uptime},
+ {"Max Processes", process_limit},
+ {"Processes", process_count},
+ {"Run Queue", run_queue},
+ {"IO Input", io_input},
+ {"IO Output", io_output}
+ ]}
+ ],
+ {Info, Stat}.
+
+%%%%%%%%%%%%%%%%%%%%%%% Callbacks %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_info(refresh_interval, #sys_wx_state{panel = Panel,
+ node = Node} = State) ->
+ try
+ update_syspage(State)
+ catch error:{badrpc, _} ->
+ observer_wx:return_to_localnode(Panel, Node)
+ end,
+ {noreply, State};
+
+handle_info({node, Node}, #sys_wx_state{panel = Panel} = State) ->
+ UpdState = State#sys_wx_state{node = Node},
+ try
+ update_syspage(UpdState),
+ {noreply, UpdState}
+ catch error:{badrpc, _} ->
+ observer_wx:return_to_localnode(Panel, Node),
+ {noreply, State}
+ end;
+
+handle_info({active, Node}, #sys_wx_state{parent = Parent, panel = Panel,
+ timer = Timer} = State) ->
+ UpdState = State#sys_wx_state{node = Node},
+ create_sys_menu(Parent),
+ try
+ update_syspage(UpdState),
+ {noreply, UpdState#sys_wx_state{timer=observer_lib:start_timer(Timer)}}
+ catch error:{badrpc, _} ->
+ observer_wx:return_to_localnode(Panel, Node),
+ {noreply, State}
+ end;
+
+
+handle_info(not_active, #sys_wx_state{timer = Timer} = State) ->
+ {noreply, State#sys_wx_state{timer = observer_lib:stop_timer(Timer)}};
+
+handle_info(Info, State) ->
+ io:format("~p:~p: Unhandled info: ~p~n", [?MODULE, ?LINE, Info]),
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+code_change(_, _, State) ->
+ {stop, not_yet_implemented, State}.
+
+handle_call(Msg, _From, State) ->
+ io:format("~p~p: Unhandled Call ~p~n",[?MODULE, ?LINE, Msg]),
+ {reply, ok, State}.
+
+handle_cast(Msg, State) ->
+ io:format("~p~p: Unhandled cast ~p~n",[?MODULE, ?LINE, Msg]),
+ {noreply, State}.
+
+handle_event(#wx{id = ?ID_REFRESH, event = #wxCommand{type = command_menu_selected}},
+ #sys_wx_state{node = Node, panel = Panel} = State) ->
+ try
+ update_syspage(State)
+ catch error:{badrpc, _} ->
+ observer_wx:return_to_localnode(Panel, Node)
+ end,
+ {noreply, State};
+
+handle_event(#wx{id = ?ID_REFRESH_INTERVAL,
+ event = #wxCommand{type = command_menu_selected}},
+ #sys_wx_state{timer = Timer0, parent_notebook = Notebook} = State) ->
+ Timer = observer_lib:interval_dialog(Notebook, Timer0, 1, 5*60),
+ {noreply, State#sys_wx_state{timer=Timer}};
+
+handle_event(Event, State) ->
+ io:format("~p:~p: Unhandled event ~p\n", [?MODULE,?LINE,Event]),
+ {noreply, State}.
+
+
+sys_info() ->
+ {{_,Input},{_,Output}} = erlang:statistics(io),
+ [{process_count, erlang:system_info(process_count)},
+ {process_limit, erlang:system_info(process_limit)},
+ {uptime, {time_ms, element(1, erlang:statistics(wall_clock))}},
+ {run_queue, erlang:statistics(run_queue)},
+ {io_input, {bytes, Input}},
+ {io_output, {bytes, Output}},
+ {logical_processors, erlang:system_info(logical_processors)},
+ {logical_processors_available, erlang:system_info(logical_processors_available)},
+ {logical_processors_online, erlang:system_info(logical_processors_online)},
+
+ {total, {bytes, erlang:memory(total)}},
+ %%{processes_used, erlang:memory(processes_used)},
+ {processes, {bytes, erlang:memory(processes)}},
+ %%{atom_used, erlang:memory(atom_used)},
+ {atom, {bytes, erlang:memory(atom)}},
+ {binary, {bytes, erlang:memory(binary)}},
+ {code, {bytes, erlang:memory(code)}},
+ {ets, {bytes, erlang:memory(ets)}},
+
+ {otp_release, erlang:system_info(otp_release)},
+ {version, erlang:system_info(version)},
+ {system_architecture, erlang:system_info(system_architecture)},
+ {kernel_poll, erlang:system_info(kernel_poll)},
+ {smp_support, erlang:system_info(smp_support)},
+ {threads, erlang:system_info(threads)},
+ {thread_pool_size, erlang:system_info(thread_pool_size)},
+ {wordsize_internal, erlang:system_info({wordsize, internal})},
+ {wordsize_external, erlang:system_info({wordsize, external})}
+ ].
diff --git a/lib/observer/src/observer_trace_wx.erl b/lib/observer/src/observer_trace_wx.erl
new file mode 100644
index 0000000000..0ab7db121b
--- /dev/null
+++ b/lib/observer/src/observer_trace_wx.erl
@@ -0,0 +1,628 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer_trace_wx).
+
+-export([start_link/2, add_processes/2]).
+-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
+ handle_event/2, handle_cast/2]).
+
+-behaviour(wx_object).
+
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+
+-define(OPTIONS, 301).
+-define(SAVE_BUFFER, 302).
+-define(CLOSE, 303).
+-define(CLEAR, 304).
+-define(SAVE_TRACEOPTS, 305).
+-define(LOAD_TRACEOPTS, 306).
+-define(TOGGLE_TRACE, 307).
+-define(ADD_NEW, 308).
+-define(ADD_TP, 309).
+-define(PROCESSES, 350).
+-define(MODULES, 351).
+-define(FUNCTIONS, 352).
+-define(TRACERWIN, 353).
+
+-record(state,
+ {parent,
+ panel,
+ p_view,
+ m_view,
+ f_view,
+ nodes = [],
+ toggle_button,
+ tpids = [], %% #tpid
+ def_trace_opts = [],
+ tpatterns = dict:new(), % Key =:= Module::atom, Value =:= {M, F, A, MatchSpec}
+ match_specs = []}). % [ #match_spec{} ]
+
+-record(tpid, {pid, opts}).
+
+start_link(Notebook, ParentPid) ->
+ wx_object:start_link(?MODULE, [Notebook, ParentPid], []).
+
+add_processes(Tracer, Pids) when is_list(Pids) ->
+ wx_object:cast(Tracer, {add_processes, Pids}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+init([Notebook, ParentPid]) ->
+ wx:batch(fun() -> create_window(Notebook, ParentPid) end).
+
+create_window(Notebook, ParentPid) ->
+ %% Create the window
+ Panel = wxPanel:new(Notebook, [{size, wxWindow:getClientSize(Notebook)}]),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ Splitter = wxSplitterWindow:new(Panel, [{size, wxWindow:getClientSize(Panel)}]),
+ ProcessView = create_process_view(Splitter),
+ {MatchSpecView,ModView,FuncView} = create_matchspec_view(Splitter),
+ wxSplitterWindow:setSashGravity(Splitter, 0.5),
+ wxSplitterWindow:setMinimumPaneSize(Splitter,50),
+ wxSplitterWindow:splitHorizontally(Splitter, ProcessView, MatchSpecView),
+ wxSizer:add(Sizer, Splitter, [{flag, ?wxEXPAND bor ?wxALL}, {border, 5}, {proportion, 1}]),
+ %% Buttons
+ Buttons = wxBoxSizer:new(?wxHORIZONTAL),
+ ToggleButton = wxToggleButton:new(Panel, ?TOGGLE_TRACE, "Start Trace", []),
+ wxSizer:add(Buttons, ToggleButton),
+ New = wxButton:new(Panel, ?ADD_NEW, [{label, "Trace New Processes"}]),
+ wxSizer:add(Buttons, New),
+ ATP = wxButton:new(Panel, ?ADD_TP, [{label, "Add Trace Pattern"}]),
+ wxSizer:add(Buttons, ATP),
+ wxMenu:connect(Panel, command_togglebutton_clicked, []),
+ wxMenu:connect(Panel, command_button_clicked, []),
+ wxSizer:add(Sizer, Buttons, [{flag, ?wxALL},{border, 2}, {proportion,0}]),
+ wxWindow:setSizer(Panel, Sizer),
+ {Panel, #state{parent=ParentPid, panel=Panel,
+ p_view=ProcessView, m_view=ModView, f_view=FuncView,
+ toggle_button = ToggleButton,
+ match_specs=default_matchspecs()}}.
+
+default_matchspecs() ->
+ Ms = [{"Return Trace", [{'_', [], [{return_trace}]}], "fun(_) -> return_trace() end"},
+ {"Exception Trace", [{'_', [], [{exception_trace}]}], "fun(_) -> exception_trace() end"},
+ {"Message Caller", [{'_', [], [{message,{caller}}]}], "fun(_) -> message(caller()) end"},
+ {"Message Dump", [{'_', [], [{message,{process_dump}}]}], "fun(_) -> message(process_dump()) end"}],
+ [make_ms(Name,Term,FunStr) || {Name,Term,FunStr} <- Ms].
+
+create_process_view(Parent) ->
+ Style = ?wxLC_REPORT bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES,
+ Grid = wxListCtrl:new(Parent, [{winid, ?PROCESSES}, {style, Style}]),
+ Li = wxListItem:new(),
+ AddListEntry = fun({Name, Align, DefSize}, Col) ->
+ wxListItem:setText(Li, Name),
+ wxListItem:setAlign(Li, Align),
+ wxListCtrl:insertColumn(Grid, Col, Li),
+ wxListCtrl:setColumnWidth(Grid, Col, DefSize),
+ Col + 1
+ end,
+ ListItems = [{"Process Id", ?wxLIST_FORMAT_CENTER, 120},
+ {"Trace Options", ?wxLIST_FORMAT_LEFT, 300}],
+ lists:foldl(AddListEntry, 0, ListItems),
+ wxListItem:destroy(Li),
+
+ %% wxListCtrl:connect(Grid, command_list_item_activated),
+ %% wxListCtrl:connect(Grid, command_list_item_selected),
+ wxListCtrl:connect(Grid, size, [{skip, true}]),
+
+ wxWindow:setFocus(Grid),
+ Grid.
+
+create_matchspec_view(Parent) ->
+ Panel = wxPanel:new(Parent),
+ MainSz = wxBoxSizer:new(?wxHORIZONTAL),
+ Style = ?wxLC_REPORT bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES,
+ Splitter = wxSplitterWindow:new(Panel, []),
+ Modules = wxListCtrl:new(Splitter, [{winid, ?MODULES}, {style, Style}]),
+ Funcs = wxListCtrl:new(Splitter, [{winid, ?FUNCTIONS}, {style, Style}]),
+ Li = wxListItem:new(),
+ wxListItem:setText(Li, "Modules"),
+ wxListCtrl:insertColumn(Modules, 0, Li),
+ wxListItem:setText(Li, "Functions"),
+ wxListCtrl:insertColumn(Funcs, 0, Li),
+ wxListCtrl:setColumnWidth(Funcs, 0, 150),
+ wxListItem:setText(Li, "Match Spec"),
+ wxListCtrl:insertColumn(Funcs, 1, Li),
+ wxListCtrl:setColumnWidth(Funcs, 1, 300),
+ wxListItem:destroy(Li),
+ wxSplitterWindow:setSashGravity(Splitter, 0.0),
+ wxSplitterWindow:setMinimumPaneSize(Splitter,50),
+ wxSplitterWindow:splitVertically(Splitter, Modules, Funcs, [{sashPosition, 150}]),
+ wxSizer:add(MainSz, Splitter, [{flag, ?wxEXPAND}, {proportion, 1}]),
+
+ wxListCtrl:connect(Modules, size, [{skip, true}]),
+ wxListCtrl:connect(Funcs, size, [{skip, true}]),
+ wxListCtrl:connect(Modules, command_list_item_selected),
+ %% wxListCtrl:connect(Funcs, command_list_item_selected),
+ wxPanel:setSizer(Panel, MainSz),
+ {Panel, Modules, Funcs}.
+
+create_menues(Parent) ->
+ Menus = [{"File", [#create_menu{id = ?LOAD_TRACEOPTS, text = "Load settings"},
+ #create_menu{id = ?SAVE_TRACEOPTS, text = "Save settings"}]
+ }],
+ observer_wx:create_menus(Parent, Menus).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%Main window
+handle_event(#wx{obj=Obj, event=#wxSize{size={W,_}}}, State) ->
+ case wx:getObjectType(Obj) =:= wxListCtrl of
+ true ->
+ wx:batch(fun() ->
+ Cols = wxListCtrl:getColumnCount(Obj),
+ Last = lists:foldl(fun(I, Last) ->
+ Last - wxListCtrl:getColumnWidth(Obj, I)
+ end, W-?LCTRL_WDECR, lists:seq(0, Cols - 2)),
+ Size = max(150, Last),
+ wxListCtrl:setColumnWidth(Obj, Cols-1, Size)
+ end);
+ false ->
+ ok
+ end,
+ {noreply, State};
+
+handle_event(#wx{id=?ADD_NEW}, State = #state{panel=Parent, def_trace_opts=TraceOpts}) ->
+ case observer_traceoptions_wx:process_trace(Parent, TraceOpts) of
+ {ok, Opts} ->
+ Process = #tpid{pid=new, opts=Opts},
+ {noreply, do_add_processes([Process], State#state{def_trace_opts=Opts})};
+ cancel ->
+ {noreply, State}
+ end;
+
+handle_event(#wx{id=?ADD_TP},
+ State = #state{panel=Parent, nodes=Nodes, match_specs=Ms}) ->
+ Node = case Nodes of
+ [N|_] -> N;
+ [] -> node()
+ end,
+ case observer_traceoptions_wx:trace_pattern(self(), Parent, Node, Ms) of
+ cancel ->
+ {noreply, State};
+ Patterns ->
+ {noreply, do_add_patterns(Patterns, State)}
+ end;
+
+handle_event(#wx{id=?MODULES, event=#wxList{type=command_list_item_selected, itemIndex=Row}},
+ State = #state{tpatterns=TPs, m_view=Mview, f_view=Fview}) ->
+ Module = list_to_atom(wxListCtrl:getItemText(Mview, Row)),
+ update_functions_view(dict:fetch(Module, TPs), Fview),
+ {noreply, State};
+
+
+handle_event(#wx{event = #wxCommand{type = command_togglebutton_clicked, commandInt = 1}},
+ #state{panel = Panel,
+ nodes = Nodes,
+ tpids = TProcs,
+ tpatterns = TPs,
+ toggle_button = ToggleBtn} = State) ->
+ LogWin = wxFrame:new(Panel, ?TRACERWIN, "Trace Log", [{size, {750, 800}}]),
+ Text = wxTextCtrl:new(LogWin, ?wxID_ANY,
+ [{style, ?wxTE_MULTILINE bor ?wxTE_RICH2 bor
+ ?wxTE_DONTWRAP bor ?wxTE_READONLY}]),
+ Font = observer_wx:get_attrib({font, fixed}),
+ Attr = wxTextAttr:new(?wxBLACK, [{font, Font}]),
+ true = wxTextCtrl:setDefaultStyle(Text, Attr),
+ Env = wx:get_env(),
+ Write = fun(Trace) ->
+ wx:set_env(Env),
+ wxTextCtrl:appendText(Text, textformat(Trace))
+ end,
+ {ok, _} = ttb:tracer(Nodes, [{file, {local,"/tmp/foo"}}, {shell, {only, Write}}]),
+ setup_ttb(dict:to_list(TPs), TProcs),
+ wxFrame:connect(LogWin, close_window, [{skip, true}]),
+ wxFrame:show(LogWin),
+ wxToggleButton:setLabel(ToggleBtn, "Stop Trace"),
+ {noreply, State};
+
+handle_event(#wx{event = #wxCommand{type = command_togglebutton_clicked, commandInt = 0}},
+ #state{toggle_button = ToggleBtn} = State) ->
+ %%Stop tracing
+ ttb:stop(nofetch),
+ wxToggleButton:setLabel(ToggleBtn, "Start Trace"),
+ {noreply, State};
+
+handle_event(#wx{id=?TRACERWIN, event=#wxClose{}},
+ #state{toggle_button = ToggleBtn} = State) ->
+ %%Stop tracing
+ ttb:stop(nofetch),
+ wxToggleButton:setLabel(ToggleBtn, "Start Trace"),
+ {noreply, State};
+
+%% handle_event(#wx{id = ?CLEAR, event = #wxCommand{type = command_menu_selected}},
+%% #state{text_ctrl = TxtCtrl} = State) ->
+%% wxTextCtrl:clear(TxtCtrl),
+%% {noreply, State};
+
+%% handle_event(#wx{id = ?SAVE_BUFFER, event = #wxCommand{type = command_menu_selected}},
+%% #state{frame = Frame, text_ctrl = TxtCtrl} = State) ->
+%% Dialog = wxFileDialog:new(Frame, [{style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}]),
+%% case wxFileDialog:showModal(Dialog) of
+%% ?wxID_OK ->
+%% Path = wxFileDialog:getPath(Dialog),
+%% wxDialog:destroy(Dialog),
+%% case filelib:is_file(Path) of
+%% true ->
+%% observer_wx:create_txt_dialog(Frame, "File already exists: " ++ Path ++ "\n",
+%% "Error", ?wxICON_ERROR);
+%% false ->
+%% wxTextCtrl:saveFile(TxtCtrl, [{file, Path}])
+%% end;
+%% _ ->
+%% wxDialog:destroy(Dialog),
+%% ok
+%% end,
+%% {noreply, State};
+
+handle_event(#wx{id = ?SAVE_TRACEOPTS,
+ event = #wxCommand{type = command_menu_selected}},
+ #state{panel = Panel,
+ def_trace_opts = TraceOpts,
+ match_specs = MatchSpecs,
+ tpatterns = TracePatterns
+ } = State) ->
+ Dialog = wxFileDialog:new(Panel, [{style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}]),
+ case wxFileDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ Path = wxFileDialog:getPath(Dialog),
+ write_file(Panel, Path, TraceOpts, MatchSpecs, dict:to_list(TracePatterns));
+ _ ->
+ ok
+ end,
+ wxDialog:destroy(Dialog),
+ {noreply, State};
+
+handle_event(#wx{id = ?LOAD_TRACEOPTS,
+ event = #wxCommand{type = command_menu_selected}},
+ #state{panel = Panel} = State) ->
+ Dialog = wxFileDialog:new(Panel, [{style, ?wxFD_FILE_MUST_EXIST}]),
+ State2 = case wxFileDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ Path = wxFileDialog:getPath(Dialog),
+ read_settings(Path, State);
+ _ ->
+ State
+ end,
+ wxDialog:destroy(Dialog),
+ {noreply, State2};
+
+handle_event(#wx{id=ID, event = What}, State) ->
+ io:format("~p:~p: Unhandled event: ~p, ~p ~n", [?MODULE, self(), ID, What]),
+ {noreply, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+handle_call(Msg, _From, State) ->
+ io:format("~p~p: Got Call ~p~n",[?MODULE, ?LINE, Msg]),
+ {reply, ok, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+handle_cast({add_processes, Pids}, State = #state{panel=Parent, def_trace_opts=TraceOpts}) ->
+ case observer_traceoptions_wx:process_trace(Parent, TraceOpts) of
+ {ok, Opts} ->
+ POpts = [#tpid{pid=Pid, opts=Opts} || Pid <- Pids],
+ {noreply, do_add_processes(POpts, State#state{def_trace_opts=Opts})};
+ cancel ->
+ {noreply, State}
+ end;
+handle_cast(Msg, State) ->
+ io:format("~p ~p: Unhandled cast ~p~n", [?MODULE, ?LINE, Msg]),
+ {noreply, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+handle_info({active, _Node}, State=#state{parent=Parent}) ->
+ create_menues(Parent),
+ {noreply, State};
+
+handle_info(not_active, State) ->
+ {noreply, State};
+
+handle_info({update_ms, NewMs}, State) ->
+ {noreply, State#state{match_specs=NewMs}};
+
+handle_info(Any, State) ->
+ io:format("~p~p: received unexpected message: ~p\n", [?MODULE, self(), Any]),
+ {noreply, State}.
+
+terminate(_Reason, #state{nodes=_Nodes}) ->
+ %% case observer_wx:try_rpc(Node, erlang, whereis, [dbg]) of
+ %% undefined -> fine;
+ %% Pid -> exit(Pid, kill)
+ %% end,
+ ok.
+
+code_change(_, _, State) ->
+ {stop, not_yet_implemented, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+do_add_processes(POpts, S0=#state{p_view=LCtrl, tpids=OldPids, nodes=Ns0}) ->
+ case merge_pids(POpts, OldPids) of
+ {OldPids, [], []} ->
+ S0;
+ {Pids, New, _Changed} ->
+ update_process_view(Pids, LCtrl),
+ Ns1 = lists:usort([node(Pid) || #tpid{pid=Pid} <- New, is_pid(Pid)]),
+ Nodes = case ordsets:subtract(Ns1, Ns0) of
+ [] -> Ns0; %% No new Nodes
+ NewNs ->
+ %% Handle new nodes
+ %% BUGBUG add trace patterns for new nodes
+ ordsets:union(NewNs, Ns0)
+ end,
+ S0#state{tpids=Pids, nodes=Nodes}
+ end.
+
+update_process_view(Pids, LCtrl) ->
+ wxListCtrl:deleteAllItems(LCtrl),
+ wx:foldl(fun(#tpid{pid=Pid, opts=Opts}, Row) ->
+ _Item = wxListCtrl:insertItem(LCtrl, Row, ""),
+ ?EVEN(Row) andalso
+ wxListCtrl:setItemBackgroundColour(LCtrl, Row, ?BG_EVEN),
+ wxListCtrl:setItem(LCtrl, Row, 0, observer_lib:to_str(Pid)),
+ wxListCtrl:setItem(LCtrl, Row, 1, observer_lib:to_str(Opts)),
+ Row+1
+ end, 0, Pids).
+
+do_add_patterns({Module, NewPs}, State=#state{tpatterns=TPs0, m_view=Mview, f_view=Fview}) ->
+ Old = case dict:find(Module, TPs0) of
+ {ok, Prev} -> Prev;
+ error -> []
+ end,
+ case merge_patterns(NewPs, Old) of
+ {Old, [], []} ->
+ State;
+ {MPatterns, _New, _Changed} ->
+ TPs = dict:store(Module, MPatterns, TPs0),
+ update_modules_view(lists:sort(dict:fetch_keys(TPs)), Module, Mview),
+ update_functions_view(dict:fetch(Module, TPs), Fview),
+ State#state{tpatterns=TPs}
+ end.
+
+update_modules_view(Mods, Module, LCtrl) ->
+ wxListCtrl:deleteAllItems(LCtrl),
+ wx:foldl(fun(Mod, Row) ->
+ _Item = wxListCtrl:insertItem(LCtrl, Row, ""),
+ ?EVEN(Row) andalso
+ wxListCtrl:setItemBackgroundColour(LCtrl, Row, ?BG_EVEN),
+ wxListCtrl:setItem(LCtrl, Row, 0, observer_lib:to_str(Mod)),
+ (Mod =:= Module) andalso
+ wxListCtrl:setItemState(LCtrl, Row, 16#FFFF, ?wxLIST_STATE_SELECTED),
+ Row+1
+ end, 0, Mods).
+
+update_functions_view(Funcs, LCtrl) ->
+ wxListCtrl:deleteAllItems(LCtrl),
+ wx:foldl(fun(#tpattern{fa=FA, ms=#match_spec{str=Ms}}, Row) ->
+ _Item = wxListCtrl:insertItem(LCtrl, Row, ""),
+ ?EVEN(Row) andalso wxListCtrl:setItemBackgroundColour(LCtrl, Row, ?BG_EVEN),
+ wxListCtrl:setItem(LCtrl, Row, 0, observer_lib:to_str({func,FA})),
+ wxListCtrl:setItem(LCtrl, Row, 1, Ms),
+ Row+1
+ end, 0, Funcs).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+merge_pids([N1=#tpid{pid=new}|Ns], [N2=#tpid{pid=new}|Old]) ->
+ {Pids, New, Changed} = merge_pids_1(Ns,Old),
+ {[N1|Pids], New, [{N2,N2}|Changed]};
+merge_pids([N1=#tpid{pid=new}|Ns], Old) ->
+ {Pids, New, Changed} = merge_pids_1(Ns,Old),
+ {[N1|Pids], [N1|New], Changed};
+merge_pids(Ns, [N2=#tpid{pid=new}|Old]) ->
+ {Pids, New, Changed} = merge_pids_1(Ns,Old),
+ {[N2|Pids], New, Changed};
+merge_pids(New, Old) ->
+ merge_pids_1(New, Old).
+
+merge_pids_1(New, Old) ->
+ merge(lists:sort(New), Old, #tpid.pid, [], [], []).
+
+merge_patterns(New, Old) ->
+ merge(lists:sort(New), Old, #tpattern.fa, [], [], []).
+
+
+merge([N|Ns], [N|Os], El, New, Ch, All) ->
+ merge(Ns, Os, El, New, Ch, [N|All]);
+merge([N|Ns], [O|Os], El, New, Ch, All)
+ when element(El, N) == element(El, O) ->
+ merge(Ns, Os, El, New, [{O,N}|Ch], [N|All]);
+merge([N|Ns], Os=[O|_], El, New, Ch, All)
+ when element(El, N) < element(El, O) ->
+ merge(Ns, Os, El, [N|New], Ch, [N|All]);
+merge(Ns=[N|_], [O|Os], El, New, Ch, All)
+ when element(El, N) > element(El, O) ->
+ merge(Ns, Os, El, New, Ch, [O|All]);
+merge([], Os, _El, New, Ch, All) ->
+ {lists:reverse(All, Os), New, Ch};
+merge(Ns, [], _El, New, Ch, All) ->
+ {lists:reverse(All, Ns), Ns++New, Ch}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+setup_ttb(TPs, TPids) ->
+ _R1 = [setup_tps(FTP, []) || {_, FTP} <- TPs],
+ _R2 = [ttb:p(Pid, dbg_flags(Flags)) || #tpid{pid=Pid, opts=Flags} <- TPids],
+ [#tpid{pid=_Pid, opts=_Flags}|_] = TPids,
+ %% io:format("ttb:p(pid(\"~w\", ~w).", [Pid, Flags]),
+ %% io:format("TTB ~w ~w~n",[R2, R1]),
+ ok.
+
+%% Sigh order is important
+setup_tps([First=#tpattern{fa={_,'_'}}|Rest], Prev) ->
+ setup_tp(First),
+ [setup_tp(TP) || TP <- lists:reverse(Prev)],
+ setup_tps(Rest, []);
+setup_tps([First=#tpattern{fa={F,_}}|Rest], Prev = [#tpattern{fa={F,_}}|_]) ->
+ setup_tps(Rest, [First|Prev]);
+setup_tps([First|Rest], Prev) ->
+ [setup_tp(TP) || TP <- lists:reverse(Prev)],
+ setup_tps(Rest, [First]);
+setup_tps([], Prev) ->
+ [setup_tp(TP) || TP <- lists:reverse(Prev)].
+
+setup_tp(#tpattern{m=M,fa={F,A}, ms=#match_spec{term=Ms}}) ->
+ ttb:tpl(M,F,A,Ms).
+
+dbg_flags(Flags) ->
+ [dbg_flag(Flag) || Flag <- Flags].
+
+dbg_flag(send) -> s;
+dbg_flag('receive') -> r;
+dbg_flag(functions) -> c;
+dbg_flag(on_spawn) -> sos;
+dbg_flag(on_link) -> sol;
+dbg_flag(on_first_spawn) -> sofs;
+dbg_flag(on_first_link) -> sofl;
+dbg_flag(events) -> p.
+
+textformat(Trace) when element(1, Trace) == trace_ts, tuple_size(Trace) >= 4 ->
+ format_trace(Trace, tuple_size(Trace)-1, element(tuple_size(Trace),Trace));
+textformat(Trace) when element(1, Trace) == drop, tuple_size(Trace) =:= 2 ->
+ io_lib:format("*** Dropped ~p messages.~n", [element(2,Trace)]);
+textformat(Trace) when element(1, Trace) == seq_trace, tuple_size(Trace) >= 3 ->
+ io_lib:format("*** Seq trace not implmented.~n", []);
+textformat(_) ->
+ "".
+
+format_trace(Trace, Size, TS0={_,_,MS}) ->
+ {_,{H,M,S}} = calendar:now_to_local_time(TS0),
+ TS = io_lib:format("~.2.0w:~.2.0w:~.2.0w:~.6.0w", [H,M,S,MS]),
+ From = element(2, Trace),
+ case element(3, Trace) of
+ 'receive' ->
+ case element(4, Trace) of
+ {dbg,ok} -> "";
+ Message ->
+ io_lib:format("~s (~100p) << ~100p ~n", [TS,From,Message])
+ end;
+ 'send' ->
+ Message = element(4, Trace),
+ To = element(5, Trace),
+ io_lib:format("~s (~100p) ~100p ! ~100p ~n", [TS,From,To,Message]);
+ call ->
+ case element(4, Trace) of
+ MFA when Size == 5 ->
+ Message = element(5, Trace),
+ io_lib:format("~s (~100p) call ~s (~100p) ~n", [TS,From,ffunc(MFA),Message]);
+ MFA ->
+ io_lib:format("~s (~100p) call ~s ~n", [TS,From,ffunc(MFA)])
+ end;
+ return_from ->
+ MFA = element(4, Trace),
+ Ret = element(5, Trace),
+ io_lib:format("~s (~100p) returned from ~s -> ~100p ~n", [TS,From,ffunc(MFA),Ret]);
+ return_to ->
+ MFA = element(4, Trace),
+ io_lib:format("~s (~100p) returning to ~s ~n", [TS,From,ffunc(MFA)]);
+ spawn when Size == 5 ->
+ Pid = element(4, Trace),
+ MFA = element(5, Trace),
+ io_lib:format("~s (~100p) spawn ~100p as ~s ~n", [TS,From,Pid,ffunc(MFA)]);
+ Op ->
+ io_lib:format("~s (~100p) ~100p ~s ~n", [TS,From,Op,ftup(Trace,4,Size)])
+ end.
+
+%%% These f* functions returns non-flat strings
+
+%% {M,F,[A1, A2, ..., AN]} -> "M:F(A1, A2, ..., AN)"
+%% {M,F,A} -> "M:F/A"
+ffunc({M,F,Argl}) when is_list(Argl) ->
+ io_lib:format("~100p:~100p(~s)", [M, F, fargs(Argl)]);
+ffunc({M,F,Arity}) ->
+ io_lib:format("~100p:~100p/~100p", [M,F,Arity]);
+ffunc(X) -> io_lib:format("~100p", [X]).
+
+%% Integer -> "Integer"
+%% [A1, A2, ..., AN] -> "A1, A2, ..., AN"
+fargs(Arity) when is_integer(Arity) -> integer_to_list(Arity);
+fargs([]) -> [];
+fargs([A]) -> io_lib:format("~100p", [A]); %% last arg
+fargs([A|Args]) -> [io_lib:format("~100p,", [A]) | fargs(Args)];
+fargs(A) -> io_lib:format("~100p", [A]). % last or only arg
+
+%% {A_1, A_2, ..., A_N} -> "A_Index A_Index+1 ... A_Size"
+ftup(Trace, Index, Index) ->
+ io_lib:format("~100p", [element(Index, Trace)]);
+ftup(Trace, Index, Size) ->
+ [io_lib:format("~100p ", [element(Index, Trace)])
+ | ftup(Trace, Index+1, Size)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+write_file(Frame, Filename, TraceOps, MatchSpecs, TPs) ->
+ FormatMS = fun(#match_spec{name=Id, term=T, func=F}) ->
+ io_lib:format("[{name,\"~s\"}, {term, ~w}, {func, \"~s\"}]",
+ [Id, T, F])
+ end,
+ FormatTP = fun({Module, FTPs}) ->
+ List = format_ftp(FTPs, FormatMS),
+ io_lib:format("{tp, ~w, [~s]}.~n",[Module, List])
+ end,
+ Str =
+ ["%%%\n%%% This file is generated by Observer\n",
+ "%%%\n%%% DO NOT EDIT!\n%%%\n",
+ [["{ms, ", FormatMS(Ms), "}.\n"] || Ms <- MatchSpecs],
+ "{traceopts, ", io_lib:format("~w",[TraceOps]) ,"}.\n",
+ [FormatTP(TP) || TP <- TPs]
+ ],
+ case file:write_file(Filename, list_to_binary(Str)) of
+ ok ->
+ success;
+ {error, Reason} ->
+ FailMsg = file:format_error(Reason),
+ observer_wx:create_txt_dialog(Frame, FailMsg, "Error", ?wxICON_ERROR)
+ end.
+
+format_ftp([#tpattern{fa={F,A}, ms=Ms}], FormatMS) ->
+ io_lib:format("{~w, ~w, ~s}", [F,A,FormatMS(Ms)]);
+format_ftp([#tpattern{fa={F,A}, ms=Ms}|Rest], FormatMS) ->
+ [io_lib:format("{~w, ~w, ~s},~n ", [F,A,FormatMS(Ms)]),
+ format_ftp(Rest, FormatMS)].
+
+read_settings(Filename, #state{match_specs=Ms0, def_trace_opts=TO0} = State) ->
+ case file:consult(Filename) of
+ {ok, Terms} ->
+ Ms = lists:usort(Ms0 ++ [parse_ms(MsList) || {ms, MsList} <- Terms]),
+ TOs = lists:usort(TO0 ++ proplists:get_value(traceopts, Terms, [])),
+ lists:foldl(fun parse_tp/2,
+ State#state{match_specs=Ms, def_trace_opts=TOs},
+ Terms);
+ {error, _} ->
+ observer_wx:create_txt_dialog(State#state.panel, "Could not load settings",
+ "Error", ?wxICON_ERROR),
+ State
+ end.
+
+parse_ms(Opts) ->
+ Name = proplists:get_value(name, Opts, "TracePattern"),
+ Term = proplists:get_value(term, Opts, [{'_',[],[ok]}]),
+ FunStr = proplists:get_value(term, Opts, "fun(_) -> ok end"),
+ make_ms(Name, Term, FunStr).
+
+make_ms(Name, Term, FunStr) ->
+ #match_spec{name=Name, term=Term, str=io_lib:format("~w", Term), func = FunStr}.
+
+parse_tp({tp, Mod, FAs}, State) ->
+ Patterns = [#tpattern{m=Mod,fa={F,A}, ms=parse_ms(List)} ||
+ {F,A,List} <- FAs],
+ do_add_patterns({Mod, Patterns}, State);
+parse_tp(_, State) ->
+ State.
diff --git a/lib/observer/src/observer_traceoptions_wx.erl b/lib/observer/src/observer_traceoptions_wx.erl
new file mode 100644
index 0000000000..bad05ec016
--- /dev/null
+++ b/lib/observer/src/observer_traceoptions_wx.erl
@@ -0,0 +1,572 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer_traceoptions_wx).
+
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+
+-export([process_trace/2, trace_pattern/4]).
+
+-compile(export_all).
+
+process_trace(Parent, Default) ->
+ Dialog = wxDialog:new(Parent, ?wxID_ANY, "Process Options",
+ [{style, ?wxDEFAULT_DIALOG_STYLE bor ?wxRESIZE_BORDER}]),
+ Panel = wxPanel:new(Dialog),
+ MainSz = wxBoxSizer:new(?wxVERTICAL),
+ PanelSz = wxBoxSizer:new(?wxHORIZONTAL),
+ LeftSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, "Tracing options"}]),
+ RightSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, "Inheritance options:"}]),
+
+ FuncBox = wxCheckBox:new(Panel, ?wxID_ANY, "Trace function call", []),
+ check_box(FuncBox, lists:member(functions, Default)),
+ SendBox = wxCheckBox:new(Panel, ?wxID_ANY, "Trace send message", []),
+ check_box(SendBox, lists:member(send, Default)),
+ RecBox = wxCheckBox:new(Panel, ?wxID_ANY, "Trace receive message", []),
+ check_box(RecBox, lists:member('receive', Default)),
+ EventBox = wxCheckBox:new(Panel, ?wxID_ANY, "Trace process events", []),
+ check_box(EventBox, lists:member(events, Default)),
+
+ {SpawnBox, SpwnAllRadio, SpwnFirstRadio} =
+ optionpage_top_right(Panel, RightSz, [{flag, ?wxBOTTOM},{border, 5}], "spawn"),
+ {LinkBox, LinkAllRadio, LinkFirstRadio} =
+ optionpage_top_right(Panel, RightSz, [{flag, ?wxBOTTOM},{border, 5}], "link"),
+ SpawnBool = lists:member(on_spawn, Default) orelse lists:member(on_first_spawn, Default),
+ LinkBool = lists:member(on_link, Default) orelse lists:member(on_first_link, Default),
+ check_box(SpawnBox, SpawnBool),
+ check_box(LinkBox, LinkBool),
+ enable(SpawnBox, [SpwnAllRadio, SpwnFirstRadio]),
+ enable(LinkBox, [LinkAllRadio, LinkFirstRadio]),
+ wxRadioButton:setValue(SpwnAllRadio, lists:member(on_spawn, Default)),
+ wxRadioButton:setValue(SpwnFirstRadio, lists:member(on_first_spawn, Default)),
+ wxRadioButton:setValue(LinkAllRadio, lists:member(on_link, Default)),
+ wxRadioButton:setValue(LinkFirstRadio, lists:member(on_first_link, Default)),
+
+ wxSizer:add(LeftSz, FuncBox, []),
+ wxSizer:add(LeftSz, SendBox, []),
+ wxSizer:add(LeftSz, RecBox, []),
+ wxSizer:add(LeftSz, EventBox, []),
+ wxSizer:add(LeftSz, 150, -1),
+
+ wxSizer:add(PanelSz, LeftSz, [{flag, ?wxEXPAND}]),
+ wxSizer:add(PanelSz, RightSz,[{flag, ?wxEXPAND}]),
+ wxPanel:setSizer(Panel, PanelSz),
+ wxSizer:add(MainSz, Panel, [{flag, ?wxEXPAND}, {proportion,1}]),
+ Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
+ wxSizer:add(MainSz, Buttons, [{flag, ?wxEXPAND bor ?wxALL}, {border, 5}]),
+ wxWindow:setSizerAndFit(Dialog, MainSz),
+ wxSizer:setSizeHints(MainSz, Dialog),
+ wxCheckBox:connect(SpawnBox, command_checkbox_clicked,
+ [{callback, fun(#wx{event=#wxCommand{}},_) ->
+ enable(SpawnBox, [SpwnAllRadio, SpwnFirstRadio])
+ end}]),
+ wxCheckBox:connect(LinkBox, command_checkbox_clicked,
+ [{callback, fun(#wx{event=#wxCommand{}},_) ->
+ enable(LinkBox, [LinkAllRadio, LinkFirstRadio])
+ end}]),
+
+ Res = case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ All = [{SendBox, send}, {RecBox, 'receive'},
+ {FuncBox, functions}, {EventBox, events},
+ {{SpawnBox, SpwnAllRadio}, on_spawn},
+ {{SpawnBox,SpwnFirstRadio}, on_first_spawn},
+ {{LinkBox, LinkAllRadio}, on_link},
+ {{LinkBox, LinkFirstRadio}, on_first_link}],
+ Check = fun({Box, Radio}) ->
+ wxCheckBox:getValue(Box) andalso wxRadioButton:getValue(Radio);
+ (Box) ->
+ wxCheckBox:getValue(Box)
+ end,
+ Opts = [Id || {Tick, Id} <- All, Check(Tick)],
+ {ok, lists:reverse(Opts)};
+ ?wxID_CANCEL ->
+ cancel
+ end,
+ wxDialog:destroy(Dialog),
+ Res.
+
+trace_pattern(ParentPid, Parent, Node, MatchSpecs) ->
+ try
+ Module = module_selector(Parent, Node),
+ MFAs = function_selector(Parent, Node, Module),
+ MatchSpec = select_matchspec(ParentPid, Parent, MatchSpecs),
+ {Module, [#tpattern{m=M,fa={F,A},ms=MatchSpec} || {M,F,A} <- MFAs]}
+ catch cancel -> cancel
+ end.
+
+module_selector(Parent, Node) ->
+ Dialog = wxDialog:new(Parent, ?wxID_ANY, "Select Module",
+ [{style, ?wxDEFAULT_DIALOG_STYLE bor ?wxRESIZE_BORDER},
+ {size, {400, 400}}]),
+ Panel = wxPanel:new(Dialog),
+ PanelSz = wxBoxSizer:new(?wxVERTICAL),
+ MainSz = wxBoxSizer:new(?wxVERTICAL),
+
+ TxtCtrl = wxTextCtrl:new(Panel, ?wxID_ANY),
+ ListBox = wxListBox:new(Panel, ?wxID_ANY, [{style, ?wxLB_SINGLE}]),
+ wxSizer:add(PanelSz, TxtCtrl, [{flag, ?wxEXPAND}]),
+ wxSizer:add(PanelSz, ListBox, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxPanel:setSizer(Panel, PanelSz),
+ wxSizer:add(MainSz, Panel, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 1}]),
+ Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
+ wxSizer:add(MainSz, Buttons, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 0}]),
+ wxWindow:setSizer(Dialog, MainSz),
+ OkId = wxDialog:getAffirmativeId(Dialog),
+ OkButt = wxWindow:findWindowById(OkId),
+ wxWindow:disable(OkButt),
+ wxWindow:setFocus(TxtCtrl),
+ %% init data
+ Modules = get_modules(Node),
+ AllModules = [{atom_to_list(X), X} || X <- Modules],
+ filter_listbox_data("", AllModules, ListBox),
+ wxTextCtrl:connect(TxtCtrl, command_text_updated,
+ [{callback, fun(#wx{event=#wxCommand{cmdString=Input}}, _) ->
+ filter_listbox_data(Input, AllModules, ListBox)
+ end}]),
+ wxListBox:connect(ListBox, command_listbox_doubleclicked,
+ [{callback, fun(_, _) -> wxDialog:endModal(Dialog, ?wxID_OK) end}]),
+ wxListBox:connect(ListBox, command_listbox_selected,
+ [{callback, fun(#wx{event=#wxCommand{commandInt=Id}}, _) ->
+ Id >= 0 andalso wxWindow:enable(OkButt)
+ end}]),
+
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ SelId = wxListBox:getSelection(ListBox),
+ case SelId >= 0 of
+ true ->
+ Module = wxListBox:getClientData(ListBox, SelId),
+ wxDialog:destroy(Dialog),
+ Module;
+ false ->
+ wxDialog:destroy(Dialog),
+ throw(cancel)
+ end;
+ ?wxID_CANCEL ->
+ wxDialog:destroy(Dialog),
+ throw(cancel)
+ end.
+
+function_selector(Parent, Node, Module) ->
+ Dialog = wxDialog:new(Parent, ?wxID_ANY, "Trace Functions",
+ [{style, ?wxDEFAULT_DIALOG_STYLE bor ?wxRESIZE_BORDER},
+ {size, {400, 400}}]),
+
+ Panel = wxPanel:new(Dialog),
+ PanelSz = wxBoxSizer:new(?wxVERTICAL),
+ MainSz = wxBoxSizer:new(?wxVERTICAL),
+
+ TxtCtrl = wxTextCtrl:new(Panel, ?wxID_ANY),
+ ListBox = wxCheckListBox:new(Panel, ?wxID_ANY, [{style, ?wxLB_EXTENDED}]),
+ wxSizer:add(PanelSz, TxtCtrl, [{flag, ?wxEXPAND}]),
+ wxSizer:add(PanelSz, ListBox, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ SelAllBtn = wxButton:new(Panel, ?wxID_ANY, [{label, "Check Visible"}]),
+ DeSelAllBtn = wxButton:new(Panel, ?wxID_ANY, [{label, "Uncheck Visible"}]),
+ ButtonSz = wxBoxSizer:new(?wxHORIZONTAL),
+ [wxSizer:add(ButtonSz, Button, []) || Button <- [SelAllBtn, DeSelAllBtn]],
+ wxSizer:add(PanelSz, ButtonSz, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 0}]),
+ wxPanel:setSizer(Panel, PanelSz),
+ wxSizer:add(MainSz, Panel, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 1}]),
+
+ Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
+ wxSizer:add(MainSz, Buttons, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 0}]),
+ wxWindow:setSizer(Dialog, MainSz),
+ wxWindow:setFocus(TxtCtrl),
+ %% Init
+ Functions = observer_wx:try_rpc(Node, Module, module_info, [functions]),
+ Choices = lists:sort([{Name, Arity} || {Name, Arity} <- Functions,
+ not(erl_internal:guard_bif(Name, Arity))]),
+ ParsedChoices = parse_function_names(Choices),
+ filter_listbox_data("", ParsedChoices, ListBox, false),
+ %% Setup Event handling
+ wxTextCtrl:connect(TxtCtrl, command_text_updated,
+ [{callback,
+ fun(#wx{event=#wxCommand{cmdString=Input}}, _) ->
+ filter_listbox_data(Input, ParsedChoices, ListBox, false)
+ end}]),
+ Self = self(),
+
+ %% Sigh clientdata in checklistbox crashes on windows, wx-bug I presume.
+ %% Don't have time to investigate now, workaround file bug report later
+ GetClientData = fun(LB, N) ->
+ String = wxListBox:getString(LB, N),
+ {_, Data} = lists:keyfind(String, 1, ParsedChoices),
+ Data
+ end,
+ wxCheckListBox:connect(ListBox, command_checklistbox_toggled,
+ [{callback,
+ fun(#wx{event=#wxCommand{commandInt=N}}, _) ->
+ Self ! {ListBox, wxCheckListBox:isChecked(ListBox, N),
+ GetClientData(ListBox, N)}
+ end}]),
+ Check = fun(Id, Bool) ->
+ wxCheckListBox:check(ListBox, Id, [{check, Bool}]),
+ Self ! {ListBox, Bool, GetClientData(ListBox, Id)}
+ end,
+ wxButton:connect(SelAllBtn, command_button_clicked,
+ [{callback, fun(#wx{}, _) ->
+ Count = wxListBox:getCount(ListBox),
+ [Check(SelId, true) ||
+ SelId <- lists:seq(0, Count-1),
+ not wxCheckListBox:isChecked(ListBox, SelId)]
+ end}]),
+ wxButton:connect(DeSelAllBtn, command_button_clicked,
+ [{callback, fun(#wx{}, _) ->
+ Count = wxListBox:getCount(ListBox),
+ [Check(SelId, false) ||
+ SelId <- lists:seq(0, Count-1),
+ wxCheckListBox:isChecked(ListBox, SelId)]
+ end}]),
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ wxDialog:destroy(Dialog),
+ case get_checked_funcs(ListBox, []) of
+ [] -> [{Module, '_', '_'}];
+ FAs ->
+ [{Module, F, A} || {F,A} <- FAs]
+ end;
+ ?wxID_CANCEL ->
+ wxDialog:destroy(Dialog),
+ throw(cancel)
+ end.
+
+get_checked_funcs(ListBox, Acc) ->
+ receive
+ {ListBox, true, FA} ->
+ get_checked_funcs(ListBox, [FA|lists:delete(FA,Acc)]);
+ {ListBox, false, FA} ->
+ get_checked_funcs(ListBox, lists:delete(FA,Acc))
+ after 0 ->
+ lists:reverse(Acc)
+ end.
+
+select_matchspec(Pid, Parent, MatchSpecs) ->
+ Dialog = wxDialog:new(Parent, ?wxID_ANY, "Trace Match Specifications",
+ [{style, ?wxDEFAULT_DIALOG_STYLE bor ?wxRESIZE_BORDER},
+ {size, {400, 400}}]),
+
+ Panel = wxPanel:new(Dialog),
+ PanelSz = wxBoxSizer:new(?wxVERTICAL),
+ MainSz = wxBoxSizer:new(?wxVERTICAL),
+ TxtSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, "Match specification:"}]),
+ BtnSz = wxBoxSizer:new(?wxHORIZONTAL),
+ SavedSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, "Saved match specifications:"}]),
+
+ TextCtrl = create_styled_txtctrl(Panel),
+ wxSizer:add(TxtSz, TextCtrl, [{flag, ?wxEXPAND}, {proportion, 1}]),
+
+ AddMsBtn = wxButton:new(Panel, ?wxID_ANY, [{label, "New"}]),
+ EditMsBtn = wxButton:new(Panel, ?wxID_ANY, [{label, "Edit"}]),
+ DelMsBtn = wxButton:new(Panel, ?wxID_ANY, [{label, "Delete"}]),
+ wxSizer:add(BtnSz, AddMsBtn),
+ wxSizer:add(BtnSz, EditMsBtn),
+ wxSizer:add(BtnSz, DelMsBtn),
+
+ ListBox = wxListBox:new(Panel, ?wxID_ANY, []),
+ wxSizer:add(SavedSz, ListBox, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxSizer:add(PanelSz, TxtSz, [{flag, ?wxEXPAND}, {proportion, 1}]),
+ wxSizer:add(PanelSz, BtnSz),
+ wxSizer:add(PanelSz, SavedSz, [{flag, ?wxEXPAND}, {proportion, 1}]),
+
+ wxWindow:setSizer(Panel, PanelSz),
+ wxSizer:add(MainSz, Panel, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 1}]),
+ Buttons = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL),
+ wxSizer:add(MainSz, Buttons, [{flag, ?wxEXPAND bor ?wxALL},
+ {border, 5}, {proportion, 0}]),
+ wxWindow:setSizer(Dialog, MainSz),
+ OkId = wxDialog:getAffirmativeId(Dialog),
+ OkButt = wxWindow:findWindowById(OkId),
+ wxWindow:disable(OkButt),
+ wxWindow:disable(EditMsBtn),
+ wxWindow:disable(DelMsBtn),
+
+ Choices = ms_names(MatchSpecs),
+ filter_listbox_data("", Choices, ListBox),
+
+ Add = fun(_,_) ->
+ case edit_ms(TextCtrl, new, Parent) of
+ Ms = #match_spec{} -> add_and_select(-1, Ms, ListBox);
+ Else -> Else
+ end
+ end,
+ Edit = fun(_,_) ->
+ SelId = wxListBox:getSelection(ListBox),
+ case SelId >= 0 of
+ true ->
+ #match_spec{name=Name} = wxListBox:getClientData(ListBox,SelId),
+ case edit_ms(TextCtrl, Name, Parent) of
+ Ms = #match_spec{} -> add_and_select(SelId, Ms, ListBox);
+ Else -> Else
+ end;
+ false ->
+ ok
+ end
+ end,
+ Del = fun(_,_) ->
+ SelId = wxListBox:getSelection(ListBox),
+ case SelId >= 0 of
+ true ->
+ wxListBox:delete(ListBox, SelId);
+ false ->
+ ok
+ end
+ end,
+ Sel = fun(#wx{event=#wxCommand{commandInt=Id}}, _) ->
+ case Id >= 0 of
+ true ->
+ wxWindow:enable(OkButt),
+ wxWindow:enable(EditMsBtn),
+ wxWindow:enable(DelMsBtn),
+ #match_spec{func=Str} = wxListBox:getClientData(ListBox,Id),
+ wxStyledTextCtrl:setText(TextCtrl, Str);
+ false ->
+ try
+ wxWindow:disable(OkButt),
+ wxWindow:disable(EditMsBtn),
+ wxWindow:disable(DelMsBtn)
+ catch _:_ -> ok
+ end
+ end
+ end,
+ wxButton:connect(AddMsBtn, command_button_clicked, [{callback,Add}]),
+ wxButton:connect(EditMsBtn, command_button_clicked, [{callback,Edit}]),
+ wxButton:connect(DelMsBtn, command_button_clicked, [{callback,Del}]),
+ wxListBox:connect(ListBox, command_listbox_selected, [{callback, Sel}]),
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ SelId = wxListBox:getSelection(ListBox),
+ Count = wxListBox:getCount(ListBox),
+ MSs = [wxListBox:getClientData(ListBox, Id) ||
+ Id <- lists:seq(0, Count-1)],
+ Pid ! {update_ms, MSs},
+ MS = lists:nth(SelId+1, MSs),
+ wxDialog:destroy(Dialog),
+ MS;
+ ?wxID_CANCEL ->
+ wxDialog:destroy(Dialog),
+ throw(cancel)
+ end.
+
+edit_ms(TextCtrl, Label0, Parent) ->
+ Str = ensure_last_is_dot(wxStyledTextCtrl:getText(TextCtrl)),
+ try
+ MatchSpec = ms_from_string(Str),
+ Label = case Label0 == new of
+ true -> get_label(Parent);
+ _ -> Label0
+ end,
+ #match_spec{name=Label, term=MatchSpec,
+ str=io_lib:format("~w",[MatchSpec]),
+ func=Str}
+ catch
+ throw:cancel ->
+ ok;
+ throw:Error ->
+ observer_wx:create_txt_dialog(Parent, Error, "Error", ?wxICON_ERROR),
+ ok
+ end.
+
+get_label(Frame) ->
+ Dialog = wxTextEntryDialog:new(Frame, "Enter alias: "),
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ wxTextEntryDialog:getValue(Dialog);
+ ?wxID_CANCEL ->
+ throw(cancel)
+ end.
+
+ms_from_string(Str) ->
+ try
+ Tokens = case erl_scan:string(Str) of
+ {ok, Ts, _} -> Ts;
+ {error, {SLine, SMod, SError}, _} ->
+ throw(io_lib:format("~w: ~s", [SLine,SMod:format_error(SError)]))
+ end,
+ Exprs = case erl_parse:parse_exprs(Tokens) of
+ {ok, T} -> T;
+ {error, {PLine, PMod, PError}} ->
+ throw(io_lib:format("~w: ~s", [PLine,PMod:format_error(PError)]))
+ end,
+ Term = case Exprs of
+ [{'fun', _, {clauses, Clauses}}|_] ->
+ case ms_transform:transform_from_shell(dbg,Clauses,orddict:new()) of
+ {error, [{_,[{MSLine,Mod,MSInfo}]}],_} ->
+ throw(io_lib:format("~w: ~p", [MSLine,Mod:format_error(MSInfo)]));
+ {error, _} ->
+ throw("Could not convert fun() to match spec");
+ Ms ->
+ Ms
+ end;
+ [Expr|_] ->
+ erl_parse:normalise(Expr)
+ end,
+ case erlang:match_spec_test([], Term, trace) of
+ {ok, _, _, _} -> Term;
+ {error, List} -> throw([[Error, $\n] || {_, Error} <- List])
+ end
+ catch error:_Reason ->
+ %% io:format("Bad term: ~s~n ~p in ~p~n", [Str, _Reason, erlang:get_stacktrace()]),
+ throw("Invalid term")
+ end.
+
+add_and_select(Id, MS0, ListBox) ->
+ [{Str,User}] = ms_names([MS0]),
+ Sel = case Id >= 0 of
+ true ->
+ wxListBox:setString(ListBox, Id, Str),
+ wxListBox:setClientData(ListBox, Id, User),
+ Id;
+ false ->
+ wxListBox:append(ListBox, Str, User)
+ end,
+ wxListBox:setSelection(ListBox, Sel).
+
+filter_listbox_data(Input, Data, ListBox) ->
+ filter_listbox_data(Input, Data, ListBox, true).
+
+filter_listbox_data(Input, Data, ListBox, AddClientData) ->
+ FilteredData = [X || X = {Str, _} <- Data, re:run(Str, Input) =/= nomatch],
+ wxListBox:clear(ListBox),
+ wxListBox:appendStrings(ListBox, [Str || {Str,_} <- FilteredData]),
+ AddClientData andalso
+ wx:foldl(fun({_, Term}, N) ->
+ wxListBox:setClientData(ListBox, N, Term),
+ N+1
+ end, 0, FilteredData),
+ FilteredData.
+
+get_modules(Node) ->
+ lists:sort([Module || {Module, _} <- observer_wx:try_rpc(Node, code, all_loaded, [])]).
+
+optionpage_top_right(Panel, TopRightSz, Options, Text) ->
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ ChkBox = wxCheckBox:new(Panel, ?wxID_ANY, "Inherit on " ++ Text, []),
+ RadioSz = wxBoxSizer:new(?wxVERTICAL),
+ Radio1 = wxRadioButton:new(Panel, ?wxID_ANY, "All " ++ Text, [{style, ?wxRB_GROUP}]),
+ Radio2 = wxRadioButton:new(Panel, ?wxID_ANY, "First " ++ Text ++ " only", []),
+ wxSizer:add(Sizer, ChkBox, []),
+ wxSizer:add(RadioSz, Radio1, []),
+ wxSizer:add(RadioSz, Radio2, []),
+ wxSizer:add(Sizer, RadioSz, [{flag, ?wxLEFT},{border, 20}]),
+ wxSizer:add(TopRightSz, Sizer, Options),
+ {ChkBox, Radio1, Radio2}.
+
+
+create_styled_txtctrl(Parent) ->
+ FixedFont = observer_wx:get_attrib({font, fixed}),
+ Ed = wxStyledTextCtrl:new(Parent),
+ wxStyledTextCtrl:styleClearAll(Ed),
+ wxStyledTextCtrl:styleSetFont(Ed, ?wxSTC_STYLE_DEFAULT, FixedFont),
+ wxStyledTextCtrl:setLexer(Ed, ?wxSTC_LEX_ERLANG),
+ wxStyledTextCtrl:setMarginType(Ed, 1, ?wxSTC_MARGIN_NUMBER),
+ wxStyledTextCtrl:setSelectionMode(Ed, ?wxSTC_SEL_LINES),
+ wxStyledTextCtrl:setUseHorizontalScrollBar(Ed, false),
+
+ Styles = [{?wxSTC_ERLANG_DEFAULT, {0,0,0}},
+ {?wxSTC_ERLANG_COMMENT, {160,53,35}},
+ {?wxSTC_ERLANG_VARIABLE, {150,100,40}},
+ {?wxSTC_ERLANG_NUMBER, {5,5,100}},
+ {?wxSTC_ERLANG_KEYWORD, {130,40,172}},
+ {?wxSTC_ERLANG_STRING, {170,45,132}},
+ {?wxSTC_ERLANG_OPERATOR, {30,0,0}},
+ {?wxSTC_ERLANG_ATOM, {0,0,0}},
+ {?wxSTC_ERLANG_FUNCTION_NAME, {64,102,244}},
+ {?wxSTC_ERLANG_CHARACTER,{236,155,172}},
+ {?wxSTC_ERLANG_MACRO, {40,144,170}},
+ {?wxSTC_ERLANG_RECORD, {40,100,20}},
+ {?wxSTC_ERLANG_SEPARATOR,{0,0,0}},
+ {?wxSTC_ERLANG_NODE_NAME,{0,0,0}}],
+ SetStyle = fun({Style, Color}) ->
+ wxStyledTextCtrl:styleSetFont(Ed, Style, FixedFont),
+ wxStyledTextCtrl:styleSetForeground(Ed, Style, Color)
+ end,
+ [SetStyle(Style) || Style <- Styles],
+ wxStyledTextCtrl:setKeyWords(Ed, 0, keyWords()),
+ Ed.
+
+
+keyWords() ->
+ L = ["after","begin","case","try","cond","catch","andalso","orelse",
+ "end","fun","if","let","of","query","receive","when","bnot","not",
+ "div","rem","band","and","bor","bxor","bsl","bsr","or","xor"],
+ lists:flatten([K ++ " " || K <- L] ++ [0]).
+
+
+enable(CheckBox, Radio) ->
+ case wxCheckBox:isChecked(CheckBox) of
+ false ->
+ [wxWindow:disable(R) || R <- Radio];
+ true ->
+ [wxWindow:enable(R) || R <- Radio]
+ end.
+
+
+check_box(ChkBox, Bool) ->
+ case Bool of
+ true ->
+ wxCheckBox:set3StateValue(ChkBox, ?wxCHK_CHECKED);
+ false ->
+ ignore
+ end.
+
+parse_function_names(Choices) ->
+ StrList = [{atom_to_list(Name) ++ "/" ++ integer_to_list(Arity), Term}
+ || Term = {Name, Arity} <- Choices],
+ parse_function_names(StrList, []).
+
+parse_function_names([], Acc) ->
+ lists:reverse(Acc);
+parse_function_names([{H, Term}|T], Acc) ->
+ IsFun = re:run(H, ".*-fun-\\d*?-"),
+ IsLc = re:run(H, ".*-lc\\$\\^\\d*?/\\d*?-\\d*?-"),
+ IsLbc = re:run(H, ".*-lbc\\$\\^\\d*?/\\d*?-\\d*?-"),
+ Parsed =
+ if IsFun =/= nomatch -> "Fun: " ++ H;
+ IsLc =/= nomatch -> "List comprehension: " ++ H;
+ IsLbc =/= nomatch -> "Bit comprehension: " ++ H;
+ true ->
+ H
+ end,
+ parse_function_names(T, [{Parsed, Term} | Acc]).
+
+ms_names(MatchSpecList) ->
+ MsOrAlias = fun(#match_spec{name = A, str = M}) ->
+ case A of
+ "" -> M;
+ _ -> A ++ " " ++ M
+ end
+ end,
+ [{MsOrAlias(X), X} || X <- MatchSpecList].
+
+ensure_last_is_dot([]) ->
+ ".";
+ensure_last_is_dot(String) ->
+ case lists:last(String) =:= $. of
+ true ->
+ String;
+ false ->
+ String ++ "."
+ end.
diff --git a/lib/observer/src/observer_tv.hrl b/lib/observer/src/observer_tv.hrl
new file mode 100644
index 0000000000..05e4f928d0
--- /dev/null
+++ b/lib/observer/src/observer_tv.hrl
@@ -0,0 +1,34 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-record(tab, {name,
+ id = ignore,
+ size,
+ memory=0, %% In bytes
+ owner,
+ reg_name,
+ protection = public,
+ type=set,
+ keypos=1,
+ heir=none,
+ compressed=false,
+ fixed=false,
+ %% Mnesia Info
+ storage,
+ index
+ }).
diff --git a/lib/observer/src/observer_tv_table.erl b/lib/observer/src/observer_tv_table.erl
new file mode 100644
index 0000000000..7b5cdb44b9
--- /dev/null
+++ b/lib/observer/src/observer_tv_table.erl
@@ -0,0 +1,801 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+
+-module(observer_tv_table).
+
+-export([start_link/2]).
+
+%% wx_object callbacks
+-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
+ handle_event/2, handle_sync_event/3, handle_cast/2]).
+
+-export([get_table/3]).
+
+-include("observer_defs.hrl").
+-import(observer_lib, [to_str/1]).
+
+-behaviour(wx_object).
+-include_lib("wx/include/wx.hrl").
+-include("observer_tv.hrl").
+
+-define(ID_TABLE_INFO, 400).
+-define(ID_REFRESH, 401).
+-define(ID_REFRESH_INTERVAL, 402).
+-define(ID_EDIT, 403).
+-define(ID_DELETE, 404).
+-define(ID_SEARCH, 405).
+
+-define(SEARCH_ENTRY, 420).
+-define(GOTO_ENTRY, 421).
+
+-define(DEFAULT_COL_WIDTH, 150).
+
+-record(state,
+ {
+ parent,
+ frame,
+ grid,
+ status,
+ sizer,
+ search,
+ selected,
+ node=node(),
+ columns,
+ pid,
+ source,
+ tab,
+ attrs,
+ timer
+ }).
+
+-record(opt,
+ {
+ sort_key=2,
+ sort_incr=true
+ }).
+
+-record(search,
+ {enable=true, % Subwindow is enabled
+ win, % Sash Sub window obj
+ name, % name
+
+ search, % Search input ctrl
+ goto, % Goto input ctrl
+ radio, % Radio buttons
+
+ find % Search string
+ }).
+
+-record(find, {start, % start pos
+ strlen, % Found
+ found % false
+ }).
+
+start_link(Parent, Opts) ->
+ wx_object:start_link(?MODULE, [Parent, Opts], []).
+
+init([Parent, Opts]) ->
+ Source = proplists:get_value(type, Opts),
+ Table = proplists:get_value(table, Opts),
+ Node = proplists:get_value(node, Opts),
+ Title0 = atom_to_list(Table#tab.name) ++ " @ " ++ atom_to_list(Node),
+ Title = case Source of
+ ets -> "TV Ets: " ++ Title0;
+ mnesia -> "TV Mnesia: " ++ Title0
+ end,
+ Frame = wxFrame:new(Parent, ?wxID_ANY, Title, [{size, {800, 300}}]),
+ IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"),
+ Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]),
+ wxFrame:setIcon(Frame, Icon),
+ wxIcon:destroy(Icon),
+ MenuBar = wxMenuBar:new(),
+ create_menus(MenuBar),
+ wxFrame:setMenuBar(Frame, MenuBar),
+ %% wxFrame:setAcceleratorTable(Frame, AccelTable),
+ wxMenu:connect(Frame, command_menu_selected),
+
+ StatusBar = wxFrame:createStatusBar(Frame, []),
+ try
+ TabId = table_id(Table),
+ ColumnNames = column_names(Node, Source, TabId),
+ KeyPos = key_pos(Node, Source, TabId),
+
+ Attrs = observer_lib:create_attrs(),
+
+ Self = self(),
+ Holder = spawn_link(fun() ->
+ init_table_holder(Self, Table, Source,
+ length(ColumnNames), Node, Attrs)
+ end),
+
+ Panel = wxPanel:new(Frame),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ Style = ?wxLC_REPORT bor ?wxLC_VIRTUAL bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES,
+ Grid = wxListCtrl:new(Panel, [{style, Style},
+ {onGetItemText,
+ fun(_, Item,Col) -> get_row(Holder, Item, Col+1) end},
+ {onGetItemAttr,
+ fun(_, Item) -> get_attr(Holder, Item) end}
+ ]),
+ wxListCtrl:connect(Grid, command_list_item_activated),
+ wxListCtrl:connect(Grid, command_list_item_selected),
+ wxListCtrl:connect(Grid, command_list_col_click),
+ wxListCtrl:connect(Grid, size, [{skip, true}]),
+ wxWindow:setFocus(Grid),
+
+ Search = search_area(Panel),
+ wxSizer:add(Sizer, Grid,
+ [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]),
+ wxSizer:add(Sizer, Search#search.win,
+ [{flag,?wxEXPAND bor ?wxLEFT bor ?wxRIGHT bor
+ ?wxRESERVE_SPACE_EVEN_IF_HIDDEN},
+ {border, 5}]),
+ wxWindow:setSizer(Panel, Sizer),
+ wxSizer:hide(Sizer, Search#search.win),
+
+ Cols = add_columns(Grid, 0, ColumnNames),
+ wxFrame:show(Frame),
+ {Panel, #state{frame=Frame, grid=Grid, status=StatusBar, search=Search,
+ sizer = Sizer,
+ parent=Parent, columns=Cols,
+ pid=Holder, source=Source, tab=Table#tab{keypos=KeyPos},
+ attrs=Attrs}}
+ catch node_or_table_down ->
+ wxFrame:destroy(Frame),
+ stop
+ end.
+
+add_columns(Grid, Start, ColumnNames) ->
+ Li = wxListItem:new(),
+ AddListEntry = fun(Name, Col) ->
+ wxListItem:setText(Li, to_str(Name)),
+ wxListItem:setAlign(Li, ?wxLIST_FORMAT_LEFT),
+ wxListCtrl:insertColumn(Grid, Col, Li),
+ wxListCtrl:setColumnWidth(Grid, Col, ?DEFAULT_COL_WIDTH),
+ Col + 1
+ end,
+ Cols = lists:foldl(AddListEntry, Start, ColumnNames),
+ wxListItem:destroy(Li),
+ Cols.
+
+create_menus(MB) ->
+ File = wxMenu:new(),
+ wxMenu:append(File, ?ID_TABLE_INFO, "Table Information\tCtrl-I"),
+ wxMenu:append(File, ?wxID_CLOSE, "Close"),
+ wxMenuBar:append(MB, File, "File"),
+ Edit = wxMenu:new(),
+ wxMenu:append(Edit, ?ID_EDIT, "Edit Object"),
+ wxMenu:append(Edit, ?ID_DELETE, "Delete Object\tCtrl-D"),
+ wxMenu:appendSeparator(Edit),
+ wxMenu:append(Edit, ?ID_SEARCH, "Search\tCtrl-S"),
+ wxMenu:appendSeparator(Edit),
+ wxMenu:append(Edit, ?ID_REFRESH, "Refresh\tCtrl-R"),
+ wxMenu:append(Edit, ?ID_REFRESH_INTERVAL, "Refresh interval..."),
+ wxMenuBar:append(MB, Edit, "Edit"),
+ Help = wxMenu:new(),
+ wxMenu:append(Help, ?wxID_HELP, "Help"),
+ wxMenuBar:append(MB, Help, "Help"),
+ ok.
+
+search_area(Parent) ->
+ HSz = wxBoxSizer:new(?wxHORIZONTAL),
+ wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Find:"),
+ [{flag,?wxALIGN_CENTER_VERTICAL}]),
+ TC1 = wxTextCtrl:new(Parent, ?SEARCH_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]),
+ wxSizer:add(HSz, TC1, [{proportion,3}, {flag, ?wxEXPAND}]),
+ Nbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Next"),
+ wxRadioButton:setValue(Nbtn, true),
+ wxSizer:add(HSz,Nbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
+ Pbtn = wxRadioButton:new(Parent, ?wxID_ANY, "Previous"),
+ wxSizer:add(HSz,Pbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
+ Cbtn = wxCheckBox:new(Parent, ?wxID_ANY, "Match Case"),
+ wxSizer:add(HSz,Cbtn,[{flag,?wxALIGN_CENTER_VERTICAL}]),
+ wxSizer:add(HSz, 15,15, [{proportion,1}, {flag, ?wxEXPAND}]),
+ wxSizer:add(HSz, wxStaticText:new(Parent, ?wxID_ANY, "Goto Entry:"),
+ [{flag,?wxALIGN_CENTER_VERTICAL}]),
+ TC2 = wxTextCtrl:new(Parent, ?GOTO_ENTRY, [{style, ?wxTE_PROCESS_ENTER}]),
+ wxSizer:add(HSz, TC2, [{proportion,0}, {flag, ?wxEXPAND}]),
+ wxTextCtrl:connect(TC1, command_text_updated),
+ wxTextCtrl:connect(TC1, command_text_enter),
+ wxTextCtrl:connect(TC1, kill_focus),
+ wxTextCtrl:connect(TC2, command_text_enter),
+ wxWindow:connect(Parent, command_button_clicked),
+
+ #search{name='Search Area', win=HSz,
+ search=TC1,goto=TC2,radio={Nbtn,Pbtn,Cbtn}}.
+
+edit(Index, #state{pid=Pid, frame=Frame}) ->
+ Str = get_row(Pid, Index, all),
+ Dialog = wxTextEntryDialog:new(Frame, "Edit object:", [{value, Str}]),
+ case wxTextEntryDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ New = wxTextEntryDialog:getValue(Dialog),
+ wxTextEntryDialog:destroy(Dialog),
+ case Str =:= New of
+ true -> ok;
+ false ->
+ complete_edit(Index, New, Pid)
+ end;
+ ?wxID_CANCEL ->
+ wxTextEntryDialog:destroy(Dialog)
+ end.
+
+complete_edit(Row, New0, Pid) ->
+ New = case lists:reverse(New0) of
+ [$.|_] -> New0;
+ _ -> New0 ++ "."
+ end,
+ try
+ {ok, Tokens, _} = erl_scan:string(New),
+ {ok, Term} = erl_parse:parse_term(Tokens),
+ Pid ! {edit, Row, Term}
+ catch _:{badmatch, {error, {_, _, Err}}} ->
+ self() ! {error, ["Parse error: ", Err]};
+ _Err ->
+ self() ! {error, ["Syntax error in: ", New]}
+ end.
+
+handle_event(#wx{id=?ID_REFRESH},State = #state{pid=Pid}) ->
+ Pid ! refresh,
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_col_click, col=Col}},
+ State = #state{pid=Pid}) ->
+ Pid ! {sort, Col+1},
+ {noreply, State};
+
+handle_event(#wx{event=#wxSize{size={W,_}}}, State=#state{grid=Grid}) ->
+ wx:batch(fun() ->
+ Cols = wxListCtrl:getColumnCount(Grid),
+ Last = lists:foldl(fun(I, Last) ->
+ Last - wxListCtrl:getColumnWidth(Grid, I)
+ end, W-?LCTRL_WDECR, lists:seq(0, Cols - 2)),
+ Size = max(?DEFAULT_COL_WIDTH, Last),
+ wxListCtrl:setColumnWidth(Grid, Cols-1, Size)
+ end),
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_item_selected, itemIndex=Index}},
+ State = #state{pid=Pid, grid=Grid, status=StatusBar}) ->
+ N = wxListCtrl:getItemCount(Grid),
+ Str = get_row(Pid, Index, all),
+ wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w: ~s",[N, Str])),
+ {noreply, State#state{selected=Index}};
+
+handle_event(#wx{event=#wxList{type=command_list_item_activated, itemIndex=Index}},
+ State) ->
+ edit(Index, State),
+ {noreply, State};
+
+handle_event(#wx{id=?ID_EDIT}, State = #state{selected=undefined}) ->
+ {noreply, State};
+handle_event(#wx{id=?ID_EDIT}, State = #state{selected=Index}) ->
+ edit(Index, State),
+ {noreply, State};
+
+handle_event(#wx{id=?ID_DELETE}, State = #state{selected=undefined}) ->
+ {noreply, State};
+handle_event(#wx{id=?ID_DELETE},
+ State = #state{pid=Pid, status=StatusBar, selected=Index}) ->
+ Str = get_row(Pid, Index, all),
+ Pid ! {delete, Index},
+ wxStatusBar:setStatusText(StatusBar, io_lib:format("Deleted object: ~s",[Str])),
+ {noreply, State};
+
+handle_event(#wx{id=?wxID_CLOSE}, State) ->
+ {stop, normal, State};
+
+handle_event(Help = #wx{id=?wxID_HELP}, State = #state{parent=Parent}) ->
+ Parent ! Help,
+ {noreply, State};
+
+handle_event(#wx{id=?GOTO_ENTRY, event=#wxCommand{cmdString=Str}},
+ State = #state{grid=Grid}) ->
+ try
+ Row0 = list_to_integer(Str),
+ Row1 = min(0, Row0),
+ Row = max(wxListCtrl:getItemCount(Grid)-1,Row1),
+ wxListCtrl:ensureVisible(Grid, Row),
+ ok
+ catch _:_ -> ok
+ end,
+ {noreply, State};
+
+%% Search functionality
+handle_event(#wx{id=?ID_SEARCH},
+ State = #state{sizer=Sz, search=Search}) ->
+ wxSizer:show(Sz, Search#search.win),
+ wxWindow:setFocus(Search#search.search),
+ wxSizer:layout(Sz),
+ {noreply, State};
+handle_event(#wx{id=?SEARCH_ENTRY, event=#wxFocus{}},
+ State = #state{search=Search, pid=Pid}) ->
+ Pid ! {mark_search_hit, false},
+ {noreply, State#state{search=Search#search{find=undefined}}};
+handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=""}},
+ State = #state{search=Search, pid=Pid}) ->
+ Pid ! {mark_search_hit, false},
+ {noreply, State#state{search=Search#search{find=undefined}}};
+handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{type=command_text_enter,cmdString=Str}},
+ State = #state{grid=Grid, pid=Pid, status=SB,
+ search=Search=#search{radio={Next0, _, Case0},
+ find=Find}})
+ when Find =/= undefined ->
+ Dir = wxRadioButton:getValue(Next0) xor wx_misc:getKeyState(?WXK_SHIFT),
+ Case = wxCheckBox:getValue(Case0),
+ Pos = if Find#find.found, Dir -> %% Forward Continuation
+ Find#find.start+1;
+ Find#find.found -> %% Backward Continuation
+ Find#find.start-1;
+ Dir -> %% Forward wrap
+ 0;
+ true -> %% Backward wrap
+ wxListCtrl:getItemCount(Grid)-1
+ end,
+ Pid ! {mark_search_hit, false},
+ case search(Pid, Str, Pos, Dir, Case) of
+ false ->
+ wxStatusBar:setStatusText(SB, "Not found"),
+ Pid ! {mark_search_hit, Find#find.start},
+ wxListCtrl:refreshItem(Grid, Find#find.start),
+ {noreply, State#state{search=Search#search{find=#find{found=false}}}};
+ Row ->
+ wxListCtrl:ensureVisible(Grid, Row),
+ wxListCtrl:refreshItem(Grid, Row),
+ Status = "Found: (Hit Enter for next, Shift-Enter for previous)",
+ wxStatusBar:setStatusText(SB, Status),
+ {noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}}
+ end;
+handle_event(#wx{id=?SEARCH_ENTRY, event=#wxCommand{cmdString=Str}},
+ State = #state{grid=Grid, pid=Pid, status=SB,
+ search=Search=#search{radio={Next0, _, Case0},
+ find=Find}}) ->
+ try
+ Dir = wxRadioButton:getValue(Next0),
+ Case = wxCheckBox:getValue(Case0),
+ Start = case Dir of
+ true -> 0;
+ false -> wxListCtrl:getItemCount(Grid)-1
+ end,
+ Cont = case Find of
+ undefined ->
+ #find{start=Start, strlen=length(Str)};
+ #find{strlen=Old} when Old < length(Str) ->
+ Find#find{start=Start, strlen=length(Str)};
+ _ ->
+ Find#find{strlen=length(Str)}
+ end,
+
+ Pid ! {mark_search_hit, false},
+ case search(Pid, Str, Cont#find.start, Dir, Case) of
+ false ->
+ wxStatusBar:setStatusText(SB, "Not found"),
+ {noreply, State};
+ Row ->
+ wxListCtrl:ensureVisible(Grid, Row),
+ wxListCtrl:refreshItem(Grid, Row),
+ Status = "Found: (Hit Enter for next, Shift-Enter for previous)",
+ wxStatusBar:setStatusText(SB, Status),
+ {noreply, State#state{search=Search#search{find=#find{start=Row, found=true}}}}
+ end
+ catch _:_ -> {noreply, State}
+ end;
+
+handle_event(#wx{id=?ID_TABLE_INFO},
+ State = #state{frame=Frame, node=Node, source=Source, tab=Table}) ->
+ observer_tv_wx:display_table_info(Frame, Node, Source, Table),
+ {noreply, State};
+
+handle_event(#wx{id=?ID_REFRESH_INTERVAL},
+ State = #state{grid=Grid, timer=Timer0}) ->
+ Timer = observer_lib:interval_dialog(Grid, Timer0, 10, 5*60),
+ {noreply, State#state{timer=Timer}};
+
+handle_event(Event, State) ->
+ io:format("~p:~p, handle event ~p\n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+handle_sync_event(Event, _Obj, _State) ->
+ io:format("~p:~p, handle sync_event ~p\n", [?MODULE, ?LINE, Event]),
+ ok.
+
+handle_call(Event, From, State) ->
+ io:format("~p:~p, handle call (~p) ~p\n", [?MODULE, ?LINE, From, Event]),
+ {noreply, State}.
+
+handle_cast(Event, State) ->
+ io:format("~p:~p, handle cast ~p\n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+handle_info({no_rows, N}, State = #state{grid=Grid, status=StatusBar}) ->
+ wxListCtrl:setItemCount(Grid, N),
+ wxStatusBar:setStatusText(StatusBar, io_lib:format("Objects: ~w",[N])),
+ {noreply, State};
+handle_info({new_cols, New}, State = #state{grid=Grid, columns=Cols0}) ->
+ Cols = add_columns(Grid, Cols0, New),
+ {noreply, State#state{columns=Cols}};
+handle_info({refresh, Min, Max}, State = #state{grid=Grid}) ->
+ wxListCtrl:refreshItems(Grid, Min, Max),
+ {noreply, State};
+handle_info({error, Error}, State = #state{frame=Frame}) ->
+ Dlg = wxMessageDialog:new(Frame, Error),
+ wxMessageDialog:showModal(Dlg),
+ wxMessageDialog:destroy(Dlg),
+ {noreply, State};
+
+handle_info(Event, State) ->
+ io:format("~p:~p, handle info ~p\n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+terminate(_Event, #state{pid=Pid, attrs=Attrs}) ->
+ %% ListItemAttr are not auto deleted
+ #attrs{odd=Odd, deleted=D, changed=Ch, searched=S} = Attrs,
+ wxListItemAttr:destroy(Odd),
+ wxListItemAttr:destroy(D),
+ wxListItemAttr:destroy(Ch),
+ wxListItemAttr:destroy(S),
+ unlink(Pid),
+ exit(Pid, window_closed),
+ ok.
+
+code_change(_, _, State) ->
+ State.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%% Table holder needs to be in a separate process otherwise
+%% the callback get_row/3 may deadlock if the process do
+%% wx calls when callback is invoked.
+get_row(Table, Item, Column) ->
+ Ref = erlang:monitor(process, Table),
+ Table ! {get_row, self(), Item, Column},
+ receive
+ {'DOWN', Ref, _, _, _} -> "";
+ {Table, Res} ->
+ erlang:demonitor(Ref),
+ Res
+ end.
+
+get_attr(Table, Item) ->
+ Ref = erlang:monitor(process, Table),
+ Table ! {get_attr, self(), Item},
+ receive
+ {'DOWN', Ref, _, _, _} -> "";
+ {Table, Res} ->
+ erlang:demonitor(Ref),
+ Res
+ end.
+
+search(Table, Str, Row, Dir, Case) ->
+ Ref = erlang:monitor(process, Table),
+ Table ! {search, [Str, Row, Dir, Case]},
+ receive
+ {'DOWN', Ref, _, _, _} -> "";
+ {Table, Res} ->
+ erlang:demonitor(Ref),
+ Res
+ end.
+
+-record(holder, {node, parent, pid,
+ table=[], n=0, columns,
+ temp=[],
+ search,
+ source, tabid,
+ sort,
+ key,
+ type,
+ attrs
+ }).
+
+init_table_holder(Parent, Table, MnesiaOrEts, Cols, Node, Attrs) ->
+ TabId = case Table#tab.id of
+ ignore -> Table#tab.name;
+ Id -> Id
+ end,
+ self() ! refresh,
+ table_holder(#holder{node=Node, parent=Parent,
+ source=MnesiaOrEts, tabid=TabId, columns=Cols,
+ sort=#opt{sort_key=Table#tab.keypos, sort_incr=true},
+ type=Table#tab.type, key=Table#tab.keypos,
+ attrs=Attrs}).
+
+table_holder(S0 = #holder{parent=Parent, pid=Pid, table=Table}) ->
+ receive
+ {get_attr, From, Row} ->
+ get_attr(From, Row, S0),
+ table_holder(S0);
+ {get_row, From, Row, Col} ->
+ get_row(From, Row, Col, Table),
+ table_holder(S0);
+ {Pid, Data} ->
+ S1 = handle_new_data_chunk(Data, S0),
+ table_holder(S1);
+ {sort, Col} ->
+ table_holder(sort(Col, S0));
+ {search, Data} ->
+ table_holder(search(Data, S0));
+ {mark_search_hit, Row} ->
+ Old = S0#holder.search,
+ is_integer(Old) andalso (Parent ! {refresh, Old, Old}),
+ table_holder(S0#holder{search=Row});
+ refresh when is_pid(Pid) ->
+ %% Already getting the table...
+ %% io:format("ignoring refresh", []),
+ table_holder(S0);
+ refresh ->
+ GetTab = rpc:call(S0#holder.node, ?MODULE, get_table,
+ [self(), S0#holder.tabid, S0#holder.source]),
+ table_holder(S0#holder{pid=GetTab});
+ {delete, Row} ->
+ delete_row(Row, S0),
+ table_holder(S0);
+ {edit, Row, Term} ->
+ edit_row(Row, Term, S0),
+ table_holder(S0);
+ What ->
+ io:format("Table holder got ~p~n",[What]),
+ table_holder(S0)
+ end.
+
+handle_new_data_chunk(Data, S0 = #holder{columns=Cols, parent=Parent}) ->
+ S1 = #holder{columns=NewCols} = handle_new_data_chunk2(Data, S0),
+ case NewCols =:= Cols of
+ true -> S1;
+ false ->
+ Parent ! {new_cols, lists:seq(Cols+1, NewCols)},
+ S1
+ end.
+
+handle_new_data_chunk2('$end_of_table',
+ S0 = #holder{parent=Parent, sort=Opt,
+ key=Key,
+ table=Old, temp=New}) ->
+ Table = merge(Old, New, Key),
+ N = length(Table),
+ Parent ! {no_rows, N},
+ sort(Opt#opt.sort_key, S0#holder{n=N, pid=undefine,
+ sort=Opt#opt{sort_key = undefined},
+ table=Table, temp=[]});
+handle_new_data_chunk2(Data, S0 = #holder{columns=Cols0, source=ets, temp=Tab0}) ->
+ {Tab, Cols} = parse_ets_data(Data, Cols0, Tab0),
+ S0#holder{columns=Cols, temp=Tab};
+handle_new_data_chunk2(Data, S0 = #holder{source=mnesia, temp=Tab}) ->
+ S0#holder{temp=(Data ++ Tab)}.
+
+parse_ets_data([[Rec]|Rs], C, Tab) ->
+ parse_ets_data(Rs, max(tuple_size(Rec), C), [Rec|Tab]);
+parse_ets_data([Recs|Rs], C0, Tab0) ->
+ {Tab, Cols} = parse_ets_data(Recs, C0, Tab0),
+ parse_ets_data(Rs, Cols, Tab);
+parse_ets_data([], Cols, Tab) ->
+ {Tab, Cols}.
+
+sort(Col, S=#holder{n=N, parent=Parent, sort=Opt0, table=Table0}) ->
+ {Opt, Table} = sort(Col, Opt0, Table0),
+ Parent ! {refresh, 0, N-1},
+ S#holder{sort=Opt, table=Table}.
+
+sort(Col, Opt = #opt{sort_key=Col, sort_incr=Bool}, Table) ->
+ {Opt#opt{sort_incr=not Bool}, lists:reverse(Table)};
+sort(Col, S=#opt{sort_incr=true}, Table) ->
+ {S#opt{sort_key=Col}, keysort(Col, Table)};
+sort(Col, S=#opt{sort_incr=false}, Table) ->
+ {S#opt{sort_key=Col}, lists:reverse(keysort(Col, Table))}.
+
+keysort(Col, Table) ->
+ Sort = fun([A0|_], [B0|_]) ->
+ A = try element(Col, A0) catch _:_ -> [] end,
+ B = try element(Col, B0) catch _:_ -> [] end,
+ case A == B of
+ true -> A0 =< B0;
+ false -> A < B
+ end;
+ (A0, B0) when is_tuple(A0), is_tuple(B0) ->
+ A = try element(Col, A0) catch _:_ -> [] end,
+ B = try element(Col, B0) catch _:_ -> [] end,
+ case A == B of
+ true -> A0 =< B0;
+ false -> A < B
+ end
+ end,
+ lists:sort(Sort, Table).
+
+search([Str, Row, Dir0, CaseSens],
+ S=#holder{parent=Parent, table=Table}) ->
+ Opt = case CaseSens of
+ true -> [];
+ false -> [caseless]
+ end,
+ {ok, Re} = re:compile(Str, Opt),
+ Dir = case Dir0 of
+ true -> 1;
+ false -> -1
+ end,
+ Res = search(Row, Dir, Re, Table),
+ Parent ! {self(), Res},
+ S#holder{search=Res}.
+
+search(Row, Dir, Re, Table) ->
+ Res = try lists:nth(Row+1, Table) of
+ Term ->
+ Str = io_lib:format("~w", [Term]),
+ re:run(Str, Re)
+ catch _:_ -> no_more
+ end,
+ case Res of
+ nomatch -> search(Row+Dir, Dir, Re, Table);
+ no_more -> false;
+ {match,_} -> Row
+ end.
+
+get_row(From, Row, Col, Table) ->
+ case lists:nth(Row+1, Table) of
+ [Object|_] when Col =:= all ->
+ From ! {self(), io_lib:format("~w", [Object])};
+ [Object|_] when tuple_size(Object) >= Col ->
+ From ! {self(), io_lib:format("~w", [element(Col, Object)])};
+ _ ->
+ From ! {self(), ""}
+ end.
+
+get_attr(From, Row, #holder{attrs=Attrs, search=Row}) ->
+ What = Attrs#attrs.searched,
+ From ! {self(), What};
+get_attr(From, Row, #holder{table=Table, attrs=Attrs}) ->
+ What = case lists:nth(Row+1, Table) of
+ [_|deleted] -> Attrs#attrs.deleted;
+ [_|changed] -> Attrs#attrs.changed;
+ [_|new] -> Attrs#attrs.changed;
+ _ when (Row rem 2) > 0 ->
+ Attrs#attrs.odd;
+ _ ->
+ Attrs#attrs.even
+ end,
+ From ! {self(), What}.
+
+merge([], New, _Key) ->
+ [[N] || N <- New]; %% First time
+merge(Old, New, Key) ->
+ merge2(keysort(Key, Old), keysort(Key, New), Key).
+
+merge2([[Obj|_]|Old], [Obj|New], Key) ->
+ [[Obj]|merge2(Old, New, Key)];
+merge2([[A|_]|Old], [B|New], Key)
+ when element(Key, A) == element(Key, B) ->
+ [[B|changed]|merge2(Old, New, Key)];
+merge2([[A|_]|Old], New = [B|_], Key)
+ when element(Key, A) < element(Key, B) ->
+ [[A|deleted]|merge2(Old, New, Key)];
+merge2(Old = [[A|_]|_], [B|New], Key)
+ when element(Key, A) > element(Key, B) ->
+ [[B|new]|merge2(Old, New, Key)];
+merge2([], New, _Key) ->
+ [[N|new] || N <- New];
+merge2(Old, [], _Key) ->
+ [[O|deleted] || [O|_] <- Old].
+
+
+delete_row(Row, S0 = #holder{parent=Parent}) ->
+ case delete(Row, S0) of
+ ok ->
+ self() ! refresh;
+ {error, Err} ->
+ Parent ! {error, "Could not delete object: " ++ Err}
+ end.
+
+
+delete(Row, #holder{tabid=Id, table=Table,
+ source=Source, node=Node}) ->
+ [Object|_] = lists:nth(Row+1, Table),
+ try
+ case Source of
+ ets ->
+ true = rpc:call(Node, ets, delete_object, [Id, Object]);
+ mnesia ->
+ ok = rpc:call(Node, mnesia, dirty_delete_object, [Id, Object])
+ end,
+ ok
+ catch _:_Error ->
+ {error, "node or table is not available"}
+ end.
+
+edit_row(Row, Term, S0 = #holder{parent=Parent}) ->
+ case delete(Row, S0) of
+ ok ->
+ case insert(Term, S0) of
+ ok -> self() ! refresh;
+ Err -> Parent ! {error, Err}
+ end;
+ {error, Err} ->
+ Parent ! {error, "Could not edit object: " ++ Err}
+ end.
+
+insert(Object, #holder{tabid=Id, source=Source, node=Node}) ->
+ try
+ case Source of
+ ets ->
+ true = rpc:call(Node, ets, insert, [Id, Object]);
+ mnesia ->
+ ok = rpc:call(Node, mnesia, dirty_write, [Id, Object])
+ end,
+ ok
+ catch _:_Error ->
+ {error, "node or table is not available"}
+ end.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+get_table(Parent, Table, Module) ->
+ spawn(fun() ->
+ link(Parent),
+ get_table2(Parent, Table, Module)
+ end).
+
+get_table2(Parent, Table, Type) ->
+ Size = case Type of
+ ets -> ets:info(Table, size);
+ mnesia -> mnesia:table_info(Table, size)
+ end,
+ case Size > 0 of
+ false ->
+ Parent ! {self(), '$end_of_table'},
+ normal;
+ true when Type =:= ets ->
+ Mem = ets:info(Table, memory),
+ Average = Mem div Size,
+ NoElements = max(10, 20000 div Average),
+ get_ets_loop(Parent, ets:match(Table, '$1', NoElements));
+ true ->
+ Mem = mnesia:table_info(Table, memory),
+ Average = Mem div Size,
+ NoElements = max(10, 20000 div Average),
+ Ms = [{'$1', [], ['$1']}],
+ Get = fun() ->
+ get_mnesia_loop(Parent, mnesia:select(Table, Ms, NoElements, read))
+ end,
+ %% Not a transaction, we don't want to grab locks when inspecting the table
+ mnesia:async_dirty(Get)
+ end.
+
+get_ets_loop(Parent, '$end_of_table') ->
+ Parent ! {self(), '$end_of_table'};
+get_ets_loop(Parent, {Match, Cont}) ->
+ Parent ! {self(), Match},
+ get_ets_loop(Parent, ets:match(Cont)).
+
+get_mnesia_loop(Parent, '$end_of_table') ->
+ Parent ! {self(), '$end_of_table'};
+get_mnesia_loop(Parent, {Match, Cont}) ->
+ Parent ! {self(), Match},
+ get_ets_loop(Parent, mnesia:select(Cont)).
+
+column_names(Node, Type, Table) ->
+ case Type of
+ ets -> [1, 2];
+ mnesia ->
+ Attrs = rpc:call(Node, mnesia, table_info, [Table, attributes]),
+ is_list(Attrs) orelse throw(node_or_table_down),
+ ["Record Name"|Attrs]
+ end.
+
+table_id(#tab{id=ignore, name=Name}) -> Name;
+table_id(#tab{id=Id}) -> Id.
+
+key_pos(_, mnesia, _) -> 2;
+key_pos(Node, ets, TabId) ->
+ KeyPos = rpc:call(Node, ets, info, [TabId, keypos]),
+ is_integer(KeyPos) orelse throw(node_or_table_down),
+ KeyPos.
diff --git a/lib/observer/src/observer_tv_wx.erl b/lib/observer/src/observer_tv_wx.erl
new file mode 100644
index 0000000000..ded03fadb1
--- /dev/null
+++ b/lib/observer/src/observer_tv_wx.erl
@@ -0,0 +1,475 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+-module(observer_tv_wx).
+
+-export([start_link/2, display_table_info/4]).
+
+%% wx_object callbacks
+-export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3,
+ handle_event/2, handle_sync_event/3, handle_cast/2]).
+
+-export([get_table_list/1]). %% RPC called move to runtime tools?
+
+-behaviour(wx_object).
+-include_lib("wx/include/wx.hrl").
+-include("observer_defs.hrl").
+-include("observer_tv.hrl").
+
+-define(GRID, 500).
+-define(ID_REFRESH, 401).
+-define(ID_REFRESH_INTERVAL, 402).
+-define(ID_ETS, 403).
+-define(ID_MNESIA, 404).
+-define(ID_UNREADABLE, 405).
+-define(ID_SYSTEM_TABLES, 406).
+-define(ID_TABLE_INFO, 407).
+
+-record(opt, {type=ets,
+ sys_hidden=true,
+ unread_hidden=true,
+ sort_key=2,
+ sort_incr=true
+ }).
+
+-record(state,
+ {
+ parent,
+ grid,
+ node=node(),
+ opt=#opt{},
+ selected,
+ tabs,
+ timer
+ }).
+
+start_link(Notebook, Parent) ->
+ wx_object:start_link(?MODULE, [Notebook, Parent], []).
+
+init([Notebook, Parent]) ->
+ Panel = wxPanel:new(Notebook),
+ Sizer = wxBoxSizer:new(?wxVERTICAL),
+ Style = ?wxLC_REPORT bor ?wxLC_SINGLE_SEL bor ?wxLC_HRULES,
+ Grid = wxListCtrl:new(Panel, [{winid, ?GRID}, {style, Style}]),
+ wxSizer:add(Sizer, Grid, [{flag, ?wxEXPAND bor ?wxALL},
+ {proportion, 1}, {border, 5}]),
+ wxWindow:setSizer(Panel, Sizer),
+ Li = wxListItem:new(),
+ AddListEntry = fun({Name, Align, DefSize}, Col) ->
+ wxListItem:setText(Li, Name),
+ wxListItem:setAlign(Li, Align),
+ wxListCtrl:insertColumn(Grid, Col, Li),
+ wxListCtrl:setColumnWidth(Grid, Col, DefSize),
+ Col + 1
+ end,
+ ListItems = [{"Table Name", ?wxLIST_FORMAT_LEFT, 200},
+ {"Table Id", ?wxLIST_FORMAT_RIGHT, 100},
+ {"Objects", ?wxLIST_FORMAT_RIGHT, 100},
+ {"Size (kB)", ?wxLIST_FORMAT_RIGHT, 100},
+ {"Owner Pid", ?wxLIST_FORMAT_CENTER, 150},
+ {"Owner Name", ?wxLIST_FORMAT_LEFT, 200}
+ ],
+ lists:foldl(AddListEntry, 0, ListItems),
+ wxListItem:destroy(Li),
+
+ wxListCtrl:connect(Grid, command_list_item_activated),
+ wxListCtrl:connect(Grid, command_list_item_selected),
+ wxListCtrl:connect(Grid, command_list_col_click),
+ wxListCtrl:connect(Grid, size, [{skip, true}]),
+
+ wxWindow:setFocus(Grid),
+ Timer = observer_lib:start_timer(10),
+ {Panel, #state{grid=Grid, parent=Parent, timer=Timer}}.
+
+handle_event(#wx{id=?ID_REFRESH},
+ State = #state{node=Node, grid=Grid, opt=Opt}) ->
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ {noreply, State#state{tabs=Tabs}};
+
+handle_event(#wx{event=#wxList{type=command_list_col_click, col=Col}},
+ State = #state{node=Node, grid=Grid,
+ opt=Opt0=#opt{sort_key=Key, sort_incr=Bool}}) ->
+ Opt = case Col+2 of
+ Key -> Opt0#opt{sort_incr=not Bool};
+ NewKey -> Opt0#opt{sort_key=NewKey}
+ end,
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ wxWindow:setFocus(Grid),
+ {noreply, State#state{opt=Opt, tabs=Tabs}};
+
+handle_event(#wx{id=Id}, State = #state{node=Node, grid=Grid, opt=Opt0})
+ when Id >= ?ID_ETS, Id =< ?ID_SYSTEM_TABLES ->
+ Opt = case Id of
+ ?ID_ETS -> Opt0#opt{type=ets};
+ ?ID_MNESIA -> Opt0#opt{type=mnesia};
+ ?ID_UNREADABLE -> Opt0#opt{unread_hidden= not Opt0#opt.unread_hidden};
+ ?ID_SYSTEM_TABLES -> Opt0#opt{sys_hidden= not Opt0#opt.sys_hidden}
+ end,
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ wxWindow:setFocus(Grid),
+ {noreply, State#state{opt=Opt, tabs=Tabs}};
+
+handle_event(#wx{event=#wxSize{size={W,_}}}, State=#state{grid=Grid}) ->
+ wx:batch(fun() ->
+ Cols = wxListCtrl:getColumnCount(Grid),
+ Last = lists:foldl(fun(I, Last) ->
+ Last - wxListCtrl:getColumnWidth(Grid, I)
+ end, W-?LCTRL_WDECR, lists:seq(0, Cols - 2)),
+ Size = max(200, Last),
+ wxListCtrl:setColumnWidth(Grid, Cols-1, Size)
+ end),
+ {noreply, State};
+
+handle_event(#wx{obj=Grid, event=#wxList{type=command_list_item_activated,
+ itemIndex=Index}},
+ State=#state{grid=Grid, node=Node, opt=#opt{type=Type}, tabs=Tabs}) ->
+ Table = lists:nth(Index+1, Tabs),
+ case Table#tab.protection of
+ private ->
+ self() ! {error, "Table has 'private' protection and can not be read"};
+ _ ->
+ observer_tv_table:start_link(Grid, [{node,Node}, {type,Type}, {table,Table}])
+ end,
+ {noreply, State};
+
+handle_event(#wx{event=#wxList{type=command_list_item_selected, itemIndex=Index}},
+ State) ->
+ {noreply, State#state{selected=Index}};
+
+handle_event(#wx{id=?ID_TABLE_INFO},
+ State = #state{grid=Grid, node=Node, opt=#opt{type=Type}, tabs=Tabs, selected=Sel}) ->
+ case Sel of
+ undefined ->
+ {noreply, State};
+ R when is_integer(R) ->
+ Table = lists:nth(Sel+1, Tabs),
+ display_table_info(Grid, Node, Type, Table),
+ {noreply, State}
+ end;
+
+handle_event(#wx{id=?ID_REFRESH_INTERVAL},
+ State = #state{grid=Grid, timer=Timer0}) ->
+ Timer = observer_lib:interval_dialog(Grid, Timer0, 10, 5*60),
+ {noreply, State#state{timer=Timer}};
+
+handle_event(Event, State) ->
+ io:format("~p:~p, handle event ~p\n", [?MODULE, ?LINE, Event]),
+ {noreply, State}.
+
+handle_sync_event(_Event, _Obj, _State) ->
+ ok.
+
+handle_call(_Event, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Event, State) ->
+ {noreply, State}.
+
+handle_info(refresh_interval, State = #state{node=Node, grid=Grid, opt=Opt}) ->
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ {noreply, State#state{tabs=Tabs}};
+
+handle_info({active, Node}, State = #state{parent=Parent, grid=Grid, opt=Opt,
+ timer=Timer0}) ->
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ wxWindow:setFocus(Grid),
+ create_menus(Parent, Opt),
+ Timer = observer_lib:start_timer(Timer0),
+ {noreply, State#state{node=Node, tabs=Tabs, timer=Timer}};
+
+handle_info(not_active, State = #state{timer = Timer0}) ->
+ Timer = observer_lib:stop_timer(Timer0),
+ {noreply, State#state{timer=Timer}};
+
+handle_info({node, Node}, State = #state{grid=Grid, opt=Opt}) ->
+ Tables = get_tables(Node, Opt),
+ Tabs = update_grid(Grid, Opt, Tables),
+ wxWindow:setFocus(Grid),
+ {noreply, State#state{node=Node, tabs=Tabs}};
+
+handle_info({error, Error}, State) ->
+ handle_error(Error),
+ {noreply, State};
+
+handle_info(_Event, State) ->
+ {noreply, State}.
+
+terminate(_Event, _State) ->
+ ok.
+
+code_change(_, _, State) ->
+ State.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+create_menus(Parent, #opt{sys_hidden=Sys, unread_hidden=UnR, type=Type}) ->
+ MenuEntries = [{"View",
+ [#create_menu{id = ?ID_TABLE_INFO, text = "Table information\tCtrl-I"},
+ separator,
+ #create_menu{id = ?ID_ETS, text = "&Ets Tables",
+ type=radio, check=Type==ets},
+ #create_menu{id = ?ID_MNESIA, text = "&Mnesia Tables",
+ type=radio, check=Type==mnesia},
+ separator,
+ #create_menu{id = ?ID_UNREADABLE, text = "View &Unreadable Tables",
+ type=check, check=not UnR},
+ #create_menu{id = ?ID_SYSTEM_TABLES, text = "View &System Tables",
+ type=check, check=not Sys},
+ separator,
+ #create_menu{id = ?ID_REFRESH, text = "Refresh\tCtrl-R"},
+ #create_menu{id = ?ID_REFRESH_INTERVAL, text = "Refresh Interval..."}
+ ]}],
+ observer_wx:create_menus(Parent, MenuEntries).
+
+get_tables(Node, Opt) ->
+ case rpc:call(Node, ?MODULE, get_table_list, [Opt]) of
+ {badrpc, Error} ->
+ self() ! {error, Error},
+ [];
+ {error, Error} ->
+ self() ! {error, Error},
+ [];
+ Result ->
+ Result
+ end.
+
+get_table_list(#opt{type=ets, unread_hidden=HideUnread, sys_hidden=HideSys}) ->
+ Info = fun(Id, Acc) ->
+ try
+ TabId = case ets:info(Id, named_table) of
+ true -> ignore;
+ false -> Id
+ end,
+ Name = ets:info(Id, name),
+ Protection = ets:info(Id, protection),
+ ignore(HideUnread andalso Protection == private, unreadable),
+ Owner = ets:info(Id, owner),
+ RegName = case catch process_info(Owner, registered_name) of
+ [] -> ignore;
+ {registered_name, ProcName} -> ProcName
+ end,
+ ignore(HideSys andalso ordsets:is_element(RegName, sys_processes()), system_tab),
+ ignore(HideSys andalso ordsets:is_element(Name, sys_tables()), system_tab),
+ ignore((RegName == mnesia_monitor)
+ andalso Name /= schema
+ andalso is_atom((catch mnesia:table_info(Name, where_to_read))), mnesia_tab),
+ Memory = ets:info(Id, memory) * erlang:system_info(wordsize),
+ Tab = #tab{name = Name,
+ id = TabId,
+ protection = Protection,
+ owner = Owner,
+ size = ets:info(Id, size),
+ reg_name = RegName,
+ type = ets:info(Id, type),
+ keypos = ets:info(Id, keypos),
+ heir = ets:info(Id, heir),
+ memory = Memory,
+ compressed = ets:info(Id, compressed),
+ fixed = ets:info(Id, fixed)
+ },
+ [Tab|Acc]
+ catch _:_What ->
+ %% io:format("Skipped ~p: ~p ~n",[Id, _What]),
+ Acc
+ end
+ end,
+ lists:foldl(Info, [], ets:all());
+get_table_list(#opt{type=mnesia, sys_hidden=HideSys}) ->
+ Owner = ets:info(schema, owner),
+ Owner /= undefined orelse
+ throw({error, "Mnesia is not running on: " ++ atom_to_list(node())}),
+ {registered_name, RegName} = process_info(Owner, registered_name),
+ Info = fun(Id, Acc) ->
+ try
+ Name = Id,
+ ignore(HideSys andalso ordsets:is_element(Name, mnesia_tables()), system_tab),
+ ignore(Name =:= schema, mnesia_tab),
+ Storage = mnesia:table_info(Id, storage_type),
+ Tab0 = #tab{name = Name,
+ owner = Owner,
+ size = mnesia:table_info(Id, size),
+ reg_name = RegName,
+ type = mnesia:table_info(Id, type),
+ keypos = 2,
+ memory = mnesia:table_info(Id, memory) * erlang:system_info(wordsize),
+ storage = Storage,
+ index = mnesia:table_info(Id, index)
+ },
+ Tab = if Storage == disc_only_copies ->
+ Tab0#tab{fixed = element(2, dets:info(Id, safe_fixed)) /= []};
+ (Storage == ram_copies) orelse
+ (Storage == disc_copies) ->
+ Tab0#tab{fixed = ets:info(Id, fixed),
+ compressed = ets:info(Id, compressed)};
+ true -> Tab0
+ end,
+ [Tab|Acc]
+ catch _:_What ->
+ %% io:format("Skipped ~p: ~p ~n",[Id, _What]),
+ Acc
+ end
+ end,
+ lists:foldl(Info, [], mnesia:system_info(tables)).
+
+display_table_info(Parent0, Node, Source, Table) ->
+ Parent = observer_lib:get_wx_parent(Parent0),
+ Title = "Table Info: " ++ atom_to_list(Table#tab.name),
+ Frame = wxMiniFrame:new(Parent, ?wxID_ANY, Title,
+ [{style, ?wxSYSTEM_MENU bor ?wxCAPTION
+ bor ?wxCLOSE_BOX bor ?wxRESIZE_BORDER}]),
+
+ IdInfo = {"Identification and Owner",
+ [{"Name", Table#tab.name},
+ {"Id", case Table#tab.id of
+ ignore -> Table#tab.name;
+ Id -> Id
+ end},
+ {"Named table", Table#tab.id == ignore},
+ {"Owner", Table#tab.owner},
+ {"Owner Name", case Table#tab.reg_name of
+ ignore -> "-";
+ Id -> Id
+ end},
+ {"Heir", Table#tab.heir},
+ {"Node", Node}]},
+ MnesiaSettings = case Source of
+ ets -> [];
+ mnesia ->
+ [{"Local storage type", case Table#tab.storage of
+ unknown -> "Not available";
+ ST -> ST
+ end},
+ {"Index positions", list_to_strings(Table#tab.index)}]
+ end,
+ Settings = {"Settings",
+ [{"Source", Source},
+ {"Key Position", Table#tab.keypos},
+ {"Table Type", Table#tab.type},
+ {"Protection Mode", Table#tab.protection},
+ {"Fixed", Table#tab.fixed}
+ | MnesiaSettings ]},
+ Memory = {"Memory Usage",
+ [{"Number of objects", Table#tab.size},
+ {"Memory allocated", {bytes, Table#tab.memory}},
+ {"Compressed", Table#tab.compressed}]},
+
+ {_, Sizer, _} = observer_lib:display_info(Frame, [IdInfo,Settings,Memory]),
+ wxSizer:setSizeHints(Sizer, Frame),
+ wxFrame:center(Frame),
+ wxFrame:show(Frame).
+
+list_to_strings([]) -> "None";
+list_to_strings([A]) -> integer_to_list(A);
+list_to_strings([A,B]) ->
+ integer_to_list(A) ++ " ," ++ list_to_strings(B).
+
+sys_tables() ->
+ [ac_tab, asn1,
+ cdv_dump_index_table, cdv_menu_table, cdv_decode_heap_table,
+ cell_id, cell_pos, clist,
+ cover_internal_data_table, cover_collected_remote_data_table, cover_binary_code_table,
+ code, code_names, cookies,
+ corba_policy, corba_policy_associations,
+ dets, dets_owners, dets_registry,
+ disk_log_names, disk_log_pids,
+ eprof, erl_atom_cache, erl_epmd_nodes,
+ etop_accum_tab, etop_tr,
+ ets_coverage_data,
+ file_io_servers,
+ gs_mapping, gs_names, gstk_db,
+ gstk_grid_cellid, gstk_grid_cellpos, gstk_grid_id,
+ httpd,
+ id,
+ ign_req_index, ign_requests,
+ index,
+ inet_cache, inet_db, inet_hosts,
+ 'InitialReferences',
+ int_db,
+ interpreter_includedirs_macros,
+ ir_WstringDef,
+ lmcounter, locks,
+% mnesia_decision,
+ mnesia_gvar, mnesia_stats,
+% mnesia_transient_decision,
+ pg2_table,
+ queue,
+ schema,
+ shell_records,
+ snmp_agent_table, snmp_local_db2, snmp_mib_data, snmp_note_store, snmp_symbolic_ets,
+ tkFun, tkLink, tkPriv,
+ ttb, ttb_history_table,
+ udp_fds, udp_pids
+ ].
+
+sys_processes() ->
+ [auth, code_server, global_name_server, inet_db,
+ mnesia_recover, net_kernel, timer_server, wxe_master].
+
+mnesia_tables() ->
+ [ir_AliasDef, ir_ArrayDef, ir_AttributeDef, ir_ConstantDef,
+ ir_Contained, ir_Container, ir_EnumDef, ir_ExceptionDef,
+ ir_IDLType, ir_IRObject, ir_InterfaceDef, ir_ModuleDef,
+ ir_ORB, ir_OperationDef, ir_PrimitiveDef, ir_Repository,
+ ir_SequenceDef, ir_StringDef, ir_StructDef, ir_TypedefDef,
+ ir_UnionDef, logTable, logTransferTable, mesh_meas,
+ mesh_type, mnesia_clist, orber_CosNaming,
+ orber_objkeys, user
+ ].
+
+handle_error(Foo) ->
+ Str = io_lib:format("ERROR: ~s~n",[Foo]),
+ observer_lib:display_info_dialog(Str).
+
+update_grid(Grid, Opt, Tables) ->
+ wx:batch(fun() -> update_grid2(Grid, Opt, Tables) end).
+update_grid2(Grid, #opt{sort_key=Sort,sort_incr=Dir}, Tables) ->
+ wxListCtrl:deleteAllItems(Grid),
+ Update =
+ fun(#tab{name = Name, id = Id, owner = Owner, size = Size, memory = Memory,
+ protection = Protection, reg_name = RegName}, Row) ->
+ _Item = wxListCtrl:insertItem(Grid, Row, ""),
+ if (Row rem 2) =:= 0 ->
+ wxListCtrl:setItemBackgroundColour(Grid, Row, ?BG_EVEN);
+ true -> ignore
+ end,
+ if Protection == private ->
+ wxListCtrl:setItemTextColour(Grid, Row, {200,130,50});
+ true -> ignore
+ end,
+
+ lists:foreach(fun({_, ignore}) -> ignore;
+ ({Col, Val}) ->
+ wxListCtrl:setItem(Grid, Row, Col, observer_lib:to_str(Val))
+ end,
+ [{0,Name}, {1,Id}, {2,Size}, {3, Memory div 1024},
+ {4,Owner}, {5,RegName}]),
+ Row + 1
+ end,
+ ProcInfo = case Dir of
+ false -> lists:reverse(lists:keysort(Sort, Tables));
+ true -> lists:keysort(Sort, Tables)
+ end,
+ lists:foldl(Update, 0, ProcInfo),
+ ProcInfo.
+
+ignore(true, Reason) -> throw(Reason);
+ignore(_,_ ) -> ok.
diff --git a/lib/observer/src/observer_wx.erl b/lib/observer/src/observer_wx.erl
new file mode 100644
index 0000000000..f9dec1ab1b
--- /dev/null
+++ b/lib/observer/src/observer_wx.erl
@@ -0,0 +1,537 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 2011. 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%
+-module(observer_wx).
+
+-behaviour(wx_object).
+
+-export([start/0]).
+-export([create_menus/2, get_attrib/1, get_tracer/0,
+ create_txt_dialog/4, try_rpc/4, return_to_localnode/2]).
+
+-export([init/1, handle_event/2, handle_cast/2, terminate/2, code_change/3,
+ handle_call/3, handle_info/2, check_page_title/1]).
+
+%% Includes
+-include_lib("wx/include/wx.hrl").
+
+-include("observer_defs.hrl").
+
+%% Defines
+
+-define(ID_PING, 1).
+-define(ID_CONNECT, 2).
+-define(ID_NOTEBOOK, 3).
+
+-define(FIRST_NODES_MENU_ID, 1000).
+-define(LAST_NODES_MENU_ID, 2000).
+
+-define(TRACE_STR, "Trace Overview").
+
+%% Records
+-record(state,
+ {frame,
+ menubar,
+ menus = [],
+ status_bar,
+ notebook,
+ main_panel,
+ pro_panel,
+ tv_panel,
+ sys_panel,
+ trace_panel,
+ active_tab,
+ node,
+ nodes
+ }).
+
+start() ->
+ wx_object:start(?MODULE, [], []).
+
+create_menus(Object, Menus) when is_list(Menus) ->
+ wx_object:call(Object, {create_menus, Menus}).
+
+get_attrib(What) ->
+ wx_object:call(observer, {get_attrib, What}).
+
+get_tracer() ->
+ wx_object:call(observer, get_tracer).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+init(_Args) ->
+ register(observer, self()),
+ wx:new(),
+ catch wxSystemOptions:setOption("mac.listctrl.always_use_generic", 1),
+ Frame = wxFrame:new(wx:null(), ?wxID_ANY, "Observer",
+ [{size, {1000, 500}}, {style, ?wxDEFAULT_FRAME_STYLE}]),
+ IconFile = filename:join(code:priv_dir(observer), "erlang_observer.png"),
+ Icon = wxIcon:new(IconFile, [{type,?wxBITMAP_TYPE_PNG}]),
+ wxFrame:setIcon(Frame, Icon),
+ wxIcon:destroy(Icon),
+
+ State = #state{frame = Frame},
+ UpdState = setup(State),
+ net_kernel:monitor_nodes(true),
+ process_flag(trap_exit, true),
+ {Frame, UpdState}.
+
+setup(#state{frame = Frame} = State) ->
+ %% Setup Menubar & Menus
+ MenuBar = wxMenuBar:new(),
+
+ {Nodes, NodeMenus} = get_nodes(),
+ DefMenus = default_menus(NodeMenus),
+ observer_lib:create_menus(DefMenus, MenuBar, default),
+
+ wxFrame:setMenuBar(Frame, MenuBar),
+ StatusBar = wxFrame:createStatusBar(Frame, []),
+ wxFrame:setTitle(Frame, atom_to_list(node())),
+ wxStatusBar:setStatusText(StatusBar, atom_to_list(node())),
+
+ %% Setup panels
+ Panel = wxPanel:new(Frame, []),
+ Notebook = wxNotebook:new(Panel, ?ID_NOTEBOOK, [{style, ?wxBK_DEFAULT}]),
+
+ %% System Panel
+ SysPanel = observer_sys_wx:start_link(Notebook, self()),
+ wxNotebook:addPage(Notebook, SysPanel, "System", []),
+
+ %% Setup sizer create early to get it when window shows
+ MainSizer = wxBoxSizer:new(?wxVERTICAL),
+
+ wxSizer:add(MainSizer, Notebook, [{proportion, 1}, {flag, ?wxEXPAND}]),
+ wxPanel:setSizer(Panel, MainSizer),
+
+ wxNotebook:connect(Notebook, command_notebook_page_changing),
+ wxFrame:connect(Frame, close_window, [{skip, true}]),
+ wxMenu:connect(Frame, command_menu_selected),
+ wxFrame:show(Frame),
+
+ %% I postpone the creation of the other tabs so they can query/use
+ %% the window size
+
+ %% Process Panel
+ ProPanel = observer_pro_wx:start_link(Notebook, self()),
+ wxNotebook:addPage(Notebook, ProPanel, "Processes", []),
+
+ %% Table Viewer Panel
+ TVPanel = observer_tv_wx:start_link(Notebook, self()),
+ wxNotebook:addPage(Notebook, TVPanel, "Table Viewer", []),
+
+ %% Trace Viewer Panel
+ TracePanel = observer_trace_wx:start_link(Notebook, self()),
+ wxNotebook:addPage(Notebook, TracePanel, ?TRACE_STR, []),
+
+ %% Force redraw (window needs it)
+ wxWindow:refresh(Panel),
+
+ SysPid = wx_object:get_pid(SysPanel),
+ SysPid ! {active, node()},
+ UpdState = State#state{main_panel = Panel,
+ notebook = Notebook,
+ menubar = MenuBar,
+ status_bar = StatusBar,
+ sys_panel = SysPanel,
+ pro_panel = ProPanel,
+ tv_panel = TVPanel,
+ trace_panel = TracePanel,
+ active_tab = SysPid,
+ node = node(),
+ nodes = Nodes
+ },
+ %% Create resources which we don't want to duplicate
+ SysFont = wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT),
+ SysFontSize = wxFont:getPointSize(SysFont),
+ Modern = wxFont:new(SysFontSize, ?wxFONTFAMILY_MODERN, ?wxFONTSTYLE_NORMAL, ?wxFONTWEIGHT_NORMAL),
+ put({font, modern}, Modern),
+ put({font, fixed}, Modern),
+ UpdState.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+%%Callbacks
+handle_event(#wx{event=#wxNotebook{type=command_notebook_page_changing}},
+ #state{active_tab=Previous, node=Node} = State) ->
+ Pid = get_active_pid(State),
+ Previous ! not_active,
+ Pid ! {active, Node},
+ {noreply, State#state{active_tab=Pid}};
+
+handle_event(#wx{event = #wxClose{}}, State) ->
+ {stop, normal, State};
+
+handle_event(#wx{id = ?wxID_EXIT, event = #wxCommand{type = command_menu_selected}}, State) ->
+ {stop, normal, State};
+
+handle_event(#wx{id = ?wxID_HELP, event = #wxCommand{type = command_menu_selected}}, State) ->
+ External = "http://www.erlang.org/doc/apps/observer/index.html",
+ Internal = filename:join([code:lib_dir(observer),"doc", "html", "index.html"]),
+ Help = case filelib:is_file(Internal) of
+ true -> Internal;
+ false -> External
+ end,
+ wx_misc:launchDefaultBrowser(Help) orelse
+ create_txt_dialog(State#state.frame, "Could not launch browser: ~n " ++ Help,
+ "Error", ?wxICON_ERROR),
+ {noreply, State};
+
+handle_event(#wx{id = ?wxID_ABOUT, event = #wxCommand{type = command_menu_selected}},
+ State = #state{frame=Frame}) ->
+ AboutString = "Observe an erlang system\n"
+ "Authors: Olle Mattson & Magnus Eriksson & Dan Gudmundsson",
+ Style = [{style, ?wxOK bor ?wxSTAY_ON_TOP},
+ {caption, "About"}],
+ wxMessageDialog:showModal(wxMessageDialog:new(Frame, AboutString, Style)),
+ {noreply, State};
+
+
+handle_event(#wx{id = ?ID_CONNECT, event = #wxCommand{type = command_menu_selected}},
+ #state{frame = Frame} = State) ->
+ UpdState = case create_connect_dialog(connect, State) of
+ cancel ->
+ State;
+ {value, [], _, _} ->
+ create_txt_dialog(Frame, "Node must have a name",
+ "Error", ?wxICON_ERROR),
+ State;
+ {value, NodeName, LongOrShort, Cookie} -> %Shortname,
+ try
+ case connect(list_to_atom(NodeName), LongOrShort, list_to_atom(Cookie)) of
+ {ok, set_cookie} ->
+ change_node_view(node(), State);
+ {error, set_cookie} ->
+ create_txt_dialog(Frame, "Could not set cookie",
+ "Error", ?wxICON_ERROR),
+ State;
+ {error, net_kernel, _Reason} ->
+ create_txt_dialog(Frame, "Could not enable node",
+ "Error", ?wxICON_ERROR),
+ State
+ end
+ catch _:_ ->
+ create_txt_dialog(Frame, "Could not enable node",
+ "Error", ?wxICON_ERROR),
+ State
+ end
+ end,
+ {noreply, UpdState};
+
+handle_event(#wx{id = ?ID_PING, event = #wxCommand{type = command_menu_selected}},
+ #state{frame = Frame} = State) ->
+ UpdState = case create_connect_dialog(ping, State) of
+ cancel -> State;
+ {value, Value} when is_list(Value) ->
+ try
+ Node = list_to_atom(Value),
+ case net_adm:ping(Node) of
+ pang ->
+ create_txt_dialog(Frame, "Connect failed", "Pang", ?wxICON_EXCLAMATION),
+ State;
+ pong ->
+ change_node_view(Node, State)
+ end
+ catch _:_ ->
+ create_txt_dialog(Frame, "Connect failed", "Pang", ?wxICON_EXCLAMATION),
+ State
+ end
+ end,
+ {noreply, UpdState};
+
+handle_event(#wx{id = Id, event = #wxCommand{type = command_menu_selected}}, State)
+ when Id > ?FIRST_NODES_MENU_ID, Id < ?LAST_NODES_MENU_ID ->
+
+ Node = lists:nth(Id - ?FIRST_NODES_MENU_ID, State#state.nodes),
+ UpdState = change_node_view(Node, State),
+ {noreply, UpdState};
+
+handle_event(Event, State) ->
+ Pid = get_active_pid(State),
+ Pid ! Event,
+ {noreply, State}.
+
+handle_cast(_Cast, State) ->
+ {noreply, State}.
+
+handle_call({create_menus, TabMenus}, _From,
+ State = #state{menubar=MenuBar, menus=PrevTabMenus}) ->
+ wx:batch(fun() ->
+ clean_menus(PrevTabMenus, MenuBar),
+ observer_lib:create_menus(TabMenus, MenuBar, plugin)
+ end),
+ {reply, ok, State#state{menus=TabMenus}};
+
+handle_call({get_attrib, Attrib}, _From, State) ->
+ {reply, get(Attrib), State};
+
+handle_call(get_tracer, _From, State=#state{trace_panel=TraceP}) ->
+ {reply, TraceP, State};
+
+handle_call(_Msg, _From, State) ->
+ {reply, ok, State}.
+
+handle_info({nodeup, _Node}, State) ->
+ State2 = update_node_list(State),
+ {noreply, State2};
+
+handle_info({nodedown, Node},
+ #state{frame = Frame} = State) ->
+ State2 = case Node =:= State#state.node of
+ true ->
+ change_node_view(node(), State);
+ false ->
+ State
+ end,
+ State3 = update_node_list(State2),
+ Msg = ["Node down: " | atom_to_list(Node)],
+ create_txt_dialog(Frame, Msg, "Node down", ?wxICON_EXCLAMATION),
+ {noreply, State3};
+
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+terminate(_Reason, #state{frame = Frame}) ->
+ wxFrame:destroy(Frame),
+ ok.
+
+code_change(_, _, State) ->
+ {stop, not_yet_implemented, State}.
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+try_rpc(Node, Mod, Func, Args) ->
+ case
+ rpc:call(Node, Mod, Func, Args) of
+ {badrpc, Reason} ->
+ error_logger:error_report([{node, Node},
+ {call, {Mod, Func, Args}},
+ {reason, {badrpc, Reason}}]),
+ error({badrpc, Reason});
+ Res ->
+ Res
+ end.
+
+return_to_localnode(Frame, Node) ->
+ case node() =/= Node of
+ true ->
+ create_txt_dialog(Frame, "Error occured on remote node",
+ "Error", ?wxICON_ERROR),
+ disconnect_node(Node);
+ false ->
+ ok
+ end.
+
+create_txt_dialog(Frame, Msg, Title, Style) ->
+ MD = wxMessageDialog:new(Frame, Msg, [{style, Style}]),
+ wxMessageDialog:setTitle(MD, Title),
+ wxDialog:showModal(MD),
+ wxDialog:destroy(MD).
+
+connect(NodeName, 0, Cookie) ->
+ connect2(NodeName, shortnames, Cookie);
+connect(NodeName, 1, Cookie) ->
+ connect2(NodeName, longnames, Cookie).
+
+connect2(NodeName, Opts, Cookie) ->
+ case net_adm:names() of
+ {ok, _} -> %% Epmd is running
+ ok;
+ {error, address} ->
+ Epmd = os:find_executable("epmd"),
+ os:cmd(Epmd)
+ end,
+ case net_kernel:start([NodeName, Opts]) of
+ {ok, _} ->
+ case is_alive() of
+ true ->
+ erlang:set_cookie(node(), Cookie),
+ {ok, set_cookie};
+ false ->
+ {error, set_cookie}
+ end;
+ {error, Reason} ->
+ {error, net_kernel, Reason}
+ end.
+
+change_node_view(Node, State = #state{pro_panel=Pro, sys_panel=Sys, tv_panel=Tv}) ->
+ lists:foreach(fun(Pid) -> wx_object:get_pid(Pid) ! {node, Node} end,
+ [Pro, Sys, Tv]),
+ StatusText = ["Observer - " | atom_to_list(Node)],
+ wxFrame:setTitle(State#state.frame, StatusText),
+ wxStatusBar:setStatusText(State#state.status_bar, StatusText),
+ State#state{node = Node}.
+
+check_page_title(Notebook) ->
+ Selection = wxNotebook:getSelection(Notebook),
+ wxNotebook:getPageText(Notebook, Selection).
+
+get_active_pid(#state{notebook=Notebook, pro_panel=Pro, sys_panel=Sys, tv_panel=Tv, trace_panel=Trace}) ->
+ Panel = case check_page_title(Notebook) of
+ "Processes" -> Pro;
+ "System" -> Sys;
+ "Table Viewer" -> Tv;
+ ?TRACE_STR -> Trace
+ end,
+ wx_object:get_pid(Panel).
+
+create_connect_dialog(ping, #state{frame = Frame}) ->
+ Dialog = wxTextEntryDialog:new(Frame, "Connect to node"),
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ Value = wxTextEntryDialog:getValue(Dialog),
+ wxDialog:destroy(Dialog),
+ {value, Value};
+ ?wxID_CANCEL ->
+ wxDialog:destroy(Dialog),
+ cancel
+ end;
+create_connect_dialog(connect, #state{frame = Frame}) ->
+ Dialog = wxDialog:new(Frame, ?wxID_ANY, "Distribute node "),
+
+ VSizer = wxBoxSizer:new(?wxVERTICAL),
+ RadioBoxSizer = wxBoxSizer:new(?wxHORIZONTAL),
+
+ Choices = ["Short name", "Long name"],
+ RadioBox = wxRadioBox:new(Dialog, 1, "",
+ ?wxDefaultPosition,
+ ?wxDefaultSize,
+ Choices,
+ [{majorDim, 2},
+ {style, ?wxHORIZONTAL}]),
+
+ NameText = wxStaticText:new(Dialog, ?wxID_ANY, "Node name: "),
+ NameCtrl = wxTextCtrl:new(Dialog, ?wxID_ANY, [{size, {200, 25}}]),
+ wxTextCtrl:setValue(NameCtrl, "observer"),
+ CookieText = wxStaticText:new(Dialog, ?wxID_ANY, "Secret cookie: "),
+ CookieCtrl = wxTextCtrl:new(Dialog, ?wxID_ANY,
+ [{size, {200, 25}}, {style, ?wxTE_PASSWORD}]),
+
+ BtnSizer = wxDialog:createStdDialogButtonSizer(Dialog, ?wxID_DEFAULT),
+ Flags = [{flag, ?wxEXPAND bor ?wxALL}, {border, 5}],
+ wxSizer:add(RadioBoxSizer, RadioBox, Flags),
+
+ wxSizer:add(VSizer, RadioBoxSizer, Flags),
+ wxSizer:addSpacer(VSizer, 10),
+ wxSizer:add(VSizer, NameText),
+ wxSizer:add(VSizer, NameCtrl, Flags),
+ wxSizer:addSpacer(VSizer, 10),
+ wxSizer:add(VSizer, CookieText),
+ wxSizer:add(VSizer, CookieCtrl, Flags),
+ wxSizer:addSpacer(VSizer, 10),
+ wxSizer:add(VSizer, BtnSizer, [{flag, ?wxALIGN_LEFT}]),
+
+ wxWindow:setSizer(Dialog, VSizer),
+ CookiePath = filename:join(os:getenv("HOME"), ".erlang.cookie"),
+ DefaultCookie = case filelib:is_file(CookiePath) of
+ true ->
+ {ok, Bin} = file:read_file(CookiePath),
+ binary_to_list(Bin);
+ false ->
+ ""
+ end,
+ wxTextCtrl:setValue(CookieCtrl, DefaultCookie),
+ case wxDialog:showModal(Dialog) of
+ ?wxID_OK ->
+ NameValue = wxTextCtrl:getValue(NameCtrl),
+ NameLngthValue = wxRadioBox:getSelection(RadioBox),
+ CookieValue = wxTextCtrl:getValue(CookieCtrl),
+ wxDialog:destroy(Dialog),
+ {value, NameValue, NameLngthValue, CookieValue};
+ ?wxID_CANCEL ->
+ wxDialog:destroy(Dialog),
+ cancel
+ end.
+
+default_menus(NodesMenuItems) ->
+ Quit = #create_menu{id = ?wxID_EXIT, text = "Quit"},
+ About = #create_menu{id = ?wxID_ABOUT, text = "About"},
+ Help = #create_menu{id = ?wxID_HELP},
+ NodeMenu = case erlang:is_alive() of
+ true -> {"Nodes", NodesMenuItems ++
+ [#create_menu{id = ?ID_PING, text = "Connect Node"}]};
+ false -> {"Nodes", NodesMenuItems ++
+ [#create_menu{id = ?ID_CONNECT, text = "Enable distribution"}]}
+ end,
+ case os:type() =:= {unix, darwin} of
+ false ->
+ FileMenu = {"File", [Quit]},
+ HelpMenu = {"Help", [About,Help]},
+ [FileMenu, NodeMenu, HelpMenu];
+ true ->
+ %% On Mac quit and about will be moved to the "default' place
+ %% automagicly, so just add them to a menu that always exist.
+ %% But not to the help menu for some reason
+ {Tag, Menus} = NodeMenu,
+ [{Tag, Menus ++ [Quit,About]}, {"&Help", [Help]}]
+ end.
+
+clean_menus(Menus, MenuBar) ->
+ remove_menu_items(Menus, MenuBar).
+
+remove_menu_items([{MenuStr = "File", Menus}|Rest], MenuBar) ->
+ MenuId = wxMenuBar:findMenu(MenuBar, MenuStr),
+ Menu = wxMenuBar:getMenu(MenuBar, MenuId),
+ Items = [wxMenu:findItem(Menu, Tag) || #create_menu{text=Tag} <- Menus],
+ [wxMenu:delete(Menu, MItem) || MItem <- Items],
+ case os:type() =:= {unix, darwin} of
+ true ->
+ wxMenuBar:remove(MenuBar, MenuId),
+ wxMenu:destroy(Menu);
+ false ->
+ ignore
+ end,
+ remove_menu_items(Rest, MenuBar);
+remove_menu_items([{"Nodes", _}|_], _MB) ->
+ ok;
+remove_menu_items([{Tag, _Menus}|Rest], MenuBar) ->
+ MenuId = wxMenuBar:findMenu(MenuBar, Tag),
+ Menu = wxMenuBar:getMenu(MenuBar, MenuId),
+ wxMenuBar:remove(MenuBar, MenuId),
+ Items = wxMenu:getMenuItems(Menu),
+ [wxMenu:'Destroy'(Menu, Item) || Item <- Items],
+ wxMenu:destroy(Menu),
+ remove_menu_items(Rest, MenuBar);
+remove_menu_items([], _MB) ->
+ ok.
+
+get_nodes() ->
+ Nodes = [node()| nodes()],
+ {_, Menues} =
+ lists:foldl(fun(Node, {Id, Acc}) when Id < ?LAST_NODES_MENU_ID ->
+ {Id + 1, [#create_menu{id=Id + ?FIRST_NODES_MENU_ID,
+ text=atom_to_list(Node)} | Acc]}
+ end, {1, []}, Nodes),
+ {Nodes, lists:reverse(Menues)}.
+
+update_node_list(State = #state{menubar=MenuBar}) ->
+ {Nodes, NodesMenuItems} = get_nodes(),
+ NodeMenuId = wxMenuBar:findMenu(MenuBar, "Nodes"),
+ NodeMenu = wxMenuBar:getMenu(MenuBar, NodeMenuId),
+ wx:foreach(fun(Item) -> wxMenu:'Destroy'(NodeMenu, Item) end,
+ wxMenu:getMenuItems(NodeMenu)),
+
+ Index = wx:foldl(fun(Record, Index) ->
+ observer_lib:create_menu_item(Record, NodeMenu, Index)
+ end, 0, NodesMenuItems),
+
+ Dist = case erlang:is_alive() of
+ true -> #create_menu{id = ?ID_PING, text = "Connect node"};
+ false -> #create_menu{id = ?ID_CONNECT, text = "Enable distribution"}
+ end,
+ observer_lib:create_menu_item(Dist, NodeMenu, Index),
+ State#state{nodes = Nodes}.
diff --git a/lib/observer/src/ttb.erl b/lib/observer/src/ttb.erl
index 1471be92e5..61fd6d1787 100644
--- a/lib/observer/src/ttb.erl
+++ b/lib/observer/src/ttb.erl
@@ -75,29 +75,41 @@ do_tracer(Nodes0,PI,Client,Traci) ->
do_tracer(Clients,PI,Traci).
do_tracer(Clients,PI,Traci) ->
- ShellOutput = proplists:get_value(shell, Traci, false),
- {ClientSucc,Succ} =
+ Shell = proplists:get_value(shell, Traci, false),
+ DefShell = fun(Trace) -> dbg:dhandler(Trace, standard_io) end,
+ {ClientSucc,Succ} =
lists:foldl(
- fun({N,{local,File},TF},{CS,S}) ->
- TF2 = case ShellOutput of
- only -> none;
- _ -> TF
- end,
- [_Sname,Host] = string:tokens(atom_to_list(N),"@"),
+ fun({N,{local,File},TF},{CS,S}) ->
+ {TF2, FileInfo, ShellOutput} =
+ case Shell of
+ only -> {none, shell_only, DefShell};
+ true -> {TF, {file,File}, DefShell};
+ {only,Fun} -> {none, shell_only, Fun};
+ Fun when is_function(Fun) -> {TF, {file,File}, Fun};
+ _ -> {TF, {file,File}, false}
+ end,
+ Host = case N of
+ nonode@nohost ->
+ {ok, H} = inet:gethostname(),
+ H;
+ _ ->
+ [_,H] = string:tokens(atom_to_list(N),"@"),
+ H
+ end,
case catch dbg:tracer(N,port,dbg:trace_port(ip,0)) of
{ok,N} ->
{ok,Port} = dbg:trace_port_control(N,get_listen_port),
{ok,T} = dbg:get_tracer(N),
rpc:call(N,seq_trace,set_system_tracer,[T]),
dbg:trace_client(ip,{Host,Port},
- {fun ip_to_file/2,{{file,File}, ShellOutput}}),
+ {fun ip_to_file/2,{FileInfo, ShellOutput}}),
{[{N,{local,File,Port},TF2}|CS], [N|S]};
Other ->
display_warning(N,{cannot_open_ip_trace_port,
Host,
Other}),
{CS, S}
- end;
+ end;
({N,C,_}=Client,{CS,S}) ->
case catch dbg:tracer(N,port,dbg:trace_port(file,C)) of
{ok,N} ->
@@ -620,7 +632,7 @@ stop_opts(Opts) ->
case {FormatData, lists:member(return_fetch_dir, Opts)} of
{false, true} ->
{fetch, FetchDir}; % if we specify return_fetch_dir, the data should be fetched
- {false, false} ->
+ {false, false} ->
case lists:member(nofetch,Opts) of
false -> {fetch, FetchDir};
true -> nofetch
@@ -1275,10 +1287,10 @@ display_warning(Item,Warning) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Trace client which reads an IP port and puts data directly to a file.
%%% This is used when tracing remote nodes with no file system.
-ip_to_file({metadata,_,_},{_, only} = State) ->
+ip_to_file({metadata,_,_},{shell_only, _} = State) ->
State;
-ip_to_file(Trace, {_, only} = State) ->
- dbg:dhandler(Trace, standard_io),
+ip_to_file(Trace, {shell_only, Fun} = State) ->
+ Fun(Trace),
State;
ip_to_file(Trace,{{file,File}, ShellOutput}) ->
Fun = dbg:trace_port(file,File), %File can be a filename or a wrap spec
@@ -1302,8 +1314,8 @@ ip_to_file(Trace,{Port, ShellOutput}) ->
erlang:port_command(Port,B),
{Port, ShellOutput}.
-show_trace(Trace, true) ->
- dbg:dhandler(Trace, standard_io);
+show_trace(Trace, Fun) when is_function(Fun) ->
+ Fun(Trace);
show_trace(_, _) ->
ok.