%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1996-2012. 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). -compile([{nowarn_deprecated_function,{gs,config,2}}, {nowarn_deprecated_function,{gs,create,3}}, {nowarn_deprecated_function,{gs,destroy,1}}, {nowarn_deprecated_function,{gs,read,2}}, {nowarn_deprecated_function,{gs,start,1}}]). %%%--------------------------------------------------------------------- %%% 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 = erlang: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). %% 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}}).