%% %% %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, create_menu/2, 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). %% Records -record(state, {frame, menubar, status_bar, notebook, main_panel, pro_panel, tv_panel, sys_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}). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% init(_Args) -> wx:new(), 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), wxFrame:show(Frame), net_kernel:monitor_nodes(true), {Frame, UpdState}. setup(#state{frame = Frame} = State) -> %% Setup Menubar & Menus MenuBar = wxMenuBar:new(), {Nodes, NodeMenus} = get_nodes(), DefMenus = default_menus(NodeMenus), create_menu(DefMenus, MenuBar), 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}]), %% Setup sizer MainSizer = wxBoxSizer:new(?wxVERTICAL), %% System Panel SysPanel = observer_sys_wx:start_link(Notebook, self()), wxNotebook:addPage(Notebook, SysPanel, "System", []), %% 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", []), wxSizer:add(MainSizer, Notebook, [{proportion, 1}, {flag, ?wxEXPAND}]), wxPanel:setSizer(Panel, MainSizer), wxNotebook:connect(Notebook, command_notebook_page_changed), wxFrame:connect(Frame, close_window, [{skip, true}]), wxMenu:connect(Frame, command_menu_selected, [{skip, true}]), 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, active_tab = SysPid, node = node(), nodes = Nodes }, UpdState. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%Callbacks handle_event(#wx{obj = Notebook, id = ?ID_NOTEBOOK, event = #wxNotebook{type = command_notebook_page_changed}}, #state{active_tab=Previous, node=Node, notebook = Notebook} = State) -> io:format("Command notebook changed ~n"), 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) -> io:format("~p ~p, you clicked close", [?MODULE, ?LINE]), {stop, normal, State}; handle_event(#wx{id = ?wxID_HELP, event = #wxCommand{type = command_menu_selected}}, State) -> io:format("~p ~p, you clicked help", [?MODULE, ?LINE]), {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) -> io:format("~p:~p: Got cast ~p~n", [?MODULE, ?LINE, Cast]), {noreply, State}. handle_call({create_menus, TabMenus}, _From, State = #state{menubar=MenuBar}) -> wx:batch(fun() -> {_Nodes, NodeMenus} = get_nodes(), DefMenus = default_menus(NodeMenus), Menus = merge_menus(DefMenus, TabMenus), clean_menus(MenuBar), create_menu(Menus, MenuBar) end), {reply, ok, State}; handle_call(Msg, _From, State) -> io:format("~p~p: Got Call ~p~n",[?MODULE, ?LINE, Msg]), {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) -> io:format("~p, ~p, Handle info: ~p~n", [?MODULE, ?LINE, Info]), {noreply, State}. terminate(Reason, #state{frame = Frame}) -> wxFrame:destroy(Frame), io:format("~p terminating. Reason: ~p~n", [?MODULE, Reason]), 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_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}) -> Panel = case check_page_title(Notebook) of "Processes" -> Pro; "System" -> Sys; "Table Viewer" -> Tv 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}}, {style, ?wxDEFAULT}]), wxTextCtrl:setValue(NameCtrl, "observer"), CookieText = wxStaticText:new(Dialog, ?wxID_ANY, "Secret cookie: "), CookieCtrl = wxTextCtrl:new(Dialog, ?wxID_ANY, [{size, {200, 25}}, {style, ?wxDEFAULT}]), 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, IoDevice} = file:open(CookiePath, read), case file:read_line(IoDevice) of {ok, Cookie} -> Cookie; _ -> "" end; 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) -> FileMenu = {"File", [#create_menu{id = ?wxID_EXIT, text = "Quit"}]}, HelpMenu = {"Help", [#create_menu{id = ?wxID_HELP, text = "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, [FileMenu, NodeMenu, HelpMenu]. clean_menus(MenuBar) -> Count = wxMenuBar:getMenuCount(MenuBar), remove_menu_item(MenuBar, Count). remove_menu_item(MenuBar, Item) when Item > -1 -> Menu = wxMenuBar:getMenu(MenuBar, Item), wxMenuBar:remove(MenuBar, Item), wxMenu:destroy(Menu), remove_menu_item(MenuBar, Item-1); remove_menu_item(_, _) -> ok. merge_menus([{Label, Items}|Default], [{Label, TabItems}|TabMenus]) -> [{Label, TabItems ++ Items} | merge_menus(Default, TabMenus)]; merge_menus([Menu = {"File", _}|Default], TabMenus) -> [Menu | merge_menus(Default, TabMenus)]; merge_menus(Default = [{"Nodes", _}|_], TabMenus) -> TabMenus ++ Default. create_menu(Menus, MenuBar) -> Add = fun({Name, MenuItems}) -> Menu = wxMenu:new(), lists:foreach(fun(Record) -> create_menu_item(Record, Menu) end, MenuItems), wxMenuBar:append(MenuBar, Menu, Name) end, wx:foreach(Add, Menus), ok. create_menu_item(#create_menu{id = Id, text = Text, type = Type, check = Check}, Menu) -> case Type of append -> wxMenu:append(Menu, Id, Text); check -> wxMenu:appendCheckItem(Menu, Id, Text), wxMenu:check(Menu, Id, Check); radio -> wxMenu:appendRadioItem(Menu, Id, Text), wxMenu:check(Menu, Id, Check); separator -> wxMenu:appendSeparator(Menu) end; create_menu_item(separator, Menu) -> wxMenu:appendSeparator(Menu). 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)), wx:foreach(fun(Record) -> create_menu_item(Record, NodeMenu) end, NodesMenuItems), case erlang:is_alive() of true -> create_menu_item(#create_menu{id = ?ID_PING, text = "Connect node"}, NodeMenu); false -> create_menu_item(#create_menu{id = ?ID_CONNECT, text = "Enable distribution"}, NodeMenu) end, State#state{nodes = Nodes}.