%%
%% %CopyrightBegin%
%%
%% SPDX-License-Identifier: Apache-2.0
%%
%% Copyright Ericsson AB 1996-2025. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%----------------------------------------------------------------------
%%
%% Information centre for appmon. Must be present on each node
%% monitored.
%%
%%
%%	A worklist is maintained that contain all current work that
%%	should be performed at each timeout. Each entry in the
%%	worklist describes where the result shall be sent and a list
%%	of options relevant for that particular task
%%
%%
%% Maintenance Note:
%%
%%	This module is supposed to be updated by any who would like to
%%	subscribe for information. The idea is that several tools
%%	could use this module for their core information gathering
%%	services.
%%
%%	The module is based on the notion of tasks. Each task should
%%	have a nice public interface function which should handle task
%%	administration. Tasks are identified by a "key" consisting of
%%	three items, the requesting pid, the name of the task and the
%%	task auxiliary parameter. The requesting pid is the pid of the
%%	callee (in the appmon case it can be the node window for
%%	instance), the task name is whatever name the task is given
%%	(in the appmon case it can be app, app_ctrl or load). The task
%%	name can be seen as the type of the task. The task auxiliary
%%	parameter is an all purpose parameter that have a different
%%	meaning for each type of task so in appmon the Aux for app
%%	contains the root pid of the monitored application and in
%%	app_ctrl it contains the node name (just to distinguish from
%%	the other app_ctrl tasks, if any) while the Aux parameter is
%%	not used for the load task at all.
%%
%%	Each task also carries a list of options for
%%	customisation. The options valid for a task is completely
%%	internal to that task type except for the timeout option which
%%	is used by do_work to determine the interval at which to
%%	perform the task. The timeout option may also have the value
%%	at_most_once that indicates that the task should not be done
%%	more than once, in appmon the remote port (or process) info
%%	(pinfo) task is such a task that is only done once for each
%%	call. Note that the only way to change or update options is to
%%	call the public interface function for the task, this will
%%	merge the old options with the new ones and also force the
%%	task to be executed.
%%
%%	All tasks are managed by the do_work function. The basic
%%	functionality being that the result of the task is compared to
%%	the previous result and a delivery is sent to the callee if
%%	they differ. Most tasks are then done on a regular basis using
%%	the timer module for a delay.
%%	
%%	There are a limited number of places where the module need to
%%	be updated when new services are added, they are all marked
%%	with "Maintenance Note", and here is a quick guide:
%%
%%	First implement the task. Put the functions in this module
%%	among the other task implementations. Currently all task
%%	implementations should be put in this file to make it simple
%%	to monitor a node, this module should be the only one
%%	needed. Then add your implementation to the do_work2 function
%%	and finally add a public interface function among the other
%%	public interface functions. Voila.
%%
%%
%%
%%	Future ideas:
%%
%%	Appmon should maybe be enhanced to show all processes on a
%%	node. First put all processes in an ets P, then pick those
%%	that belong to applications (the normal way), then try to find
%%	those processes that are roots in process link trees and pick
%%	them. The final step would be to do something with those
%%	processes that are left.
%%
%%----------------------------------------------------------------------
-module(appmon_info).
-moduledoc false.
-behaviour(gen_server).

%% Exported functions
-export([start_link/3, app/4, pinfo/4, load/4, app_ctrl/4]).

%% For internal use (RPC call)
-export([start_link2/3]).

%% For debugging
-export([status/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
	 terminate/2, code_change/3]).


%%----------------------------------------------------------------------
%% The records
%%
%%	state is used for keeping track of all tasks.
%%
%%	db is the database used in the app task.
%%

-record(state, {starter, opts=[], work=[], clients=[]}).
-record(db, {q, p, links, links2}).


%%----------------------------------------------------------------------
%% Macros
%%

-define(MK_KEY(CMD, AUX, FROM, OPTS), {CMD, AUX, FROM}).
-define(MK_DOIT(KEY), {do_it, KEY}).
-define(ifthen(P,S), if P -> S; true -> ok end).


