diff options
author | Erlang/OTP <otp@erlang.org> | 2009-11-20 14:54:40 +0000 |
---|---|---|
committer | Erlang/OTP <otp@erlang.org> | 2009-11-20 14:54:40 +0000 |
commit | 84adefa331c4159d432d22840663c38f155cd4c1 (patch) | |
tree | bff9a9c66adda4df2106dfd0e5c053ab182a12bd /lib/appmon/src/appmon.erl | |
download | otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.gz otp-84adefa331c4159d432d22840663c38f155cd4c1.tar.bz2 otp-84adefa331c4159d432d22840663c38f155cd4c1.zip |
The R13B03 release.OTP_R13B03
Diffstat (limited to 'lib/appmon/src/appmon.erl')
-rw-r--r-- | lib/appmon/src/appmon.erl | 1079 |
1 files changed, 1079 insertions, 0 deletions
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, X<R, Y>T, Y<B -> + 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}}). |