%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-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(pman_main).
-compile([{nowarn_deprecated_function,{gs,config,2}},
{nowarn_deprecated_function,{gs,read,2}}]).
%% 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.