%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2018. 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%
%%
-module(memsup).
-behaviour(gen_server).

%% API
-export([start_link/0]). % for supervisor
-export([get_memory_data/0, get_system_memory_data/0,
	 get_check_interval/0, set_check_interval/1,
	 get_procmem_high_watermark/0, set_procmem_high_watermark/1,
	 get_sysmem_high_watermark/0, set_sysmem_high_watermark/1,
	 get_helper_timeout/0, set_helper_timeout/1,
	 get_os_wordsize/0]).
-export([dummy_reply/1, param_type/2, param_default/1]).

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

%% Other exports
-export([format_status/2]).

-include("memsup.hrl").

-record(state,
	{os,                  % {OSfamily,OSname} | OSfamily
	 port_mode,           % bool()

	 mem_usage,           % undefined | {Alloc, Total}
	 worst_mem_user,      % undefined | {Pid, Alloc}

	 sys_only,            % bool()  memsup_system_only
	 timeout,             % int()   memory_check_interval, ms
	 helper_timeout,      % int()   memsup_helper_timeout, ms
	 sys_mem_watermark,   % float() system_memory_high_watermark, %
	 proc_mem_watermark,  % float() process_memory_high_watermark, %

	 pid,                 % undefined | pid()
	 wd_timer,            % undefined | TimerRef
	 ext_wd_timer,        % undefined | TimerRef
	 pending = [],        % [reg | {reg,From} | {ext,From}]
	 ext_pending = []     % [{ext,From}]
	}).

%%----------------------------------------------------------------------
%% API
%%----------------------------------------------------------------------

start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

get_os_wordsize() ->
    os_mon:call(memsup, get_os_wordsize, infinity).

get_memory_data() ->
    os_mon:call(memsup, get_memory_data, infinity).

get_system_memory_data() ->
    os_mon:call(memsup, get_system_memory_data, infinity).

get_check_interval() ->
    os_mon:call(memsup, get_check_interval, infinity).
set_check_interval(Minutes) ->
    case param_type(memory_check_interval, Minutes) of
	true ->
	    MS = minutes_to_ms(Minutes), % for backwards compatibility
	    os_mon:call(memsup, {set_check_interval, MS}, infinity);
	false ->
	    erlang:error(badarg)
    end.

get_procmem_high_watermark() ->
    os_mon:call(memsup, get_procmem_high_watermark, infinity).
set_procmem_high_watermark(Float) ->
    case param_type(process_memory_high_watermark, Float) of
	true ->
	    os_mon:call(memsup, {set_procmem_high_watermark, Float},
			infinity);
	false ->
	    erlang:error(badarg)
    end.

get_sysmem_high_watermark() ->
    os_mon:call(memsup, get_sysmem_high_watermark, infinity).
set_sysmem_high_watermark(Float) ->
    case param_type(system_memory_high_watermark, Float) of
	true ->
	    os_mon:call(memsup, {set_sysmem_high_watermark, Float},
			infinity);
	false ->
	    erlang:error(badarg)
    end.

get_helper_timeout() ->
    os_mon:call(memsup, get_helper_timeout, infinity).
set_helper_timeout(Seconds) ->
    case param_type(memsup_helper_timeout, Seconds) of
	true ->
	    os_mon:call(memsup, {set_helper_timeout, Seconds}, infinity);
	false ->
	    erlang:error(badarg)
    end.

dummy_reply(get_memory_data) ->
    dummy_reply(get_memory_data,
		os_mon:get_env(memsup, memsup_system_only));
dummy_reply(get_system_memory_data) ->
    [];
dummy_reply(get_os_wordsize) ->
    0;
dummy_reply(get_check_interval) ->
    minutes_to_ms(os_mon:get_env(memsup, memory_check_interval));
dummy_reply({set_check_interval, _}) ->
    ok;
dummy_reply(get_procmem_high_watermark) ->
    trunc(100 * os_mon:get_env(memsup, process_memory_high_watermark));
dummy_reply({set_procmem_high_watermark, _}) ->
    ok;
dummy_reply(get_sysmem_high_watermark) ->
    trunc(100 * os_mon:get_env(memsup, system_memory_high_watermark));
dummy_reply({set_sysmem_high_watermark, _}) ->
    ok;
dummy_reply(get_helper_timeout) ->
    os_mon:get_env(memsup, memsup_helper_timeout);
dummy_reply({set_helper_timeout, _}) ->
    ok.
dummy_reply(get_memory_data, true) ->
    {0,0,undefined};
