%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2009. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%
%% ---------------------------------------------------------------
%% Purpose: Create a trace window with process
%% information or a help window with information
%% about pman.
%%
%% ---------------------------------------------------------------
-module(pman_shell).
-compile([{nowarn_deprecated_function,{gs,config,2}},
{nowarn_deprecated_function,{gs,destroy,1}},
{nowarn_deprecated_function,{gs,start,0}},
{nowarn_deprecated_function,{gs,start,1}}]).
%% ---------------------------------------------------------------
%% The user interface exports
%% ---------------------------------------------------------------
-export([start_list/3,
start/2,
start/1,
find_shell/0]).
%% ---------------------------------------------------------------
%% Includes
%% ---------------------------------------------------------------
-include("assert.hrl").
-include("pman_options.hrl").
-include("pman_buf.hrl").
%% ---------------------------------------------------------------
%% Internal record declarations
%% ---------------------------------------------------------------
-record(pman_shell,{win,
editor,
pid,
buffer,
father,
shell_flag, % boolean, true for shell
trace_options, % Keeps trace options
db}). % DB for trace windows
%%
%% Constants
%%
-define (PMAN_DB, pman_db). % The pman db for trace windows
%% ---------------------------------------------------------------
%% start/1, start/2
%%
%% Starts a new trace shell process.
%%
%% start(Pid, DefaultOptions)
%% Pid The Pid of the process to trace
%% DefaultOptions The default trace options passed along from
%% the calling process.
%%
%%
%% start(Pid)
%% Pid The Pid of the process to trace
%%
%% start(Pid) starts without using any default options except for those
%% hardwired into the application. (See pman_options.hrl).
%%
%%
%% Return: Both functions return a process id
%% ---------------------------------------------------------------
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% start_list/3 - Starts a trace window for each of the processes
%% in the list
start_list(LIPid, Father, Options) ->
StartFun = fun(Pid) ->
start({Pid,Father}, Options)
end,
lists:foreach(StartFun, LIPid).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% start/1 - Starts a trace window for the specified Pid.
%%
start(Pid) ->
start(Pid, #trace_options{}).
%%
%% start/2
%%
start(Pid,DefaultOptions) when is_pid(Pid) ->
start({Pid,self()}, DefaultOptions);
start(Var,DefaultOptions) ->
Db = db_start(),
spawn_link(fun() -> internal(Var, DefaultOptions, Db) end).
%% ---------------------------------------------------------------
%% Initialize the enviroment for tracing/viewing Object
%%
%% Object can either be {shell,Shell} or a Pid.
%% The main loop is then called, which handles trace and event
%% requests. The window dies whenever Supervisor dies, while
%% message windows die whenever their parent dies.
%% ---------------------------------------------------------------
internal({Object,Supervisor}, DefaultOptions, Db) ->
%% (???) This call will cause minor problems when the window has been
%% invoked with proc/1 from for instance the shell. The shell
%% does not handle the exit-signals, so it will exit
%% when the window is exited.
%% First check that no other process is tracing the process we want
%% to trace. There is no well defined way of doing this, so the
%% code below is used instead. (???)
pman_relay:start(Object), %(???) Uses proc. dict.
Pid = pman_process:get_pid(Object),
case pman_relay:ok_to_trace(Pid) of
%% Tracing cannot be performed on the specified process
false ->
T = lists:flatten(io_lib:format("ERROR: Process ~p is already being~ntraced by some other process.~nOr there may be a problem communicating with it.",[Pid])),
tool_utils:notify(gs:start(),T),
exit(quit);
%% Tracing can be performed, go ahead!
true ->
case db_insert_key (Db, Pid) of
true ->
link(Supervisor),
process_flag(trap_exit, true),
case catch pman_win:window(Object) of
{'EXIT', badrpc} ->
T = "ERROR: Could not access node",
pman_win:dialog_window(gs:start(),T);
{'EXIT', dead} ->
T = "ERROR: The process is dead",
pman_win:dialog_window(gs:start(),T);
{'EXIT',_W} ->
T = "ERROR: Untracable process \n(unexpected EXIT reason)",
pman_win:dialog_window(gs:start(),T);
{Win, Ed} ->
init_monitor_loop(Win,
Ed,
Object,
Supervisor,
DefaultOptions,
Db)
end;
false ->
T = lists:flatten(io_lib:format("ERROR: Process ~p is already being~ntraced by some other process.",[Pid])),
tool_utils:notify(gs:start(),T),
exit(quit);
Error ->
Error
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% init_monitor_loop/5
init_monitor_loop(Win,Ed,Object,Supervisor, DefaultOptions, Db) ->
process_flag(priority, max),
%% Most default options come from the main window. Now we must set
%% the default file name to something that is shows what process
%% is being traced.
%% Find out an appropriate file name to write the trace output
%% to if the output should go to a file.
FileName = case pman_process:is_pid_or_shell(Object) of
true ->
default_file_name(pman_process:get_pid(Object));
false ->
"NoName"
end,
Buff = pman_buf:start(Ed, FileName),
case pman_process:is_running(Object) of
%% We are tracing a shell process.
{true,{shell,Pid}} ->
safe_link(Pid),
NewDefaultOptions =
DefaultOptions#trace_options{file=FileName},
perform_option_changes(Pid, NewDefaultOptions, Buff),
monitor_loop(#pman_shell{win=Win, editor=Ed, pid=Pid, buffer=Buff,
father = Supervisor,
shell_flag = true,
trace_options = NewDefaultOptions,
db = Db});
%% We are tracing an ordinary process.
{true,Pid} ->
safe_link(Pid),
NewDefaultOptions =
DefaultOptions#trace_options{file=FileName},
perform_option_changes(Pid, NewDefaultOptions, Buff),
monitor_loop(#pman_shell{win=Win, editor=Ed, pid=Pid, buffer=Buff,
father = Supervisor,
shell_flag = false,
trace_options = NewDefaultOptions,
db = Db});
%% The process being traced is dead.
false ->
monitor_loop(#pman_shell{win=Win, editor=Ed, pid=nopid,
buffer=Buff,
father = Supervisor,
shell_flag = false,
trace_options= DefaultOptions,
db = Db})
end.
%% ----------------------------------------------------------------
%% What is the Pid of the shell on our node?
%% ----------------------------------------------------------------
find_shell() ->
case shell:whereis_evaluator() of
undefined -> % noshell
noshell;
Pid ->
Pid
end.
%% ---------------------------------------------------------------
%% Functions called in case of an exit message
%% ---------------------------------------------------------------
clean_up(Win, Buff,Pid) ->
%% (???) Unlinks the traced process, but since we are using a safe link
%% it is probably unnecessary.
safe_unlink(Pid),
%% Kill helper processes
exit(Buff#buffer.converter, topquit),
exit(Buff#buffer.buffer, topquit),
gs:destroy(Win).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% exit_cmd/3 - Takes care of the necessary details when
%% a linked process terminates.
exit_cmd(Pid,_Reason, State) ->
case State#pman_shell.shell_flag of
%% This clause handles the case when a shell process dies.
%% Since it is restarted and the intention is to continue tracing
%% the restarted shell process, we need to handle it separately by
%% finding the new shell process.
true ->
NewShell = find_shell(),
safe_link(NewShell),
pman_relay:start(NewShell),
%% Update the window title with the new PID
Title = pman_win:title({shell, NewShell}),
Win = State#pman_shell.win,
gse:config(Win,[{title,Title}]),
pman_relay:trac(NewShell, true, flags()),
B = State#pman_shell.buffer,
B#buffer.converter!{raw,[{shell_died, Pid, NewShell}]},
State#pman_shell{pid=NewShell};
%% This clause handles the case when a traced process that is
%% not a shell process dies.
false ->
B = State#pman_shell.buffer,
B#buffer.converter!{raw,[{died, Pid}]},
lists:foreach(fun(X) -> gse:disable(X) end,
['Options',
'Kill',
'LinksMenu']),
State#pman_shell{pid=undefined}
end.
flags() ->
[send, 'receive', call, procs,
set_on_spawn, set_on_first_spawn, set_on_link, set_on_first_link].
options_to_flaglists(Options) ->
AssocList =
[{Options#trace_options.send, send},
{Options#trace_options.treceive, 'receive'},
{Options#trace_options.inherit_on_1st_spawn, set_on_first_spawn},
{Options#trace_options.inherit_on_all_spawn, set_on_spawn},
{Options#trace_options.inherit_on_1st_link, set_on_first_link},
{Options#trace_options.inherit_on_all_link, set_on_link},
{Options#trace_options.events, procs},
{Options#trace_options.functions,call}],
TrueFun = fun ({Option,Flag}) ->
case Option of
true -> Flag;
_Otherwise -> false
end
end,
TrueFlags = mapfilter(TrueFun, AssocList),
FalseFun = fun ({Option,Flag}) ->
case Option of
false -> Flag;
_Otherwise -> false
end
end,
FalseFlags = mapfilter(FalseFun, AssocList),
{TrueFlags,FalseFlags}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% mapfilter/2 - Combines the functionality of lists:map and
%% lists:filter. mapfilter applies the function argument to
%% each element in the list. All returned values that are
%% not false will occur in the resulting list.
%%
%% Arguments:
%% Fun A fun that takes one argument
%% List A list. Each element will become an argument to Fun.
%%
%% Returns:
%% A list of all results from the map operation that are not false.
%%
mapfilter(Fun,[E|Es]) ->
case apply(Fun,[E]) of
false ->
mapfilter(Fun,Es);
Value -> [Value | mapfilter(Fun,Es)]
end;
mapfilter(_Fun, []) -> [].
perform_option_changes(Pid,Options,Buffer) ->
%% Notify the trace output functionality
%% if the destination is supposed to go to a file...
case Options#trace_options.to_file of
true ->
FName = Options#trace_options.file,
Buffer#buffer.converter!{file,FName};
false ->
done
end,
%%...then set the trace flags of the traced process
{OnFlags, OffFlags} = options_to_flaglists(Options),
case catch begin
%% (???) Note that the following calls cannot actually fail
%% This may be a problem. And the catch appears unnecessary
%% However, it may become necessary to let the
%% pman_relay:trac/3 function retrun appropriate values.
pman_relay:trac(Pid,true, OnFlags),
pman_relay:trac(Pid,false, OffFlags)
end of
true ->
ok;
_ -> pman_win:format("** Illegal trace request ** \n", [])
end.
%% ---------------------------------------------------------------
%% Take care of the command executed by the user.
execute_cmd(Cmd,Shell_data) ->
Window = Shell_data#pman_shell.win,
Editor = Shell_data#pman_shell.editor,
Shell = Shell_data#pman_shell.pid,
Buffer = Shell_data#pman_shell.buffer,
TraceOptions = Shell_data#pman_shell.trace_options,
case Cmd of
'Close' ->
db_delete_key (Shell_data#pman_shell.db, Shell_data#pman_shell.pid),
clean_up(Window, Buffer, Shell),
exit(quit);
'Destroy' ->
db_delete_key (Shell_data#pman_shell.db, Shell_data#pman_shell.pid),
exit(Buffer#buffer.buffer,topquit),
safe_unlink(Shell),
exit(Buffer#buffer.converter,topquit),
exit(Buffer#buffer.buffer,topquit),
exit(quit);
'Clear' when is_pid(Shell) ->
New_buffer = pman_buf:clear(Buffer,pman_win:display(Shell),
TraceOptions#trace_options.file),
Shell_data#pman_shell{buffer = New_buffer};
'Save buffer' ->
DefaultFile = "Pman_buffer." ++ default_file_name(Shell),
Result = tool_utils:file_dialog([{type,save},
{file,DefaultFile}]),
case Result of
{ok, UserFile, _State} ->
Buffer#buffer.buffer!{save_buffer,UserFile};
{error,_Reason} ->
true
end,
Shell_data;
'Help' ->
HelpFile = filename:join([code:lib_dir(pman), "doc", "html", "index.html"]),
tool_utils:open_help(gs:start([{kernel, true}]), HelpFile),
Shell_data;
'Kill' when is_pid(Shell) ->
exit(Buffer#buffer.converter,killed),
exit(Buffer#buffer.buffer,killed),
lists:foreach(fun(X) -> gse:disable(X) end,
['TraceMenu',
'Clear']),
catch exit(Shell, kill),
Shell_data#pman_shell{pid = undefined};
'All Links' when is_pid(Shell) ->
LIPid = pman_process:pinfo(Shell, links),
?ALWAYS_ASSERT("Just a brutal test"),
start_list(LIPid,
Shell_data#pman_shell.father,
Shell_data#pman_shell.trace_options),
Shell_data;
'Module' when is_pid(Shell) ->
{ModuleName,_,_} = pman_process:function_info(Shell),
pman_module_info:start(ModuleName),
Shell_data;
'Options' when is_pid(Shell) ->
case pman_options:dialog(Window,
"Trace Options for Process",
TraceOptions) of
{error, _Reason} ->
Shell_data;
Options ->
perform_option_changes(Shell, Options, Buffer),
Shell_data#pman_shell{trace_options=Options}
end;
{trac,Choice,Bool} when is_pid(Shell) ->
pman_relay:trac(Shell, Bool, [Choice]),
Shell_data;
{configure,{X,Y}} ->
configure (Editor, X, Y),
Shell_data;
Pid when is_pid(Pid) ->
pman_shell:start({Pid, Shell_data#pman_shell.father},
Shell_data#pman_shell.trace_options),
Shell_data;
_Other ->
?ALWAYS_ASSERT("Received unexpected event"),
Shell_data
end.
default_file_name(Shell) when is_pid(Shell) ->
[A,B,C] = string:tokens(pid_to_list(Shell),[$.,$<,$>]),
"pman_trace." ++ A ++ "_" ++ B ++ "_" ++ C;
default_file_name(_OTHER) ->
"shell".
%% Key accellerators
key(e) -> 'Clear';
key(s) -> 'Save buffer';
key(c) -> 'Close';
key(a) -> 'All';
key(r) -> 'Reset';
key(m) -> 'Module';
key(l) -> 'All Links';
key(k) -> 'Kill';
key(h) -> 'Help';
key(z) -> 'Close';
key(O) -> O.
%% ---------------------------------------------------------------
%% The main loop takes care of data coming in from the traces, as
%% well as exit signals from proceses we are monitoring. Events
%% caused by the user or window manager are also handled here.
%% ---------------------------------------------------------------
monitor_loop(Shell_data) ->
receive
%% WM destroy
{gs,_Window,destroy,[],[]} -> %%Avoid links menus
execute_cmd('Destroy', Shell_data);
%% Handle EXIT signal from parent process
{'EXIT', _Pid, topquit} ->
clean_up(Shell_data#pman_shell.win,
Shell_data#pman_shell.buffer,
Shell_data#pman_shell.pid),
exit(topquit);
%% (???) Ignore "stray" EXIT signal from converter
{'EXIT', _Pid, win_killed} ->
monitor_loop(Shell_data);
%% Handle EXIT signal from safely linked Pid
%% This is received when a traced process dies.
{'SAFE_EXIT', Pid, Reason} ->
New_Shell_data = exit_cmd(Pid, Reason,Shell_data ),
monitor_loop(New_Shell_data);
%% Handle EXIT signal from processes where we expect
%% some EXIT signals, such as the file_dialog opened, and possibly
%% others.
{'EXIT', _Pid, _Reason} ->
monitor_loop(Shell_data);
%% Handle incoming trace messages
Message when is_tuple(Message) , element(1,Message) == trace->
{L, Suspended} = collect_tracs([Message]),
Buffer = Shell_data#pman_shell.buffer,
Buffer#buffer.converter!{raw,L},
lists:foreach(fun(P) -> erlang:resume_process(P) end, Suspended),
monitor_loop(Shell_data);
%% All other messages on the form {...,...,...}
Message when is_tuple(Message) ->
do_link_stuff(Shell_data),
New_Shell_data = process_gs_event(Message,Shell_data),
monitor_loop(New_Shell_data);
%% Catch all for unexpected messages
_Anything ->
?ALWAYS_ASSERT("Received unexpected event"),
monitor_loop(Shell_data)
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% process_event/1 - Error handling wrapper for gs_cmd
process_gs_event(Message, Shell_data) ->
case catch gs_cmd(Message,Shell_data) of
%%
%% Error exits from gs_cmd
{'EXIT', badrpc} ->
Text = "\nERROR: Could not access node",
pman_win:msg_win(Text),
Shell_data;
{'EXIT', dead} ->
Text = "\nERROR: The process is dead",
pman_win:msg_win(Text),
Shell_data;
%% A controlled application initiated termination
{'EXIT', quit} ->
db_delete_key (Shell_data#pman_shell.db, Shell_data#pman_shell.pid),
exit(quit);
{'EXIT',Reason} ->
db_delete_key (Shell_data#pman_shell.db, Shell_data#pman_shell.pid),
io:format("Debug info, Reason: ~p~n",[Reason]),
?ALWAYS_ASSERT("Unexpected EXIT reason"),
exit({unexpected_EXIT_reason,Reason});
%%
%% "Proper" exits from gs_cmd
New_Shell_data ->
New_Shell_data
end.
gs_cmd(Cmd, Shell_data) ->
case Cmd of
%%User Command
{gs, Command, click, _Data, _Args} ->
execute_cmd(Command,Shell_data);
%%Key accellerator
{gs,_Window,keypress,_D,[Key,_,0,1]} ->
execute_cmd(key(Key),Shell_data);
%%Window Resize
{gs,_Window,configure,_,[X,Y|_]} ->
execute_cmd({configure,{X,Y}},Shell_data);
{gs, _Object, _Event, _Data, _Args} ->
?ALWAYS_ASSERT("Unhandled gs event"),
Shell_data
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% (???) do_link_stuff/1 - I have no clue.
%%
do_link_stuff(Shell_data) ->
%% This appears to be code to execute for adding
%% dynamic links menus.
case Shell_data#pman_shell.pid of
undefined ->
ok;
Pid ->
case pman_process:pinfo(Pid, links) of
Links when is_list(Links) ->
pman_win:links_menus(Links);
undefined ->
ok
end
end.
%% (???) Process dictionary used to safe Pid-Pid pairs.
%%
%% safe_link/1 - Spawns a process, that links to the Pid, and sends
%% a message to the caller when the linked process dies.
%%
%% Since we (think we) need to link to the traced process, we want
%% to do it in a way that has the smallest possible risk. The process
%% that links to the Pid is small and simple, which is safer than if
%% the calling process would link directly to the Pid.
safe_link(Pid) when is_pid(Pid) ->
Self = self(),
PidSafe = spawn_link(fun() -> safe_init(Self, Pid) end),
put(Pid, PidSafe).
%% safe_unlink/1 - Removes a safe link
%%
safe_unlink(Pid) when is_pid(Pid) ->
PidSafe = get(Pid),
PidSafe ! {unlink, self(), Pid},
erase(Pid);
safe_unlink(_Anything)->
true.
%% safe_init/2 - Initialize a simple receive loop that controls safe linking
%% to application processes.
%%
safe_init(Caller, Pid) ->
process_flag(trap_exit, true),
link(Pid),
safe_loop(Caller, Pid).
%% safe_loop/2 - Simply waits for an exit signal from the linked Pid,
%% all other messages are disregarded.
%%
safe_loop(Caller, Pid) ->
receive
%% Linked process dies
{'EXIT' , Pid, Reason} ->
Caller ! {'SAFE_EXIT', Pid, Reason};
%% Caller dies
{'EXIT', Caller, _Reason} ->
unlink(Pid);
%% Unlink request
{unlink, Caller, Pid} ->
unlink(Pid);
%% Ignore everything else
_Anything ->
safe_loop(Caller, Pid)
end.
configure (Editor, W, H) ->
gs:config (Editor, [{width, W - 3},
{height, H - 40}]).
%%% The DB is used to avoid multiple trace windows
%%% of the same process.
%%% db_start /0
%%%
db_start() ->
case ets:info(?PMAN_DB) of
undefined -> ets:new(?PMAN_DB, [public, named_table]);
_ -> ?PMAN_DB
end.
%%% db_insert_key /2
%%%
db_insert_key (Db, Pid) ->
case ets:lookup (Db, Pid) of
[] ->
case catch ets:insert (Db, {Pid}) of
true ->
true;
_Error ->
error_insert_db
end;
_already_exists ->
false
end.
%%% db_delete_key /2
%%%
db_delete_key (Db, Pid) ->
ets:delete (Db, Pid).
%% Function to collect all trace messages in the receive queue.
%% Returns: {Messages,SuspendedProcesses}
collect_tracs(Ack) -> collect_tracs(Ack, ordsets:new()).
collect_tracs(Ack, Procs) ->
receive
Trac when is_tuple(Trac), element(1, Trac) == trace ->
P = suspend(Trac, Procs),
collect_tracs([Trac | Ack], P)
after 0 ->
{lists:reverse(Ack), ordsets:to_list(Procs)}
end.
suspend({trace,From,call,_Func}, Suspended) when node(From) == node() ->
case ordsets:is_element(From, Suspended) of
true -> Suspended;
false ->
case (catch erlang:suspend_process(From)) of
true ->
ordsets:add_element(From, Suspended);
_ ->
Suspended
end
end;
suspend(_Other, Suspended) -> Suspended.