%% %% %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). %% --------------------------------------------------------------- %% 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.