dummy_reply(get_memory_data, false) ->
    {0,0,{self(),0}}.

param_type(memsup_system_only, Val) when Val==true; Val==false -> true;
param_type(memory_check_interval, Val) when is_integer(Val),
                                            Val>0 -> true;
param_type(memsup_helper_timeout, Val) when is_integer(Val),
                                            Val>0 -> true;
param_type(system_memory_high_watermark, Val) when is_number(Val),
						   0=<Val,
						   Val=<1 -> true;
param_type(process_memory_high_watermark, Val) when is_number(Val),
						    0=<Val,
						    Val=<1 -> true;
param_type(_Param, _Val) -> false.

param_default(memsup_system_only) -> false;
param_default(memory_check_interval) -> 1;
param_default(memsup_helper_timeout) -> 30;
param_default(system_memory_high_watermark) -> 0.80;
param_default(process_memory_high_watermark) -> 0.05.

%%----------------------------------------------------------------------
%% gen_server callbacks
%%----------------------------------------------------------------------

init([]) ->
    process_flag(trap_exit, true),
    process_flag(priority, low),

    OS = os:type(),
    PortMode = case OS of
		   {unix, darwin} -> false;
		   {unix, freebsd} -> false;
		   {unix, dragonfly} -> false;
		   % Linux supports this.
		   {unix, linux} -> true;
		   {unix, openbsd} -> true;
		   {unix, netbsd} -> true;
		   {unix, irix64} -> true;
		   {unix, irix} -> true;
		   {unix, sunos} -> true;
		   {win32, _OSname} -> false;
		   _ ->
		       exit({unsupported_os, OS})
	       end,
    Pid = if
	      PortMode ->
		  spawn_link(fun() -> port_init() end);
	      not PortMode ->
		  undefined
	  end,

    %% Read the values of some configuration parameters
    SysOnly = os_mon:get_env(memsup, memsup_system_only),
    Timeout = os_mon:get_env(memsup, memory_check_interval),
    HelperTimeout = os_mon:get_env(memsup, memsup_helper_timeout),
    SysMem = os_mon:get_env(memsup, system_memory_high_watermark),
    ProcMem = os_mon:get_env(memsup, process_memory_high_watermark),

    %% Initiate first data collection
    self() ! time_to_collect,

    {ok, #state{os=OS, port_mode=PortMode,

		sys_only           = SysOnly,
		timeout            = minutes_to_ms(Timeout),
		helper_timeout     = sec_to_ms(HelperTimeout),
		sys_mem_watermark  = SysMem,
		proc_mem_watermark = ProcMem,

		pid=Pid}}.

