src/quicer_listener.erl

%%--------------------------------------------------------------------
%% Copyright (c) 2021-2024 EMQ Technologies Co., Ltd. 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.
%%--------------------------------------------------------------------
-module(quicer_listener).

-behaviour(gen_server).

-include("quicer_types.hrl").

%% API
-export([
    start_link/3,
    start_listener/3,
    stop_listener/1,
    lock/2,
    unlock/2,
    reload/2,
    reload/3,
    get_conf/2,
    get_handle/2,
    count_conns/1
]).

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

-record(state, {
    name :: atom(),
    listen_on :: quicer:listen_on(),
    listener :: quicer:listener_handle(),
    conn_sup :: pid(),
    alpn :: [string()],
    opts_tab :: ets:tid()
}).

-export_type([
    listener_name/0,
    listener_opts/0
]).

-type listener_name() :: atom().

-type listener_opts() :: {
    listen_opts() | proplists:proplist(),
    conn_opts() | proplists:proplist(),
    stream_opts() | proplists:proplist()
}.

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%% @end
%%--------------------------------------------------------------------
-spec start_link(
    Name :: listener_name(),
    ListenOn :: quicer:listen_on(),
    Options ::
        {quicer:listener_opts(), quicer:conn_opts(), quicer:stream_opts()}
) ->
    {ok, Pid :: pid()}
    | {error, Error :: {already_started, pid()}}
    | {error, Error :: term()}
    | ignore.
start_link(Name, ListenOn, Opts) ->
    gen_server:start_link({local, Name}, ?MODULE, [Name, ListenOn, Opts], []).

start_listener(Name, ListenOn, Options) ->
    quicer_listener_sup:start_listener(Name, ListenOn, Options).

stop_listener(Name) ->
    quicer_listener_sup:stop_listener(Name).

-spec lock(pid(), timeout()) -> ok | {error, _}.
lock(Pid, Timeout) ->
    gen_server:call(Pid, lock, Timeout).

-spec unlock(pid(), timeout()) -> ok | {error, _}.
unlock(Pid, Timeout) ->
    gen_server:call(Pid, unlock, Timeout).

%% @doc Reload the listener with new *listener* opts.
%% @NOTE: the acceptor opts and stream opts are not reloaded.
%%%       if you want to reload them, you should restart the listener (terminate and spawn).
%% @end
-spec reload(pid(), NewConf :: map()) -> ok | {error, _}.
reload(Pid, NewConf) ->
    gen_server:call(Pid, {reload, NewConf}, infinity).

%% @doc Reload the listener with new *listener* opts and new listen_on.
%% @NOTE: the acceptor opts and stream opts are not reloaded.
%%%       if you want to reload them, you should restart the listener (terminate and spawn).
%% @end
-spec reload(pid(), quicer:listen_on(), NewConf :: listener_opts()) ->
    ok | {error, _}.
reload(Pid, ListenOn, NewConf) ->
    gen_server:call(Pid, {reload, ListenOn, NewConf}, infinity).

-spec get_handle(pid(), timeout()) -> quicer:listener_handle().
get_handle(Pid, Timeout) ->
    gen_server:call(Pid, get_handle, Timeout).

%% @doc count accepted but not yet closed connections
-spec count_conns(pid()) -> non_neg_integer().
count_conns(Pid) ->
    gen_server:call(Pid, count_conns, infinity).

%% @doc get the listener configuration
-spec get_conf(pid(), timeout()) -> listener_opts().
get_conf(Pid, Timeout) ->
    gen_server:call(Pid, get_conf, Timeout).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%% @end
%%--------------------------------------------------------------------
-spec init(Args :: term()) ->
    {ok, State :: term()}
    | {ok, State :: term(), Timeout :: timeout()}
    | {ok, State :: term(), hibernate}
    | {stop, Reason :: term()}
    | ignore.

init([Name, ListenOn, {LOpts, COpts, SOpts}]) when is_list(LOpts) ->
    init([Name, ListenOn, {maps:from_list(LOpts), COpts, SOpts}]);