%%----------------------------------------------------------------------
%% Public interface
%%
%%	The Aux parameter is an auxiliary parameter that can be used
%%	freely by the requesting process, it is included in the work
%%	task key. appmon uses it for storing the node name when
%%	requesting load and app_ctrl tasks, and appmon_a uses it for
%%	storing application name when requesting app task.
%%
%%	Maintenance Note: Put new tasks at the end, please.
%%


%% Do not use gen_server:start_link because we do not want the
%% appmon_info to die when initiating process dies unless special
%% conditions apply.
%% Uhu, we don't??? Made a fix so that this process DOES indeed die
%% if it's starter dies. /Gunilla
start_link(Node, Client, Opts) ->
    rpc:call(Node, ?MODULE, start_link2, [self(), Client, Opts]).
start_link2(Starter, Client, Opts) ->
    Name = {local, ?MODULE},
    Args = {Starter, Opts, Client},
    case gen_server:start(Name, ?MODULE, Args, []) of
	{ok, Pid} ->
	    {ok, Pid};
	{error, {already_started, Pid}} -> 
	    register_client(Pid, Client),
	    {ok, Pid}
    end.
	

%% app_ctrl
%%
%%	Monitors which applications exist on a node
%%
app_ctrl(Serv, Aux, OnOff, Opts) ->
    gen_server:cast(Serv, {self(), app_ctrl, Aux, OnOff, Opts}).


%% load
%%
%%	Monitors load on a node
%%
load(Serv, Aux, OnOff, Opts) ->
    gen_server:cast(Serv, {self(), load, Aux, OnOff, Opts}).


%% app
%%
%%	Monitors one application given by name (this ends up in a
%%	process tree
%%
app(Serv, AppName, OnOff, Opts) ->
    gen_server:cast(Serv, {self(), app, AppName, OnOff, Opts}).

%% pinfo
%%
%%	Process or Port info
%%
pinfo(Serv, Pid, OnOff, Opt) ->
    gen_server:cast(Serv, {self(), pinfo, Pid, OnOff, Opt}).

%% register_client
%%
%%	Registers a client (someone subscribing for information)
%%

register_client(Serv, P) ->
    link(Serv),
    gen_server:call(Serv, {register_client, P}).

%% status
%%
%%	Status of appmon_info
%%

status() ->
    gen_server:cast(?MODULE, status).

%%----------------------------------------------------------------------
%%
%% Gen server administration
%%
%%----------------------------------------------------------------------

init({Starter, Opts, Pid}) ->
    link(Pid),
    process_flag(trap_exit, true),
    WorkStore = ets:new(workstore, [set, public]),
    {ok, #state{starter=Starter, opts=Opts, work=WorkStore,
		clients=[Pid]}}.

