From 84adefa331c4159d432d22840663c38f155cd4c1 Mon Sep 17 00:00:00 2001 From: Erlang/OTP Date: Fri, 20 Nov 2009 14:54:40 +0000 Subject: The R13B03 release. --- lib/appmon/src/Makefile | 101 ++++ lib/appmon/src/appmon.app.src | 24 + lib/appmon/src/appmon.appup.src | 18 + lib/appmon/src/appmon.erl | 1079 +++++++++++++++++++++++++++++++++++++ lib/appmon/src/appmon_a.erl | 1117 +++++++++++++++++++++++++++++++++++++++ lib/appmon/src/appmon_dg.erl | 205 +++++++ lib/appmon/src/appmon_dg.hrl | 45 ++ lib/appmon/src/appmon_info.erl | 963 +++++++++++++++++++++++++++++++++ lib/appmon/src/appmon_lb.erl | 689 ++++++++++++++++++++++++ lib/appmon/src/appmon_place.erl | 194 +++++++ lib/appmon/src/appmon_txt.erl | 302 +++++++++++ lib/appmon/src/appmon_web.erl | 1037 ++++++++++++++++++++++++++++++++++++ lib/appmon/src/process_info.erl | 662 +++++++++++++++++++++++ 13 files changed, 6436 insertions(+) create mode 100644 lib/appmon/src/Makefile create mode 100644 lib/appmon/src/appmon.app.src create mode 100644 lib/appmon/src/appmon.appup.src create mode 100644 lib/appmon/src/appmon.erl create mode 100644 lib/appmon/src/appmon_a.erl create mode 100644 lib/appmon/src/appmon_dg.erl create mode 100644 lib/appmon/src/appmon_dg.hrl create mode 100644 lib/appmon/src/appmon_info.erl create mode 100644 lib/appmon/src/appmon_lb.erl create mode 100644 lib/appmon/src/appmon_place.erl create mode 100644 lib/appmon/src/appmon_txt.erl create mode 100644 lib/appmon/src/appmon_web.erl create mode 100644 lib/appmon/src/process_info.erl (limited to 'lib/appmon/src') diff --git a/lib/appmon/src/Makefile b/lib/appmon/src/Makefile new file mode 100644 index 0000000000..43f4f085b8 --- /dev/null +++ b/lib/appmon/src/Makefile @@ -0,0 +1,101 @@ +# +# %CopyrightBegin% +# +# Copyright Ericsson AB 1996-2009. 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 +include $(ERL_TOP)/make/$(TARGET)/otp.mk + +# ---------------------------------------------------- +# Application version +# ---------------------------------------------------- +include ../vsn.mk +VSN=$(APPMON_VSN) + +# ---------------------------------------------------- +# Release directory specification +# ---------------------------------------------------- +RELSYSDIR = $(RELEASE_PATH)/lib/appmon-$(VSN) + +# ---------------------------------------------------- +# Target Specs +# ---------------------------------------------------- + +MODULES= \ + appmon \ + appmon_a \ + appmon_dg \ + appmon_info \ + appmon_place \ + appmon_txt \ + appmon_lb \ + process_info \ + appmon_web + +HRL_FILES= appmon_dg.hrl + +ERL_FILES= $(MODULES:%=%.erl) + +TARGET_FILES= $(MODULES:%=../ebin/%.$(EMULATOR)) $(APP_TARGET) $(APPUP_TARGET) + +APP_FILE= appmon.app +APPUP_FILE= appmon.appup + +APP_SRC= $(APP_FILE).src +APPUP_SRC= $(APPUP_FILE).src + +APP_TARGET= ../ebin/$(APP_FILE) +APPUP_TARGET= ../ebin/$(APPUP_FILE) + +# ---------------------------------------------------- +# FLAGS +# ---------------------------------------------------- +ERL_COMPILE_FLAGS += +warn_obsolete_guard + +# ---------------------------------------------------- +# Targets +# ---------------------------------------------------- + +debug opt: $(TARGET_FILES) + +clean: + rm -f $(TARGET_FILES) + rm -f core + +docs: + +# ---------------------------------------------------- +# Special Build Targets +# ---------------------------------------------------- + +$(APP_TARGET): $(APP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +$(APPUP_TARGET): $(APPUP_SRC) ../vsn.mk + sed -e 's;%VSN%;$(VSN);' $< > $@ + +# ---------------------------------------------------- +# Release Target +# ---------------------------------------------------- +include $(ERL_TOP)/make/otp_release_targets.mk + +release_spec: opt + $(INSTALL_DIR) $(RELSYSDIR)/src + $(INSTALL_DATA) $(ERL_FILES) $(HRL_FILES) $(RELSYSDIR)/src + $(INSTALL_DIR) $(RELSYSDIR)/ebin + $(INSTALL_DATA) $(TARGET_FILES) $(RELSYSDIR)/ebin + +release_docs_spec: + diff --git a/lib/appmon/src/appmon.app.src b/lib/appmon/src/appmon.app.src new file mode 100644 index 0000000000..2e1aa3ef3b --- /dev/null +++ b/lib/appmon/src/appmon.app.src @@ -0,0 +1,24 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1997-2009. 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% +{application, appmon, + [{description, "DEVTOOLS CXC 138 16"}, + {vsn, "%VSN%"}, + {modules, [appmon, appmon_a, appmon_dg, appmon_info, + appmon_lb, appmon_place, appmon_txt,process_info,appmon_web]}, + {registered,[appmon, appmon_info, appmon_txt,webappmon_server,proc_info]}, + {applications, [kernel, stdlib]}]}. diff --git a/lib/appmon/src/appmon.appup.src b/lib/appmon/src/appmon.appup.src new file mode 100644 index 0000000000..0d918b6081 --- /dev/null +++ b/lib/appmon/src/appmon.appup.src @@ -0,0 +1,18 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2001-2009. 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% +{"%VSN%",[],[]}. diff --git a/lib/appmon/src/appmon.erl b/lib/appmon/src/appmon.erl new file mode 100644 index 0000000000..6f5d2824d2 --- /dev/null +++ b/lib/appmon/src/appmon.erl @@ -0,0 +1,1079 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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(appmon). +-behaviour(gen_server). + +%%%--------------------------------------------------------------------- +%%% Appmon main module. +%%% Creates the main window and receives load and application +%%% information from all connected nodes. +%%%--------------------------------------------------------------------- + +%% External exports +-export([start/0, stop/0]). + +%% gen_server callbacks +-export([init/1, handle_cast/2, handle_info/2, terminate/2]). +-export([handle_call/3, code_change/3]). % not used + +%% Canvas button data +-record(canvasbutton, {text, ul, ll, rect, x, y, w, h}). + +%% Options - all the fields are GS radio buttons +-record(options, {single, many, time, queue, prog, linear}). + +%% Main window data +-record(win, {name, % atom() Monitored node + window, % gsobj() + wwindow, % int() Window width + hwindow, % int() Window height + options, % #options{} + canvas, % gsobj() + wcanvas, % int() Canvas width + hcanvas, % int() Canvas height + l1, l2, % gsobj() Canvas lines + leds, % [gsobj()] Load meter + nodelabel, % {gsobj(),gsobj()} + appobjs=[], % [gsobj()] Buttons etc. + nodemenu}). % gsobj() Node menu + +%% Node data +-record(mnode, {name, % atom() Node name + status, % alive | dead + pid, % pid() + apps, % [{Pid,App,Descr}] + load}). % {Old, New} + +%% Internal state data +-record(state, {gs, % pid() + wins=[], % [#win()] GUIs + window_mode, % single | many + load_mode1, % time | queue + load_mode2, % prog | linear + lbpid, % pid() + mnodes=[]}). % [#mnode{}] + +%%%--------------------------------------------------------------------- +%%% External exports +%%%--------------------------------------------------------------------- + +start() -> + gen_server:start({local, appmon}, ?MODULE, [], []). + +stop() -> + gen_server:cast(appmon, stop). + + +%%%--------------------------------------------------------------------- +%%% gen_server callbacks +%%%--------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%%---------------------------------------------------------------------- +init([]) -> + process_flag(trap_exit, true), + + %% Subscribe to {nodeup,Node} and {nodedown,Node} messages + net_kernel:monitor_nodes(true), + + LbPid = appmon_lb:start(self ()), + + %% Check which remote nodes have appmon code available (OTP-4887) + NodesOk = lists:filter(fun(Node) -> check_node(Node) end, nodes()), + Nodes = [node()|NodesOk], + + %% Start monitoring the existing nodes + MNodes = mk_mnodes(Nodes, LbPid), + + %% Draw the main window + GS = gs:start([{kernel,true}]), + GUI = draw_win(GS, node()), + + %% Update the Nodes menu with all known nodes + lists:foreach(fun(Node) -> + display_addnode(GUI, Node) + end, + Nodes), + + %% Mark the default options as selected in the Options menu + display_setopt(GUI, single), + display_setopt(GUI, time), + display_setopt(GUI, prog), + + {ok, #state{gs=GS, wins=[GUI], + window_mode=single, load_mode1=time, load_mode2=prog, + lbpid=LbPid, mnodes=MNodes}}. + +check_node(Node) -> + case rpc:call(Node, code, which, [appmon]) of + File when is_list(File) -> + true; + _ -> % non_existing (| cover_compiled) + false + end. + +%%---------------------------------------------------------------------- +%% Func: handle_call/3 +%% Returns: {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | (terminate/2 is called) +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +handle_call(norequest, _From, State) -> + {reply, null, State}. + +%%---------------------------------------------------------------------- +%% Func: handle_cast/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +handle_cast(stop, State) -> + {stop, normal, State}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/2 +%% Returns: {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} (terminate/2 is called) +%%---------------------------------------------------------------------- +%% Load information from a node +handle_info({delivery, _Serv, load, Node, Load}, State) -> + + %% Update node information + MNode = get_mnode(Node, State#state.mnodes), + MNode1 = MNode#mnode{load=Load}, + MNodes = replace_mnode(Node, MNode1, State#state.mnodes), + + %% If Node is currently displayed, update graphics + case get_win(Node, State#state.wins) of + {ok, GUI} -> + display_load(GUI, Load); + false -> + ignore + end, + + {noreply, State#state{mnodes=MNodes}}; + +%% Application information from a node +handle_info({delivery, _Serv, app_ctrl, Node, Apps}, State) -> + + %% Update node information + MNode = get_mnode(Node, State#state.mnodes), + MNode1 = MNode#mnode{apps=Apps}, + MNodes = replace_mnode(Node, MNode1, State#state.mnodes), + + %% If Node is currently displayed, update graphics + Wins = case get_win(Node, State#state.wins) of + {ok, GUI} -> + draw_clear(GUI), + GUI1 = draw_apps(GUI, Apps), + replace_win(Node, GUI1, State#state.wins); + false -> + State#state.wins + end, + + appmon_lb:add_apps (State#state.lbpid, Apps, Node), + {noreply, State#state{wins=Wins, mnodes=MNodes}}; + +handle_info({nodeup, Node}, State) -> + + %% First, make sure appmon code is available at remode node, + %% or the node should be ignored (OTP-3591) + case check_node(Node) of + true -> + + %% If this is a previously unknown node, update window's + %% 'Nodes' menu + case get_mnode(Node, State#state.mnodes) of + false -> + display_addnode(State#state.wins, Node); + _OldMnode -> + ignore + end, + + %% Update node information (=> state is automatically + %% changed to 'alive') + MNode = mk_mnode(Node, State#state.lbpid), + MNodes = replace_mnode(Node, MNode, State#state.mnodes), + + %% If Node is currently displayed, update graphics + case get_win(Node, State#state.wins) of + {ok, GUI} -> + display_nodeup(GUI, Node); + false -> + ignore + end, + + appmon_lb:update_status(State#state.lbpid, Node, alive), + {noreply, State#state{mnodes=MNodes}}; + + false -> + {noreply, State} + end; + +handle_info({nodedown, Node}, State) -> + + %% If this is a previously unknown node, ignore the message. + %% (The situation occurs when failing to connect to another node). + %% Otherwise, update the node information. + case get_mnode(Node, State#state.mnodes) of + false -> + {noreply, State}; + MNode -> + MNode1 = MNode#mnode{status=dead}, + MNodes = replace_mnode(Node, MNode1, State#state.mnodes), + + %% If Node is currently displayed, update graphics + Wins = case get_win(Node, State#state.wins) of + {ok, GUI} -> + display_nodedown(GUI), + GUI1 = draw_clear(GUI), + replace_win(Node, GUI1, State#state.wins); + false -> + State#state.wins + end, + + appmon_lb:remove_node(State#state.lbpid, Node), + {noreply, State#state{wins=Wins, mnodes=MNodes}} + end; + +%% Application 'button' events +handle_info({gs, _Obj, buttonpress, Data, _Arg}, State) -> + {canvasbutton, CBtn, _App} = Data, + press(CBtn), + {noreply, State}; +handle_info({gs, _Obj, buttonrelease, Data, [_,X,Y|_]}, State) -> + {canvasbutton, CBtn, {application, App, Node}} = Data, + release(CBtn), + + %% Check that mouse button was released over the button! + L = CBtn#canvasbutton.x, R = L + CBtn#canvasbutton.w, + T = CBtn#canvasbutton.y, B = T + CBtn#canvasbutton.h, + if + X>L, XT, Y + MNode = get_mnode(Node, State#state.mnodes), + {value, {Pid, _App, _Descr}} = + lists:keysearch(App, 2, MNode#mnode.apps), + appmon_a:start(Node, App, Pid); + true -> + ignore + end, + {noreply, State}; + +handle_info({gs, _Button, click, Data, _Arg}, State) -> + ThisNode = node(), + case Data of + + %% File menu item + listbox -> + appmon_lb:open_win(State#state.lbpid, + parse_nodes(State#state.mnodes)), + {noreply, State}; + {close, WinObj} -> + {ok, GUI} = get_win2(WinObj, State#state.wins), + gs:destroy(WinObj), + + %% Terminate if this was the only open window + case remove_win(GUI#win.name, State#state.wins) of + [] -> + {stop, normal, State}; + Wins -> + {noreply, State#state{wins=Wins}} + end; + exit -> + {stop, normal, State}; + + %% Actions menu item + {action, Action, WinObj} -> + {ok, GUI} = get_win2(WinObj, State#state.wins), + Node = GUI#win.name, + + if + Node==ThisNode -> + case Action of + ping -> + %% Ignore - makes no sense to ping yourself + ignore; + _ -> % reboot | restart | stop + apply(init, Action, []) + end; + + Node/=ThisNode -> + case Action of + ping -> + net_adm:ping(Node); + _ -> % reboot | restart | stop + rpc:cast(Node, init, Action, []) + end + end, + {noreply, State}; + + %% Options menu item + {window_mode, Mode} -> + + %% Update windows so they all show the same options + lists:foreach(fun(GUI) -> + display_setopt(GUI, Mode) + end, + State#state.wins), + {noreply, State#state{window_mode=Mode}}; + + {option, Tag, Option} -> + + %% Update windows so they all show the same options + lists:foreach(fun(GUI) -> + display_setopt(GUI, Tag) + end, + State#state.wins), + + %% Update all appmon_info processes about which kind of + %% load data is desired + lists:foreach(fun(MNode) -> + appmon_info:load(MNode#mnode.pid, + MNode#mnode.name, + true, + Option) + end, + State#state.mnodes), + + if + Tag==time; Tag==queue -> + {noreply, State#state{load_mode1=Tag}}; + Tag==prog; Tag==linear -> + {noreply, State#state{load_mode2=Tag}} + end; + + %% Nodes menu item + {node, Node, WinObj} -> + + %% Check first if this window is already displayed + case get_win(Node, State#state.wins) of + {ok, GUI} -> + + %% Node is already displayed, raise its window + gs:config(GUI#win.window, raise), + + {noreply, State}; + + %% Node is not displayed + false -> + + %% Redraw existing window or create a new window + %% depending on window mode + case State#state.window_mode of + + single -> + {ok, GUI} = + get_win2(WinObj, State#state.wins), + + %% Clear window and correct the node name + draw_clear(GUI), + GUI1 = draw_nodename(GUI, Node), + + %% Update window with the correct node name + %% and the applications running at the node + MNode = get_mnode(Node, State#state.mnodes), + GUI2 = case MNode#mnode.status of + dead -> + display_nodedown(GUI1), + GUI1; + alive -> + display_nodeup(GUI1, Node), + draw_apps(GUI1, + MNode#mnode.apps) + end, + Wins = replace_win(GUI#win.name, GUI2, + State#state.wins), + + {noreply, State#state{wins=Wins}}; + + many -> + GUI = draw_win(State#state.gs, Node), + + %% Update Nodes menu with all known nodes - + %% use MNodes to get them in the right order + lists:foreach(fun(MNode) -> + Name = + MNode#mnode.name, + display_addnode(GUI, + Name) + end, + State#state.mnodes), + + %% Mark selected options in the Options menu + display_setopt(GUI, many), + display_setopt(GUI, State#state.load_mode1), + display_setopt(GUI, State#state.load_mode2), + + %% Add the applications running at the node + MNode = get_mnode(Node, State#state.mnodes), + + GUI1 = case MNode#mnode.status of + dead -> + display_nodedown(GUI), + GUI; + alive -> + display_nodeup(GUI, Node), + draw_apps(GUI, + MNode#mnode.apps) + end, + Wins = [GUI1|State#state.wins], + + {noreply, State#state{wins=Wins}} + end + end; + + %% Help menu = Help button + help -> + HelpFile = filename:join([code:lib_dir(appmon), + "doc", "html", "part_frame.html"]), + case State#state.wins of + [Win] -> + tool_utils:open_help(Win#win.window, HelpFile); + _ -> + tool_utils:open_help(State#state.gs, HelpFile) + end, + {noreply, State}; + + _Other -> + {noreply, State} + end; +handle_info({gs, WinObj, configure, _, [WWindow, HWindow|_]}, State) -> + {ok, GUI} = get_win2(WinObj, State#state.wins), + GUI1 = draw_resize(GUI, WWindow, HWindow), + display_scrollbar(GUI1), + Wins = replace_win(GUI#win.name, GUI1, State#state.wins), + {noreply, State#state{wins=Wins}}; +handle_info({gs, WinObj, destroy, _, _}, State) -> % OTP-1179 + {ok, GUI} = get_win2(WinObj, State#state.wins), + + %% Terminate if this was the only open window + case remove_win(GUI#win.name, State#state.wins) of + [] -> + {stop, normal, State}; + Wins -> + {noreply, State#state{wins=Wins}} + end; + +handle_info(stop, State) -> + {stop, normal, State}; +handle_info({'EXIT', Pid, Reason}, State) -> + case Reason of + shutdown -> + %% Appmon is being asked to shut down, eg during reboot + {stop, Reason, State}; + _ -> + case State#state.gs of + + %% GS exited, kill appmon + {0, Pid} -> + {stop, normal, State}; + + _ -> + {noreply, State} + end + end; +handle_info(_Info, State) -> + {noreply, State}. + +%%---------------------------------------------------------------------- +%% Func: terminate/2 +%% Purpose: Shutdown the server +%% Returns: any (ignored by gen_server) +%%---------------------------------------------------------------------- +terminate(_Reason, State) -> + bcast(State#state.mnodes, {kill}), + appmon_lb:stop(State#state.lbpid), + ok. + +%%---------------------------------------------------------------------- +%% Func: code_change/3 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState} +%%---------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%%--------------------------------------------------------------------- +%%% Internal functions +%%%--------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% MNode manipulating functions +%%---------------------------------------------------------------------- + +%% mk_mnodes(Nodes, LbPid) -> MNodes +%% Nodes -> [atom()] +%% LbPid -> pid() +%% MNodes -> [#mnode{}] +mk_mnodes([Node|Nodes], LbPid) -> + [mk_mnode(Node, LbPid) | mk_mnodes(Nodes, LbPid)]; +mk_mnodes([], _LbPid) -> + []. + +mk_mnode(Node, LbPid) -> + + %% Create an appmon process at the node + {ok, Pid} = appmon_info:start_link(Node, self(), []), + + appmon_lb:add_node(LbPid, Node), + appmon_info:load(Pid, Node, true, [{timeout,1000}]), + appmon_info:app_ctrl(Pid, Node, true, []), + + #mnode{name=Node, status=alive, pid=Pid}. + +%% get_mnode(Node, MNodes) -> MNode | false +%% Node -> atom() +%% MNodes -> [#mnode{}] +%% MNode -> #mnode{} +get_mnode(Node, MNodes) -> + case lists:keysearch(Node, #mnode.name, MNodes) of + {value, MNode} -> + MNode; + false -> + false + end. + +%% replace_mnode(Node, MNode, MNodes1) -> Mnodes2 +%% Node -> atom() +%% MNode -> #mnode{} +%% MNodes1 -> MNodes2 -> [#mnode{}] +%% Replaces, or adds if previously not included, the mnode with name +%% Node in MNodes1 with MNode. +replace_mnode(Node, MNode, [#mnode{name=Node} | MNodes]) -> + [MNode | MNodes]; +replace_mnode(Node, MNode, [MNode2 | MNodes]) -> + [MNode2 | replace_mnode(Node, MNode, MNodes)]; +replace_mnode(_Node, MNode, []) -> + [MNode]. + + +%%---------------------------------------------------------------------- +%% GUI list manipulating functions +%%---------------------------------------------------------------------- + +%% get_win(Node, Wins) -> Win +%% Node -> atom() +%% Wins -> [#win{}] +%% Win -> #win{} +get_win(Node, Wins) -> + case lists:keysearch(Node, #win.name, Wins) of + {value, Win} -> + {ok, Win}; + false -> + false + end. + +%% get_win2(WinObj, Wins) -> Win +%% Window -> gsobj() +%% Wins -> [#win{}] +%% Win -> #win{} +get_win2(WinObj, Wins) -> + case lists:keysearch(WinObj, #win.window, Wins) of + {value, Win} -> + {ok, Win}; + false -> + false + end. + +%% replace_win(Node, Win, Wins) -> Wins2 +%% Node -> atom() +%% Win -> #win{} +%% Wins -> Wins2 -> [#win{}] +replace_win(Node, Win, Wins) -> + lists:keyreplace(Node, #win.name, Wins, Win). + +%% remove_win(Node, Wins) -> Wins2 +%% Node -> atom() +%% Wins -> Wins2 -> [#win{}] +remove_win(Node, Wins) -> + lists:keydelete(Node, #win.name, Wins). + + + + +%%---------------------------------------------------------------------- +%% GUI manipulating functions +%%---------------------------------------------------------------------- +-define(PAD, 10). % Pad between objects +-define(PAD2, 4*?PAD). % Pad betw. node lbl and app + +-define(hMENUBAR, 25). % Note: Hardwired in Tcl/Tk + +-define(xNODELBL, 60). % Node label +-define(yNODELBL, 35). +-define(hNODELBL, 20). + +-define(xMETER, 5). % Meter +-define(yMETER, ?yNODELBL). +-define(wMETER, 20). +-define(hMETER, ?hNODELBL + ?PAD + ?PAD2 + ?hBTN). +-define(LEDCOUNT, 16). + +-define(xBTN, ?xNODELBL). % Application buttons +-define(yBTN, ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2). +-define(wBTN, 70). % min width +-define(hBTN, 20). + +-define(wCANVAS, 470 + ?wMETER + 3*?PAD). % Canvas +-define(hCANVAS, ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2 + ?hBTN + 2*?PAD). + +-define(wWIN, ?wCANVAS). % Window +-define(hWIN, ?hMENUBAR + ?hCANVAS). + +%%--Main window--------------------------------------------------------- + +draw_win(GS, Node) -> + + %% Main window + NodeStr = atom_to_list(Node), + Win = gs:create(window, GS, [{title, + "APPMON: Overview on " ++ NodeStr}, + {width, ?wWIN}, {height, ?hWIN}, + {configure, true}]), + Canvas = gs:create(canvas, Win, [{x, 0}, {y, ?hMENUBAR}, + {width, ?wCANVAS}, + {height, ?hCANVAS}]), + L1 = gs:create(line, Canvas, [{coords, + [{0,?yNODELBL-?PAD}, + {?wCANVAS,?yNODELBL-?PAD}]}]), + L2 = gs:create(line, Canvas, [{coords, + [{0,?hCANVAS-?PAD}, + {?wCANVAS,?hCANVAS-?PAD}]}]), + + %% Standard buttons + MenuBar = gs:create(menubar, Win, [{height, ?hMENUBAR}]), + + FileMenuBtn = gs:create(menubutton, MenuBar, + [{label, {text,"File"}}]), + FileMenu = gs:create(menu, FileMenuBtn, []), + gs:create(menuitem, FileMenu, [{label, {text,"Show List Box..."}}, + {data, listbox}]), + gs:create(menuitem, FileMenu, [{label, {text, "Close"}}, + {data, {close, Win}}]), + gs:create(menuitem, FileMenu, [{itemtype, separator}]), + gs:create(menuitem, FileMenu, [{label, {text, "Exit"}}, + {data, exit}]), + + ActionMenuBtn = gs:create(menubutton, MenuBar, + [{label,{text,"Actions"}}]), + ActionMenu = gs:create(menu, ActionMenuBtn, []), + gs:create(menuitem, ActionMenu, [{label, {text,"Reboot"}}, + {data, {action, reboot, Win}}]), + gs:create(menuitem, ActionMenu, [{label, {text,"Restart"}}, + {data, {action, restart, Win}}]), + gs:create(menuitem, ActionMenu, [{label, {text,"Stop"}}, + {data, {action, stop, Win}}]), + gs:create(menuitem, ActionMenu, [{label, {text,"Ping"}}, + {data, {action, ping, Win}}]), + + OptMenuBtn = gs:create(menubutton, MenuBar, + [{label, {text,"Options"}}]), + OptMenu = gs:create(menu, OptMenuBtn, []), + G0 = now(), % Group identity unique per window! + SMI = gs:create(menuitem, OptMenu, [{label, {text,"One window"}}, + {itemtype, radio}, {group, G0}, + {data, {window_mode, single}}]), + MMI = gs:create(menuitem, OptMenu, [{label, {text,"Many windows"}}, + {itemtype, radio}, {group, G0}, + {data, {window_mode, many}}]), + gs:create(menuitem, OptMenu, [{itemtype, separator}]), + G1 = now(), + TMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: time"}}, + {itemtype, radio}, {group, G1}, + {data, + {option, time, + [{load_method,time}]}}]), + QMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: queue"}}, + {itemtype, radio}, {group, G1}, + {data, + {option, queue, + [{load_method,queue}]}}]), + G2 = now(), + PMI = gs:create(menuitem, OptMenu, + [{label, {text,"Load: progressive"}}, + {itemtype, radio}, {group, G2}, + {data, {option, prog, [{load_scale,prog}]}}]), + LMI = gs:create(menuitem, OptMenu, [{label, {text,"Load: linear"}}, + {itemtype, radio}, {group, G2}, + {data, + {option, linear, + [{load_scale,linear}]}}]), + + NodeMenuBtn = gs:create(menubutton, MenuBar, + [{label, {text,"Nodes"}}]), + NodeMenu = gs:create(menu, NodeMenuBtn, []), + + HelpMenuBtn = gs:create(menubutton, MenuBar, + [{label, {text,"Help"}}, {side, right}]), + HelpMenu = gs:create(menu, HelpMenuBtn, []), + gs:create(menuitem, HelpMenu, [{label, {text,"Help"}}, + {data, help}]), + + %% Meter + HLed = trunc((?hMETER)/(?LEDCOUNT)), + Leds = draw_leds(?LEDCOUNT, Canvas, ?yMETER, HLed, []), + leds_down(Leds, ?LEDCOUNT, 0), + gs:create(text, Canvas, [{coords, + [{?xMETER, ?yMETER+HLed*?LEDCOUNT}]}, + {anchor, nw}, + {font, {screen,8}}, + {text, "Load"}]), + gs:create(text, Canvas, [{coords, [{?xMETER+?wMETER, ?yMETER}]}, + {anchor, nw}, + {font, {screen,8}}, + {text, "Hi"}]), + gs:create(text, Canvas, [{coords, [{?xMETER+?wMETER, + ?yMETER+HLed*?LEDCOUNT}]}, + {anchor, w}, + {font, {screen,8}}, + {text, "Lo"}]), + + %% Node label + WNodeLbl = 8*length(NodeStr)+10, + NLRect = gs:create(rectangle, Canvas, + [{coords, [{?xNODELBL,?yNODELBL}, + {?xNODELBL+WNodeLbl, + ?yNODELBL+?hNODELBL}]}, + {fill, black}]), + Xc = ?xNODELBL + round(WNodeLbl/2), + Yc = ?yNODELBL + round(?hNODELBL/2), + NLText = gs:create(text, Canvas, [{text, NodeStr}, + {fg, {250,235,215}}, + {coords, [{Xc,Yc}]}, + {anchor, c}]), + NodeLbl = {NLRect, NLText}, + + gs:config(Win, {map, true}), + #win{name=Node, + window=Win, wwindow=?wWIN, hwindow=?hCANVAS, + options=#options{single=SMI, many=MMI, + time=TMI, queue=QMI, prog=PMI, linear=LMI}, + canvas=Canvas, wcanvas=?wCANVAS, hcanvas=?hCANVAS, + l1=L1, l2=L2, leds=Leds, nodelabel=NodeLbl, nodemenu=NodeMenu}. + +draw_leds(N, Canvas, Y, HLed, Leds) when N>0 -> + Led = gs:create(rectangle, Canvas, + [{coords, + [{?xMETER,Y}, {?xMETER+?wMETER,Y+HLed}]}]), + draw_leds(N-1, Canvas, Y+HLed, HLed, [Led | Leds]); +draw_leds(0, _Canvas, _Y, _HLed, Leds) -> + Leds. + +%%--Draw functions------------------------------------------------------ +%% Functions that modify the GUI and its data (win{}) + +%% Display the node name in the window title +%% (The name in the node label is changed by display_nodeup|nodedown) +%% Used when a changing the node to display +draw_nodename(GUI, Node) -> + NodeStr = atom_to_list(Node), + gs:config(GUI#win.window, + {title, "APPMON: Overview on " ++ NodeStr}), + GUI#win{name=Node}. + +%% Resize the canvas (when the window has been resized) +draw_resize(GUI, W, H) -> + Hc = H - ?hMENUBAR, + gs:config(GUI#win.canvas, [{width, W}, {height, Hc}]), + Yline1 = ?yNODELBL-?PAD, + Yline2 = ?hCANVAS-?PAD, + gs:config(GUI#win.l1, [{coords, [{0,Yline1},{W,Yline1}]}]), + gs:config(GUI#win.l2, [{coords, [{0,Yline2},{W,Yline2}]}]), + GUI#win{wwindow=W, hwindow=Hc}. + +%% Clear the GUI from applications and connecting lines +draw_clear(GUI) -> + draw_clear2(GUI#win.appobjs), + gs:config(GUI#win.canvas, [{hscroll, false}]), + GUI#win{appobjs=[]}. +draw_clear2([CBtn | AppObjs]) when is_record(CBtn, canvasbutton) -> + gs:destroy(CBtn#canvasbutton.text), + gs:destroy(CBtn#canvasbutton.ul), + gs:destroy(CBtn#canvasbutton.ll), + gs:destroy(CBtn#canvasbutton.rect), + draw_clear2(AppObjs); +draw_clear2([GSObj | AppObjs]) -> + gs:destroy(GSObj), + draw_clear2(AppObjs); +draw_clear2([]) -> + ignore. + +%% Display the applications, which are a list of tuples: {Pid,App,Descr} +%% Display them in the reversed order to get them chronologically +%% from left to right. +draw_apps(GUI, Apps) -> + {AppObjs, WCanvas} = draw_apps(GUI, lists:reverse(Apps), ?xNODELBL, + undefined, 0, []), + NewGUI = GUI#win{wcanvas=WCanvas, appobjs=AppObjs}, + display_scrollbar(NewGUI), + NewGUI. + +draw_apps(GUI, [App | Apps], X, Lx0, N, GSObjs) -> + + %% Some necessary data + {_Pid, AppName, _Descr} = App, + Text = atom_to_list(AppName), + Width = max(8*length(Text)+10, ?wBTN), + + %% Connect the application to the node label with a line + %% Lx0 = leftmost X coordinate (above previous application button) + %% Lx = X coordinate, Ly1, Ly2 = top and bottom Y coordinates + Lx = X + trunc(Width/2), + Line = case N of + %% First (leftmost application) - draw a vertical line + %% between the node label and application button + 0 -> + Ly1 = ?yNODELBL + ?hNODELBL +?PAD, + Ly2 = Ly1 + ?PAD2, + gs:create(line, GUI#win.canvas, + [{coords, [{Lx, Ly1}, {Lx, Ly2}]}]); + %% Nth application, N>1 - draw a horizontal line from + %% line connecting to the previous application button, + %% to above this application button, then vertically down + %% to the application button + _ -> + Ly1 = ?yNODELBL + ?hNODELBL + ?PAD + ?PAD2/2, + Ly2 = Ly1 + ?PAD2/2, + gs:create(line, GUI#win.canvas, + [{coords, [{Lx0, Ly1}, {Lx, Ly1}, + {Lx, Ly2}]}]) + end, + + %% The application is represented using a 'canvasbutton' + Data = {application, AppName, GUI#win.name}, + AppBtn = canvasbutton(GUI#win.canvas, Text, X, ?yBTN, Width, ?hBTN, + Data), + + draw_apps(GUI, Apps, X+Width+?PAD, Lx, N+1, [AppBtn, Line|GSObjs]); +draw_apps(_GUI, [], X, _N, _Lx0, GSObjs) -> + {GSObjs, X}. + +%%--Display functions--------------------------------------------------- +%% Functions that modify the GUI but not its data + +%% Add a new node to the Nodes menu +%% Used when a new node has connected +display_addnode([GUI|GUIs], Node) -> + display_addnode(GUI, Node), + display_addnode(GUIs, Node); +display_addnode([], _Node) -> + ignore; +display_addnode(GUI, Node) -> + Txt = "Show " ++ atom_to_list(Node), + gs:create(menuitem, GUI#win.nodemenu, + [{label, {text,Txt}}, + {data, {node, Node, GUI#win.window}}]). + +%% Show that a node has come back up +display_nodeup(GUI, Node) -> + {Rect, Text} = GUI#win.nodelabel, + + %% Check coordinates for the rectangle and compute the new width + [{L, T}, {_R, B}] = gs:read(Rect, coords), + NodeStr = atom_to_list(Node), + W = 8*length(NodeStr)+10, + + gs:config(Rect, [{coords, [{L, T}, {L+W, B}]}, {fill, black}]), + gs:config(Text, [{text, NodeStr}, {fg, {250,235,215}}, + {coords, + [{L+round(W/2), T+round((?hNODELBL)/2)}]}]). + +%% Show that a node has gone down +display_nodedown(GUI) -> + {Rect, Text} = GUI#win.nodelabel, + + [{L, T}, {_R, B}] = gs:read(Rect, coords), + gs:config(Rect, [{coords, [{L, T}, {L+114, B}]}, {fill, gray}]), + gs:config(Text, [{text, "No connection"}, {fg, black}, + {coords, [{L+57, T+round((?hNODELBL)/2)}]}]). + +%% Add/remove scrollbars as necessary +display_scrollbar(GUI) -> + + WWindow = GUI#win.wwindow, + HWindow = GUI#win.hwindow, + WCanvas = GUI#win.wcanvas, + HCanvas = GUI#win.hcanvas, + if + WCanvas>WWindow -> + gs:config(GUI#win.canvas, + [{hscroll, bottom}, + {scrollregion,{0,0,WCanvas,HCanvas}}]); + true -> + gs:config(GUI#win.canvas, [{hscroll, false}]) + end, + if + HCanvas>HWindow -> + gs:config(GUI#win.canvas, + [{vscroll, left}, + {scrollregion,{0,0,WCanvas,HCanvas}}]); + + true -> + gs:config(GUI#win.canvas, [{vscroll, false}]) + end. + +%% Select option radio buttons +display_setopt(GUI, Option) -> + gs:config(radiobutton(GUI, Option), {select,true}). + +radiobutton(GUI, single) -> (GUI#win.options)#options.single; +radiobutton(GUI, many) -> (GUI#win.options)#options.many; +radiobutton(GUI, time) -> (GUI#win.options)#options.time; +radiobutton(GUI, queue) -> (GUI#win.options)#options.queue; +radiobutton(GUI, prog) -> (GUI#win.options)#options.prog; +radiobutton(GUI, linear) -> (GUI#win.options)#options.linear. + +%% Display load +%% Used when load information is received from the displayed node +-define(highloadfg, {255,99,71}). +-define(midloadfg, yellow). +-define(lowloadfg, green). +-define(highloadbg, {140,157,178}). +-define(midloadbg, ?highloadbg). +-define(lowloadbg, ?highloadbg). + +display_load(GUI, {Old, New}) -> + if + Old == New -> + true; + Old > New -> + leds_down(GUI#win.leds, Old, New); + true -> + leds_up(GUI#win.leds, Old, New) + end. + +leds_down(_Leds, Old, New) when Old == New -> + done; +leds_down(Leds, Old, New) when Old > New -> + reset_led(Leds, Old), + leds_down(Leds, Old-1, New). +leds_up(_Leds, Old, New) when Old == New -> + done; +leds_up(Leds, Old, New) when Old < New -> + set_led(Leds, Old), + leds_up(Leds, Old+1, New). + +led_on_col(N) when N > 13 -> ?highloadfg; +led_on_col(N) when N > 9 -> ?midloadfg; +led_on_col(_) -> ?lowloadfg. + +led_off_col(N) when N > 13 -> ?highloadbg; +led_off_col(N) when N > 9 -> ?midloadbg; +led_off_col(_) -> ?lowloadbg. + +reset_led(_Leds, 0) -> ok; +reset_led(Leds, N) -> + gs:config(lists:nth(N, Leds), [{fill, led_off_col(N)}]). + +set_led(_Leds, 0) -> ok; +set_led(Leds, N) -> + gs:config(lists:nth(N, Leds), [{fill, led_on_col(N)}]). + +%%---------------------------------------------------------------------- +%% Utilities +%%---------------------------------------------------------------------- + +bcast(MNodes, Msg) -> + lists:foreach(fun(MNode) -> + case MNode#mnode.status of + alive -> + MNode#mnode.pid ! Msg; + dead -> + ignore + end + end, + MNodes). + +max(X, Y) when X>Y -> X; +max(_, Y) -> Y. + +%% parse_nodes(MNodes) -> NodeApps +%% MNodes -> [#mnode{}] +%% NodeApps -> [{Node, Status, Apps}] +%% Node -> atom() +%% Status -> alive | dead +%% Apps -> [{Pid, App}] +%% Pid -> pid() +%% App -> atom() +parse_nodes(MNodes) -> + parse_nodes(MNodes, []). +parse_nodes([MNode|MNodes], NodeApps) -> + Apps = parse_apps(MNode#mnode.apps, []), + parse_nodes(MNodes, + [{MNode#mnode.name,MNode#mnode.status,Apps}|NodeApps]); +parse_nodes([], NodeApps) -> + NodeApps. + +parse_apps([{Pid, App, _Descr}|Rest], Apps) -> + parse_apps(Rest, [{Pid, App}|Apps]); +parse_apps([], Apps) -> + Apps. + +%%---------------------------------------------------------------------- +%% Canvas buttons +%%---------------------------------------------------------------------- + +canvasbutton(Canvas, Text, X, Y, W, H, Data) -> + + %% Draw a rectangle (for event catching) + Rect = gs:create(rectangle, Canvas, [{coords, [{X,Y}, {X+W,Y+H}]}, + {fill, gs:read(Canvas, bg)}, + {buttonpress, true}, + {buttonrelease, true}]), + + %% Make the rectangle area look like a 3D button by using lines + Ul = gs:create(line, Canvas, [{coords, [{X,Y+H},{X,Y},{X+W,Y}]}, + {fg, white}, {width, 2}]), + Ll = gs:create(line, Canvas, [{coords, [{X,Y+H},{X+W,Y+H},{X+W,Y}]}, + {fg, {87,87,87}}, {width, 2}]), + + %% Write the text in the middle + Xc = X + round(W/2), + Yc = Y + round(H/2), + T = gs:create(text, Canvas, [{text, Text}, {coords, [{Xc,Yc}]}, + {anchor, c}, + {buttonpress, true}, + {buttonrelease, true}]), + + %% Create the canvasbutton object + CBtn = #canvasbutton{text=T, ul=Ul, ll=Ll, rect=Rect, + x=X, y=Y, w=W, h=H}, + + %% Configure the data + gs:config(T, {data, {canvasbutton, CBtn, Data}}), + gs:config(Rect, {data, {canvasbutton, CBtn, Data}}), + + CBtn. + +press(Canvasbutton) -> + gs:config(Canvasbutton#canvasbutton.ul, {fg, {87,87,87}}), + gs:config(Canvasbutton#canvasbutton.ll, {fg, white}). + +release(Canvasbutton) -> + gs:config(Canvasbutton#canvasbutton.ul, {fg, white}), + gs:config(Canvasbutton#canvasbutton.ll, {fg, {87,87,87}}). diff --git a/lib/appmon/src/appmon_a.erl b/lib/appmon/src/appmon_a.erl new file mode 100644 index 0000000000..b0b5847343 --- /dev/null +++ b/lib/appmon/src/appmon_a.erl @@ -0,0 +1,1117 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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(appmon_a). + +%%---------------------------------------------------------------------- +%% +%% Monitors an application, i.e its supervision tree. +%% +%%---------------------------------------------------------------------- +%% +%% +%% INTRODUCTION +%% ------------ +%% +%% This file contains a description of the files involved +%% and the communication between the appmon_a display +%% manager and the appmon_a2 information gatherer. Further +%% information on the placement algorithm can be found in +%% the place.erl file. +%% +%% +%% FILES +%% ----- +%% +%% The supervision tree graphical software consists of +%% the following files: +%% +%% appmon_a Gen server driving the process display window. +%% Responsible for assigning gs identifiers to all +%% processes and process link +%% appmon_a2 The process information gathering routines. +%% Works by following the process links from application +%% master once every second +%% dg The process database is implemented as a shared +%% digraph (see manual pages for digraph) and this is +%% the routines handling this digraph. Since the digraph +%% is shared appmon_a2 will put some info into it that the +%% appmon_a later will modify. The structures used are +%% described in dg.hrl +%% place Places a tree, decides the x and y coordinates (not +%% necessarily corresponding to window coordinates) of +%% processes (or vertices to be specific). Note that +%% special routines are used to transform the possibly +%% cyclic digraph into a strict tree before trying to +%% place it. +%% +%% +%% +%% IMPLEMENTATION DETAIL +%% --------------------- +%% +%% The appmon_a module will follow links between processes, +%% starting with the application master. A unique +%% reference is used to prevent infinite recursion. Note +%% that this process and link gathering is done in the +%% live digraph so that already known processes are +%% updated with the reference and new ones are added to +%% the digraph. After all processes and links have been +%% added or updated a search is made for those processes +%% and links that have an old reference. These are those +%% processes and links that are not present in the +%% application any more. Those are extracted from the +%% digraph and then deleted and the extracts are then +%% used (by appmon_a) to delete the appropriate gs +%% objects. The responsibilities of appmon_a is thus 1) add +%% all new processes and links to the digraph and 2) make +%% a list of all those objects from the digraph that have +%% been deleted. +%% +%% When appmon_a2 has gathered all necessary information it +%% notifies the appmon_a display manager. Note that this is +%% implemented as a call (as opposed to a cast) to +%% prevent appmon_a2 from changing the digraph while appmon_a +%% uses it. appmon_a places all processes using the place +%% module. place will place the processes in the x y +%% planes, hopefully in a nice way, re-forming the +%% digraph during the process into a strict tree using +%% some simple heuristics, some links that makes the +%% graph cyclic will be considered secondary and later +%% coloured red. Note that the process links are not +%% placed since their coordinates are those of the +%% processes that they are links between. The place +%% module is only concerned at a fairly high level of +%% abstraction. Currently its x coordinates are used as +%% real coordinates while the y coordinates must be +%% scaled to correct values, thus the x plane is +%% continous and the y plane is disctrete. +%% +%% Having placed processes the new ones are drawn on the +%% display along with all new process links, then all +%% processes and process links are moved to their +%% possibly new positions. The place module is not +%% sensitive to changes in position and therefore has no +%% concept of which nodes will have to be moved. hence +%% all nodes are moved (but most of them probably to the +%% same position as before) +%% +%% +%% +%% +%%---------------------------------------------------------------------- + + + +-export([start/2, start/3, stop/0]). + + +-record(astate, {app, name, client, digraph}). + +-import(lists, [foreach/2]). + +%% gen server stuff +-behaviour(gen_server). +-export([init/1, handle_cast/2, handle_info/2, terminate/2]). +-export([handle_call/3, code_change/3]). + + +-define(APPSPACE, 10). % The space between apps +-define(NODEAREA_H, 90). % The height of a node +-define(BUTTAREA_H, 80). % The button area height +-define(APPBUTT_H, 20). % Height of appl button +-define(EDITORW, 260). + +-define(MAXWIDTH, 800). +-define(MINWIDTH, 382). +-define(MAXHEIGHT, 450). +-define(MINHEIGHT, 325). + +-define(SUPVIEWTXT, "Sup. view"). +-define(PROCVIEWTXT, "Proc. view"). +-define(CLOSETXT, "Close"). +-define(REFRESHTXT, "Refresh"). +-define(SAVEOPTSTXT, "Save options"). +-define(HELPTXT, "Help"). + +-define(CHARWIDTH, 7). %Should use GS primitives + +-define( darkkhaki, {189, 183, 107}). +-define( palegoldenrod, {238, 232, 170}). +-define( peachpuff4, {139, 119, 101}). +-define( red, red). +-define( darkgrey, {169, 169, 169}). +-define( lightgrey, {211, 211, 211}). +-define( royalblue, {65, 105, 225}). +-define( aquamarine4, {69, 139, 116}). +-define( palegreen4, {84, 139, 84}). +-define( darkseagreen, {105, 139, 105}). +-define( f_line_col, {150, 150, 255}). + + +-include("appmon_dg.hrl"). + + +%%------------------------------------------------------------ +%%------------------------------------------------------------ + + +start(NodeName, AppName) -> + gen_server:start_link(?MODULE, {NodeName, AppName, AppName}, []). + +start(NodeName, AppName, AppId) -> + gen_server:start_link(?MODULE, {NodeName, AppName, AppId}, []). + + +stop() -> + ok. + + + +%%------------------------------------------------------------ +%% Public interface + + +%%------------------------------------------------------------ +%% Administration + +%% AppName is the name of the application, usually an atom like sasl +%% or kernel, AppId is the application pid or the application name, +%% either goes. +init({NodeName, AppName, AppId}) -> + process_flag(trap_exit, true), + {ok, Client} = appmon_info:start_link(NodeName, self(), []), + init_ref(), + init_foreign_places(), + DG = digraph:new([cyclic, private]), + State = #astate{app=AppId, name=AppName, client=Client, digraph=DG}, + refresh(State), + setup_base_win(NodeName, AppName), + {ok, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +handle_call(norequest, _From, State) -> + {reply, null, State}. + +%%------------------------------------------------------------ +%% handle casts + +handle_cast({ping, _Node, _From}, State) -> + {noreply, State}; +handle_cast(_Other, State) -> + {noreply, State}. + + + +%%------------------------------------------------------------ +%% handle info + +handle_info({gs, _, click, _, [?CLOSETXT|_]}, State) -> + {stop, normal, State}; +handle_info({gs, _, destroy, _, _}, State) -> + {stop, normal, State}; +handle_info({gs, _, click, _, [?REFRESHTXT|_]}, State) -> + refresh(State), + {noreply, State}; +handle_info({gs, _, click, _, [?HELPTXT|_]}, State) -> + HelpFile = filename:join([code:lib_dir(appmon), + "doc", "html", "part_frame.html"]), + tool_utils:open_help(win(), HelpFile), + {noreply, State}; +handle_info({gs, Id, click, {mode, Mode}, _}, State) -> + %%io:format("handle_info: Setting mode: ~p~n", [Mode]), + set_mode(Id, Mode), + {noreply, State}; +handle_info({gs, _, click, _, [?SUPVIEWTXT|_]}, State) -> + refresh(State, [{info_type, sup}]), + {noreply, State}; +handle_info({gs, _, click, _, [?PROCVIEWTXT|_]}, State) -> + refresh(State, [{info_type, link}]), + {noreply, State}; +handle_info({gs, Id, buttonpress, _,[1, X, Y|_]}, State) -> + %%io:format("Id clicked: ~p~n", [gs:read(Id, {find, {X, Y}})]), + catch find_pid(State, Id, X, Y), + set_default_mode(), + {noreply, State}; +handle_info({gs, Win, configure, _Data, [W, H|_]}, State) -> + case win() of Win -> user_driven_resize(W, H); + _-> ok + end, + {noreply, State}; + +handle_info({delivery, _S, pinfo, _N, Res}, State) -> + appmon_txt:print(Res), + {noreply, State}; +handle_info({delivery, S, app, N, Res}, State) -> + {delivery, _Serv, app, _Name, {Root, Vs, Ls, SecLs}} = + flush({delivery, S, app, N, Res}), + update2(Vs, Root, Ls, SecLs, State), + {noreply, State}; + +handle_info({kill}, State) -> + {stop, normal, State}; +handle_info({state}, State) -> + {noreply, State}; +handle_info({'EXIT', _Pid, _Reason}, State) -> + {noreply, State}; +handle_info(_Other, State) -> + {noreply, State}. + + +%% Refresh sets new options for the request and forces an update of +%% the screen ant status. +refresh(State) -> + refresh(State, []). +refresh(State, Opts) -> + appmon_info:app(State#astate.client, + State#astate.name, true, Opts). + + + +%% find_pid finds the pid of the clicked object. The scenario is that +%% the user clicks on an item in his window, that ObjId is searched +%% for among all nodes (vertices) and if found action is taken +%% depending on the current mode (see handle_info) +find_pid(State, Id, X, Y) -> + %% Try to manage both versions of GS, remove first case later. + ObjList = case gs:read(Id, {find, {X, Y}}) of + {error, _} -> + gs:read(Id, {hit, {X, Y}}); % Try new format + Num when is_integer(Num) -> [Num]; + _Other -> [] + end, + DG = State#astate.digraph, + All = appmon_dg:get(all, DG), + find_pid2(ObjList, All, DG, State). + +find_pid2([Id | Ids], All, DG, State) -> + case search_for_pid(All, DG, Id) of + {ok, _KeyStr, Pid} -> + handle_proc_press(mode(), Pid, State); + _ -> find_pid2(Ids, All, DG, State) + end; +find_pid2([], _All, _DG, _State) -> ok. + +search_for_pid([V|Vs], DG, ObjId) -> + VD = appmon_dg:get(data, DG, V), + if ObjId==VD#vdata.txt_obj -> + {ok, V, VD#vdata.type}; + true -> search_for_pid(Vs, DG, ObjId) + end; +search_for_pid([], _DG, _ObjId) -> false. + + +%% +%% called when a process has been clicked on. +%% +handle_proc_press(info, Pid, State) -> + appmon_info:pinfo(State#astate.client, Pid, true, + [{timeout, at_most_once}]); +handle_proc_press(send, Pid, _State) -> + {P, RawStr} = two_entries(winroot(), 250, 70, + "Send", "To: ", "Msg: ", + pid_to_list(Pid), "", bg()), + Str = case lists:last(RawStr) of + 46 -> RawStr; + _ -> RawStr++"." + end, + case erl_scan:string(Str) of + {ok, Tokens, _} -> + case erl_parse:parse_term(Tokens) of + {ok, Term} -> + case catch list_to_pid(P) of + To when is_pid(To) -> To ! Term; + _ -> error + end; + _Error -> error + end; + _Error -> error + end; +handle_proc_press(trace, Pid, _State) -> + case trace_state(Pid) of + true -> + io:format("Removing trace on ~p~n", [Pid]), + sys:trace(Pid, false), + set_trace_state(Pid, false); + _Other -> + io:format("Putting trace on ~p~n", [Pid]), + sys:trace(Pid, true, 1000), + set_trace_state(Pid, true) + end; +handle_proc_press(kill, Pid, _State) -> + exit(Pid, kill). + + +trace_state(Pid) -> get({trace_state, Pid}). +set_trace_state(Pid, State) -> put({trace_state, Pid}, State). + +set_default_mode() -> + {Id, Mode} = get(default_mode), + case mode() of + Mode -> true; + _Other -> set_mode(Id, Mode) + end. +set_default_mode(Id, Mode) -> + put(default_mode, {Id, Mode}), + select(Id), %Dirty workaround + set_default_mode(). + +set_mode(Id, Mode) -> + %%io:format("mode=~p~n", [Mode]), + set_mode(Mode), + deselect(), + select(Id). + +set_mode(Mode) -> put(mode, Mode). +mode() -> get(mode). + +flush({delivery, S, A, N, R}) -> + receive + {delivery, S, A, N, R2} -> + flush({delivery, S, A, N, R2}) + after 0 -> + {delivery, S, A, N, R} + end. + + +%%------------------------------------------------------------ +%% Real stuff +%% +update2(Vs, Root, Ls, SecLs, State) -> + DG = State#astate.digraph, + Ref = mk_ref(), + Added = add_procs(Vs, DG, Ref), + AddedLs = add_links(Ls, DG, Ref, primary), + AddedLs2 = add_links(SecLs, DG, Ref, secondary), + DelLs = del_links(appmon_dg:eget(all, DG), DG, Ref), + Dels = del_procs(appmon_dg:get(all, DG), DG, Ref), + + LastX = appmon_place:place(DG, Root), + Width = lists:max(LastX), + Height = length(LastX), + + %% Delete things from screen + del(Dels), del(DelLs), + + %% Add vertices to screen + foreach(fun(V) -> draw_node(DG, V) end, Added), + + %% Add edges to screen + foreach(fun(E) -> draw_edge(DG, E) end, AddedLs), + foreach(fun(E) -> draw_edge(DG, E) end, AddedLs2), + + %% Move vertices on screen + foreach(fun(V) -> move_vertex(DG, V) end, appmon_dg:get(all, DG)), + + tree_driven_resize(Width, Height), + + gs:config(win(), {map, true}), %Make win visible + ok. + +%% Make an integer reference, could have used make_ref BIF but didn't +mk_ref() -> put(reference, get(reference)+1). +init_ref() -> put(reference, 0). + + +%% Add processes (vertices) to digraph, use the string repr of pid as +%% key in digraph. +add_procs([{Pid, Str}|Vs], DG, Ref) -> + case appmon_dg:add(DG, Str, mk_vdata(Str, Pid, Ref), Ref) of + known -> add_procs(Vs, DG, Ref); + updated -> add_procs(Vs, DG, Ref); + _ -> + case lists:member(32, Str) of + true -> + appmon_dg:set(x, DG, Str, foreign), % UNHOLY! + add_procs(Vs, DG, Ref); % Don't add foreign + _ -> [Str | add_procs(Vs, DG, Ref)] + end + end; +add_procs([], _DG, _Ref) -> []. + + +%% Add links to digraph. NOTE that foreign links get a special weight +%% and that no link is added if it goes to a process not in the set of +%% vertices. +%% +%% OTP-1970: Check that linked-to processes really exist. +%% +add_links([{V1, V2}|Ls], DG, Ref, Weight) -> + L = case lists:member(32, V2) of + true -> {V1, V2, foreign}; + _ -> {V1, V2, Weight} + end, + case appmon_dg:get(data, DG, V2) of + false -> add_links(Ls, DG, Ref, Weight); + VD -> + if VD#vdata.ref == Ref -> % OTP-1970 + case appmon_dg:eadd(DG, L, mk_edata(L, Ref), Ref) of + known -> add_links(Ls, DG, Ref, Weight); + updated -> add_links(Ls, DG, Ref, Weight); + _Other -> [L | add_links(Ls, DG, Ref, Weight)] + end; + true -> + add_links(Ls, DG, Ref, Weight) + end + end; +add_links([], _DG, _Ref, _Weight) -> []. + +%% Make an edge data structure +mk_edata(_L, Ref) -> + #edata{ref=Ref}. + +%% Make a vertex data structure, note that pid can be either a pid or +%% a port, we're not picky here. +mk_vdata(P, Pid, Ref) -> + #vdata{ref=Ref, type=Pid, txt=P, width=width(P)}. +width(Txt) -> ?CHARWIDTH*length(Txt)+10. % Should use GS stuff instead + + +%% Delete those processes that have the wrong reference from the +%% digraph. Returns a list of deleted procs and their data (to be able +%% to erase things on screen). +del_procs([V|Vs], DG, Ref) -> + VD = appmon_dg:get(data, DG, V), + if VD#vdata.ref /= Ref -> appmon_dg:del(DG, V), + [{V, VD} | del_procs(Vs, DG, Ref)]; + true -> del_procs(Vs, DG, Ref) + end; +del_procs([], _DG, _Ref) -> []. + + +%% Deletes links that have the wrong reference from the digraph, note +%% that the weight of the edge is not considered here. Returns a list +%% of deleted links and their data (to be able to erase things on +%% screen). +del_links([L | Ls], DG, Ref) -> + ED = appmon_dg:eget(data, DG, L), + if ED#edata.ref /= Ref -> appmon_dg:edel(DG, L), + [{L, ED} | del_links(Ls, DG, Ref)]; + true -> del_links(Ls, DG, Ref) + end; +del_links([], _DG, _Ref) -> []. + +%% Del deletes the GS objects of the list of should-be-deleted +%% items. Returns nothing particular. +del(L) -> + lists:foreach(fun({{V1, V2, Weight}, D}) when is_record(D, edata) -> + if Weight== foreign -> + dealloc_foreign({V1, V2, Weight}); + true -> ok end, + destroy(D#edata.line); + ({_I, D}) when is_record(D, vdata) -> + destroy(D#vdata.sym_obj), + destroy(D#vdata.txt_obj) + end, L). + + +move_vertex(DG, V) -> + VData = appmon_dg:get(data, DG, V), +%% io:format("Vertex ~p data: x:~p, oldx:~p, y:~p, oldy:~p offs:~p~n", +%% [V, VData#vdata.x, VData#vdata.origx, +%% VData#vdata.y, VData#vdata.origy, offsetx()]), + if VData#vdata.x == foreign -> ok; + true -> + X = VData#vdata.x, + Y = scaley(VData#vdata.y), + OldX = VData#vdata.origx, + OldY = scaley(VData#vdata.origy), + if X==OldX, Y==OldY -> true; + true -> + %%io:format("Moving vertex: ~p~n", [V]), + + W = VData#vdata.width, + {BoxC, TxtC} = calc_box_coords(X, Y, W), + + %% move the symbol and text + gs:config(VData#vdata.sym_obj, [{coords, BoxC}]), + gs:config(VData#vdata.txt_obj, [{coords, TxtC}]), + foreach(fun(E) -> move_edge(DG, E) end, + appmon_dg:get(edges, DG, V)) + end + end. + + +move_edge(DG, E) -> + {{V1, V2, Weight}, V1, V2, ED} = appmon_dg:eget(edge, DG, E), + VD1 = appmon_dg:get(data, DG, V1), + VD2 = appmon_dg:get(data, DG, V2), + Line = ED#edata.line, + move_line(DG, VD1, VD2, Line, Weight). +move_line(DG, VD1, VD2, Line, Weight) when is_list(Line) -> + move_line(DG, VD1, VD2, hd(Line), Weight); +move_line(_DG, VD1, VD2, Line, Weight) -> + Coords = calc_coords(VD1, VD2, Weight), + gs:config(Line, [{coords, Coords} | line_opts(Weight)]). + +%% Draw the vertex on the canvas +draw_node(DG, V) -> + %%io:format("Drawing~n",[]), + Data = appmon_dg:get(data, DG, V), + + X = Data#vdata.x, + Y = scaley(Data#vdata.y), + + {Sym, Txt} = draw(rectangle, sup_col(), Data#vdata.txt, X, Y, + Data#vdata.width), + + appmon_dg:av(DG, V, Data#vdata{sym_obj=Sym, txt_obj=Txt}), + true. + +%% Draws a symbol (rectangle for instance) on the canvas. +draw(Symbol, Col, Txt, X, Y, W) -> + {BoxC, TxtC} = calc_box_coords(X, Y, W), + Box = gs:create(Symbol, canvas(), [{coords, BoxC}, {fill, Col}]), + + TxtObj = gs:create(text, canvas(), [{coords, TxtC}, + {anchor, c}, + %%{buttonpress, true}, + {text, Txt}]), + {Box, TxtObj}. + +%% Returns {BoxCoords, TextCoords} +calc_box_coords(X, Y, W) -> + {[{X, Y-radius()}, {X+W, Y+radius()}], [{X+trunc(W/2), Y}]}. + + +%% Draw a line on the canvas +draw_edge(DG, E) -> + {V1, V2, Weight} = E, + Line = draw_line(DG, V1, V2, Weight), +%% io:format("Line: ~p~n", [Line]), + appmon_dg:eset(line, DG, E, Line). + + +%% From is parent, To is child. If To is not a record then we are +%% dealing with a link to a process on another node. Find a suitable +%% place at the left margin and write the process name there and draw +%% a line to it. +%% +draw_line(DG, From, To, foreign) -> + VD1 = appmon_dg:get(data, DG, From), + Y = find_foreign_place(VD1#vdata.y+0.5, foreign_places()), + add_foreign_place({From, To, foreign}, Y), +%% io:format("New Y: ~p~n", [Y]), + appmon_dg:set(x, DG, To, 0), + appmon_dg:set(y, DG, To, Y), + VD2 = appmon_dg:get(data, DG, To), + Coords = calc_coords(VD1, VD2, foreign), +%% io:format("Node coords: ~p~n", [Coords]), + L = gs:create(line, canvas(), [{coords, Coords} | line_opts(foreign)]), + T = gs:create(text, canvas(), [{coords, [{0, 5+scaley(Y)}]}, + {anchor, nw}, {fg, f_line_col()}, +%% {font, {screen, 10}}, + {text, To}]), + [L, T]; + +draw_line(DG, From, To, Weight) -> + VD1 = appmon_dg:get(data, DG, From), + VD2 = appmon_dg:get(data, DG, To), + Coords = calc_coords(VD1, VD2, Weight), + gs:create(line, canvas(), [{coords, Coords} | line_opts(Weight)]). + +%%---------------------------------------------------------------------- +%% +%% Line coordinate calculation +%% +%% Calculate coordinates for edges (links, lines). Primaries have a +%% nice knee and secondaries are oriented differently. If weight is +%% foreign then we will calculate a nice line to the left margin. +%% +calc_coords(From, To, foreign) -> + Y = scaley(To#vdata.y), + X1 = From#vdata.x+trunc(From#vdata.width/2), + Y1 = scaley(From#vdata.y)+radius(), + [{0, Y}, {X1-40, Y}, {X1, Y1}]; +calc_coords(From, To, primary) -> + X1 = From#vdata.x+trunc(From#vdata.width/2), + Y1 = scaley(From#vdata.y)+radius(), + + X2 = To#vdata.x+trunc(To#vdata.width/2), + Y2 = scaley(To#vdata.y)-radius(), + + Y3 = trunc((Y1+Y2)/2), + [{X1, Y1}, {X1, Y3}, {X2, Y3}, {X2, Y2}]; + +calc_coords(V1, V2, _Weight) -> + Y1 = scaley(V1#vdata.y), + X1 = V1#vdata.x, + X1w = X1+V1#vdata.width, + Y2 = scaley(V2#vdata.y), + X2 = V2#vdata.x, + X2w = X2+V2#vdata.width, + + if Y1 == Y2 -> calc_u(X1, X1w, Y1, X2, X2w); + X1w < X2 -> calc_s(X1w, Y1, X2, Y2); + X2w < X1 -> calc_s(X1, Y1, X2w, Y2); + true -> + D1 = abs(X1-X2), D2 = abs(X1w-X2w), + if D1 > D2 -> calc_rbrack(X1w, Y1, X2w, Y2); + true -> calc_lbrack(X1, Y1, X2, Y2) + end + end. + +%% Calculates line coordinates that will go from bottom of one node to +%% bottom of another on the same level. The line will form a nice "U". +calc_u(X1, X1w, Y, X2, X2w) -> + X3 = trunc((X1+X1w)/2), + X4 = trunc((X2+X2w)/2), + Y2 = Y+radius(), + Y3 = Y2+20, + [{X3, Y2}, {X3, Y3}, {X4, Y3}, {X4, Y2}]. + +%% Calculates line coordinates that will go from right (or left) side +%% to right (or left) side, thus forming a "[" or a "]" (bracket). +calc_rbrack(X1, Y1, X2, Y2) -> + X3 = 20 + if X1 > X2 -> X1; + true -> X2 + end, + [{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}]. +calc_lbrack(X1, Y1, X2, Y2) -> + X3 = -20 + if X1 < X2 -> X1; + true -> X2 + end, + [{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}]. + +%% Calculates line coordinates that will form a nice "S" +calc_s(X1, Y1, X2, Y2) -> + X3 = trunc((X1+X2)/2), + [{X1, Y1}, {X3, Y1}, {X3, Y2}, {X2, Y2}]. + + +%% Options for lines (edges, links) +line_opts(foreign) -> [{width, 2}, {smooth, true}, {fg, f_line_col()}]; +line_opts(primary) -> [{width, 2}, {smooth, false}, {fg, line_col()}]; +line_opts(_) -> [{width, 2}, {smooth, true}, {fg, sec_line_col()}]. + + + +%%---------------------------------------------------------------------- +%% +%% Handling of links to foreign processes +%% +%%---------------------------------------------------------------------- +dealloc_foreign(L) -> +%% io:format("deallocing foreign: ~p~n", [L]), + put(foreign_places, lists:keydelete(L, 1, foreign_places())). +add_foreign_place(V, Y) -> +%% io:format("Adding foreign: ~p~n", [V]), + put(foreign_places, [{V, Y} | foreign_places()]). +foreign_places() -> + get(foreign_places). +init_foreign_places() -> + put(foreign_places, []). + +%% Find a good place for the foreign node +find_foreign_place(StartY, L) -> + case lists:keysearch(StartY, 2, L) of + {value, _} -> find_foreign_place(StartY + 1, L); + _ -> StartY + end. + + +%%------------------------------------------------------------ +%% +%% Graphical stuff +%% + +setup_base_win(NodeName, AppName) -> + set_winroot(gs:start([{kernel,true}])), + + W = ?MINWIDTH, H = ?MINHEIGHT, + + Name = "APPMON: " ++ atom_to_list(AppName) ++ " on " ++ + atom_to_list(NodeName), + + set_win(gs:create(window, winroot(), [{title, Name}, %%{bg, red}, + {x, 250}, {y, 100}, + {width, W}, {bg, win_col()}, + {height, H+?BUTTAREA_H}])), + %% standard buttons + mk_std_butts(win(), W), + set_canvas(gs:create(canvas, win(),[{x,0}, {y,?BUTTAREA_H}, + {width, W}, {height, H}, + {bg, bg()}, + {buttonpress, true}])), + + set_old_win_size(width, gs:read(win(), width)), + set_old_win_size(height, gs:read(win(), height)), + +%% gs:config(win(), {map, true}), %Make win visible + ok. + + +nice_line_coords(W, H) -> + [{0,H-10}, {W,H-10}]. + +%%------------------------------ +%% Button stuff + +mk_butt_area(Win, W) -> + H = ?BUTTAREA_H, + F = gs:create(frame, Win,[{x,0}, {y,0}, %%{bg, frame_col()}, + {width,W}, {height,H}]), + C = gs:create(canvas,F,[{x,0}, {y,0}, {width, W}, {height, H-9}, + {bg, bg()}]), + L = gs:create(line,C,[{coords,nice_line_coords(W, H)}]), + + MB = gs:create(menubar, Win, []), + + FMB = gs:create(menubutton, MB, [{label, {text, "File"}}]), + FM = gs:create(menu, FMB, []), + gs:create(menuitem, FM, [{label, {text, ?CLOSETXT}}]), + + OMB = gs:create(menubutton, MB, [{label, {text, "Options"}}]), + OM = gs:create(menu, OMB, []), + gs:create(menuitem, OM, [{label, {text, ?REFRESHTXT}}]), + Group = now(), + gs:create(menuitem, OM, [{itemtype, separator}]), + gs:create(menuitem, OM, [{label, {text, ?SUPVIEWTXT}}, {itemtype, radio}, + {group, Group}]), + gs:create(menuitem, OM, [{label, {text, ?PROCVIEWTXT}}, {select, true}, + {group, Group}, {itemtype, radio}]), + + HMB = gs:create(menubutton, MB, [{label, {text, "Help"}}, {side, right}]), + HM = gs:create(menu, HMB, []), + gs:create(menuitem, HM, [{label, {text, ?HELPTXT}}]), + + {F, C, L}. + +mk_std_butts(Win, W) -> + {F, C, L} = mk_butt_area(Win, W), + set_bframe(F), set_bcanvas(C), set_bline(L), + + IButt = mk_mode_butt({text, "Info"}, {mode, info}, 10), + mk_mode_butt({text, "Send"}, {mode, send}, 90), + mk_mode_butt({text, "Trace"}, {mode, trace}, 170), + mk_mode_butt({text, "Kill"}, {mode, kill}, 250), + + set_default_mode(IButt, info), + + true. + +select(Id) -> + gs:config(Id, {bg, sel_col()}), + set_selected(Id). + +deselect() -> + gs:config(selected(), {bg, de_sel_col()}). + +mk_mode_butt(Label, Data, X) -> + gs:create(button, bframe(), [{label, Label}, {x, X}, {y, 35}, + {data, Data}, {width, 70}, {height, 25}]). + +%%------------------------------------------------------------ +%% Graphical utilities + +mk_frame(P, X, Y, W, H, BG) -> + gs:create(frame, P, [{x, X}, {y, Y}, {width, W}, {height, H}, {bg, BG}]). + +mk_butt(P, X, Y, W, H, Txt) -> + gs:create(button, P, [{x, X}, {y, Y}, {height, H}, {width, W}, + {label, {text, Txt}}]). + +mk_butt(P, X, Y, Txt) -> + mk_butt(P, X, Y, 70, 20, Txt). + +mk_label(P, X, Y, W, H, Txt, BG) -> + gs:create(label, P, [{x, X}, {y, Y}, {height, H}, {width, W}, + {label, {text, Txt}}, {bg, BG}]). + +mk_entry(P, X, Y, W, H, Txt, BG) -> + gs:create(entry, P, [{x, X}, {y, Y}, {height, H}, {width, W}, {text, Txt}, + {bg, BG}, {keypress, true}]). + + +two_entries(Root, W, H, Name, LTxt1, LTxt2, StartTxt1, StartTxt2, BG) -> + Win = gs:create(window, Root, [{title, Name}, %%{bg, red}, + %%{x, X}, {y, Y}, + {width, W}, {bg, BG}, + {height, H}]), + F = mk_frame(Win, 0, 0, W, H, BG), + + mk_label(F, 10, 10, 30, 20, LTxt1, BG), + mk_label(F, 10, 40, 30, 20, LTxt2, BG), + + E1 = mk_entry(F, 40, 10, 120, 20, StartTxt1, BG), + E2 = mk_entry(F, 40, 40, 120, 20, StartTxt2, BG), + + Ok = mk_butt(F, 170, 10, "Ok"), + Cn = mk_butt(F, 170, 40, "Cancel"), + gs:config(Win, {map, true}), + + Ret = case catch two_entries_loop(E1, E2, Ok, Cn) of + {P2, Msg} -> {P2, Msg}; + _Other -> + false + end, + gs:destroy(Win), + Ret. + + +two_entries_loop(E1, E2, Ok, Cn) -> + receive + {gs, Ok, click, _, _} -> + {gs:read(E1, text), + gs:read(E2, text)}; + {gs, E1, keypress, _, ['Return'|_]} -> + {gs:read(E1, text), + gs:read(E2, text)}; + {gs, E2, keypress, _, ['Return'|_]} -> + {gs:read(E1, text), + gs:read(E2, text)}; + {gs, _, keypress, _, _} -> + two_entries_loop(E1, E2, Ok, Cn); + {gs, Cn, click, _, _} -> + true + end. + +%%-------------------------------------------------------------------- +%% +%% Resizing routines. +%% +%% Resizing deals with a number of different interdependent +%% sizes. Top size is the window size. From window size all other +%% sizes are calculated, we call this the "leader" size. The +%% canvas is usually the same size as the window, except for the +%% row of buttons at the top of the windoww. The canvas is also +%% displaced when the tree is smaller than the minimum window +%% size. +%% +%% +%% Window size - the size of the outer window. Note that +%% provisions must be made for the button area at the top of the +%% window, this is called WinAdj. this is the only item taht +%% changes when the user manually resizes the window. +%% +%% Canvas size - The size of the canvas, should be equal to +%% window size less the button area. Must be adjusted when the +%% window has been manually resized. The canvas also has a +%% scrollregion which must be maintained. Note that we could have +%% used the canvas size as "leading" size, but this did not work +%% since the canvas doesn't fill the complete window when the +%% tree is smaller than the window. +%% +%% Tree size - The size of the tree. This may change whenever a +%% new tree is delivered from the info routine. +%% +%% Dim - All these size adjustments are done in some dimension +%% (width or height). +%% +%% Max, Min - The outmost window may not become larger than Max +%% size or smaller than Min size when resized by the tree +%% size. The user resizing is not restricted to these sizes. +%% +%% Scrollbars: +%% +%% Scrollbars are used whenever necessary, whenever the tree size +%% is bigger than canvas size (in any dimension). +%% +%% Invariants: +%% +%% The three sizes are not varied at the same time. When the +%% window is resized because of a new tree, then window and +%% canvas must be updated. When the user has resized, then only +%% the canvas must be changed (to fit in the window) +%% +%% Tree driven resize +%% +%% This occurs when the tree has been updated. The window may +%% grow and shrink to fit the tree, but may not be smaller than +%% Min and not bigger than Max (scrollbars will be used instead) +%% + +tree_driven_resize(TWidth, THeight) -> + gs:config(win(), {configure, false}), + Width = TWidth+20, + Height = scaley(THeight+1), + put({width, tree}, Width), + put({height, tree}, Height), + adjust_win(width, Width), + adjust_win(height, Height), + fit_tree_to_win(width, Width), + fit_tree_to_win(height, Height), + check_scroll_region(Width, Height, gs:read(canvas(), scrollregion)), + gs:config(win(), {configure, true}), + ok. + + +%% Will adjust the window size to the tree size (given the max and min +%% restrictions. +adjust_win(Dim, TreeSize) -> + case get({Dim, user_resize}) of + true -> ok; + _ -> + WinSize = gs:read(win(), Dim),%%get_dim(Dim, win()), + case get_wanted_winsize(Dim, TreeSize) + winadj(Dim) of + WinSize -> ok; + NewSize -> + %%set(Dim, win(), NewSize+winadj(Dim)) + set_old_win_size(Dim, NewSize), + gs:config(win(), {Dim, NewSize}) + end + end. + +get_wanted_winsize(Dim, Size) -> + Max = maxsize(Dim), Min = minsize(Dim), + if Size > Max -> Max; + Size < Min -> Min; + true -> Size + end. + +set_old_win_size(Dim, Size) -> put({Dim, winsize}, Size). +old_win_size(Dim) -> get({Dim, winsize}). + + +%%-------------------------------------------------------------------- +%% +%% user_driven_resize +%% +%% This is when the user drags the window to some size. This is +%% basically the same as a tree resize, only this time the window +%% itself must not be fiddled with. When the window has been +%% resized this way then normal tree driven resize is not allow +%% to alter the size in that dimension. User overrides. +%% +user_driven_resize(W, H) -> + gs:config(win(), {configure, false}), + check_user_resize(width, W), + check_user_resize(height, H), + check_scroll_region(get({width, tree}), get({height, tree}), + gs:read(canvas(), scrollregion)), + gs:config(win(), {configure, true}). + +check_user_resize(Dim, Size) -> + case old_win_size(Dim) of + Size -> false; + _ -> + put({Dim, user_resize}, true), + set_old_win_size(Dim, Size), + fit_tree_to_win(Dim, get({Dim, tree})) + end. + + + +%%-------------------------------------------------------------------- +%% +%% General resizing routines +%% +%% fit_tree_to_win - Will fit the canvas into a pre-sized window in +%% one dimension. +%% +fit_tree_to_win(Dim, TreeSize) -> + Size = gs:read(win(), Dim) - winadj(Dim), + set_canvas_offset(Dim, Size, TreeSize), + set_button_width(Dim, Size), + if TreeSize > Size -> + gs:config(canvas(), {trans_dim2vh(Dim), trans_dim2enable(Dim)}); + TreeSize < Size -> + gs:config(canvas(), {trans_dim2vh(Dim), false}); + true -> + gs:config(canvas(), {trans_dim2vh(Dim), false}) + end. + + +%%------------------------------ +%% Set the canvas width and displacement in x. +set_canvas_offset(height, Size, _) -> + gs:config(canvas(), {height, Size}); +set_canvas_offset(width, Size, Size) -> + gs:config(canvas(), [{x, 0}, {width, Size}]); +set_canvas_offset(width, Size, TreeSize) when Size + gs:config(canvas(), [{x, 0}, {width, Size}]); +set_canvas_offset(width, Size, TreeSize) when Size>TreeSize-> + Val = trunc((Size-TreeSize)/2), + gs:config(canvas(), [{x, Val}, {width, Size-Val}]). + +%%------------------------------ +%% Set the button area width +set_button_width(height,_) -> ok; +set_button_width(width, W) -> + gs:config(bcanvas(), [{width, W}]), + gs:config(bframe(), [{width, W}]), + gs:config(bline(), [{coords, nice_line_coords(W, ?BUTTAREA_H)}]). + + +%%------------------------------ +%% Update the scrollregion size if needed. +check_scroll_region(W, H, {_, _, W, H}) -> ok; +check_scroll_region(W, H, {_, _, _, _}) -> + gs:config(canvas(), {scrollregion, {0, 0, W, H}}). + + +%% Window sizing primitives +winadj(width) -> 0; +winadj(height) -> ?BUTTAREA_H. +maxsize(width) -> ?MAXWIDTH; +maxsize(height) -> ?MAXHEIGHT. +minsize(width) -> ?MINWIDTH; +minsize(height) -> ?MINHEIGHT. + + + +trans_dim2vh(width) -> hscroll; +trans_dim2vh(height) -> vscroll. +trans_dim2enable(width) -> bottom; +trans_dim2enable(height) -> right. + + + + + +%%------------------------------------------------------------ +%% Global Window info + +winroot() -> get(winroot). +win() -> get(win). +canvas() -> get(canvas). +bframe() -> get(bframe). +bcanvas() -> get(bcanvas). +bline() -> get(bline). +set_winroot(X) -> put(winroot, X). +set_win(X) -> put(win, X). +set_canvas(X) -> put(canvas, X). +set_bframe(X) -> put(bframe, X). +set_bcanvas(X) -> put(bcanvas, X). +set_bline(X) -> put(bline, X). + +sup_col() -> ?darkkhaki. +%%work_col() -> ?orange. +bg() -> ?palegoldenrod. +line_col() -> ?peachpuff4. %% saddlebrown.darkgoldenrod +f_line_col() -> ?royalblue. %% saddlebrown.darkgoldenrod +sec_line_col() -> ?red. +win_col() -> bg(). %%darkolivegreen. + +sel_col() -> ?darkgrey. +de_sel_col() -> ?lightgrey. +set_selected(Id)-> put(selected, Id). +selected() -> get(selected). + +scaley(Y) -> 55*Y. +radius() -> 10. + +destroy(undefined) -> true; +destroy(L) when is_list(L) -> lists:foreach(fun(X) -> destroy(X) end , L); +destroy(Win) -> gs:destroy(Win). + diff --git a/lib/appmon/src/appmon_dg.erl b/lib/appmon/src/appmon_dg.erl new file mode 100644 index 0000000000..f53defa946 --- /dev/null +++ b/lib/appmon/src/appmon_dg.erl @@ -0,0 +1,205 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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% +%%------------------------------------------------------------ +%% +%% Digraph handling for process view GUI. Feeble attempt at data +%% separation. Provides functional interface to the data structures +%% vdata and edata, v for vertex and e for edge. +%% +%%------------------------------------------------------------ +-module(appmon_dg). + +-include("appmon_dg.hrl"). + +%% Exports for vertices +-export([get/3, get/2, set/4, av/3, add/4, del/2, visited/3]). + +%% Exports for edges +-export([eget/2, eget/3, eset/4, eadd/4, edel/2, ae/3]). + +%% Exports for convenience +-export([print_dg/1]). + + +%%------------------------------------------------------------ + + +eget(all, DG) -> + digraph:edges(DG). + +eget(data, DG, E) -> + case digraph:edge(DG, E) of + {_, _V1, _V2, Data} -> Data; + _Other -> false + end; +eget(edge, DG, {V1, V2}) -> + case digraph:edge(DG, {V1, V2}) of + {E, W1, W2, ED} -> {E, W1, W2, ED}; + Other -> + case digraph:edge(DG, {V2, V1}) of + {E, W1, W2, ED} -> {E, W1, W2, ED}; + Other -> false + end + end; + +%% Weight in edge name +eget(edge, DG, {V1, V2, Weight}) -> + case digraph:edge(DG, {V1, V2, Weight}) of + {E, W1, W2, ED} -> {E, W1, W2, ED}; + _Other -> false + end; +eget(in, DG, V) -> + efilter(digraph:in_edges(DG, V)). + +efilter(Es) -> + lists:filter(fun({_V1, _V2, primary}) -> true; + (_E) -> false end, + Es). + +eset(ref, DG, E, Ref) -> + {E2, _V1, _V2, D} = eget(edge, DG, E), + update_e(DG, E2, D#edata{ref=Ref}); +eset(line, DG, E, Line) -> + {E2, _V1, _V2, D} = eget(edge, DG, E), + update_e(DG, E2, D#edata{line=Line}). + +edel(DG, E) -> + digraph:del_edge(DG, E). + +eadd(DG, E, D, Ref) -> + case eget(edge, DG, E) of + {_, _, _, ED} when is_record(ED, edata), ED#edata.ref == Ref -> + known; + {_, _, _, ED} when is_record(ED, edata), ED#edata.ref /= Ref -> + update_e(DG, E, ED#edata{ref=Ref}), + updated; + _Other -> + ae(DG, E, D) + end. + +ae(DG, {V1, V2, Weight}, D) -> + digraph:add_edge(DG, {V1, V2, Weight}, V1, V2, D). + +update_e(DG, {V1, V2, Weight}, D) -> + digraph:del_edge(DG, {V1, V2, Weight}), + digraph:add_edge(DG, {V1, V2, Weight}, V1, V2, D). + +%% Filter destination vertex from a list of edges +vfilter(Vs) -> + lists:map(fun({_V1, V2, _Weight}) -> V2; + ({_V1, V2}) -> V2 + end, Vs). + +get(all, DG) -> + digraph:vertices(DG). + +get(data, DG, {V1, V2}) -> + case digraph:edge(DG, {V1, V2}) of + {_,_,_,Data} -> Data; + _Other -> false + end; +get(data, DG, V) -> + case digraph:vertex(DG, V) of + {_,Data} -> Data; + _Other -> false + end; + +%% Return all children of vertex V (those which V has edges to) +get(out, DG, V) -> + vfilter(efilter(digraph:out_edges(DG, V))); +get(in, DG, V) -> + digraph:in_neighbours(DG, V); +get(edges, DG, V) -> + digraph:edges(DG, V); +get(w, DG, V) -> + Data = get(data, DG, V), + Data#vdata.width; +get(x, DG, V) -> + Data = get(data, DG, V), + Data#vdata.x. + +set(type, DG, V, Type) -> + D = get(data, DG, V), + av(DG, V, D#vdata{type=Type}); + +set(ref, DG, V, Ref) -> + D = get(data, DG, V), + av(DG, V, D#vdata{ref=Ref}); + +set(y, DG, V, Y) -> + D = get(data, DG, V), + av(DG, V, D#vdata{y=Y}); + +set(data, DG, V, D) when is_record(D, vdata)-> + av(DG, V, D); + +set(x, DG, V, X) -> + D = get(data, DG, V), + if D#vdata.x /= X -> + av(DG, V, D#vdata{x=X}); + true -> true + end. + +visited(DG, {V1, V2}, Ref) -> % for edge + D = eget(data, DG, {V1, V2}), + if is_record(D, edata), D#edata.ref == Ref -> true; + true -> false + end; +visited(DG, V, Ref) -> + D = get(data, DG, V), + if is_record(D, vdata), D#vdata.ref == Ref -> true; + true -> false + end. + +add(DG, V, D, Ref) -> + case get(data, DG, V) of + D2 when is_record(D2, vdata), D2#vdata.ref==Ref -> + io:format("Ooops in ~p:add vertex~n", [?MODULE]), + known; + D2 when is_record(D2, vdata) -> + %%io:format("~p touch vertex ~p~n", [self(), V]), + set(ref, DG, V, Ref), + set(type, DG, V, D#vdata.type), + save_coords(DG, V), + updated; + _Other -> + av(DG, V, D), added + end. + +save_coords(DG, V) -> + D = get(data, DG, V), + D2 = D#vdata{origx=D#vdata.x, origy=D#vdata.y}, + av(DG, V, D2). + +del(DG, V) -> + digraph:del_vertex(DG, V). + + +av(DG, V, D) -> + digraph:add_vertex(DG, V, D). + +print_dg(DG) -> + io:format("Vertices:~n", []), + lists:foreach(fun(V) -> io:format(" ~p ~p~n", + [V, get(data, DG, V)]) end, + get(all, DG)), + io:format("Edges:~n", []), + lists:foreach(fun(V) -> io:format(" ~p ~p~n", + [V, eget(edge, DG, V)]) end, + eget(all, DG)), + true. diff --git a/lib/appmon/src/appmon_dg.hrl b/lib/appmon/src/appmon_dg.hrl new file mode 100644 index 0000000000..c3485cf1fd --- /dev/null +++ b/lib/appmon/src/appmon_dg.hrl @@ -0,0 +1,45 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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% + + + +%% +%% The dg package record definitions +%% +%% This is the declarations of the datastructures used for the +%% application monitoring software. All fields are reserved for the +%% implementation except those stated otherwise +%% + +-record(vdata, {ref, % + type, % + x, % + origx=-1, % + y, % + origy=-1, % + txt="", % Set by user + width=0, % Set by user + sym_obj=undefined, % + txt_obj}). % + + + +-record(edata, {ref, % + line, % + weight}). % + diff --git a/lib/appmon/src/appmon_info.erl b/lib/appmon/src/appmon_info.erl new file mode 100644 index 0000000000..4e36d3a13f --- /dev/null +++ b/lib/appmon/src/appmon_info.erl @@ -0,0 +1,963 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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% +%% +%%---------------------------------------------------------------------- +%% +%% Information centre for appmon. Must be present on each node +%% monitored. +%% +%% +%% A worklist is maintained that contain all current work that +%% should be performed at each timeout. Each entry in the +%% worklist describes where the result shall be sent and a list +%% of options relevant for that particular task +%% +%% +%% Maintenance Note: +%% +%% This module is supposed to be updated by any who would like to +%% subscribe for information. The idea is that several tools +%% could use this module for their core information gathering +%% services. +%% +%% The module is based on the notion of tasks. Each task should +%% have a nice public interface function which should handle task +%% administration. Tasks are identified by a "key" consisting of +%% three items, the requesting pid, the name of the task and the +%% task auxillary parameter. The requesting pid is the pid of the +%% callee (in the appmon case it can be the node window for +%% instance), the task name is whatever name the task is given +%% (in the appmon case it can be app, app_ctrl or load). The task +%% name can be seen as the type of the task. The task auxillary +%% parameter is an all purpose parameter that have a different +%% meaning for each type of task so in appmon the Aux for app +%% contains the root pid of the monitored application and in +%% app_ctrl it contains the node name (just to distinguish from +%% the other app_ctrl tasks, if any) while the Aux parameter is +%% not used for the load task at all. +%% +%% Each task also carries a list of options for +%% customisation. The options valid for a task is completely +%% internal to that task type except for the timeout option which +%% is used by do_work to determine the interval at which to +%% perform the task. The timeout option may also have the value +%% at_most_once that indicates that the task should not be done +%% more than once, in appmon the remote port (or process) info +%% (pinfo) task is such a task that is only done once for each +%% call. Note that the only way to change or update options is to +%% call the public interface function for the task, this will +%% merge the old options with the new ones and also force the +%% task to be executed. +%% +%% All tasks are managed by the do_work function. The basic +%% functionality being that the result of the task is compared to +%% the previous result and a delivery is sent to the callee if +%% they differ. Most tasks are then done on a regular basis using +%% the timer module for a delay. +%% +%% There are a limited number of places where the module need to +%% be updated when new services are added, they are all marked +%% with "Maintenance Note", and here is a quick guide: +%% +%% First implement the task. Put the functions in this module +%% among the other task implementations. Currently all task +%% implementations should be put in this file to make it simple +%% to monitor a node, this module should be the only one +%% needed. Then add your implementation to the do_work2 function +%% and finally add a public interface function among the other +%% public interface functions. Voila. +%% +%% +%% +%% Future ideas: +%% +%% Appmon should maybe be enhanced to show all processes on a +%% node. First put all processes in an ets P, then pick those +%% that belong to applications (the normal way), then try to find +%% those processes that are roots in process link trees and pick +%% them. The final step would be to do something with those +%% processes that are left. +%% +%%---------------------------------------------------------------------- +-module(appmon_info). +-behaviour(gen_server). + +%% Exported functions +-export([start_link/3, app/4, pinfo/4, load/4, app_ctrl/4]). + +%% For internal use (RPC call) +-export([start_link2/3]). + +%% For debugging +-export([status/0]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + + +%%---------------------------------------------------------------------- +%% The records +%% +%% state is used for keeping track of all tasks. +%% +%% db is the database used in the app task. +%% + +-record(state, {starter, opts=[], work=[], clients=[]}). +-record(db, {q, p, links, links2}). + + +%%---------------------------------------------------------------------- +%% Macros +%% + +-define(MK_KEY(CMD, AUX, FROM, OPTS), {CMD, AUX, FROM}). +-define(MK_DOIT(KEY), {do_it, KEY}). +-define(ifthen(P,S), if P -> S; true -> ok end). + + +%%---------------------------------------------------------------------- +%% Public interface +%% +%% The Aux parameter is an auxillary parameter that can be used +%% freely by the requesting process, it is included in the work +%% task key. appmon uses it for storing the node name when +%% requesting load and app_ctrl tasks, and appmon_a uses it for +%% storing application name when requesting app task. +%% +%% Maintenance Note: Put new tasks at the end, please. +%% + + +%% Do not use gen_server:start_link because we do not want the +%% appmon_info to die when initiating process dies unless special +%% conditions apply. +%% Uhu, we don't??? Made a fix so that this proces DOES indeed die +%% if it's starter dies. /Gunilla +start_link(Node, Client, Opts) -> + rpc:call(Node, ?MODULE, start_link2, [self(), Client, Opts]). +start_link2(Starter, Client, Opts) -> + Name = {local, ?MODULE}, + Args = {Starter, Opts, Client}, + case gen_server:start(Name, ?MODULE, Args, []) of + {ok, Pid} -> + {ok, Pid}; + {error, {already_started, Pid}} -> + register_client(Pid, Client), + {ok, Pid} + end. + + +%% app_ctrl +%% +%% Monitors which applications exist on a node +%% +app_ctrl(Serv, Aux, OnOff, Opts) -> + gen_server:cast(Serv, {self(), app_ctrl, Aux, OnOff, Opts}). + + +%% load +%% +%% Monitors load on a node +%% +load(Serv, Aux, OnOff, Opts) -> + gen_server:cast(Serv, {self(), load, Aux, OnOff, Opts}). + + +%% app +%% +%% Monitors one application given by name (this ends up in a +%% process tree +%% +app(Serv, AppName, OnOff, Opts) -> + gen_server:cast(Serv, {self(), app, AppName, OnOff, Opts}). + +%% pinfo +%% +%% Process or Port info +%% +pinfo(Serv, Pid, OnOff, Opt) -> + gen_server:cast(Serv, {self(), pinfo, Pid, OnOff, Opt}). + +%% register_client +%% +%% Registers a client (someone subscribing for information) +%% + +register_client(Serv, P) -> + link(Serv), + gen_server:call(Serv, {register_client, P}). + +%% status +%% +%% Status of appmon_info +%% + +status() -> + gen_server:cast(?MODULE, status). + +%%---------------------------------------------------------------------- +%% +%% Gen server administration +%% +%%---------------------------------------------------------------------- + +init({Starter, Opts, Pid}) -> + link(Pid), + process_flag(trap_exit, true), + WorkStore = ets:new(workstore, [set, public]), + {ok, #state{starter=Starter, opts=Opts, work=WorkStore, + clients=[Pid]}}. + +terminate(_Reason, State) -> + ets:delete(State#state.work), + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%%---------------------------------------------------------------------- +%% +%% Gen server calls +%% +%%---------------------------------------------------------------------- + +handle_call({register_client, Pid}, _From, State) -> + NewState = case lists:member(Pid, State#state.clients) of + true -> State; + _ -> State#state{clients=[Pid | State#state.clients]} + end, + {reply, ok, NewState}; +handle_call(_Other, _From, State) -> + {reply, ok, State}. + +%%---------------------------------------------------------------------- +%% +%% Gen server casts +%% +%%---------------------------------------------------------------------- + +%% Cmd = app_ctrl | load | app | pinfo +handle_cast({From, Cmd, Aux, OnOff, Opts}, State) -> + NewState = update_worklist(Cmd, Aux, From, OnOff, Opts, State), + {noreply, NewState}; +handle_cast(status, State) -> + print_state(State), + {noreply, State}; +handle_cast(_Other, State) -> + {noreply, State}. + + +%%---------------------------------------------------------------------- +%% +%% Gen server info's +%% +%%---------------------------------------------------------------------- + +handle_info({do_it, Key}, State) -> + ok = do_work(Key, State), + {noreply, State}; + +handle_info({'EXIT', Pid, Reason}, State) -> + case State#state.starter of + Pid -> + {stop, Reason, State}; + _Other -> + Work = State#state.work, + del_work(ets:match(Work, {{'$1','$2',Pid}, '_', '_', '_'}), + Pid, Work), + case lists:delete(Pid, State#state.clients) of + [] -> case get_opt(stay_resident, State#state.opts) of + true -> {noreply, State#state{clients=[]}}; + _ -> {stop, normal, State} + end; + NewClients -> {noreply, State#state{clients=NewClients}} + end + end; +handle_info(_Other, State) -> + {noreply, State}. + + +%%---------------------------------------------------------------------- +%% +%% Doing actual work +%% +%%---------------------------------------------------------------------- + +do_work(Key, State) -> + WorkStore = State#state.work, + {Cmd, Aux, From, _OldRef, Old, Opts} = retrieve(WorkStore, Key), + {ok, Result} = do_work2(Cmd, Aux, From, Old, Opts), + if + Result==Old -> ok; + true -> + From ! {delivery, self(), Cmd, Aux, Result} + end, + case get_opt(timeout, Opts) of + at_most_once -> + del_task(Key, WorkStore); + T when is_integer(T) -> + {ok, Ref} = timer:send_after(T, ?MK_DOIT(Key)), + store(WorkStore, Key, Ref, Result, Opts) + end, + ok. + + +%%---------------------------------------------------------------------- +%% +%% Name: do_work2 +%% +%% Maintenance Note: Add a clause here for each new task. +%% +do_work2(load, _Aux, _From, Old, Opts) -> + calc_load(Old, Opts); +do_work2(app_ctrl, _Aux, _From, _Old, _Opts) -> + calc_app_on_node(); +do_work2(app, Aux, _From, _Old, Opts) -> + calc_app_tree(Aux, Opts); +do_work2(pinfo, Aux, _From, _Old, _Opts) -> + calc_pinfo(pinfo, Aux); +do_work2(Cmd, Aux, _From, _Old, _Opts) -> + {Cmd, Aux}. + + +retrieve(Tab, Key) -> + case ets:lookup(Tab, Key) of + [{{Cmd, Aux, From}, Ref, Old, Opts}] -> + {Cmd, Aux, From, Ref, Old, Opts}; + _Other -> + false + end. + +store(Tab, Key, Ref, Old, Opts) -> + ets:insert(Tab, {Key, Ref, Old, Opts}), + Key. + + +%%---------------------------------------------------------------------- +%% +%% WorkStore handling +%% +%%---------------------------------------------------------------------- + +update_worklist(Cmd, Aux, From, true, Opts, State) -> + add_task(Cmd, Aux, From, Opts, State), + State; +update_worklist(Cmd, Aux, From, _Other, _Opts, State) -> + del_task(Cmd, Aux, From, State#state.work), + State. + +%% First check if a task like this already exists and if so cancel its +%% timer and make really sure that no stray do it command will come +%% later. Then start a new timer for the task and store it i +%% WorkStorage +add_task(Cmd, Aux, From, Opts, State) -> + WorkStore = State#state.work, + Key = ?MK_KEY(Cmd, Aux, From, Opts), + OldOpts = del_task(Key, WorkStore), + store(WorkStore, Key, nil, nil, ins_opts(Opts, OldOpts)), + catch do_work(Key, State), + ok. + +%% Delete a list of tasks belonging to a pid +del_work([[Cmd, Aux] | Ws], Pid, Work) -> + del_task(Cmd, Aux, Pid, Work), + del_work(Ws, Pid, Work); +del_work([], _Pid, _Work) -> ok. + +%% Must return old options or empty list +del_task(Cmd, Aux, From, WorkStore) -> + del_task(?MK_KEY(Cmd, Aux, From, []), WorkStore). +del_task(Key, WorkStore) -> + OldStuff = retrieve(WorkStore, Key), + ets:delete(WorkStore, Key), + case OldStuff of + {_Cmd, _Aux, _From, Ref, _Old, Opts} -> + if + Ref /= nil -> + timer:cancel(Ref), + receive + {do_it, Key} -> + Opts + after 10 -> + Opts + end; + true -> Opts + end; + _ -> + [] + end. + + +%% +%% Maintenance Note: +%% +%% Add new task implementations somewhere here below. +%% + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% BEGIN OF calc_app_tree +%% +%% App tree is the process tree shown in the application window +%% +%% The top (root) pid is found by calling +%% application_controller:get_master(AppName) and this is done in +%% calc_app_on_node (before the call to calc_app_tree). +%% +%% We are going to add processes to the P ets and we are doing it +%% in a two step process. First all prospect processes are put on +%% the queue Q. Then we examine the front of Q and add this +%% process to P if it's not already in P. Then all children of +%% the process is put on the queue Q and the process is repeated. +%% +%% We also maintain two link ets'es, one for primary links and +%% one for secondary links. These databases are updated at the +%% same time as the queue is updated with children. +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + + +calc_app_tree(Name, Opts) -> + Mode = get_opt(info_type, Opts), + case application_controller:get_master(Name) of + Pid when is_pid(Pid) -> + DB = new_db(Mode, Pid), + GL = groupl(Pid), + R = case catch do_find_proc(Mode, DB, GL, find_avoid()) of + {ok, DB2} -> + {ok, {format(Pid), + format(ets:tab2list(DB2#db.p)), + format(ets:tab2list(DB2#db.links)), + format(ets:tab2list(DB2#db.links2))}}; + {error, Reason} -> + {error, Reason}; + Other -> + {error, Other} + end, + ets:delete(DB#db.p), + ets:delete(DB#db.links), + ets:delete(DB#db.links2), + R; + _ -> + {ok, {[], [], [], []}} + end. + +get_pid(P) when is_pid(P) -> P; +get_pid(P) when is_port(P) -> P; +get_pid(X) when is_tuple(X) -> element(2, X). + + +%---------------------------------------------------------------------- +%%--------------------------------------------------------------------- +%% Handling process trees of processses that are linked to each other + +do_find_proc(Mode, DB, GL, Avoid) -> + case get_next(DB) of + {{value, V}, DB2} -> + do_find_proc2(V, Mode, DB2, GL, Avoid); + {empty, DB2} -> + {ok, DB2} + end. + +do_find_proc2(X, Mode, DB, GL, Avoid) when is_port(X) -> + %% There used to be a broken attempt here to handle ports, + %% but the rest of appmon can't handle ports, so now we + %% explicitly ignore ports. + do_find_proc(Mode, DB, GL, Avoid); +do_find_proc2(X, Mode, DB, GL, Avoid) -> + Xpid = get_pid(X), + DB2 = case is_proc(DB, Xpid) of + false -> + add_proc(DB, Xpid), + C1 = find_children(X, Mode), + add_children(C1, Xpid, DB, GL, Avoid, Mode); + _ -> + DB + end, + do_find_proc(Mode, DB2, GL, Avoid). + + +%% Find children finds the children of a process. The method varies +%% with the selected mode (sup or link) and there are also some +%% processes that must be treated differently, notably the application +%% master. +%% +find_children(X, sup) when is_pid(X) -> + %% This is the first (root) process of a supervision tree and it + %% better be a supervisor, we are smoked otherwise + supervisor:which_children(X); +find_children(X, link) when is_pid(X), node(X) /= node() -> + []; +find_children(X, link) when is_pid(X) -> + case process_info(X, links) of + {links, Links} -> + lists:reverse(Links); % OTP-4082 + _ -> [] + end; +find_children({master, X}, sup) -> + case application_master:get_child(X) of + {Pid, _Name} when is_pid(Pid) -> [Pid]; + Pid when is_pid(Pid) -> [Pid] + end; +find_children({_, _X, worker, _}, sup) -> []; +find_children({_, X, supervisor, _}, sup) -> + lists:filter(fun(Thing) -> + Pid = get_pid(Thing), + if + is_pid(Pid) -> true; + true -> false + end + end, + supervisor:which_children(X)). + + +%% Add links to primary (L1) or secondary (L2) sets and return an +%% updated queue. A link is considered secondary if its endpoint is in +%% the queue of un-visited but known processes. +add_children(CList, Paren, DB, _GL, _Avoid, sup) -> + lists:foldr(fun(C, DB2) -> + case get_pid(C) of + P when is_pid(P) -> + add_prim(C, Paren, DB2); + _ -> DB2 end end, + DB, CList); + +add_children(CList, Paren, DB, GL, Avoid, _Mode) -> + lists:foldr(fun(C, DB2) -> + maybe_add_child(C, Paren, DB2, GL, Avoid) + end, DB, CList). + +%% Check if the child is already in P +maybe_add_child(C, Paren, DB, GL, Avoid) -> + case is_proc(DB, C) of + false -> + maybe_add_child_node(C, Paren, DB, GL, Avoid); + _ -> DB % In P: no action + end. + +%% Check if process on this node +maybe_add_child_node(C, Paren, DB, GL, Avoid) -> + if + node(C) /= node() -> + add_foreign(C, Paren, DB); + true -> + maybe_add_child_avoid(C, Paren, DB, GL, Avoid) + end. + +%% Check if child is on the avoid list +maybe_add_child_avoid(C, Paren, DB, GL, Avoid) -> + case lists:member(C, Avoid) of + true -> DB; + false -> + maybe_add_child_port(C, Paren, DB, GL) + end. + +%% Check if it is a port, then it is added +maybe_add_child_port(C, Paren, DB, GL) -> + if + is_port(C) -> + add_prim(C, Paren, DB); + true -> + maybe_add_child_sasl(C, Paren, DB, GL) + end. + +%% Use SASL stuff if present +maybe_add_child_sasl(C, Paren, DB, GL) -> + case check_sasl_ancestor(Paren, C) of + yes -> % Primary + add_prim(C, Paren, DB); + no -> % Secondary + add_sec(C, Paren, DB); + dont_know -> + maybe_add_child_gl(C, Paren, DB, GL) + end. + +%% Check group leader +maybe_add_child_gl(C, Paren, DB, GL) -> + case cmp_groupl(GL, groupl(C)) of + true -> maybe_add_child_sec(C, Paren, DB); + _ -> DB + end. + +%% Check if the link should be a secondary one. Note that this part is +%% pretty much a guess. +maybe_add_child_sec(C, Paren, DB) -> + case is_in_queue(DB, C) of + true -> % Yes, secondary + add_sec(C, Paren, DB); + _ -> % Primary link + add_prim(C, Paren, DB) + end. + +check_sasl_ancestor(Paren, C) -> + case lists:keysearch('$ancestors', 1, + element(2,process_info(C, dictionary))) of + {value, {_, L}} when is_list(L) -> + H = if + is_atom(hd(L)) -> whereis(hd(L)); + true -> hd(L) + end, + if + H == Paren -> yes; + true -> no + end; + _ -> dont_know + end. + + +%---------------------------------------------------------------------- +%%--------------------------------------------------------------------- +%% Primitives for the database DB of all links, processes and the +%% queue of not visited yet processes. + +-define(add_link(C, Paren, L), ets:insert(L, {Paren, C})). + +new_db(Mode, Pid) -> + P = ets:new(processes, [set, public]), + L1 = ets:new(links, [bag, public]), + L2 = ets:new(extralinks, [bag, public]), + Q = if + Mode =:= sup -> queue:in({master, Pid}, queue:new()); + true -> queue:in(Pid, queue:new()) + end, + #db{q=Q, p=P, links=L1, links2=L2}. + +get_next(DB) -> + {X, Q} = queue:out(DB#db.q), + {X, DB#db{q=Q}}. +add_proc(DB, P) -> + ets:insert(DB#db.p, {P}). +add_prim(C, Paren, DB) -> + ?add_link(get_pid(C), Paren, DB#db.links), + DB#db{q=queue:in(C, DB#db.q)}. +add_foreign(C, Paren, DB) -> + ?add_link(C, Paren, DB#db.links2), + DB#db{q=queue:in(C, DB#db.q)}. +add_sec(C, Paren, DB) -> + ?add_link(C, Paren, DB#db.links2), + DB. + +is_proc(#db{p=Tab}, P) -> + ets:member(Tab, P). + +is_in_queue(#db{q=Q}, P) -> + queue:member(P, Q). + +%% Group leader handling. No processes or Links to processes must be +%% added when group leaders differ. Note that catch all is needed +%% because net_sup is undefined when not networked but still present +%% in the kernel_sup child list. Blahh, didn't like that. +groupl(P) -> + case process_info(P, group_leader) of + {group_leader, GL} -> GL; + _Other -> nil + end. + +cmp_groupl(_GL1, nil) -> true; +cmp_groupl(GL1, GL1) -> true; +cmp_groupl(_, _) -> false. + + +%% Do some intelligent guessing as to cut in the tree +find_avoid() -> + lists:foldr(fun(X, Accu) -> + case whereis(X) of + P when is_pid(P) -> + [P|Accu]; + _ -> Accu end end, + [undefined], + [application_controller, init, error_logger, gs, + node_serv, appmon, appmon_a, appmon_info]). + + + +%%---------------------------------------------------------------------- +%% +%% Formats the output strings +%% +%%---------------------------------------------------------------------- +format([{P} | Fs]) -> % Process or port + [{P, format(P)} | format(Fs)]; +format([{P1, P2} | Fs]) -> % Link + [{format(P1), format(P2)} | format(Fs)]; +format([]) -> []; +format(P) when is_pid(P), node(P) /= node() -> + pid_to_list(P) ++ " " ++ atom_to_list(node(P)); +format(P) when is_pid(P) -> + case process_info(P, registered_name) of + {registered_name, Name} -> atom_to_list(Name); + _ -> pid_to_list(P) + end; +format(P) when is_port(P) -> + "port " ++ integer_to_list(element(2, erlang:port_info(P, id))); +format(X) -> + io:format("What: ~p~n", [X]), + "???". + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% END OF calc_app_tree +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + + + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% BEGIN OF calc_app_on_node +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + +%% Finds all applications on a node +calc_app_on_node() -> + NewApps = reality_check(application:which_applications()), + {ok, NewApps}. + + +reality_check([E|Es]) -> + N = element(1, E), + case catch application_controller:get_master(N) of + P when is_pid(P) -> [{P, N, E} | reality_check(Es)]; + _ -> reality_check(Es) + end; +reality_check([]) -> []. + + + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% END OF calc_app_on_node +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% BEGIN OF calc_load +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + +calc_load(Old, Opts) -> + L = load(Opts), + case get_opt(load_average, Opts) of + true -> + case Old of + {_, L} -> {ok, {L, L}}; + {_, O2} when abs(L-O2) < 3 -> {ok, {O2, L}}; + {_, O2} -> {ok, {O2, trunc((2*L+O2)/3)}}; + _ -> {ok, {0, L}} + end; + _ -> + case Old of + {_, O2} -> {ok, {O2, L}}; + _ -> {ok, {0, L}} + end + end. + + +load(Opts) -> + Q = get_sample(queue), + + case get_opt(load_method, Opts) of + time -> + Td = get_sample(runtime), + Tot = get_sample(tot_time), + + case get_opt(load_scale, Opts) of + linear -> + min(trunc(load_range()*(Td/Tot+Q/6)), + load_range()); + prog -> + min(trunc(load_range()*prog(Td/Tot+Q/6)), + load_range()) + end; + queue -> + case get_opt(load_scale, Opts) of + linear -> + min(trunc(load_range()*Q/6), load_range()); + prog -> + min(trunc(load_range()*prog(Q/6)), load_range()) + end + end. + +min(X,Y) when X X; +min(_,Y)->Y. + + +%% +%% T shall be within 0 and 0.9 for this to work correctly +prog(T) -> + math:sqrt(abs(T)/0.9). + + +get_sample(queue) -> statistics(run_queue); +get_sample(runtime) -> {Rt,Rd} = statistics(runtime), + delta(runtime, Rt, Rd); +get_sample(tot_time) -> {Rt,Rd} = statistics(wall_clock), + delta(tot_time, Rt, Rd). + + +%% Keeps track of differences between calls +%% Needed because somebody else might have called +%% statistics/1. +%% +%% Note that due to wrap-arounds, we use a cheating +%% delta which is correct unless somebody else +%% uses statistics/1 +delta(KeyWord, Val, CheatDelta) -> + RetVal = case get(KeyWord) of + undefined -> + Val; + Other -> + if + Other > Val -> + CheatDelta; + true -> + Val-Other + end + end, + put(KeyWord, Val), + RetVal. + + +load_range() -> 16. + + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% END OF calc_load +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% BEGIN OF calc_pinfo +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + +calc_pinfo(pinfo, Pid) when is_pid(Pid) -> + Info = process_info(Pid), + {ok, io_lib:format("Node: ~p, Process: ~p~n~p~n~n", + [node(), Pid, Info])}; +calc_pinfo(pinfo, Pid) when is_port(Pid) -> + Info = lists:map(fun(Key) ->erlang:port_info(Pid, Key) end, + [id, name, connected, links, input, output]), + + {ok, io_lib:format("Node: ~p, Port: ~p~n~p~n~n", + [node(), element(2, erlang:port_info(Pid, id)), + Info])}; +calc_pinfo(pinfo, _Pid) -> + {ok, ""}. + + +%%---------------------------------------------------------------------- +%%********************************************************************** +%% +%% +%% END OF calc_pinfo +%% +%% +%%********************************************************************** +%%---------------------------------------------------------------------- + + + +%%---------------------------------------------------------------------- +%% +%% Print the State +%% +%% -record(state, {opts=[], work=[], clients=[]}). +%% +%%---------------------------------------------------------------------- +print_state(State) -> + io:format("Status:~n Opts: ~p~n" + "Clients: ~p~n WorkStore:~n", + [State#state.opts, State#state.clients]), + print_work(ets:tab2list(State#state.work)). + +print_work([W|Ws]) -> + io:format(" ~p~n", [W]), print_work(Ws); +print_work([]) -> ok. + + +%%---------------------------------------------------------------------- +%% +%% Option handling +%% +%%---------------------------------------------------------------------- + +%% The only options ever set by a user is info_type, timeout, +%% load_scale and load_method. +get_opt(Name, Opts) -> + case lists:keysearch(Name, 1, Opts) of + {value, Val} -> element(2, Val); + false -> default(Name) + end. + +%% not all options have default values +default(info_type) -> link; +default(load_average) -> true; +default(load_method) -> time; +default(load_scale) -> prog; +default(stay_resident) -> false; +default(timeout) -> 2000. + +ins_opts([Opt | Opts], Opts2) -> + ins_opts(Opts, ins_opt(Opt, Opts2)); +ins_opts([], Opts2) -> Opts2. + +ins_opt({Opt, Val}, [{Opt, _} | Os]) -> [{Opt, Val} | Os]; +ins_opt(Opt, [Opt2 | Os]) -> [Opt2 | ins_opt(Opt, Os)]; +ins_opt(Opt, []) -> [Opt]. diff --git a/lib/appmon/src/appmon_lb.erl b/lib/appmon/src/appmon_lb.erl new file mode 100644 index 0000000000..4e433f37c5 --- /dev/null +++ b/lib/appmon/src/appmon_lb.erl @@ -0,0 +1,689 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1998-2009. 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% + +%%% Due to the fact that the application buttons in the appmon window +%%% gets too small to read when the number of applications increases, +%%% this listbox window has been created. +%%% Because of the limitations of GS a listbox was chosen to keep +%%% the nodes and applications. When it's possible to scroll a frame I +%%% think one should put in scrollbars in the appmon main window. +%%% The listbox solution is too slow with lots of applications. +%%% +%%% In the listbox the nodes are shown with their applications beneith. +%%% By double clicking on an application name, or a single click and +%%% then pressing the load button, its application window is started. + +-module(appmon_lb). + +-export ([ + start/1, + stop/1, + add_node/2, + remove_node/2, + add_apps/3, + remove_app/3, + open_win/2, + update_status/3 + ]). + +-export ([init/1]). + +-define (LB_W, 200). % List box width +-define (LB_H, 400). % List box height +-define (BUTT_W, 100). +-define (WIN_W, ?LB_W + ?BUTT_W + 25). % Window width +-define (WIN_H, ?LB_H + 20). % Window height + + +%%% #node{} +%%% +%%% The record 'node' contains the name of the node, its status and +%%% the applications running on that node. +%%% +%%% node == atom () +%%% status == alive || dead +%%% apps == [#app{}] +%%% + +-record (node, {node, %% Name of the node + status = alive, + apps = []}). + +%%% #app{} +%%% +%%% The record 'app' contains the name of the application and its pid +%%% +%%% app == atom () +%%% pid == pid () +%%% + +-record (app, {app, + pid}). + + +%%% #win{} +%%% +%%% The record 'win' contains the pid of the listbox window, +%%% its x and y position, its width and height. +%%% +%%% pid == win_closed || pid () +%%% x == integer () +%%% y == integer () +%%% width == integer () +%%% height == integer () +%%% + +-record (win, {pid = win_closed, + x = 50, + y = 50, + width = ?WIN_W, + height = ?WIN_H}). + + + +%%% Every function in the interface is called with the pid +%%% of this recieve loop, called 'LbPid'. +%%% + + +%%% start /1 +%%% +%%% start returns the pid of the spawned receive loop or +%%% it will call exit/2 after a timeout. +%%% +%%% Pre: +%%% CallingPid == pid () +%%% +%%% Def: +%%% pid () || exit/2 +%%% + +start (CallingPid) -> + PidInit = spawn (?MODULE, init, [CallingPid]), + + %% Wait for a initialization completion message from + %% the spawned process before returning its Pid. + + receive + {initialization_complete, PidInit} -> + PidInit + + %% (Conditional) Failure to start within the time limit + %% will result in termination (Timeout may be infinite). + + after + 60000 -> + exit (PidInit, kill), + exit ({startup_timeout, ?MODULE}) + end. + + + +%%% stop /1 +%%% +%%% stop exits the receive loop +%%% +%%% Post: +%%% exiting the receive loop +%%% + +stop (LbPid) -> + call (LbPid, stop). + + + +%%% add_node /2 +%%% +%%% add_node adds the given node to the DB list. +%%% +%%% Pre: +%%% Node == atom () +%%% +%%% Post: +%%% Node is added to the DB list +%%% + +add_node (LbPid, Node) -> + call (LbPid, {add_node, Node}). + + + +%%% remove_node /2 +%%% +%%% remove_node removes the given node from the DB list. +%%% +%%% Pre: +%%% Node == atom () +%%% +%%% Post: +%%% Node is removed from the DB list +%%% + +remove_node (LbPid, Node) -> + call (LbPid, {remove_node, Node}). + + + +%%% add_apps /3 +%%% +%%% add_apps add the given applications to the given +%%% node in the DB list. +%%% +%%% Pre: +%%% Apps == [App] +%%% App == {Name, Pid} +%%% Name == atom () +%%% Pid == pid () +%%% Node == atom () +%%% +%%% Post: +%%% Node#node{apps = Apps} +%%% + +add_apps (LbPid, Apps, Node) -> + call (LbPid, {add_apps, Apps, Node}). + + + +%%% remove_app /3 +%%% +%%% remove_app remove the given application from the +%%% given node in the DB list. +%%% +%%% Pre: +%%% App == atom () +%%% Node == atom () +%%% +%%% Def: +%%% Node#node{apps = OldApps - App} +%%% + +remove_app (LbPid, App, Node) -> + call (LbPid, {remove_app, App, Node}). + + + +%%% open_win /3 +%%% +%%% open_win opens the listbox window with the given nodes +%%% and their applications. +%%% +%%% Pre: +%%% Nodes_apps == [{Node, Status, Apps}] +%%% Node == atom () +%%% Status == alive || dead +%%% Apps == [App] +%%% App == {AppName, AppPid} +%%% AppName == atom () +%%% AppPid == pid () +%%% +%%% Post: +%%% Window with listbox +%%% + +open_win (LbPid, Nodes_apps) -> + call (LbPid, {open_win, Nodes_apps}). + + + +%%% update_status /3 +%%% +%%% update_status changes the status for the given node. +%%% +%%% Pre: +%%% Node == atom () +%%% Status == alive || dead +%%% +%%% Def: +%%% Node#node{status = Status} +%%% + +update_status (LbPid, Node, Status) -> + call (LbPid, {update_status, Node, Status}). + + + +%%% call /2 +%%% +%%% call sends the given action to the listbox receive loop. +%%% +%%% Pre: +%%% Action == atom () || tuple () +%%% + +call (LbPid, Action) -> + LbPid ! Action. + + + +%%% init /1 +%%% + +init (CallingPid) -> + CallingPid ! {initialization_complete, self ()}, + loop (#win{}, []). + + + +%%% loop /2 +%%% +%%% loop is the recive loop for the listbox window process. +%%% +%%% Pre: +%%% Win == #win{} +%%% Data == [#node{}] +%%% + +loop (Win, Data) -> + receive + {add_node, Node} -> + NewData = add_node_1 (Node, Data), + update (NewData, Win#win.pid), + loop (Win, NewData); + + {remove_node, Node} -> + NewData = dead_node (Node, Data), + update (NewData, Win#win.pid), + loop (Win, NewData); + + {add_apps, Apps, Node} -> + NewData = add_apps_1 (Apps, Node, Data), + update (NewData, Win#win.pid), + loop (Win, NewData); + + {remove_app, App, Node} -> + NewData = remove_app_1 (App, Node, Data), + update (NewData, Win#win.pid), + loop (Win, NewData); + + {open_win, Nodes_apps} -> + NewData = parse_data ([], Nodes_apps), + NewWin = Win#win{pid = init_win ({Win#win.x, Win#win.y})}, + update (NewData, NewWin#win.pid), + loop (NewWin, NewData); + + {update_status, Node, Status} -> + NewData = update_status_1 (Node, Status, Data), + update (NewData, Win#win.pid), + loop (Win, NewData); + + stop -> + true; + + + {gs, _Id, destroy, _D, _Arg} -> + bye; + + {gs, _Id, configure, _D, [W, H | _]} -> + NewWin = configure (Win#win.pid, W, H), + loop (NewWin, Data); + + {gs, lb, doubleclick, _, _Txt} -> + load_app (gs:read (lb, selection), Data), + loop (Win, Data); + + {gs, lb, click, _, _Txt} -> + loop (Win, Data); + + {gs, close, click, _D, _Arg} -> + case Win#win.pid of + win_closed -> + true; + + _opened -> + gs:destroy (Win#win.pid) + end, + loop (#win{}, Data); + + {gs, load, click, _D, _Txt} -> + load_app (gs:read (lb, selection), Data), + loop (Win, Data); + + {gs, clear, click, _D, _Txt} -> + gs:config (lb, {selection, clear}), + loop (Win, Data); + + + _ -> + loop (Win, Data) + end. + + + +%%% init_win /1 +%%% + +init_win ({X, Y}) -> + GS = gs:start (), + + Win = gs:window (win, GS, [{x, X}, + {y, Y}, + {width, ?WIN_W}, + {height, ?WIN_H}, + {title,"Appmon: nodes and applications"}, + {configure, true}]), + + gs:listbox (lb, Win, [{x, 5}, + {y, 10}, + {width, ?LB_W}, + {height, ?LB_H}, + {vscroll, right}, + {hscroll, bottom}, + {selectmode, single}, + {click, true}, + {doubleclick, true}]), + + gs:button (load, Win, [{x, ?WIN_W - ?BUTT_W - 10}, + {y, ?WIN_H - 120}, + {width, ?BUTT_W}, + {label, {text, "Load"}}]), + + gs:button (clear, Win, [{x, ?WIN_W - ?BUTT_W - 10}, + {y, ?WIN_H - 80}, + {width, ?BUTT_W}, + {label, {text, "Clear"}}]), + + gs:button (close, Win, [{x, ?WIN_W - ?BUTT_W - 10}, + {y, ?WIN_H - 40}, + {width, ?BUTT_W}, + {label, {text, "Close"}}]), + + gs:config (Win, {map, true}), + Win. + + + +%%% add_node_1 /2 +%%% +%%% add_node adds the given node in the given window +%%% with its appications in a listbox. +%%% + +add_node_1 (Node, []) -> + [new_node (Node)]; + +add_node_1 (Node, [H | T]) -> + T1 = lists:keysort (#node.node, [new_node (Node) | T]), + [H | T1]. + + + +%%% dead_node /2 +%%% +%%% dead_node returns a list with the given node's +%%% status changed to dead. +%%% + +dead_node (Node, Data) -> + case lists:keysearch (Node, #node.node, Data) of + {value, Node_rec} -> + L = Node_rec#node.apps, + lists:keyreplace (Node, #node.node, + Data, new_node (Node, dead, L)); + + _false -> + Data + end. + + + + + +%%% add_apps_1 /3 +%%% +%%% add_apps_1 returns a list with the given application +%%% into the old list inserted. +%%% + +add_apps_1 (Apps, Node, Data) -> + case lists:keysearch (Node, #node.node, Data) of + {value, _Node_rec} -> + NewApps = parse_apps (Apps, []), + lists:keyreplace (Node, #node.node, + Data, new_node (Node, NewApps)); + + _false -> + Data + end. + + + +%%% remove_app_1 /3 +%%% +%%% remove_app_1 returns a list with the given application +%%% removed from the old list. +%%% + +remove_app_1 (App, Node, Data) -> + + case lists:keysearch (Node, #node.node, Data) of + {value, Node_rec} -> + L = Node_rec#node.apps, + L2 = lists:keydelete (App, #app.app, L), + lists:keyreplace(Node, #node.node, Data, new_node(Node,L2)); + + _false -> + Data + end. + + + +%%% configure /3 +%%% +%%% configure returns a win record after the window has been +%%% configured. +%%% + +configure (WPid, W, H) -> + X = gs:read (WPid, x), + Y = gs:read (WPid, y), + + gs:config (lb, [{width, W - ?BUTT_W - 25}, {height, H - 20}]), + gs:config (load, [{x, W - ?BUTT_W - 10}, {y, H - 120}]), + gs:config (clear, [{x, W - ?BUTT_W - 10}, {y, H - 80}]), + gs:config (close, [{x, W - ?BUTT_W - 10}, {y, H - 40}]), + + #win{pid = WPid, x = X, y = Y, width = W, height = H}. + + + + + +%%% load_app /2 +%%% +%%% load_app opens the application window by calling +%%% the appmon_a module. +%%% + +load_app ([], _Data) -> %% no application chosen + ok; + +load_app ([Index], Data) -> + App = gs:read (lb, {get, Index}), + + case string:substr (App, 1, 3) of + " " -> + AppName = list_to_atom (string:substr (App, 4)), + + case get_node (AppName, Index, Data) of + no_node -> + ok; + + NodeName -> + appmon_a:start (NodeName, AppName) + end; + + _ -> + ok + end. + + + +%%% update_status_1 /3 +%%% +%%% update_status_1 returns a list with the given +%%% node's status updated. +%%% + +update_status_1 (Node, Status, Data) -> + case lists:keysearch (Node, #node.node, Data) of + {value, Node_rec} -> + lists:keyreplace (Node, + #node.node, + Data, + new_node(Node,Status,Node_rec#node.apps)); + + _not_found -> + Data + end. + + + +%%% update /2 +%%% +%%% update updates the listbox with new data. +%%% + +update (_Data, win_closed) -> + true; + +update (Data, _Win) -> + gs:config (lb, clear), + lb_print (Data). + + + +%%% lb_print /1 +%%% +%%% lb_print prints the list into the listbox. +%%% + +lb_print ([]) -> + ok; + +lb_print ([#node{node = Node, status = Status, apps = Apps} | T]) -> + Str = io_lib:format ("~p (~p)", [Node, Status]), + gs:config (lb, {add, Str}), + + case Status of + alive -> + lb_print_apps (Apps); + + _dead -> + gs:config (lb, {add, ""}), + ok + end, + + lb_print (T). + + + +%%% lb_print_apps /1 +%%% +%%% lb_print_apps prints the applications into the listbox. +%%% + +lb_print_apps ([]) -> + ok; + +lb_print_apps ([#app{app = App} | T]) -> + Str = io_lib:format (" ~p", [App]), + gs:config (lb, {add, Str}), + lb_print_apps (T). + + + +%%% new_node /1, 2, 3 +%%% +%%% new_node returna a new node record constructed +%%% with the given data +%%% + +new_node (Node) -> + #node{node = Node}. + +new_node (Node, Apps) -> + #node{node = Node, apps = Apps}. + +new_node (Node, Status, Apps) -> + #node{node = Node, status = Status, apps = Apps}. + + + +%%% new_app /2 +%%% +%%% new_app returns a new application record +%%% constructed with the given data. +%%% + +new_app (App, Pid) -> + #app{app = App, pid = Pid}. + + + +%%% parse_apps /2 +%%% +%%% parse_apps returns a list of application records. +%%% + +parse_apps ([], [H | T]) -> + [H | lists:keysort (#app.app, T)]; + +parse_apps ([App | T], L) -> + Pid = element (1, App), + Name = element (2, App), + parse_apps (T, [new_app (Name, Pid) | L]). + + + +%%% get_node /3 +%%% +%%% get_node returns the node from the given list +%%% or else no_node if it doesn't exists. +%%% + +get_node (_App, _Index, []) -> + no_node; + +get_node (App, Index, [Node | T]) -> + Length = length (Node#node.apps) + 1, + + case Length < Index of + true -> + get_node (App, Index - Length, T); + + false -> + Node#node.node + end. + + + +%%% parse_data /2 +%%% +%%% parse_data returns a list with node records. +%%% + +parse_data (Data, []) -> + Data; + +parse_data (Data, [{Node, Status, Apps} | T]) -> + Apps_1 = parse_apps (Apps, []), + parse_data ([new_node (Node, Status, Apps_1) | Data], T). + + + diff --git a/lib/appmon/src/appmon_place.erl b/lib/appmon/src/appmon_place.erl new file mode 100644 index 0000000000..5a6ae6aa48 --- /dev/null +++ b/lib/appmon/src/appmon_place.erl @@ -0,0 +1,194 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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% +%%------------------------------------------------------------ +%% +%% Places a Digraph in a tree-like manner. The vertices in the digraph +%% is updated with x and y positions. The operation is not atomic. The +%% digraph may be cyclic but edges must then have been labeled primary +%% or secondary and the set of primary links must make up a non-cyclic +%% graph (a tree). +%% +%% +%% IMPLEMENTATION DETAIL +%% --------------------- +%% +%% The placement algorithm is straightforward, place the +%% nodes in the vertical plane (y-plane) and then place +%% nodes in the horisontal plane (x-plane). +%% +%% First all nodes are placed in the y (vertical) plane +%% by a standard traversing of the tree. We then place +%% the tree in the x (horisontal) plane. Each node is +%% placed in the middle of its children as far to the +%% left as possible, preferably at the left margin. Two +%% things can make a node not be placed at the left +%% margin and that is the case when a previous node has +%% been placed at the same vertical level as the node we +%% are trying to place (thus forcing a placement further +%% to the right), and the second case is when the middle +%% of the subtree of the node is not at the left margin +%% (which it only is when the subtree is empty). The +%% algorithm obviously depends on keeping track of the +%% rightmost positions at all depths, and this +%% information is also usefull when calculating the width +%% of the tree. +%% +%% +%% +%%------------------------------------------------------------ + + + +-module(appmon_place). + +-export([place/2]). + +-include("appmon_dg.hrl"). + + +-import(lists, [foreach/2, foldl/3]). + + +place(DG, Root) -> + case appmon_dg:get(data, DG, Root) of + false -> [0]; + _Other -> + placey(DG, Root, 1), + placex(DG, Root, []) + end. + + +%%------------------------------------------------------------ +%% +%% +%% Placing a graph in y plane +%% -------------------------- +%% +%% Place nodes in the graph in the y plane rather stupidly +%% + +placey(DG, V, Y) -> + appmon_dg:set(y, DG, V, Y), + Y1 = Y+1, + foreach(fun(C) -> placey(DG, C, Y1) end, appmon_dg:get(out, DG, V)). + + + + +%%------------------------------------------------------------ +%% +%% +%% Place a tree in the x plane +%% --------------------------- +%% +%% Place nodes in the tree in the x plane. The goal of the x +%% placement is to place all nodes as far to the left as possible +%% while maintaining a nice tree shape. +%% +%% To place a node we must first place its children, the +%% intention is to place the current node in the middle and above +%% its children. The calc_mid function will place the node in the +%% middle of its children. If the node should be placed further +%% to the right than the middle of its children, then its +%% children are moved DeltaX positions to be balanced under the +%% node. Thus at the end the node and its children form a nice +%% looking tree. +%% +%% The function also maintains the 'rightmost x on each level' +%% list LastX by putting its own position on top of the list +%% +%% + +placex(DG, V, LastX) -> + Ch = appmon_dg:get(out, DG, V), + ChLX = foldl(fun(C, Accu) -> placex(DG, C, Accu) end, + tll(LastX), + Ch), + + Width = appmon_dg:get(w, DG, V), + MyX = calc_mid(DG, Width, Ch), + DeltaX = calc_delta(MyX, hdd(LastX)+spacex()), + + appmon_dg:set(x, DG, V, MyX), + move(DG, V, [MyX+Width | ChLX], DeltaX). + + +%%------------------------------------------------------------ +%% +%% +%% Move a subtree DeltaX positions to the right +%% -------------------------------------------- +%% +%% Used when moving children to balance under an already placed +%% parent. Note that the correct LastX depends on the ordering of +%% the children which must be the same as when the children were +%% first placed. It must be ensured that hdd(NewLastX) is the +%% same as hdd(NewLastX)+DeltaX. If the order of children is +%% preserved then so is hdd(LastX). Another solution would be to +%% set hdd(LastX) from the parent +%% +%% Note the two base clauses, one for the no-children case and +%% one optimisation clause (unneccessary perhaps) for DeltaX==0 +%% + +move(_DG, _L, LastX, 0) -> LastX; +move(DG, V, LastX, DeltaX) -> move2(DG, V, LastX, DeltaX). + +move2(DG, V, LastX, DeltaX) -> + NewX = appmon_dg:get(x, DG, V)+DeltaX, + appmon_dg:set(x, DG, V, NewX), + ChLX = foldl(fun(C, LX) -> move2(DG, C, LX, DeltaX) end, + tll(LastX), + appmon_dg:get(out, DG, V)), + [max(NewX+appmon_dg:get(w, DG, V), hdd(LastX)) | ChLX]. + +max(A, B) when A>B -> A; +max(_, B) -> B. + +%%------------------------------------------------------------ +%% +%% +%% Calculate the middle position of the children +%% --------------------------------------------- +%% +%% Calculates the mid x position for a list of children. This +%% position is later compared to the position dictated by LastX +%% in calc_delta. + +calc_mid(_DG, _Width, []) -> 0; +calc_mid(DG, Width, ChList) -> + LeftMostX = appmon_dg:get(x, DG, hd(ChList)), + Z2 = lists:last(ChList), + RightMostX = appmon_dg:get(x, DG, Z2)+appmon_dg:get(w, DG, Z2), + trunc((LeftMostX+RightMostX)/2)-trunc(Width/2). + +calc_delta(Mid, Right) -> + if Right>Mid -> Right-Mid; + true -> 0 + end. + + + +%% Special head and tail +%% Handles empty list in a non-standard way +tll([]) -> []; +tll([_|T]) -> T. +hdd([]) -> 0; +hdd([H|_]) -> H. + +spacex() -> 20. % Should be macro?? diff --git a/lib/appmon/src/appmon_txt.erl b/lib/appmon/src/appmon_txt.erl new file mode 100644 index 0000000000..4e1785c53f --- /dev/null +++ b/lib/appmon/src/appmon_txt.erl @@ -0,0 +1,302 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 1996-2009. 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% +%%------------------------------------------------------------ +%% +%% Simple text viewer +%% +%%------------------------------------------------------------ + +-module(appmon_txt). +-export([start/0, start/1, print/1, fprint/1]). + +%% gen_server stuff +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2]). + +-define(LOADTXT, "Load file"). +-define(SAVETXT, "Save file"). +-define(SAVEASTXT, "Save as"). +-define(CLOSETXT, "Close"). +-define(HELPTXT, "Help"). + +%%------------------------------------------------------------ +%% +%% start/0 starts an open text viewer that can be filled with +%% whatever. +%% +%%------------------------------------------------------------ +start() -> + start([]). + +%%------------------------------------------------------------ +%% +%% start(ListOfOptions) starts an open text viewer with options +%% +%% Options can be +%% {file, FileName} - insert contents of file +%% locked - the text cannot be edited +%% {text, Text} - insert text at startup +%% +%%------------------------------------------------------------ +start(Opts) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []). + +%% Start a text viewer if necessary +print(Txt) -> + catch start(), + gen_server:call(?MODULE, {add_txt, Txt}), + ok. + +fprint(File) -> + catch start(), + gen_server:call(?MODULE, {add_file, File}), + ok. + + +%%------------------------------------------------------------ +%% gen server admin + +init(Opts) -> + process_flag(trap_exit, true), + setup_base_win(), + default_status(), + setup_opts(Opts), + {ok, []}. + +terminate(_Reason, _State) -> + ok. + +%%------------------------------------------------------------ +%% gen server stuff +handle_call({add_txt, Txt}, _From, State) -> + do_insert_text(Txt), + scroll_to_last_line(), + {reply, ok, State}; +handle_call({add_file, FileName}, _From, State) -> + do_load_file(FileName), + {reply, ok, State}; +handle_call(_Request, _From, State) -> + {reply, ok, State}. +handle_cast(_Request, State) -> + {noreply, State}. +handle_info({gs, _, click, _, [?CLOSETXT|_]}, State) -> + {stop, normal, State}; +handle_info({gs, _, click, _, [?LOADTXT|_]}, State) -> + ui_load(), + {noreply, State}; +handle_info({gs, _, configure, _, [W, H | _]}, State) -> + resize(W, H), + {noreply, State}; + +handle_info({gs, _, destroy, _, _}, State) -> + {stop, normal, State}; +handle_info(Request, State) -> + io:format("~p got info: ~p~n", [self(), Request]), + print_status("Not implemented"), + {noreply, State}. + + +%%------------------------------------------------------------ +%% Handle options + +setup_opts([Opt|Opts]) -> + setup_opt(Opt), + setup_opts(Opts); +setup_opts([]) -> ok. + +setup_opt(Opt) -> + case Opt of + {file, FileName} -> + do_load_file(FileName); + locked -> + do_lock(); + {text, Text} -> + do_insert_text(Text); + _Other -> + ok + end. + +do_load_file(FileName) -> + case catch i_load_file(FileName) of + ok -> + default_status(); + _Other -> + print_status(lists:append(["File not found: ", FileName])) + end. + +i_load_file(FileName) -> + {ok, Bin} = file:read_file(FileName), + L = binary_to_list(Bin), + i_do_clear(), + do_insert_text(L), + ok. + +ui_load() -> + Title = "Load file", + Files = get_file_list(), + case catch ui_list_dialog(Title, "File: ", Files) of + {ok, FileName} -> + do_load_file(FileName); + _Other -> + print_status("Load cancelled") + end. + +get_file_list() -> + case file:list_dir(".") of + {ok, FileList} -> lists:sort(FileList); + _Other -> [] + end. + +do_insert_text(Text) -> + gs:config(editor(), {insert, {'end', Text}}), + ok. + +%% Scrolls editor to show the last rows +scroll_to_last_line() -> + H = gs:read(editor(), size), + R = gs:read(editor(), height), + TopRow = H-R/15, + if TopRow > 0 -> gs:config(editor(), {vscrollpos, TopRow}); + true -> gs:config(editor(), {vscrollpos, 0}) + end, + ok. + +do_lock() -> + gs:config(editor(), {enable, false}). + +i_do_clear() -> + gs:config(editor(), clear). + +%%------------------------------------------------------------ +%% Graphical stuff + +label_h() -> 20. +menu_h() -> 29. + +setup_base_win() -> + H = 400, W=580, + LabelHeight=label_h(), MenuHeight=menu_h(), + + F = gs:start([{kernel,true}]), + set_winroot(F), + + Win = gs:create(window, F, [{width, W}, {height, H}, + {title,"APPMON: Process Information"}]), + + E = gs:create(editor, Win, [{x, 0}, {y, MenuHeight}, + {width, W}, + {height, H-MenuHeight-LabelHeight-1}, + {vscroll, right}]), + set_editor(E), + + L = gs:create(label, Win, [{x, 0}, {y, H-LabelHeight}, + {height,LabelHeight }, {width, W}, + {align, w}]), + set_status(L), + print_status("Loading"), + + gs:config(Win, {map, true}), + + MB = gs:create(menubar, Win, []), + + FMB = gs:create(menubutton, MB, [{label, {text, "File"}}]), + FM = gs:create(menu, FMB, []), + gs:create(menuitem, FM, [{label, {text, ?CLOSETXT}}]), + + gs:config(Win, {configure, true}), + ok. + +resize(W, H) -> + gs:config(editor(), [{width, W}, {height, H-label_h()-menu_h()}]), + gs:config(status(), [{y, H-label_h()}, {width, W}]), + ok. + +%%------------------------------------------------------------ +%% ui_list_dialog( +%% +%% Traditional dialog with a list box and a selection field that +%% is updated from the list box. +%% +%% Returns {ok, String} if successful and something else if not +%% +%% Title - the name of the window +%% LeadText - the lead text on the selection field +%% List - a list of items that will be displayed in the list box +%% +%%------------------------------------------------------------ + +ui_list_dialog(Title, LeadText, TxtList) -> + W = 200, H = 300, + + Win = gs:create(window, winroot(), [{title, Title}, + {width, W},{height, H}]), + Ok = gs:create(button, Win, [{x, 10}, {y,10}, + {width, 50}, {height, 20}, + {label, {text, "Ok"}}]), + Cn = gs:create(button, Win, [{x, 70}, {y,10}, + {width, 50}, {height, 20}, + {label, {text, "Cancel"}}]), + + gs:create(label, Win, [{x, 10}, {y, 50}, + {width, 60}, {height, 20}, + {label, {text, LeadText}}]), + Box = gs:create(entry, Win, [{x, 10}, {y, 70}, + {width, 160}, {height, 20}, + {keypress, true}]), + List = gs:create(listbox, Win, [{x, 10}, {y, 100}, {width, 180}, + {height, 190}, + {items, TxtList}, {vscroll, right}, + {hscroll, false}, {click, true}, + {doubleclick, true}, + {keypress, true}]), + gs:config(Win, {map, true}), + + RetVal = ui_load_loop(Box, List, Ok, Cn), + + gs:destroy(Win), + + RetVal. + +ui_load_loop(Box, List, Ok, Cn) -> + receive + {gs, Box, keypress, _, ['Return'|_]} -> + {ok, gs:read(Box, text)}; + {gs, Box, keypress, _, _} -> + ui_load_loop(Box, List, Ok, Cn); + {gs, Ok, click, _, _} -> + {ok, gs:read(Box, text)}; + {gs, List, doubleclick, _, [_Idx, Txt|_]} -> + {ok, Txt}; + {gs, List, click, _, [_Idx, Txt|_]} -> + gs:config(Box, {text, Txt}), + ui_load_loop(Box, List, Ok, Cn); + _Other -> + something_else + end. + +%% The status row at the bottom of the window +set_status(Id) -> put(status_row, Id). +status() -> get(status_row). +print_status(Msg) -> gs:config(get(status_row), {label, {text, Msg}}). +default_status() -> print_status("Done"). + +set_editor(X) -> put(editor, X). +editor() -> get(editor). + +winroot() -> get(winroot). +set_winroot(X) -> put(winroot, X). diff --git a/lib/appmon/src/appmon_web.erl b/lib/appmon/src/appmon_web.erl new file mode 100644 index 0000000000..e8a8422a80 --- /dev/null +++ b/lib/appmon/src/appmon_web.erl @@ -0,0 +1,1037 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2001-2009. 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% +%% + +%%%--------------------------------------------------------------------- +%%% File : webappmon.erl +%%% Author : Martin G. +%%% Purpose : Frontend to the webbased version of appmon. +%%% Created : 24 Apr 2001 by Martin G. +%%%--------------------------------------------------------------------- + +-module(appmon_web). + +%% The functions that the user can call to interact with the genserver +-export([init/1,handle_call/3,handle_cast/2,handle_info/2]). +-export([terminate/2,code_change/3]). + +-export([node_info/2,application_info/2,application_env/2]). +-export([proc_info/2,trace/2]). +-export([start/0,stop/0,start_link/0]). + +%% Export the function that returns the configuration data needed by +%% webtool +-export([configData/0]). + + +%% The following directive caters for (significantly) faster native +%% code compilation of one function in this file by the HiPE compiler +%% on register-poor architectures like the x86. +-compile([{hipe,[{regalloc,graph_color}]}]). + +-behaviour(gen_server). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Start the genserver % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +start_link()-> + gen_server:start_link({local,webappmon_server},appmon_web,[],[]). +start()-> + gen_server:start({local,webappmon_server},appmon_web,[],[]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Stop the genserver % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +stop()-> + gen_server:call(webappmon_server,stop,1000). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get the page that shows the nodes and the apps on the sel node % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +node_info(Env,Input)-> + gen_server:call(webappmon_server,{node_data,Env,Input}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get the application process tree % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +application_info(Env,Input)-> + gen_server:call(webappmon_server,{app_data,Env,Input}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get the page that shows the data about the process % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +proc_info(Env,Input)-> + gen_server:call(webappmon_server,{proc_data,Env,Input}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Get the spec on the app % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +application_env(Env,Input)-> + gen_server:call(webappmon_server,{app_env,Env,Input}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Toggle the trace flag for the selected process % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +trace(Env,Input)-> + gen_server:call(webappmon_server,{trace,Env,Input}). + +configData()-> + {appmon,[{web_data,{"WebAppmon","/appmon/main_frame.html"}}, + {alias,{"/appmon",code:priv_dir(appmon)}}, + {alias,{erl_alias,"/erl",[appmon_web]}}, + {start,{child,{backend,{process_info,start_link,[]}, + permanent,100,worker,[process_info]}}}, + {start,{child,{{local,webappmon_server}, + {appmon_web,start_link,[]}, + permanent,100,worker,[appmon_web]}}} + ]}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% Callback functions for the genserver % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +init(_Arg)-> + {ok,[]}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Create the different pages % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +handle_call({node_data,_Env,Input},_From,State)-> + {reply,app_and_node_sel_page(Input),State}; + +handle_call({app_data,_Env,Input},_From,State)-> + {reply,process_tree_page(Input),State}; + +handle_call({proc_data,_Env,Input},_From,State)-> + {reply,process_specifickation_page(Input),State}; + +handle_call({app_env,_Env,Input},_From,State)-> + {reply,application_specifickation_page(Input),State}; + +handle_call({trace,_Env,Input},_From,State)-> + {reply,toggle_trace(Input),State}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Shutdown the genserver % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +terminate(_,_State)-> + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Callback function currently not used ... % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +handle_cast(_,State)-> + {noreply,State}. + +handle_info(_,State)-> + {noreply,State}. + +code_change(_OldVsn,State,_Extra)-> + {ok,State}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% %% +%% Private functions to create the part of the sides that is common %% +%% to all sides %% +%% %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Create the Header for the page If we now the mimetype use that type%% +%% otherwise use text %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +header() -> + header("text/html"). +header(MimeType) -> + "Content-type: " ++ MimeType ++ "\r\n\r\n". + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Create the Htmlheader sett the title of the side to nothing if %% +%% we dont know the name of the side %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +html_header()-> + html_header(""). + +html_header(Part) -> + "\n" ++ + "\n" ++ + "Appmon " ++ Part ++ "\n" ++ + "\n". + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Close the Html tag and if neccessay add some clean upp %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +html_end()-> + "". + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% %% +%% The functions that creates the whole pages by collecting %% +%% the necessary data %% +%% %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Returns the page where the user see's which nodes and apps that %% +%% are availible for monitoring %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +app_and_node_sel_page(Input)-> + [header(), + html_header(), + node_body(httpd:parse_query(Input)), + html_end()]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Returns the process tree for the application whose name is %% +%% the first value in the Inputs key/value list %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +process_tree_page(Input)-> + [header(), + html_header(), + application_javascript(httpd:parse_query(Input)), + application_body(httpd:parse_query(Input)), + html_end()]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Send trace on or off to the process thats pid is the third arg of %% +%% the inputs key/val list. Then it returns the process tree for the %% +%% the application that is the first key/val pair of input %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +toggle_trace(Input)-> + send_trace(httpd:parse_query(Input)), + [header(), + html_header(), + application_javascript(httpd:parse_query(Input)), + application_body(httpd:parse_query(Input)), + html_end()]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Creates the page that shows all information about the process that %% +%% that is the first arg of th input key/val pairs %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +process_specifickation_page(Input)-> + [header(), + html_header(), + process_body(httpd:parse_query(Input)), + html_end()]. + +application_specifickation_page(Input)-> + [header(), + html_header(), + application_env_body(httpd:parse_query(Input)), + html_end()]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% The Private functions that do the job %% +%% To build the the page that shows the applications %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Build the body of the side that shows the node name and %% +%% the application list %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +node_body([{"node",Node}|_Rest])-> + case process_info:is_node(Node) of + {true,Controlled_node,Name} -> + "" ++ + node_selections_javascripts() ++ + node_selection(Controlled_node) ++ + node_title() ++ + application_tree(Controlled_node,Name) ++ + ""; + + {false,Server_node,Name} -> + "" ++ + node_selections_javascripts() ++ + node_selection(Server_node) ++ + node_title() ++ + application_tree(Server_node,Name) ++ + "" + end; + +node_body(_Whatever)-> + node_body([{atom_to_list(node),atom_to_list(node())}]). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Returns the javascript that sets a new node to monitor %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +node_selections_javascripts()-> + "". + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Insert the html code that shows the combobox where the user can %% +%% select another node to monitor %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +node_selection(Node)-> + "
\n + \n + \n +
\n + \n +
\n +
". + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Add the node we are working with in the beginning of the list and %% +%% remove it from other places so its always the first in the listbox %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +order_nodes(Node,Node_list)-> + [Node|lists:delete(Node,Node_list)]. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% Take the list of nodes and make it to a list of options to the %% +%% the combobox %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +print_nodes([])-> + []; +print_nodes([Node|Rest])-> + "