%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2016. 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(disksup).
-behaviour(gen_server).

%% API
-export([start_link/0]).
-export([get_disk_data/0,
	 get_check_interval/0, set_check_interval/1,
	 get_almost_full_threshold/0, set_almost_full_threshold/1]).
-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]).

-record(state, {threshold, timeout, os, diskdata = [],port}).

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

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

get_disk_data() ->
    os_mon:call(disksup, get_disk_data, infinity).

get_check_interval() ->
    os_mon:call(disksup, get_check_interval, infinity).
set_check_interval(Minutes) ->
    case param_type(disk_space_check_interval, Minutes) of
	true ->
	    os_mon:call(disksup, {set_check_interval, Minutes}, infinity);
	false ->
	    erlang:error(badarg)
    end.

get_almost_full_threshold() ->
    os_mon:call(disksup, get_almost_full_threshold, infinity).
set_almost_full_threshold(Float) ->
    case param_type(disk_almost_full_threshold, Float) of
	true ->
	    os_mon:call(disksup, {set_almost_full_threshold, Float}, infinity);
	false ->
	    erlang:error(badarg)
    end.

dummy_reply(get_disk_data) ->
    [{"none", 0, 0}];
dummy_reply(get_check_interval) ->
    minutes_to_ms(os_mon:get_env(disksup, disk_space_check_interval));
dummy_reply({set_check_interval, _}) ->
    ok;
dummy_reply(get_almost_full_threshold) ->
    round(os_mon:get_env(disksup, disk_almost_full_threshold) * 100);
dummy_reply({set_almost_full_threshold, _}) ->
    ok.

param_type(disk_space_check_interval, Val) when is_integer(Val),
						Val>=1 -> true;
param_type(disk_almost_full_threshold, Val) when is_number(Val),
						 0=<Val,
						 Val=<1 -> true;
param_type(disksup_posix_only, Val) when Val==true; Val==false -> true;
param_type(_Param, _Val) -> false.

param_default(disk_space_check_interval) -> 30;
param_default(disk_almost_full_threshold) -> 0.80;
param_default(disksup_posix_only) -> false.

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

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

    PosixOnly = os_mon:get_env(disksup, disksup_posix_only),
    OS = get_os(PosixOnly),
    Port = case OS of
		{unix, Flavor} when Flavor==sunos4;
				    Flavor==solaris;
				    Flavor==freebsd;
				    Flavor==dragonfly;
				    Flavor==darwin;
				    Flavor==linux;
				    Flavor==posix;
				    Flavor==openbsd;
				    Flavor==netbsd;
				    Flavor==irix64;
				    Flavor==irix ->
		   start_portprogram();
	       {win32, _OSname} ->
		   not_used;
	       _ ->
		   exit({unsupported_os, OS})
	   end,

    %% Read the values of some configuration parameters
    Threshold = os_mon:get_env(disksup, disk_almost_full_threshold),
    Timeout = os_mon:get_env(disksup, disk_space_check_interval),

    %% Initiation first disk check
    self() ! timeout,

    {ok, #state{port=Port, os=OS,
		threshold=round(Threshold*100),
		timeout=minutes_to_ms(Timeout)}}.

handle_call(get_disk_data, _From, State) ->
    {reply, State#state.diskdata, State};

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

handle_call(get_almost_full_threshold, _From, State) ->
    {reply, State#state.threshold, State};
handle_call({set_almost_full_threshold, Float}, _From, State) ->
    Threshold = round(Float * 100),
    {reply, ok, State#state{threshold=Threshold}};

handle_call({set_threshold, Threshold}, _From, State) -> % test only
    {reply, ok, State#state{threshold=Threshold}}.

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

handle_info(timeout, State) ->
    NewDiskData = check_disk_space(State#state.os, State#state.port,
				   State#state.threshold),
    {ok, _Tref} = timer:send_after(State#state.timeout, timeout),
    {noreply, State#state{diskdata = NewDiskData}};
handle_info({'EXIT', _Port, Reason}, State) ->
    {stop, {port_died, Reason}, State#state{port=not_used}};
handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, State) ->
    clear_alarms(),
    case State#state.port of
	not_used ->
	    ok;
	Port ->
	    port_close(Port)
    end,
    ok.

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

	%% Downgrade from this version
	{down, _Vsn} ->
	    State = case PrevState#state.port of
			not_used -> PrevState#state{port=noport};
			_ -> PrevState
		    end,
	    {ok, State};

	%% Upgrade to this version
	_Vsn ->
	    State = case PrevState#state.port of
			noport -> PrevState#state{port=not_used};
			_ -> PrevState
		    end,
	    {ok, State}
    end;
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

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

format_status(_Opt, [_PDict, #state{os = OS, threshold = Threshold,
				    timeout = Timeout,
				    diskdata = DiskData}]) ->
    [{data, [{"OS", OS},
	     {"Timeout", Timeout},
	     {"Threshold", Threshold},
	     {"DiskData", DiskData}]}].

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

get_os(PosixOnly) ->
    case os:type() of
	{unix, sunos} ->
            case os:version() of
		{5,_,_} -> {unix, solaris};
		{4,_,_} -> {unix, sunos4};
		V -> exit({unknown_os_version, V})
            end;
	{unix, _} when PosixOnly ->
	    {unix, posix};
        {unix, irix64} -> {unix, irix};
	OS ->
	    OS
    end.

%%--Port handling functions---------------------------------------------

start_portprogram() -> 
    open_port({spawn, "sh -s disksup 2>&1"}, [stream]).

my_cmd(Cmd0, Port) ->
    %% Insert a new line after the command, in case the command
    %% contains a comment character
    Cmd = io_lib:format("(~s\n) </dev/null; echo  \"\^M\"\n", [Cmd0]),
    Port ! {self(), {command, [Cmd, 10]}},
    get_reply(Port, []).

get_reply(Port, O) ->
    receive 
        {Port, {data, N}} -> 
            case newline(N, O) of
                {ok, Str} -> Str;
                {more, Acc} -> get_reply(Port, Acc)
            end;
        {'EXIT', Port, Reason} ->
	    exit({port_died, Reason})
    end.

newline([13|_], B) -> {ok, lists:reverse(B)};
newline([H|T], B) -> newline(T, [H|B]);
newline([], B) -> {more, B}.

%%--Check disk space----------------------------------------------------

check_disk_space({win32,_}, not_used, Threshold) ->
    Result = os_mon_sysinfo:get_disk_info(),
    check_disks_win32(Result, Threshold);
check_disk_space({unix, solaris}, Port, Threshold) ->
    Result = my_cmd("/usr/bin/df -lk", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, irix}, Port, Threshold) ->
    Result = my_cmd("/usr/sbin/df -lk",Port),
    check_disks_irix(skip_to_eol(Result), Threshold);
check_disk_space({unix, linux}, Port, Threshold) ->
    Result = my_cmd("/bin/df -lk", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, posix}, Port, Threshold) ->
    Result = my_cmd("df -k -P", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, dragonfly}, Port, Threshold) ->
    Result = my_cmd("/bin/df -k -t ufs,hammer", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, freebsd}, Port, Threshold) ->
    Result = my_cmd("/bin/df -k -l", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, openbsd}, Port, Threshold) ->
    Result = my_cmd("/bin/df -k -l", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, netbsd}, Port, Threshold) ->
    Result = my_cmd("/bin/df -k -t ffs", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, sunos4}, Port, Threshold) ->
    Result = my_cmd("df", Port),
    check_disks_solaris(skip_to_eol(Result), Threshold);
