aboutsummaryrefslogtreecommitdiffstats
path: root/lib/pman/src/pman_main.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pman/src/pman_main.erl')
-rw-r--r--lib/pman/src/pman_main.erl787
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.