terminate(_Reason, State) ->
    ets:delete(State#state.work),
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.


%%----------------------------------------------------------------------
%%
%% Gen server calls
%%
%%----------------------------------------------------------------------

handle_call({register_client, Pid}, _From, State) ->
    NewState = case lists:member(Pid, State#state.clients) of
		   true -> State;
		   _ -> State#state{clients=[Pid | State#state.clients]}
	       end,
    {reply, ok, NewState};
handle_call(_Other, _From, State) ->
    {reply, ok, State}.

%%----------------------------------------------------------------------
%%
%% Gen server casts
%%
%%----------------------------------------------------------------------

%% Cmd = app_ctrl | load | app | pinfo
handle_cast({From, Cmd, Aux, OnOff, Opts}, State) ->
    NewState = update_worklist(Cmd, Aux, From, OnOff, Opts, State),
    {noreply, NewState};
handle_cast(status, State) ->
    print_state(State),
    {noreply, State};
handle_cast(_Other, State) ->
    {noreply, State}.


%%----------------------------------------------------------------------
%%
%% Gen server info's
%%
%%----------------------------------------------------------------------

handle_info({do_it, Key}, State) ->
    ok = do_work(Key, State),
    {noreply, State};

handle_info({'EXIT', Pid, Reason}, State) ->
    case State#state.starter of
	Pid ->
	    {stop, Reason, State};
	_Other ->
	    Work = State#state.work,
	    del_work(ets:match(Work, {{'$1','$2',Pid}, '_', '_', '_'}),
		     Pid, Work),
	    case lists:delete(Pid, State#state.clients) of
		[] -> case get_opt(stay_resident, State#state.opts) of
			  true -> {noreply, State#state{clients=[]}};
			  _ -> {stop, normal, State}
		      end;
		NewClients -> {noreply, State#state{clients=NewClients}}
	    end
    end;
handle_info(_Other, State) ->
    {noreply, State}.


%%----------------------------------------------------------------------
%%
%% Doing actual work
%%
%%----------------------------------------------------------------------

do_work(Key, State) ->
    WorkStore = State#state.work,
    {Cmd, Aux, From, _OldRef, Old, Opts} = retrieve(WorkStore, Key),
    {ok, Result} = do_work2(Cmd, Aux, From, Old, Opts),
    if
        Result==Old -> ok;
        true ->
            From ! {delivery, self(), Cmd, Aux, Result},
            ok
    end,
    case get_opt(timeout, Opts) of
	at_most_once ->
	    del_task(Key, WorkStore);
	T when is_integer(T) ->
	    {ok, Ref} = timer:send_after(T, ?MK_DOIT(Key)),
	    store(WorkStore, Key, Ref, Result, Opts)
    end,
    ok.


%%----------------------------------------------------------------------
%%
%% Name: do_work2
%%
%% Maintenance Note: Add a clause here for each new task.
%%
do_work2(load, _Aux, _From, Old, Opts) ->
    calc_load(Old, Opts);
do_work2(app_ctrl, _Aux, _From, _Old, _Opts) ->
    calc_app_on_node();
do_work2(app, Aux, _From, _Old, Opts) ->
    calc_app_tree(Aux, Opts);
do_work2(pinfo, Aux, _From, _Old, _Opts) ->
    calc_pinfo(pinfo, Aux);
do_work2(Cmd, Aux, _From, _Old, _Opts) ->
    {Cmd, Aux}.


retrieve(Tab, Key) ->
    case ets:lookup(Tab, Key) of
	[{{Cmd, Aux, From}, Ref, Old, Opts}] ->
	    {Cmd, Aux, From, Ref, Old, Opts};
	_Other ->
	    false
    end.

store(Tab, Key, Ref, Old, Opts) ->
    ets:insert(Tab, {Key, Ref, Old, Opts}),
    Key.


%%----------------------------------------------------------------------
%%
%% WorkStore handling
%%
%%----------------------------------------------------------------------

update_worklist(Cmd, Aux, From, true, Opts, State) ->
    add_task(Cmd, Aux, From, Opts, State),
    State;
update_worklist(Cmd, Aux, From, _Other, _Opts, State) ->
    del_task(Cmd, Aux, From, State#state.work),
    State.

%% First check if a task like this already exists and if so cancel its
%% timer and make really sure that no stray do it command will come
%% later. Then start a new timer for the task and store it i
%% WorkStorage
add_task(Cmd, Aux, From, Opts, State) ->
    WorkStore = State#state.work,
    Key = ?MK_KEY(Cmd, Aux, From, Opts),
    OldOpts = del_task(Key, WorkStore),
    store(WorkStore, Key, nil, nil, ins_opts(Opts, OldOpts)),
    catch do_work(Key, State),
    ok.

%% Delete a list of tasks belonging to a pid
del_work([[Cmd, Aux] | Ws], Pid, Work) ->
    del_task(Cmd, Aux, Pid, Work),
    del_work(Ws, Pid, Work);
del_work([], _Pid, _Work) -> ok.

%% Must return old options or empty list
del_task(Cmd, Aux, From, WorkStore) ->
    del_task(?MK_KEY(Cmd, Aux, From, []), WorkStore).
del_task(Key, WorkStore) ->
    OldStuff = retrieve(WorkStore, Key),
    ets:delete(WorkStore, Key),
    case OldStuff of
	{_Cmd, _Aux, _From, Ref, _Old, Opts} ->
	    if
		Ref /= nil ->
                    {ok,_} = timer:cancel(Ref),
		    receive
			{do_it, Key} ->
			    Opts
		    after 10 ->
			    Opts
		    end;
		true -> Opts
	    end;
	_ ->
	    []
    end.


%%
%% Maintenance Note:
%%
%% Add new task implementations somewhere here below.
%%


%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% BEGIN OF calc_app_tree
%%
%%	App tree is the process tree shown in the application window
%%
%%	The top (root) pid is found by calling
%%	application_controller:get_master(AppName) and this is done in
%%	calc_app_on_node (before the call to calc_app_tree).
%%
%%	We are going to add processes to the P ets and we are doing it
%%	in a two step process. First all prospect processes are put on
%%	the queue Q. Then we examine the front of Q and add this
%%	process to P if it's not already in P. Then all children of
%%	the process is put on the queue Q and the process is repeated.
%%
%%	We also maintain two link ets'es, one for primary links and
%%	one for secondary links. These databases are updated at the
%%	same time as the queue is updated with children.
%%
%%**********************************************************************
%%----------------------------------------------------------------------


calc_app_tree(Name, Opts) ->
    Mode = get_opt(info_type, Opts),
    case application_controller:get_master(Name) of
	Pid when is_pid(Pid) ->
	    DB = new_db(Mode, Pid),
	    GL = groupl(Pid),
	    R = case catch do_find_proc(Mode, DB, GL, find_avoid()) of
		    {ok, DB2} ->
			{ok, {format(Pid),
			      format(ets:tab2list(DB2#db.p)),
			      format(ets:tab2list(DB2#db.links)), 
			      format(ets:tab2list(DB2#db.links2))}};
		    {error, Reason} -> 
			{error, Reason};
		    Other ->
			{error, Other}
		end,
	    ets:delete(DB#db.p),
	    ets:delete(DB#db.links),
	    ets:delete(DB#db.links2),
	    R;
	_ ->
	    {ok, {[], [], [], []}}
    end.

get_pid(P) when is_pid(P) -> P;
get_pid(P) when is_port(P) -> P;
get_pid(X) when is_tuple(X) -> element(2, X).


%----------------------------------------------------------------------
%%---------------------------------------------------------------------
%% Handling process trees of processes that are linked to each other

do_find_proc(Mode, DB, GL, Avoid) ->
    case get_next(DB) of
	{{value, V}, DB2} ->
	    do_find_proc2(V, Mode, DB2, GL, Avoid);
	{empty, DB2} ->
	    {ok, DB2}
    end.

do_find_proc2(X, Mode, DB, GL, Avoid) when is_port(X) ->
    %% There used to be a broken attempt here to handle ports,
    %% but the rest of appmon can't handle ports, so now we
    %% explicitly ignore ports.
    do_find_proc(Mode, DB, GL, Avoid);
do_find_proc2(X, Mode, DB, GL, Avoid) ->
    Xpid = get_pid(X),
    DB2 = case is_proc(DB, Xpid) of
	      false ->
		  add_proc(DB, Xpid),
		  C1 = find_children(X, Mode),
		  add_children(C1, Xpid, DB, GL, Avoid, Mode);
	      _ -> 
		  DB
	  end,
    do_find_proc(Mode, DB2, GL, Avoid).


%% Find children finds the children of a process. The method varies
%% with the selected mode (sup or link) and there are also some
%% processes that must be treated differently, notably the application
%% master.
%% 
find_children(X, sup) when is_pid(X) ->
    %% This is the first (root) process of a supervision tree and it
    %% better be a supervisor, we are smoked otherwise
    supervisor:which_children(X);
find_children(X, link) when is_pid(X), node(X) /= node() ->
    [];
find_children(X, link) when is_pid(X) ->
    case process_info(X, links) of
	{links, Links} ->
	    lists:reverse(Links); % OTP-4082
	_ -> []
    end;
find_children({master, X}, sup) -> 
    case application_master:get_child(X) of
	{Pid, _Name} when is_pid(Pid) -> [Pid];
	Pid when is_pid(Pid) -> [Pid]
    end;
find_children({_, _X, worker, _}, sup) -> [];
find_children({_, X, supervisor, _}, sup) ->
    lists:filter(fun(Thing) -> 
			 Pid = get_pid(Thing),
			 if
			     is_pid(Pid) -> true;
			     true -> false
			 end
		 end,
		 supervisor:which_children(X)).


%% Add links to primary (L1) or secondary (L2) sets and return an
%% updated queue. A link is considered secondary if its endpoint is in
%% the queue of un-visited but known processes.
add_children(CList, Paren, DB, _GL, _Avoid, sup) ->
    lists:foldr(fun(C, DB2) -> 
			case get_pid(C) of
			    P when is_pid(P) -> 
				add_prim(C, Paren, DB2);
			    _ -> DB2 end end,
		DB, CList);

add_children(CList, Paren, DB, GL, Avoid, _Mode) ->
    lists:foldr(fun(C, DB2) ->
			maybe_add_child(C, Paren, DB2, GL, Avoid)
		end, DB, CList).

%% Check if the child is already in P
maybe_add_child(C, Paren, DB, GL, Avoid) ->
    case is_proc(DB, C) of
	false -> 
	    maybe_add_child_node(C, Paren, DB, GL, Avoid);
	_ -> DB					% In P: no action
    end.

%% Check if process on this node
maybe_add_child_node(C, Paren, DB, GL, Avoid) ->
    if
	node(C) /= node() -> 
	    add_foreign(C, Paren, DB);
	true -> 
	    maybe_add_child_avoid(C, Paren, DB, GL, Avoid)
    end.

%% Check if child is on the avoid list
maybe_add_child_avoid(C, Paren, DB, GL, Avoid) ->
    case lists:member(C, Avoid) of
	true -> DB;
	false ->
	    maybe_add_child_port(C, Paren, DB, GL)
    end.

%% Check if it is a port, then it is added
maybe_add_child_port(C, Paren, DB, GL) ->
    if
	is_port(C) ->
	    add_prim(C, Paren, DB);
	true ->
	    maybe_add_child_sasl(C, Paren, DB, GL)
    end.

%% Use SASL stuff if present
maybe_add_child_sasl(C, Paren, DB, GL) ->
    case check_sasl_ancestor(Paren, C) of
	yes ->					% Primary
	    add_prim(C, Paren, DB);
	no ->					% Secondary
	    add_sec(C, Paren, DB);
	dont_know ->
	    maybe_add_child_gl(C, Paren, DB, GL)
    end.
		    
%% Check group leader
maybe_add_child_gl(C, Paren, DB, GL) ->
    case cmp_groupl(GL, groupl(C)) of
	true -> maybe_add_child_sec(C, Paren, DB);
	_ -> DB
    end.

%% Check if the link should be a secondary one. Note that this part is
%% pretty much a guess.
maybe_add_child_sec(C, Paren, DB) ->
    case is_in_queue(DB, C) of
	true ->					% Yes, secondary
	    add_sec(C, Paren, DB);
	_ ->					% Primary link
	    add_prim(C, Paren, DB)
    end.

check_sasl_ancestor(Paren, C) ->
    case lists:keysearch('$ancestors', 1, 
			 element(2,process_info(C, dictionary))) of
	{value, {_, L}} when is_list(L) ->
	    H = if
		    is_atom(hd(L)) -> whereis(hd(L));
		    true -> hd(L)
		end,
	    if
		H == Paren -> yes;
		true -> no
	    end;
	_ -> dont_know
    end.


%----------------------------------------------------------------------
%%---------------------------------------------------------------------
%% Primitives for the database DB of all links, processes and the
%% queue of not visited yet processes.

-define(add_link(C, Paren, L), ets:insert(L, {Paren, C})).

new_db(Mode, Pid) ->
    P  = ets:new(processes, [set, public]),
    L1 = ets:new(links, [bag, public]),
    L2 = ets:new(extralinks, [bag, public]),
    Q = if
	    Mode =:= sup -> queue:in({master, Pid}, queue:new());
	    true -> queue:in(Pid, queue:new())
	end,
    #db{q=Q, p=P, links=L1, links2=L2}.

get_next(DB) ->
    {X, Q} = queue:out(DB#db.q),
    {X, DB#db{q=Q}}.
add_proc(DB, P) ->
    ets:insert(DB#db.p, {P}).
add_prim(C, Paren, DB) ->
    ?add_link(get_pid(C), Paren, DB#db.links),
    DB#db{q=queue:in(C, DB#db.q)}.
add_foreign(C, Paren, DB) ->
    ?add_link(C, Paren, DB#db.links2),
    DB#db{q=queue:in(C, DB#db.q)}.
add_sec(C, Paren, DB) ->
    ?add_link(C, Paren, DB#db.links2),
    DB.

is_proc(#db{p=Tab}, P) ->
    ets:member(Tab, P).

is_in_queue(#db{q=Q}, P) ->
    queue:member(P, Q).

%% Group leader handling. No processes or Links to processes must be
%% added when group leaders differ. Note that catch all is needed
%% because net_sup is undefined when not networked but still present
%% in the kernel_sup child list. Blahh, didn't like that.
groupl(P) ->
    case process_info(P, group_leader) of
	{group_leader, GL} -> GL;
	_Other -> nil
    end.

cmp_groupl(_GL1, nil) -> true;
cmp_groupl(GL1, GL1) -> true;
cmp_groupl(_, _) -> false.


%% Do some intelligent guessing as to cut in the tree
find_avoid() ->
    lists:foldr(fun(X, Accu) -> 
		       case whereis(X) of
			   P when is_pid(P) ->
			       [P|Accu];
			   _ -> Accu end end,
		[undefined],
		[application_controller, init, gs,
		 node_serv, appmon, appmon_a, appmon_info]).



%%----------------------------------------------------------------------
%%
%% Formats the output strings
%%
%%----------------------------------------------------------------------
format([{P} | Fs]) ->				% Process or port
    [{P, format(P)} | format(Fs)];
format([{P1, P2} | Fs]) ->			% Link
    [{format(P1), format(P2)} | format(Fs)];
format([]) -> [];
format(P) when is_pid(P), node(P) /= node() ->
    pid_to_list(P) ++ " " ++ atom_to_list(node(P));
format(P) when is_pid(P) ->
    case process_info(P, registered_name) of
	{registered_name, Name} -> atom_to_list(Name);
	_ ->
            %% Needs to be unique
            case proc_lib:get_label(P) of
                undefined ->
                    pid_to_list(P);
                Label ->
                    format_label(Label, P)
            end
    end;
format(P) when is_port(P) ->
    case erlang:port_info(P, id) of
        undefined -> "port closed";
        {_, Pid} ->
            "port " ++ integer_to_list(Pid)
    end;
format(X) ->
    io:format("What: ~p~n", [X]),
    "???".

format_label(Id, Pid) when is_list(Id); is_binary(Id) ->
    try unicode:characters_to_binary(Id) of
        {error, _, _} ->
            io_lib:format("~0.tp ~w", [Id, Pid]);
        BinString ->
            io_lib:format("~ts ~w", [BinString, Pid])
    catch _:_ ->
            io_lib:format("~0.tp ~w", [Id, Pid])
    end;
format_label(Id, Pid) ->
    io_lib:format("~0.tp ~w", [Id, Pid]).


%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% END OF calc_app_tree
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------




%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% BEGIN OF calc_app_on_node
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------

%% Finds all applications on a node
calc_app_on_node() ->
    NewApps = reality_check(application:which_applications()),
    {ok, NewApps}.


reality_check([E|Es]) ->
    N = element(1, E),
    case catch application_controller:get_master(N) of
        P when is_pid(P) -> [{P, N, E} | reality_check(Es)];
        _ -> reality_check(Es)
    end;
reality_check([]) -> [].




%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% END OF calc_app_on_node
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------



%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% BEGIN OF calc_load
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------

calc_load(Old, Opts) ->
    L = load(Opts),
    case get_opt(load_average, Opts) of
	true ->
	    case Old of
		{_, L} -> {ok, {L, L}};
		{_, O2} when abs(L-O2) < 3 -> {ok, {O2, L}};
		{_, O2}	-> {ok, {O2, trunc((2*L+O2)/3)}};
		_ -> {ok, {0, L}}
	    end;
	_ -> 
	    case Old of
		{_, O2} -> {ok, {O2, L}};
		_ -> {ok, {0, L}}
	    end
    end.


load(Opts) ->
    Q   = get_sample(queue),

    case get_opt(load_method, Opts) of
	time ->
	    Td  = get_sample(runtime),
	    Tot = get_sample(tot_time),
	    
	    case get_opt(load_scale, Opts) of
		linear ->
		    erlang:min(trunc(load_range()*(Td/Tot+Q/6)),
			load_range());
		prog ->
		    erlang:min(trunc(load_range()*prog(Td/Tot+Q/6)),
			load_range())
	    end;
	queue ->
	    case get_opt(load_scale, Opts) of
		linear ->
		    erlang:min(trunc(load_range()*Q/6), load_range());
		prog ->
		    erlang:min(trunc(load_range()*prog(Q/6)), load_range())
		end
    end.


%%
%% T shall be within 0 and 0.9 for this to work correctly
prog(T) ->
    math:sqrt(abs(T)/0.9).


get_sample(queue)  -> statistics(run_queue);
get_sample(runtime)  -> {Rt,Rd} = statistics(runtime), 
			delta(runtime, Rt, Rd);
get_sample(tot_time)  -> {Rt,Rd} = statistics(wall_clock), 
			 delta(tot_time, Rt, Rd).


%% Keeps track of differences between calls
%% Needed because somebody else might have called
%% statistics/1.
%%
%% Note that due to wrap-arounds, we use a cheating 
%% delta which is correct unless somebody else
%% uses statistics/1
delta(KeyWord, Val, CheatDelta) ->
    RetVal = case get(KeyWord) of 
		 undefined ->
		     Val;
		 Other ->
		     if
			 Other > Val ->
			     CheatDelta;
			 true ->
			     Val-Other
		     end
	     end,
    put(KeyWord, Val),
    RetVal.


load_range() -> 16.



%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% END OF calc_load
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------


%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% BEGIN OF calc_pinfo
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------

calc_pinfo(pinfo, Pid) when is_pid(Pid) ->
    Info = process_info(Pid),
    {ok, io_lib:format("Node: ~p, Process: ~p~n~p~n~n",
		       [node(), Pid, Info])};
calc_pinfo(pinfo, Pid) when is_port(Pid) ->
    Info = lists:map(fun(Key) ->erlang:port_info(Pid, Key) end,
		     [id, name, connected, links, input, output]),
    
    {ok, io_lib:format("Node: ~p, Port: ~p~n~p~n~n", 
		       [node(),  element(2, erlang:port_info(Pid, id)),
			Info])};
calc_pinfo(pinfo, _Pid) ->
    {ok, ""}.


%%----------------------------------------------------------------------
%%**********************************************************************
%%
%%
%% END OF calc_pinfo
%%
%%
%%**********************************************************************
%%----------------------------------------------------------------------



%%----------------------------------------------------------------------
%%
%% Print the State
%%
%%	-record(state, {opts=[], work=[], clients=[]}).
%%
%%----------------------------------------------------------------------
print_state(State) ->
    io:format("Status:~n    Opts: ~p~n"
	      "Clients: ~p~n    WorkStore:~n",
	      [State#state.opts, State#state.clients]),
    print_work(ets:tab2list(State#state.work)).

print_work([W|Ws]) ->
    io:format("        ~p~n", [W]), print_work(Ws);
print_work([]) -> ok.
    

%%----------------------------------------------------------------------
%%
%% Option handling
%%
%%----------------------------------------------------------------------

%% The only options ever set by a user is info_type, timeout,
%% load_scale and load_method.
get_opt(Name, Opts) ->
    case lists:keysearch(Name, 1, Opts) of
	{value, Val} -> element(2, Val);
	false -> default(Name)
    end.

%% not all options have default values
default(info_type)	-> link;
default(load_average)	-> true;
default(load_method)	-> time;
default(load_scale)	-> prog;
default(stay_resident)	-> false;
default(timeout)	-> 2000.

ins_opts([Opt | Opts], Opts2) ->
    ins_opts(Opts, ins_opt(Opt, Opts2));
ins_opts([], Opts2) -> Opts2.

ins_opt({Opt, Val}, [{Opt, _} | Os]) -> [{Opt, Val} | Os];
ins_opt(Opt, [Opt2 | Os]) -> [Opt2 | ins_opt(Opt, Os)];
ins_opt(Opt, []) -> [Opt].