check_disk_space({unix, darwin}, Port, Threshold) ->
    Result = my_cmd("/bin/df -i -k -t ufs,hfs,apfs", Port),
    check_disks_susv3(skip_to_eol(Result), Threshold).

% This code works for Linux and FreeBSD as well
check_disks_solaris("", _Threshold) ->
    [];
check_disks_solaris("\n", _Threshold) ->
    [];
check_disks_solaris(Str, Threshold) ->
    case io_lib:fread("~s~d~d~d~d%~s", Str) of
	{ok, [_FS, KB, _Used, _Avail, Cap, MntOn], RestStr} ->
	    if
		Cap >= Threshold ->
		    set_alarm({disk_almost_full, MntOn}, []);
		true ->
		    clear_alarm({disk_almost_full, MntOn})
	    end,
	    [{MntOn, KB, Cap} |
	     check_disks_solaris(RestStr, Threshold)];
	_Other ->
	    check_disks_solaris(skip_to_eol(Str),Threshold)
    end.

% Parse per SUSv3 specification, notably recent OS X
check_disks_susv3("", _Threshold) ->
    [];
check_disks_susv3("\n", _Threshold) ->
    [];
check_disks_susv3(Str, Threshold) ->
    case io_lib:fread("~s~d~d~d~d%~d~d~d%~s", Str) of
    {ok, [_FS, KB, _Used, _Avail, Cap, _IUsed, _IFree, _ICap, MntOn], RestStr} ->
	    if
		Cap >= Threshold ->
		    set_alarm({disk_almost_full, MntOn}, []);
		true ->
		    clear_alarm({disk_almost_full, MntOn})
	    end,
	    [{MntOn, KB, Cap} |
	     check_disks_susv3(RestStr, Threshold)];
	_Other ->
	    check_disks_susv3(skip_to_eol(Str),Threshold)
    end.

%% Irix: like Linux with an extra FS type column and no '%'.
check_disks_irix("", _Threshold) -> [];
check_disks_irix("\n", _Threshold) -> [];
check_disks_irix(Str, Threshold) ->
    case io_lib:fread("~s~s~d~d~d~d~s", Str) of
	{ok, [_FS, _FSType, KB, _Used, _Avail, Cap, MntOn], RestStr} ->
	    if Cap >= Threshold -> set_alarm({disk_almost_full, MntOn}, []);
	       true             -> clear_alarm({disk_almost_full, MntOn}) end,
	    [{MntOn, KB, Cap} | check_disks_irix(RestStr, Threshold)];
	_Other ->
	    check_disks_irix(skip_to_eol(Str),Threshold)
    end.

check_disks_win32([], _Threshold) ->
    [];
check_disks_win32([H|T], Threshold) ->
    case io_lib:fread("~s~s~d~d~d", H) of
	{ok, [Drive,"DRIVE_FIXED",BAvail,BTot,_TotFree], _RestStr} ->
	    Cap = trunc((BTot-BAvail) / BTot * 100),
	    if
		 Cap >= Threshold ->
		    set_alarm({disk_almost_full, Drive}, []);
		true ->
		    clear_alarm({disk_almost_full, Drive})
	    end,
	    [{Drive, BTot div 1024, Cap} |
	     check_disks_win32(T, Threshold)]; % Return Total Capacity in Kbytes
	{ok,_,_RestStr} ->
	    check_disks_win32(T,Threshold);
	_Other ->
	    []
    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);
	undefined ->
	    ok
    end.

clear_alarms() ->
    lists:foreach(fun({{disk_almost_full, _MntOn} = AlarmId, set}) ->
			  alarm_handler:clear_alarm(AlarmId);
		     (_Other) ->
			  ignore
		  end,
		  get()).

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

%% Type conversion
minutes_to_ms(Minutes) ->
    trunc(60000*Minutes).

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