init([Name, ListenOn, {#{conn_acceptors := N, alpn := Alpn} = LOpts, _COpts, _SOpts} = Opts]) ->
    process_flag(trap_exit, true),
    OptsTab = init_opts_tab(Opts),
    {ok, L} = quicer:listen(ListenOn, maps:without([conn_acceptors], LOpts)),
    {ok, ConnSup} = supervisor:start_link(quicer_conn_acceptor_sup, [L, OptsTab]),
    _ = [{ok, _} = supervisor:start_child(ConnSup, [ConnSup]) || _ <- lists:seq(1, N)],
    {ok, #state{
        name = Name,
        listen_on = ListenOn,
        listener = L,
        conn_sup = ConnSup,
        opts_tab = OptsTab,
        alpn = Alpn
    }}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%% @end
%%--------------------------------------------------------------------
-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
    {reply, Reply :: term(), NewState :: term()}
    | {reply, Reply :: term(), NewState :: term(), Timeout :: timeout()}
    | {reply, Reply :: term(), NewState :: term(), hibernate}
    | {noreply, NewState :: term()}
    | {noreply, NewState :: term(), Timeout :: timeout()}
    | {noreply, NewState :: term(), hibernate}
    | {stop, Reason :: term(), Reply :: term(), NewState :: term()}
    | {stop, Reason :: term(), NewState :: term()}.
handle_call(get_handle, _From, State) ->
    {reply, {ok, State#state.listener}, State};
handle_call(lock, _From, State) ->
    Res = quicer:stop_listener(State#state.listener),
    {reply, Res, State};
handle_call(unlock, _From, State) ->
    LOpts = ets:lookup_element(State#state.opts_tab, l_opts, 2),
    Res = quicer:start_listener(
        State#state.listener,
        State#state.listen_on,
        LOpts
    ),
    {reply, Res, State};
handle_call({reload, NewConf}, _From, State) ->
    {Res, NewState} = do_reload(State#state.listen_on, NewConf, State),
    {reply, Res, NewState};
handle_call({reload, NewListenOn, NewConf}, _From, State) ->
    {Res, NewState} = do_reload(NewListenOn, NewConf, State),
    {reply, Res, NewState};
handle_call(
    count_conns,
    _From,
    #state{
        conn_sup = ConnSup,
        opts_tab = OptsTab
    } =
        State
) ->
    #{conn_acceptors := NoAcceptors} = ets:lookup_element(OptsTab, l_opts, 2),
    {active, ActiveCnt} = lists:nth(2, supervisor:count_children(ConnSup)),
    {reply, ActiveCnt - NoAcceptors, State};
handle_call(get_conf, _From, #state{opts_tab = OptsTab} = State) ->
    {reply,
        {
            ets:lookup_element(OptsTab, l_opts, 2),
            ets:lookup_element(OptsTab, c_opts, 2),
            ets:lookup_element(OptsTab, s_opts, 2)
        },
        State};
handle_call(Request, _From, State) ->
    Reply = {error, {unimpl, Request}},
    {reply, Reply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_cast(Request :: term(), State :: term()) ->
    {noreply, NewState :: term()}
    | {noreply, NewState :: term(), Timeout :: timeout()}
    | {noreply, NewState :: term(), hibernate}
    | {stop, Reason :: term(), NewState :: term()}.
handle_cast(_Request, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%% @end
%%--------------------------------------------------------------------
-spec handle_info(Info :: timeout() | term(), State :: term()) ->
    {noreply, NewState :: term()}
    | {noreply, NewState :: term(), Timeout :: timeout()}
    | {noreply, NewState :: term(), hibernate}
    | {stop, Reason :: normal | term(), NewState :: term()}.
handle_info({quic, listener_stopped, L}, #state{listener = L} = State) ->
    %% uncontroled stop:
    _ = quicer:close_listener(L),
    {stop, normal, State};
handle_info(_Info, State) ->
    {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%% @end
%%--------------------------------------------------------------------
-spec terminate(
    Reason :: normal | shutdown | {shutdown, term()} | term(),
    State :: term()
) -> any().
terminate(_Reason, #state{listener = L}) ->
    %% nif listener has no owner process so we need to close it explicitly.
    _ = quicer:close_listener(L),
    ok.

-spec do_reload(quicer:listen_on(), listener_opts(), #state{}) ->
    {ok | {error, any()}, #state{}}.
do_reload(ListenOn, {LOpts, _, _} = NewConf, #state{opts_tab = OptsTab} = State) ->
    _ = quicer:stop_listener(State#state.listener),
    Res = quicer:start_listener(
        State#state.listener,
        ListenOn,
        LOpts
    ),
    case Res of
        ok ->
            true = conf_tab_refresh(OptsTab, NewConf),
            {ok, State#state{listen_on = ListenOn}};
        Error ->
            {Error, State}
    end.

init_opts_tab({_LOpts, _COpts, _SOpts} = Opts) ->
    Tab = ets:new(quicer_listener_tab, [set, {keypos, 1}, {read_concurrency, true}]),
    %% @NOTE: Be careful with the lifecyle of the items in this table.
    %%        handles in this table cannot be released until table is gone.
    true = conf_tab_refresh(Tab, Opts),
    Tab.

conf_tab_refresh(Tab, {LOpts, COpts, SOpts}) ->
    ets:insert(Tab, [
        {l_opts, to_map(LOpts)},
        {c_opts, to_map(COpts)},
        {s_opts, to_map(SOpts)}
    ]).

to_map(Opts) when is_list(Opts) ->
    maps:from_list(Opts);
to_map(Opts) when is_map(Opts) ->
    Opts.