handle_call(get_os_wordsize, _From, State) ->
    Wordsize = get_os_wordsize(State#state.os),
    {reply, Wordsize, State};
handle_call(get_memory_data, From, State) ->
    %% Return result of latest memory check
    case State#state.mem_usage of
	{Alloc, Total} ->
	    Worst = State#state.worst_mem_user,
	    {reply, {Total, Alloc, Worst}, State};
	
	%% Special case: get_memory_data called before any memory data
	%% has been collected
	undefined ->
	    case State#state.wd_timer of
		undefined ->
		    WDTimer = erlang:send_after(State#state.timeout,
						self(),
						reg_collection_timeout),
		    Pending = [{reg,From}],
		    if
			State#state.port_mode ->
			    State#state.pid ! {self(), collect_sys},
			    {noreply, State#state{wd_timer=WDTimer,
						  pending=Pending}};
			true ->
			    OS = State#state.os,
			    Self = self(),
			    Pid = spawn_link(fun() ->
						     MU = get_memory_usage(OS),
						     Self ! {collected_sys,MU}
					     end),
			    {noreply, State#state{pid=Pid,
						  wd_timer=WDTimer,
						  pending=Pending}}
		    end;
		_TimerRef ->
		    Pending = [{reg,From} | State#state.pending],
		    {noreply, State#state{pending=Pending}}
	    end
    end;

handle_call(get_system_memory_data,From,#state{port_mode=true}=State) ->
    %% When using a port, the extensive memory collection is slightly
    %% different than a regular one
    case State#state.ext_wd_timer of
	undefined ->
	    WDTimer = erlang:send_after(State#state.helper_timeout,
					self(),
					ext_collection_timeout),
	    State#state.pid ! {self(), collect_ext_sys},
	    {noreply, State#state{ext_wd_timer=WDTimer,
				  ext_pending=[{ext,From}]}};
	_TimerRef ->
	    Pending = [{ext,From} | State#state.ext_pending],
	    {noreply, State#state{ext_pending=Pending}}
    end;
handle_call(get_system_memory_data, From, State) ->
    %% When not using a port, the regular memory collection is used
    %% for extensive memory data as well
    case State#state.wd_timer of
	undefined ->
	    WDTimer = erlang:send_after(State#state.helper_timeout,
					self(),
					reg_collection_timeout),
	    OS = State#state.os,
	    Self = self(),
	    Pid = spawn_link(fun() ->
				     MemUsage = get_memory_usage(OS),
				     Self ! {collected_sys, MemUsage}
			     end),
	    {noreply, State#state{pid=Pid, wd_timer=WDTimer,
				  pending=[{ext,From}]}};
	_TimerRef ->
	    Pending = [{ext,From} | State#state.pending],
	    {noreply, State#state{pending=Pending}}
    end;

handle_call(get_check_interval, _From, State) ->
    {reply, State#state.timeout, State};
handle_call({set_check_interval, MS}, _From, State) ->
    {reply, ok, State#state{timeout=MS}};

handle_call(get_procmem_high_watermark, _From, State) ->
    {reply, trunc(100 * State#state.proc_mem_watermark), State};
handle_call({set_procmem_high_watermark, Float}, _From, State) ->
    {reply, ok, State#state{proc_mem_watermark=Float}};

handle_call(get_sysmem_high_watermark, _From, State) ->
    {reply, trunc(100 * State#state.sys_mem_watermark), State};
handle_call({set_sysmem_high_watermark, Float}, _From, State) ->
    {reply, ok, State#state{sys_mem_watermark=Float}};

handle_call(get_helper_timeout, _From, State) ->
    {reply, ms_to_sec(State#state.helper_timeout), State};
handle_call({set_helper_timeout, Seconds}, _From, State) ->
    {reply, ok, State#state{helper_timeout=sec_to_ms(Seconds)}};

%% The following are only for test purposes (whitebox testing).
handle_call({set_sys_hw, HW}, _From, State) ->
    {reply, ok, State#state{sys_mem_watermark=HW}};
handle_call({set_pid_hw, HW}, _From, State) ->
    {reply, ok, State#state{proc_mem_watermark=HW}};
handle_call(get_state, _From, State) ->
    {reply, State, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

%% It's time to check memory
handle_info(time_to_collect, State) ->
    case State#state.wd_timer of
	undefined ->
	    WDTimer = erlang:send_after(State#state.helper_timeout,
					self(),
					reg_collection_timeout),
	    if
		State#state.port_mode ->
		    State#state.pid ! {self(), collect_sys},
		    {noreply, State#state{wd_timer=WDTimer,
					  pending=[reg]}};
		true ->
		    OS = State#state.os,
		    Self = self(),
		    Pid = spawn_link(fun() ->
					     MU = get_memory_usage(OS),
					     Self ! {collected_sys,MU}
				     end),
		    {noreply, State#state{pid=Pid, wd_timer=WDTimer,
					  pending=[reg]}}
	    end;
	_TimerRef ->
	    {noreply, State#state{pending=[reg|State#state.pending]}}
    end;

%% Memory data collected
handle_info({collected_sys, {Alloc,Total}}, State) ->

    %% Cancel watchdog timer (and as a security measure,
    %% also flush any reg_collection_timeout message)
    TimeSpent = case erlang:cancel_timer(State#state.wd_timer) of
		    false ->
			State#state.helper_timeout;
		    TimeLeft ->
			State#state.helper_timeout-TimeLeft
		end,
    flush(reg_collection_timeout),

    %% First check if this is the result of a periodic memory check
    %% and update alarms and State if this is the case
    State2 =
	case lists:member(reg, State#state.pending) of
	    true ->

		%% Check if system alarm should be set/cleared
		if
		    Alloc > State#state.sys_mem_watermark*Total ->
			set_alarm(system_memory_high_watermark, []);
		    true ->
			clear_alarm(system_memory_high_watermark)
		end,

		%% Check if process data should be collected
		case State#state.sys_only of
		    false ->
			{Pid, Bytes} = get_worst_memory_user(),
			Threshold= State#state.proc_mem_watermark*Total,

			%% Check if process alarm should be set/cleared
			if
			    Bytes > Threshold ->
				set_alarm(process_memory_high_watermark,
					  Pid);
			    true ->
				clear_alarm(process_memory_high_watermark)
			end,
			
			State#state{mem_usage={Alloc, Total},
				    worst_mem_user={Pid, Bytes}};
		    true ->
			State#state{mem_usage={Alloc, Total}}
		end;
	    false ->
		State
	end,

    %% Then send a reply to all waiting clients, in preserved time order
    Worst = State2#state.worst_mem_user,
    SysMemUsage = get_ext_memory_usage(State2#state.os, {Alloc,Total}),
    reply(State2#state.pending, {Total,Alloc,Worst}, SysMemUsage),

    %% Last, if this was a periodic check, start a timer for the next
    %% one. New timeout = interval-time spent collecting,
    _ = case lists:member(reg, State#state.pending) of
            true ->
                Time = case State2#state.timeout - TimeSpent of
                           MS when MS<0 ->
                               0;
                           MS ->
                               MS
                       end,
                erlang:send_after(Time, self(), time_to_collect);
            false ->
                ignore
        end,
    {noreply, State2#state{wd_timer=undefined, pending=[]}};
handle_info({'EXIT', Pid, normal}, State) when is_pid(Pid) ->
    %% Temporary pid terminating when job is done
    {noreply, State};

%% Timeout during data collection
handle_info(reg_collection_timeout, State) ->

    %% Cancel memory collection (and as a security measure,
    %% also flush any collected_sys message)
    if
	State#state.port_mode -> State#state.pid ! cancel;
	true -> exit(State#state.pid, cancel)
    end,
    flush(collected_sys),

    %% Issue a warning message
    Str = "OS_MON (memsup) timeout, no data collected~n",
    error_logger:warning_msg(Str),

    %% Send a dummy reply to all waiting clients, preserving time order
    reply(State#state.pending,
	  dummy_reply(get_memory_data, State#state.sys_only),
	  dummy_reply(get_system_memory_data)),

    %% If it is a periodic check which has timed out, start a timer for
    %% the next one
    %% New timeout = interval-helper timeout
    _ = case lists:member(reg, State#state.pending) of
            true ->
                Time =
                case State#state.timeout-State#state.helper_timeout of
                    MS when MS<0 -> 0;
                    MS -> MS
                end,
                erlang:send_after(Time, self(), time_to_collect);
            false ->
                ignore
        end,
    {noreply, State#state{wd_timer=undefined, pending=[]}};
handle_info({'EXIT', Pid, cancel}, State) when is_pid(Pid) ->
    %% Temporary pid terminating as ordered
    {noreply, State};

%% Extensive memory data collected (port_mode==true only)
handle_info({collected_ext_sys, SysMemUsage}, State) ->

    %% Cancel watchdog timer (and as a security mearure,
    %% also flush any ext_collection_timeout message)
    ok = erlang:cancel_timer(State#state.ext_wd_timer, [{async,true}]),
    flush(ext_collection_timeout),

    %% Send the reply to all waiting clients, preserving time order
    reply(State#state.ext_pending, undef, SysMemUsage),

    {noreply, State#state{ext_wd_timer=undefined, ext_pending=[]}};

%% Timeout during ext memory data collection (port_mode==true only)
handle_info(ext_collection_timeout, State) ->

    %% Cancel memory collection (and as a security measure,
    %% also flush any collected_ext_sys message)
    State#state.pid ! ext_cancel,
    flush(collected_ext_sys),

    %% Issue a warning message
    Str = "OS_MON (memsup) timeout, no data collected~n",
    error_logger:warning_msg(Str),

    %% Send a dummy reply to all waiting clients, preserving time order
    SysMemUsage = dummy_reply(get_system_memory_data),
    reply(State#state.ext_pending, undef, SysMemUsage),

    {noreply, State#state{ext_wd_timer=undefined, ext_pending=[]}};

%% Error in data collecting (port connected or temporary) process
handle_info({'EXIT', Pid, Reason}, State) when is_pid(Pid) ->
    {stop, Reason, State};
	    
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, State) ->
    if
	State#state.port_mode -> State#state.pid ! close;
	true -> ok
    end,
    clear_alarms(),
    ok.

%% os_mon-2.0.1
%% For live downgrade to/upgrade from os_mon-1.8[.1] and -2.0
code_change(Vsn, PrevState, "1.8") ->
    case Vsn of

	%% Downgrade from this version
	{down, _Vsn} ->

	    %% Kill the helper process, if there is one,
	    %% and flush messages from it
	    case PrevState#state.pid of
		Pid when is_pid(Pid) ->
		    unlink(Pid), % to prevent 'EXIT' message
		    exit(Pid, cancel);
		undefined -> ignore
	    end,
	    flush(collected_sys),
	    flush(collected_ext_sys),

	    %% Cancel timers, flush timeout messages
	    %% and send dummy replies to any pending clients
	    case PrevState#state.wd_timer of
		undefined ->
		    ignore;
		TimerRef1 ->
                    ok = erlang:cancel_timer(TimerRef1, [{async,true}]),
		    SysOnly = PrevState#state.sys_only,
		    MemUsage = dummy_reply(get_memory_data, SysOnly),
		    SysMemUsage1 = dummy_reply(get_system_memory_data),
		    reply(PrevState#state.pending,MemUsage,SysMemUsage1)
	    end,
	    case PrevState#state.ext_wd_timer of
		undefined ->
		    ignore;
		TimerRef2 ->
                    ok = erlang:cancel_timer(TimerRef2, [{async,true}]),
		    SysMemUsage2 = dummy_reply(get_system_memory_data),
		    reply(PrevState#state.pending, undef, SysMemUsage2)
	    end,
	    flush(reg_collection_timeout),
	    flush(ext_collection_timeout),

	    %% Downgrade to old state record
	    State = {state,
		     PrevState#state.timeout,
		     PrevState#state.mem_usage,
		     PrevState#state.worst_mem_user,
		     PrevState#state.sys_mem_watermark,
		     PrevState#state.proc_mem_watermark,
		     not PrevState#state.sys_only, % collect_procmem
		     undefined, % wd_timer
		     [],        % pending
		     undefined, % ext_wd_timer
		     [],        % ext_pending
		     PrevState#state.helper_timeout},
	    {ok, State};

	%% Upgrade to this version
	_Vsn ->

	    %% Old state record
	    {state,
	     Timeout, MemUsage, WorstMemUser,
	     SysMemWatermark, ProcMemWatermark, CollProc,
	     WDTimer, Pending, ExtWDTimer, ExtPending,
	     HelperTimeout} = PrevState,
	    SysOnly = not CollProc,

	    %% Flush memsup_helper messages
	    flush(collected_sys),
	    flush(collected_proc),
	    flush(collected_ext_sys),
		     
	    %% Cancel timers, flush timeout messages
	    %% and send dummy replies to any pending clients
	    case WDTimer of
		undefined ->
		    ignore;
		TimerRef1 ->
                    ok = erlang:cancel_timer(TimerRef1, [{async,true}]),
		    MemUsage = dummy_reply(get_memory_data, SysOnly),
		    Pending2 = lists:map(fun(From) -> {reg,From} end,
					 Pending),
		    reply(Pending2, MemUsage, undef)
	    end,
	    case ExtWDTimer of
		undefined ->
		    ignore;
		TimerRef2 ->
                    ok = erlang:cancel_timer(TimerRef2, [{async,true}]),
		    SysMemUsage = dummy_reply(get_system_memory_data),
		    ExtPending2 = lists:map(fun(From) -> {ext,From} end,
					    ExtPending),
		    reply(ExtPending2, undef, SysMemUsage)
	    end,
	    flush(reg_collection_timeout),
	    flush(ext_collection_timeout),

	    OS = os:type(),
	    PortMode = case OS of
			   {unix, darwin} -> false;
			   {unix, freebsd} -> false;
			   {unix, dragonfly} -> false;
			   {unix, linux} -> false;
			   {unix, openbsd} -> true;
			   {unix, netbsd} -> true;
			   {unix, sunos} -> true;
			   {win32, _OSname} -> false
		       end,
	    Pid = if
		      PortMode -> spawn_link(fun() -> port_init() end);
		      not PortMode -> undefined
		  end,

	    %% Upgrade to this state record
	    State = #state{os = OS,
			   port_mode = PortMode,
			   mem_usage = MemUsage,
			   worst_mem_user = WorstMemUser,
			   sys_only  = SysOnly,
			   timeout         = Timeout,
			   helper_timeout  = HelperTimeout,
			   sys_mem_watermark = SysMemWatermark,
			   proc_mem_watermark = ProcMemWatermark,
			   pid                = Pid,
			   wd_timer           = undefined,
			   ext_wd_timer       = undefined,
			   pending            = [],
			   ext_pending        = []},
	    {ok, State}
    end;
code_change(_Vsn, State, "2.0") ->

    %% Restart the port process (it must use new memsup code)
    Pid = case State#state.port_mode of
	      true ->
		  State#state.pid ! close,
		  spawn_link(fun() -> port_init() end);
	      false ->
		  State#state.pid
	  end,
    {ok, State#state{pid=Pid}};
	  
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%----------------------------------------------------------------------
%% Other exports
%%----------------------------------------------------------------------

format_status(_Opt, [_PDict, #state{timeout=Timeout, mem_usage=MemUsage,
				    worst_mem_user=WorstMemUser}]) ->
    {Allocated, Total} = MemUsage,
    WorstMemFormat = case WorstMemUser of
			 {Pid, Mem} ->
			     [{"Pid", Pid}, {"Memory", Mem}];
			 undefined ->
			     undefined
		     end,
    [{data, [{"Timeout", Timeout}]},
     {items, {"Memory Usage", [{"Allocated", Allocated},
			       {"Total", Total}]}},
     {items, {"Worst Memory User", WorstMemFormat}}].


%%----------------------------------------------------------------------
%% Internal functions
%%----------------------------------------------------------------------

%%-- Fetching kernel bit support ---------------------------------------

get_os_wordsize({unix, sunos}) ->
    String = clean_string(os:cmd("isainfo -b")),
    erlang:list_to_integer(String);
get_os_wordsize({unix, irix64})  -> 64;
get_os_wordsize({unix, irix})    -> 32;
get_os_wordsize({unix, linux})   -> get_os_wordsize_with_uname();
get_os_wordsize({unix, darwin})  -> get_os_wordsize_with_uname();
get_os_wordsize({unix, netbsd})  -> get_os_wordsize_with_uname();
get_os_wordsize({unix, freebsd}) -> get_os_wordsize_with_uname();
get_os_wordsize({unix, dragonfly}) -> get_os_wordsize_with_uname();
get_os_wordsize({unix, openbsd}) -> get_os_wordsize_with_uname();
get_os_wordsize(_)               -> unsupported_os.

get_os_wordsize_with_uname() ->
    String = clean_string(os:cmd("uname -m")),
    case String of
	"x86_64"  -> 64;
	"sparc64" -> 64;
	"amd64"   -> 64;
	"ppc64"   -> 64;
	"s390x"   -> 64;
	_         -> 32
    end.

clean_string(String) -> lists:flatten(string:lexemes(String,[[$\r,$\n]|"\n\t "])).
    

%%--Replying to pending clients-----------------------------------------

reply(Pending, MemUsage, SysMemUsage) ->
    lists:foreach(fun(reg) ->
			  ignore;
		     ({reg, From}) ->
			  gen_server:reply(From, MemUsage);
		     ({ext, From}) ->
			  gen_server:reply(From, SysMemUsage)
		  end,
		  lists:reverse(Pending)).

%%--Collect memory data, no port----------------------------------------

%% get_memory_usage(OS) -> {Alloc, Total}

%% Darwin:
%% Uses vm_stat command.
get_memory_usage({unix,darwin}) ->
    Str1 = os:cmd("/usr/bin/vm_stat"),
    PageSize = 4096,

    {[Free],        Str2} = fread_value("Pages free:~d.", Str1),
    {[Active],      Str3} = fread_value("Pages active:~d.", Str2),
    {[Inactive],    Str4} = fread_value("Pages inactive:~d.", Str3),
    {[Speculative], Str5} = fread_value("Pages speculative:~d.", Str4),
    {[Wired],       _} = fread_value("Pages wired down:~d.", Str5),

    NMemUsed  = (Wired + Active + Inactive) * PageSize,
    NMemTotal = NMemUsed + (Free + Speculative) * PageSize,
    {NMemUsed,NMemTotal};

%% FreeBSD: Look in /usr/include/sys/vmmeter.h for the format of struct
%% vmmeter
get_memory_usage({unix,OSname}) when OSname == freebsd; OSname == dragonfly ->
    PageSize  = freebsd_sysctl("vm.stats.vm.v_page_size"),
    PageCount = freebsd_sysctl("vm.stats.vm.v_page_count"),
    FreeCount = freebsd_sysctl("vm.stats.vm.v_free_count"),
    NMemUsed  = (PageCount - FreeCount) * PageSize,
    NMemTotal = PageCount * PageSize,
    {NMemUsed, NMemTotal};

%% Win32: Find out how much memory is in use by asking
%% the os_mon_sysinfo process.
get_memory_usage({win32,_OSname}) ->
    [Result|_] = os_mon_sysinfo:get_mem_info(),
    {ok, [_MemLoad, TotPhys, AvailPhys,
	  _TotPage, _AvailPage, _TotV, _AvailV], _RestStr} =
	io_lib:fread("~d~d~d~d~d~d~d", Result),
    {TotPhys-AvailPhys, TotPhys}.

fread_value(Format, Str0) ->
    case io_lib:fread(Format, skip_to_eol(Str0)) of
    	{error, {fread, input}} -> {[0], Str0};
	{ok, Value, Str1} -> {Value, Str1}
    end.

skip_to_eol([]) -> [];
skip_to_eol([$\n | T]) -> T;
skip_to_eol([_ | T]) -> skip_to_eol(T).

freebsd_sysctl(Def) ->
    list_to_integer(os:cmd("/sbin/sysctl -n " ++ Def) -- "\n").

%% get_ext_memory_usage(OS, {Alloc, Total}) -> [{Tag, Bytes}]
get_ext_memory_usage(OS, {Alloc, Total}) ->
    case OS of
	{win32, _} ->
	    [{total_memory, Total}, {free_memory, Total-Alloc},
	     {system_total_memory, Total}];
	{unix, linux} ->
	    [{total_memory, Total}, {free_memory, Total-Alloc},
	     %% corr. unless setrlimit() set
	     {system_total_memory, Total}];
	{unix, freebsd} ->
	    [{total_memory, Total}, {free_memory, Total-Alloc},
	     {system_total_memory, Total}];
	{unix, dragonfly} ->
	    [{total_memory, Total}, {free_memory, Total-Alloc},
	     {system_total_memory, Total}];
	{unix, darwin} ->
	    [{total_memory, Total}, {free_memory, Total-Alloc},
	     {system_total_memory, Total}];
	_ -> % OSs using a port
	    dummy % not sent anyway
    end.

%%--Collect memory data, using port-------------------------------------

port_init() ->
    process_flag(trap_exit, true),
    Port = start_portprogram(),
    port_idle(Port).

start_portprogram() ->
    os_mon:open_port("memsup",[{packet,1}]).

%% The connected process loops are a bit awkward (several different
%% functions doing almost the same thing) as
%%   a) strategies for receiving regular memory data and extensive
%%      memory data are different
%%   b) memory collection can be cancelled, in which case the process
%%      should still wait for port response (which should come
%%      eventually!) but not receive any requests or cancellations
%%      meanwhile to prevent getting out of synch.
port_idle(Port) ->
    receive
	{Memsup, collect_sys} ->
	    Port ! {self(), {command, [?SHOW_MEM]}},
	    get_memory_usage(Port, undefined, Memsup);
	{Memsup, collect_ext_sys} ->
	    Port ! {self(), {command, [?SHOW_SYSTEM_MEM]}},
	    get_ext_memory_usage(Port, [], Memsup);
	cancel ->
	    %% Received after reply already has been delivered...
	    port_idle(Port);
	ext_cancel ->
	    %% Received after reply already has been delivered...
	    port_idle(Port);
	close ->
	    port_close(Port);
	{Port, {data, Data}} ->
	    exit({port_error, Data});
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.

get_memory_usage(Port, Alloc, Memsup) ->
    receive
	{Port, {data, Data}} when Alloc==undefined ->
	    get_memory_usage(Port, erlang:list_to_integer(Data, 16), Memsup);
	{Port, {data, Data}} ->
	    Total = erlang:list_to_integer(Data, 16),
	    Memsup ! {collected_sys, {Alloc, Total}},
	    port_idle(Port);
	cancel ->
	    get_memory_usage_cancelled(Port, Alloc);
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.
get_memory_usage_cancelled(Port, Alloc) ->
    receive
	{Port, {data, _Data}} when Alloc==undefined ->
	    get_memory_usage_cancelled(Port, 0);
	{Port, {data, _Data}} ->
	    port_idle(Port);
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.

get_ext_memory_usage(Port, Accum, Memsup) ->
    Tab = [
	    {?MEM_SYSTEM_TOTAL,   system_total_memory},
	    {?MEM_TOTAL,          total_memory},
	    {?MEM_FREE,           free_memory},
	    {?MEM_BUFFERS,        buffered_memory},
	    {?MEM_CACHED,         cached_memory},
	    {?MEM_SHARED,         shared_memory},
	    {?MEM_LARGEST_FREE,   largest_free},
	    {?MEM_NUMBER_OF_FREE, number_of_free},
	    {?SWAP_TOTAL,	  total_swap},
	    {?SWAP_FREE,	  free_swap}
	],
    receive
	{Port, {data, [?SHOW_SYSTEM_MEM_END]}} ->
	    Memsup ! {collected_ext_sys, Accum},
	    port_idle(Port);
	{Port, {data, [Tag]}} ->
	    case lists:keysearch(Tag, 1, Tab) of
		{value, {Tag, ATag}} ->
		    get_ext_memory_usage(ATag, Port, Accum, Memsup);
		_ ->
		    exit({memsup_port_error, {Port,[Tag]}})
	    end;
	ext_cancel ->
	    get_ext_memory_usage_cancelled(Port);
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.
get_ext_memory_usage_cancelled(Port) ->
    Tab = [
	    {?MEM_SYSTEM_TOTAL,   system_total_memory},
	    {?MEM_TOTAL,          total_memory},
	    {?MEM_FREE,           free_memory},
	    {?MEM_BUFFERS,        buffered_memory},
	    {?MEM_CACHED,         cached_memory},
	    {?MEM_SHARED,         shared_memory},
	    {?MEM_LARGEST_FREE,   largest_free},
	    {?MEM_NUMBER_OF_FREE, number_of_free},
	    {?SWAP_TOTAL,	  total_swap},
	    {?SWAP_FREE,	  free_swap}
	],
    receive
	{Port, {data, [?SHOW_SYSTEM_MEM_END]}} ->
	    port_idle(Port);
	{Port, {data, [Tag]}} ->
	    case lists:keysearch(Tag, 1, Tab) of
		{value, {Tag, ATag}} ->
		    get_ext_memory_usage_cancelled(ATag, Port);
		_ ->
		    exit({memsup_port_error, {Port,[Tag]}})
	    end;
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.

get_ext_memory_usage(ATag, Port, Accum0, Memsup) ->
    receive
	{Port, {data, Data}} ->
	    Accum = [{ATag,erlang:list_to_integer(Data, 16)}|Accum0],
	    get_ext_memory_usage(Port, Accum, Memsup);
	cancel ->
	    get_ext_memory_usage_cancelled(ATag, Port);
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.
get_ext_memory_usage_cancelled(_ATag, Port) ->
    receive
	{Port, {data, _Data}} ->
	    get_ext_memory_usage_cancelled(Port);
	close ->
	    port_close(Port);
	{'EXIT', Port, Reason} ->
	    exit({port_died, Reason});
	{'EXIT', _Memsup, _Reason} ->
	    port_close(Port)
    end.

%%--Collect process data------------------------------------------------

%% get_worst_memory_user() -> {Pid, Bytes}
get_worst_memory_user()  ->
    get_worst_memory_user(processes(), self(), 0).

get_worst_memory_user([Pid|Pids], MaxPid, MaxMemBytes) ->
    case process_memory(Pid) of
	undefined ->
	    get_worst_memory_user(Pids, MaxPid, MaxMemBytes);
	MemoryBytes when MemoryBytes>MaxMemBytes ->
	    get_worst_memory_user(Pids, Pid, MemoryBytes);
	_MemoryBytes ->
	    get_worst_memory_user(Pids, MaxPid, MaxMemBytes)
    end;
get_worst_memory_user([], MaxPid, MaxMemBytes) ->
    {MaxPid, MaxMemBytes}.

process_memory(Pid) ->
    case process_info(Pid, memory) of
	{memory, Bytes} ->
	    Bytes;
	undefined -> % Pid must have died
	    undefined
    end.

%%--Alarm handling------------------------------------------------------

set_alarm(AlarmId, AlarmDescr) ->
    case get(AlarmId) of
	set ->
	    ok;
	undefined ->
	    alarm_handler:set_alarm({AlarmId, AlarmDescr}),
	    put(AlarmId, set)
    end.

clear_alarm(AlarmId) ->
    case get(AlarmId) of
	set ->
	    alarm_handler:clear_alarm(AlarmId),
	    erase(AlarmId);
	_ ->
	    ok
    end.

clear_alarms() ->
    lists:foreach(fun({system_memory_high_watermark = Id, set}) ->
			  alarm_handler:clear_alarm(Id);
		     ({process_memory_high_watermark = Id, set}) ->
			  alarm_handler:clear_alarm(Id);
		     (_Other) ->
			  ignore
		  end,
		  get()).

%%--Auxiliary-----------------------------------------------------------

%% Type conversions
minutes_to_ms(Minutes) -> trunc(60000*Minutes).
sec_to_ms(Sec) -> trunc(1000*Sec).
ms_to_sec(MS) -> MS div 1000.

flush(Msg) ->
    receive
	{Msg, _} -> true;
	Msg -> true
    after 0 ->
	    true
    end.
