%% -------------------------------------------------------------------
%%
%% Copyright (c) 2022 Alejandro M. Ramallo. All Rights Reserved.
%%
%% This file is provided to you 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.
%%
%% -------------------------------------------------------------------
%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-module(partisan_inet).
-behaviour(gen_server).
-include("partisan.hrl").
-include("partisan_logger.hrl").
-define(CHECK_MSECS, timer:seconds(2)).
-record(state, {
atomics_ref :: atomics:atomics_ref(),
pids = #{} :: #{pid() := reference()}
}).
%% API
-export([monitor/1]).
-export([net_status/0]).
-export([net_status/1]).
-export([start_link/0]).
%% gen_server callbacks
-export([code_change/3]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([init/1]).
-export([terminate/2]).
-compile({no_auto_import, [monitor/1]}).
%% =============================================================================
%% API
%% =============================================================================
%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% -----------------------------------------------------------------------------
%% @doc Returns `connected' if the host has at least one non-loopback network
%% interface address. Otherwise returns `disconnected'.
%% @end
%% -----------------------------------------------------------------------------
-spec net_status() -> connected | disconnected.
net_status() ->
case persistent_term:get({?MODULE, atomics_ref}) of
undefined ->
status_name(check_net_status());
Ref ->
status_name(atomics:get(Ref, 1))
end.
%% -----------------------------------------------------------------------------
%% @doc Returns `connected' if the host has at least one non-loopback network
%% interface address. Otherwise returns `disconnected'.
%% @end
%% -----------------------------------------------------------------------------
-spec net_status([nocache]) -> connected | disconnected.
net_status([nocache]) ->
status_name(check_net_status());
net_status(_) ->
net_status().
%% -----------------------------------------------------------------------------
%% @doc
%% @end
%% -----------------------------------------------------------------------------
-spec monitor(Flag :: boolean()) -> ok.
monitor(true) ->
gen_server:call(?MODULE, monitor);
monitor(false) ->
gen_server:call(?MODULE, demonitor).
%% =============================================================================
%% GEN_SERVER CALLBACKS
%% =============================================================================
init([]) ->
Ref = atomics:new(1, [{signed, false}]),
_ = persistent_term:put({?MODULE, atomics_ref}, Ref),
State = #state{atomics_ref = Ref},
ok = atomics:put(State#state.atomics_ref, 1, check_net_status()),
ok = schedule_check(),
{ok, State}.
handle_call(monitor, {Pid, _Tag}, State0) ->
Pids0 = State0#state.pids,
State = case maps:find(Pid, Pids0) of
{ok, _} ->
%% Already monitoring
State0;
error ->
Ref = partisan:monitor(process, Pid),
Pids = maps:put(Pid, Ref, Pids0),
State0#state{pids = Pids}
end,
{reply, ok, State};
handle_call(demonitor, {Pid, _Tag}, State0) ->
Pids0 = State0#state.pids,
State = case maps:find(Pid, Pids0) of
{ok, _} ->
Pids = maps:remove(Pid, Pids0),
State0#state{pids = Pids};
error ->
State0
end,
{reply, ok, State};
handle_call(_, _From, State) ->
{reply, {error, unknown}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info({'DOWN', _Ref, process, Pid, _Reason}, State0) ->
Pids0 = State0#state.pids,
Pids = maps:remove(Pid, Pids0),
State = State0#state{pids = Pids},
{noreply, State};
handle_info(check, State) ->
Status = check_net_status(),
case atomics:exchange(State#state.atomics_ref, 1, Status) of
Status ->
%% No change, we do nothing
ok;
0 ->
%% We were disconnected before, now connected
notify(connected, State);
1 ->
%% We were connected before, now disconnected
notify(disconnected, State)
end,
ok = schedule_check(),
{noreply, State, hibernate};
handle_info(_Msg, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% =============================================================================
%% PRIVATE
%% =============================================================================
%% @private
status_name(0) -> disconnected;
status_name(1) -> connected.
%% @private
check_net_status() ->
{ok, L} = net:getifaddrs(
fun
(#{addr := #{family := Family}, flags := Flags})
when Family == inet ->
% when Family == inet orelse Family == inet6 ->
not lists:member(loopback, Flags);
(_) ->
false
end
),
case L == [] of
true -> 0;
false -> 1
end.
%% @private
schedule_check() ->
_ = erlang:send_after(?CHECK_MSECS, self(), check),
ok.
%% @private
notify(Status, State) ->
maps:foreach(
fun(Pid, Ref) ->
case Status of
connected ->
Pid ! {network_connected, Ref};
disconnected ->
Pid ! {network_disconnected, Ref}
end
end,
State#state.pids
).