diff options
Diffstat (limited to 'lib/pman/src/pman_main.erl')
-rw-r--r-- | lib/pman/src/pman_main.erl | 787 |
1 files changed, 787 insertions, 0 deletions
diff --git a/lib/pman/src/pman_main.erl b/lib/pman/src/pman_main.erl new file mode 100644 index 0000000000..b68da1d2c3 --- /dev/null +++ b/lib/pman/src/pman_main.erl @@ -0,0 +1,787 @@ +%% +%% %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% +%% +-module(pman_main). + +%% Main process and window + +-export([init/2]). + +-record(state, {win, % GS top window + frame, % GS top frame + grid, % GS process info grid + + size, % int() No. of displayed procs + w, % int() Window width + h, % int() Window height + + hide_system=false, % bool() Auto-hide system procs + hide_new=false, % bool() Auto-hide new processes + + hide_modules, % ordset() Excluded modules + + hide_all=[], % [{node(), bool()}] Hide all + hide_pids=[], % [{node(), Ordset}] Processes + % explicitly to hide, per node + show_pids=[], % [{node(), Ordset}] Processes + % explicitly to show, per node + + shown_pids=[], % [{node(), Ordset}] Processes + % actually shown, per node + + node, % node() Current node + nodes=[], % [node()] All known nodes + + focus=1, % int() Grid line with focus + focus_pid=undefined, % pid() | undefined Proc in focus + + noshell, % bool() Noshell mode on + + options}). % term() Trace options settings + + +-include("pman_win.hrl"). + +-define(REFRESH_TIME,5000). + +-define(REQUIRES_FOCUS, % List of menus that should + ['Trace Process', % be disabled if no process + 'Kill', % is in focus + 'Hide Selected Process', + 'Module']). + +%%--Process init and loop----------------------------------------------- + +init(PidCaller, OSModuleExcluded) -> + process_flag(trap_exit, true), + + %% Monitor all nodes in a distributed system + case is_alive() of + + %% We have a distributed system + true -> net_kernel:monitor_nodes(true); + + %% No distribution + false -> ignore + end, + Nodes = [node()|nodes()], + + %% Create the main window + %% For some extremely strange reason, the frame must be resized + %% or the grid won't be visible... + GridSize = length(processes()) + 61, + {Window, Grid, Frame, Visible, W, H} = + pman_win:pman_window(GridSize, OSModuleExcluded, Nodes), + gse:resize(Frame, ?WIN_WIDTH, ?WIN_HEIGHT-?MENU_HEIGHT), + + Noshell = case pman_shell:find_shell() of + noshell -> true; + _ -> false + end, + + State1 = #state{win=Window, frame=Frame, grid=Grid, + size=Visible, + w=W, h=H, + hide_modules=OSModuleExcluded, + node=node(), + noshell=Noshell}, + + State2 = lists:foldl(fun(Node, State) -> add_node(Node, State) end, + State1, + Nodes), + + State3 = refresh(State2), + + %% Notify caller that the process appears + %% to have been started. + PidCaller ! {initialization_complete, self()}, + + %% Initiate a 'catch all' trace pattern so call tracing works + erlang:trace_pattern({'_', '_', '_'}, true, [local]), + + %% Read default options file + Options = restore_options(State3), + + loop(State3#state{options=Options}). + +add_node(Node, State) -> + pman_win:add_menu(node, [Node], "Show"), + State#state{hide_all=nl_update(Node, false, State#state.hide_all), + hide_pids=nl_update(Node, [], State#state.hide_pids), + show_pids=nl_update(Node, [], State#state.show_pids), + shown_pids=nl_update(Node, [], State#state.shown_pids), + nodes=[Node|State#state.nodes]}. + +%% Restore saved options from default file +restore_options(State)-> + File = options_file(), + case pman_options:read_from_file(File) of + {ok, Options} -> + Options; + {error, ReasonStr, DefOptions} -> + Parent = State#state.win, + Msg = io_lib:format( + "Problems reading default option file~n~s:~n~s", + [File, ReasonStr]), + tool_utils:notify(Parent, Msg), + DefOptions + end. + +options_file() -> + {ok, [[Home]]} = init:get_argument(home), + filename:join([Home, ".erlang_tools", "pman.opts"]). + +loop(State) -> + receive + {nodeup, Node} -> + case nl_exists(Node, State#state.hide_all) of + true -> + pman_win:add_menu(node, [Node], "Show"), + loop(State#state{nodes=[Node|State#state.nodes]}); + false -> + loop(add_node(Node, State)) + end; + + {nodedown, Node} -> + pman_win:remove_menu([Node]), + + Msg = io_lib:format("Node~n~p~ndown.", [Node]), + spawn_link(tool_utils, notify, [State#state.win, Msg]), + + %% We remove Node from the list of nodes but not from + %% the other lists of State, in case Node reappears later + Nodes = lists:delete(Node, State#state.nodes), + State2 = State#state{nodes=Nodes}, + + %% If it was the shown node that went down, + %% change overview to this node + if + Node==State#state.node -> + State3 = execute_cmd({node,node()}, State2, [], []), + loop(State3); + true -> + loop(State2) + end; + + %% Ignore EXIT signals from help processes + {'EXIT', _Pid, _Reason} -> + loop(State); + + %% GS events + {gs, _Obj, _Event, _Data, _Args} = Cmd -> + case gs_cmd(Cmd, State) of + stop -> + exit(topquit); + State2 -> + loop(State2) + end + + after ?REFRESH_TIME -> + State2 = refresh(State), + loop(State2) + end. + +%% gs_cmd(Event, State) -> stop | State' +gs_cmd(Event, State) -> + case Event of + + %% --- Window manager commands --- + + %% Window is moved or resized + {gs, _, configure, _Data, Args} -> + configure(Args, State); + + %% Window closed, stop Pman + {gs, _, destroy, _, _} -> + stop; + + %% --- Dynamic commands --- + + %% Click in any object where the GS Data field is a 2-tuple + {gs, _, click, Data, Args} when is_tuple(Data), size(Data)==2 -> + execute_cmd(Data, State, [], Args); + + %% Single click in the grid sets focus to selected process + {gs, _, click, {pidfunc,_,_}, [_,Row|_]} when is_integer(Row) -> + focus(Row, State); + + %% Double click in the grid starts tracing of selected process + {gs, _, doubleclick, {pidfunc,_,_}, [_Col,Row| _]} when is_integer(Row) -> + execute_cmd('Trace Process', State, [], []); + + %% Click in named GS objects + {gs, Cmd, click, Data, Args} when is_atom(Cmd); + is_atom(element(1, Cmd)) -> + execute_cmd(Cmd, State, Data, Args); + + %% --- Keyboard accelerator commands --- + + %% Move focus up and down + {gs, _, keypress, [], ['Up',_,0,0]} -> + execute_cmd(focus_previous, State, [], []); + {gs, _, keypress, [], ['Down',_,0,0]} -> + execute_cmd(focus_next, State, [], []); + + %% Other keyboard shortcuts + {gs, _, keypress, [], ['Return',_,0,0]} -> + execute_cmd('Trace Process', State, [], []); + {gs, _, keypress, [], [Key,_,0,1]} -> + execute_cmd(shortcut(Key), State, [], []); + + %% Ignore all other GS events + _Other -> + State + end. + +%% Keyboard shortcuts + +%% File menu +shortcut(o) -> 'Default Options'; +shortcut(e) -> 'Exit'; +shortcut(z) -> 'Exit'; + +%% View menu +shortcut(i) -> 'Hide All'; +shortcut(u) -> 'Hide Modules'; +shortcut(d) -> 'Hide Selected Process'; +shortcut(m) -> 'Module'; +shortcut(r) -> 'Refresh'; + +%% Trace menu +shortcut(k) -> 'Kill'; +shortcut(t) -> 'Trace Process'; +shortcut(s) -> 'Trace Shell'; + +%% Help menu +shortcut(h) -> 'Help'; + +%% Keyboard command only +shortcut(l) -> 'All Links'; + +%% Process grid traversal +shortcut(p) -> focus_previous; +shortcut(n) -> focus_next; +shortcut(_) -> dummy. + +%% configure([W,H,X,Y|_], State) -> State' +%% Window has been moved or resized +configure([W,H|_], State) -> + if + W==State#state.w, H==State#state.h -> + ignore; + + true -> + gse:resize(State#state.frame, W, H-?MENU_HEIGHT), + + Grid = State#state.grid, + case abs(W - gs:read(Grid,width) - 6) of + 0 -> + ok; %% Avoid refreshing width if possible + _Anything -> + Cols = pman_win:calc_columnwidths(W-6), + gs:config(Grid, Cols) + end, + pman_win:configwin(Grid, W, H) + end, + State. + +%% focus(Row, State) -> State' +%% Row = int() Grid row +%% User has selected a row in the grid. +%% Row==1 means header row. +focus(Row, State) -> + + Pid = case get_pid_in_focus(Row, State#state.grid) of + {true, {pidfunc,Pid0,_}} -> + pman_win:change_colour(State#state.grid, + State#state.focus, Row), + enable_pid_actions(), + Pid0; + false -> + disable_pid_actions(), + undefined + end, + + State#state{focus=Row, focus_pid=Pid}. + +%% get_pid_in_focus(Row, Grid) -> {true, Data} | false +%% Data = {pidfunc, Pid, Func} +%% Func = {Mod,Name,Arity} | term() +%% Return the data associated with the process in focus if there is one, +get_pid_in_focus(1, _Grid) -> + false; +get_pid_in_focus(Row, Grid) -> + case gs:read(Grid, {obj_at_row,Row}) of + undefined -> false; + GridLine -> + Data = gs:read(GridLine, data), + {true, Data} + end. + +%% execute_cmd(Cmd, State, Data, Args) -> stop | State' + +%% Checkbutton "Hide System Processes" +execute_cmd('Hide System', State, _Data, Args) -> + [_Text, _Group, Bool|_Rest] = Args, + State2 = State#state{hide_system=Bool}, + refresh(State2); + +%% Checkbutton "Auto-Hide New" +execute_cmd('Auto Hide New', State, _Data, Args ) -> + [_Text, _Group, Bool|_Rest] = Args, + refresh(State#state{hide_new=Bool}); + +%% File->Options... +execute_cmd('Default Options', State, _Data, _Args) -> + OldOptions = State#state.options, + NewOptions = pman_options:dialog(State#state.win, + "Default Trace Options", + OldOptions), + case NewOptions of + {error, _Reason} -> + State; + Options -> + State#state{options=Options} + end; + +%% File->Save Options +%% Save the set default options to the user's option file +execute_cmd('Save Options', State, _Data, _Args)-> + Options = State#state.options, + File = options_file(), + Parent = State#state.win, + + case pman_options:save_to_file(Options, File) of + ok -> + tool_utils:notify(Parent, "Options saved to\n" ++ File); + {error, ReasonStr} -> + Msg = io_lib:format("Could not save options to~n~s:~n~s", + [File, ReasonStr]), + tool_utils:notify(Parent, Msg) + end, + State; + +%% File->Exit +%% Exit the application +execute_cmd('Exit', _State, _Data, _Args) -> + stop; + +%% View->Hide All Processes +execute_cmd('Hide All', State, _Data, _Args) -> + Node = State#state.node, + HideAll = nl_update(Node, true, State#state.hide_all), + ShowPids = nl_del_all(State#state.node, State#state.show_pids), + State2 = State#state{hide_all=HideAll, show_pids=ShowPids}, + refresh(State2, true); + +%% View->Hide modules... +%% Opens a dialog where the user can select from a list of +%% the loaded modules. +%% The selected module is added to the list of hidden modules. +execute_cmd('Hide Modules', State, _Data, _Args) -> + + %% Get all loaded modules that are not already hidden + AllModules = lists:map(fun({Module, _File}) -> Module end, + code:all_loaded()), + ModulesSet = ordsets:subtract(ordsets:from_list(AllModules), + State#state.hide_modules), + + %% Let the user select which of the loaded modules to exclude from + %% the process overview + Title = "Module selection", + case pman_tool:select(State#state.win, Title, ModulesSet) of + Modules when is_list(Modules) -> + HideModules = ordsets:union(State#state.hide_modules, + ordsets:from_list(Modules)), + refresh(State#state{hide_modules=HideModules}); + cancelled -> State + end; + +%% View->Hide Selected Process +%% The process in focus should explicitly be hidden +execute_cmd('Hide Selected Process', State, _Data, _Args) -> + case State#state.focus_pid of + undefined -> State; + Pid -> + Node = State#state.node, + HidePids = nl_add(Node, Pid, State#state.hide_pids), + ShowPids = nl_del(Node, Pid, State#state.show_pids), + refresh(State#state{hide_pids=HidePids, show_pids=ShowPids}) + end; + +%% View->Module Info... +%% Open window with module information. +execute_cmd('Module', State, _Data, _Args) -> + case get_pid_in_focus(State#state.focus, State#state.grid) of + {true, {pidfunc, _Pid, {Module,_Name,_Arity}}} -> + pman_module_info:start(Module); + _ -> % false | {true, {pidfunc, Pid, Other}} + ignore + end, + State; + +%% View->Refresh +%% Refresh the main window. +%% (Called automatically every ?REFRESH_TIME millisecond) +execute_cmd('Refresh', State, _Data, _Args) -> + refresh(State); + +%% View->Show All Processes +%% Makes all processes visible except system processes and new +%% processes, if those buttons are checked. +%% Note: Also un-hides all hidden modules! +execute_cmd('Show All', State, _Data, _Args) -> + Node = State#state.node, + HideAll = nl_update(Node, false, State#state.hide_all), + HidePids = nl_del_all(State#state.node, State#state.hide_pids), + ShowPids = nl_del_all(State#state.node, State#state.show_pids), + State2 = State#state{hide_modules=ordsets:new(), hide_all=HideAll, + hide_pids=HidePids, show_pids=ShowPids}, + refresh(State2, true); + +%% View->Show Processes... +%% Open a list of all hidden processes, if the user selects one this +%% process should explicitly be shown +execute_cmd('Show Selected', State, _Data, _Args) -> + Node = State#state.node, + + All = pman_process:r_processes(Node), + Hidden = case nl_lookup(Node, State#state.hide_all) of + true -> + All; + false -> + Shown = nl_lookup(Node, State#state.shown_pids), + ordsets:subtract(All, Shown) + end, + + %% Selection window + Title = "Select Processes to Show", + Tuples = + lists:map(fun(Pid) -> + {M,F,A} = pman_process:function_info(Pid), + Str = case pman_process:get_name(Pid) of + " " -> + io_lib:format("~p:~p/~p", + [M, F, A]); + Name -> + io_lib:format("[~p] ~p:~p/~p", + [Name, M, F, A]) + end, + {Pid, Str} + end, + Hidden), + case pman_tool:select(State#state.win, Title, Tuples) of + Pids when is_list(Pids) -> + HidePids = nl_del(Node, Pids, State#state.hide_pids), + ShowPids = nl_add(Node, Pids, State#state.show_pids), + refresh(State#state{hide_pids=HidePids,show_pids=ShowPids}); + cancelled -> State + end; + +%% Trace->Kill +execute_cmd('Kill', State, _Data, _Args) -> + case State#state.focus_pid of + Pid when is_pid(Pid) -> + exit(Pid, kill); + undefined -> + ignore + end, + State; + +%% Trace->Selected Process +execute_cmd('Trace Process', State, _Data, _Args) -> + case State#state.focus_pid of + Pid when is_pid(Pid) -> + pman_shell:start({Pid,self()}, State#state.options); + undefined -> + ignore + end, + State; + +%% Trace->Shell Process +execute_cmd('Trace Shell', State, _Data, _Args) -> + case pman_shell:find_shell() of + noshell -> + State; + Shell -> + pman_shell:start({{shell,Shell},self()}, + State#state.options), + State#state{noshell=false} + end; + +%% Nodes->Show <Node> +%% Change shown node +execute_cmd({node,Node}, State, _Data, _Args) -> + gse:config(State#state.win, + [{title,lists:concat(["Pman: Overview on ", Node])}]), + gse:disable(Node), + catch gse:enable(State#state.node), % Menu may not exist any more + refresh(State#state{node=Node}, true); + +%% Help->Help +execute_cmd('Help', State, _Data, _Args) -> + Win = State#state.win, + HelpFile = + filename:join([code:lib_dir(pman),"doc","html","index.html"]), + tool_utils:open_help(Win, HelpFile), + State; + +%% Keyboard shortcut Ctrl-l +execute_cmd('All Links', State, _Data, _Args) -> + case State#state.focus_pid of + Pid when is_pid(Pid) -> + case process_info(Pid, links) of + {links, Pids} -> + pman_shell:start_list(Pids, self(), + State#state.options); + undefined -> + ignore + end; + undefined -> ignore + end, + State; + +%% Keyboard shortcuts for process grid traversal +execute_cmd(focus_previous, State, _Data, _Args) -> + focus(previous_row(State), State); +execute_cmd(focus_next, State, _Data, _Args) -> + focus(next_row(State), State); + +%% Keyboard combinations that are not shortcuts +execute_cmd(dummy, State, _Data, _Args) -> + State. + +%% Convenience functions for disabling/enabling menu items that require +%% that a process is selected. +disable_pid_actions() -> + lists:foreach(fun(X) -> gse:disable(X) end, ?REQUIRES_FOCUS). + +enable_pid_actions() -> + lists:foreach(fun(X) -> gse:enable(X) end, ?REQUIRES_FOCUS). + +%% refresh(State) -> State' +%% refresh(State, ForceP) -> State' +%% Refreshes the main window. +refresh(State) -> + refresh(State, false). +refresh(#state{node=Node} = State, ForceP) -> + + %% Update shown processes + + %% First, get an ordset of all processes running at the current node + All = pman_process:r_processes(Node), + + Shown = nl_lookup(Node, State#state.shown_pids), + ExpShown = nl_lookup(Node, State#state.show_pids), + + {Show, State2} = + case nl_lookup(Node, State#state.hide_all) of + + %% If the user has selected "Hide All Processes", only + %% explicitly selected processes which still exist should + %% be shown + true -> + {ordsets:intersection(ExpShown, All), State}; + + false -> + %% Compute which processes should be hidden according + %% to the flags/menu items selected + Hidden = hidden_pids(All, State), + + NotHidden = ordsets:subtract(All, Hidden), + + Show0 = case State#state.hide_new of + %% If the user has selected "Auto-Hide New", + %% then only those processes in NotHidden + %% which are already shown, should be shown, + %% together with explicitly selected + %% processes which still exist + true -> + ordsets:union( + ordsets:intersection(NotHidden,Shown), + ordsets:intersection(ExpShown, All)); + + %% Otherwise, show all processes in + %% NotHidden, together with explicitly + %% selected processes which still exist + false -> + ordsets:union( + NotHidden, + ordsets:intersection(ExpShown, All)) + end, + + ShownPids = nl_update(Node, Show0, + State#state.shown_pids), + {Show0, State#state{shown_pids=ShownPids}} + end, + + NoOfHidden = length(All) - length(Show), + + if + Show==Shown, not ForceP -> + pman_win:update(NoOfHidden), + State; + + true -> + ShowInfo = display_info(Show), + pman_win:update(State#state.grid, ShowInfo, NoOfHidden), + + %% Set the focus appropriately + State3 = case State2#state.focus_pid of + undefined -> + disable_pid_actions(), + State2; + Pid -> + Row = get_row(Pid, Show), + focus(Row, State2) + end, + + trace_shell_possible(State3), + + Size = length(Show), + case Size of + 1 -> gse:disable('Hide All'); + _ -> gse:enable('Hide All') + end, + + State3#state{size=Size} + end. + +%% hidden_pids(All, State) -> Hidden +hidden_pids(All, State) -> + + %% Processes hidden because they are system processes + HideSys = case State#state.hide_system of + true -> + lists:filter( + fun(Pid) -> + pman_process:is_system_process(Pid) + end, + All); + false -> + [] + end, + + %% Process hidden because they are executing code in a hidden module + Mods = State#state.hide_modules, + HideMod = + lists:filter(fun(Pid) -> + pman_process:is_hidden_by_module(Pid, Mods) + end, + All), + + %% Explicitly hidden processes + HideExp = nl_lookup(State#state.node, State#state.hide_pids), + + %% All hidden processes + ordsets:union([HideSys, HideMod, HideExp]). + +display_info(Pids) -> + lists:map(fun(Pid) -> + Func = pman_process:function_info(Pid), + Name = pman_process:get_name(Pid), + Msgs = pman_process:msg(Pid), + Reds = pman_process:reds(Pid), + Size = pman_process:psize(Pid), + {Pid, Func, Name, Msgs, Reds, Size} + end, + Pids). + +get_row(Pid, List) -> + get_row(Pid, List, length(List)+1). + +get_row(Pid, [Pid | _], Row) -> + Row; +get_row(Pid, [_ | T], Row) -> + get_row(Pid, T, Row-1); +get_row(_Pid, [], _Row) -> + 1. + +next_row(#state{size=Size, focus=Row}) -> + check_row(Row+1, Size). + +previous_row(#state{size=Size, focus=Row}) -> + check_row(Row-1, Size). + +check_row(1, Size) -> + Size+1; +check_row(Row, Size) when Row==Size+2 -> + 2; +check_row(Row, _Size) -> + Row. + +%% Check if node is running in noshell mode and if so disable the +%% 'Trace Shell' menu option. +trace_shell_possible(#state{noshell=true}) -> + gse:disable('Trace Shell'); +trace_shell_possible(_) -> + ok. + +%% -- Functions for manipulating {Node, Data} lists -- + +%% nl_add(Node, Elem|Elems, NList) -> NList' +nl_add(Node, Elems, [{Node, Ordset} | T]) when is_list(Elems) -> + [{Node, ordsets:union(Elems, Ordset)} | T]; +nl_add(Node, Elem, [{Node, Ordset} | T]) -> + [{Node, ordsets:add_element(Elem, Ordset)} | T]; +nl_add(Node, Elem, [H | T]) -> + [H | nl_add(Node, Elem, T)]; +nl_add(Node, Elems, []) when is_list(Elems) -> + [{Node, Elems}]; +nl_add(Node, Elem, []) -> + [{Node, ordsets:add_element(Elem, ordsets:new())}]. + +%% nl_del(Node, Elem|Elems, NList) -> NList' +nl_del(Node, Elems, [{Node, Ordset} | T]) when is_list(Elems) -> + [{Node, ordsets:subtract(Ordset, Elems)} | T]; +nl_del(Node, Elem, [{Node, Ordset} | T]) -> + [{Node, ordsets:del_element(Elem, Ordset)} | T]; +nl_del(Node, Elem, [H | T]) -> + [H | nl_del(Node, Elem, T)]; +nl_del(_Node, _Elem, []) -> + []. + +%% nl_del_all(Node, NList) -> NList' +nl_del_all(Node, [{Node, _Ordset} | T]) -> + [{Node, ordsets:new()} | T]; +nl_del_all(Node, [H | T]) -> + [H | nl_del_all(Node, T)]; +nl_del_all(_Node, []) -> + []. + +%% nl_update(Node, Val, NList) -> NList' +nl_update(Node, Val, [{Node, _OldVal} | T]) -> + [{Node, Val} | T]; +nl_update(Node, Val, [H | T]) -> + [H | nl_update(Node, Val, T)]; +nl_update(Node, Val, []) -> + [{Node, Val}]. + +%% nl_lookup(Node, NList) -> Val +nl_lookup(Node, NList) -> + {value, {_Node,Val}} = lists:keysearch(Node, 1, NList), + Val. + +%% nl_exists(Node, NList) -> bool() +nl_exists(Node, NList) -> + case lists:keysearch(Node, 1, NList) of + {value, _Val} -> + true; + false -> + false + end. |