src/nova_sup.erl

%%% @author Niclas Axelsson <niclas@burbas.se>
%%% @doc
%%% Nova supervisor
%%% @end

-module(nova_sup).

-behaviour(supervisor).

%% API
-export([start_link/0]).

%% Supervisor callbacks
-export([init/1]).

-include_lib("kernel/include/logger.hrl").
-include("nova.hrl").

-define(SERVER, ?MODULE).

%%%===================================================================
%%% API functions
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the supervisor
%%
%% @end
%%--------------------------------------------------------------------
-spec start_link() -> {ok, Pid :: pid()} | ignore | {error, Error :: any()}.
start_link() ->
    supervisor:start_link({local, ?SERVER}, ?MODULE, []).

%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Whenever a supervisor is started using supervisor:start_link/[2,3],
%% this function is called by the new process to find out about
%% restart strategy, maximum restart intensity, and child
%% specifications.
%%
%% @end
%%--------------------------------------------------------------------
init([]) ->
    %% This is a bit ugly, but we need to do this anyhow(?)
    SupFlags = #{strategy => one_for_one,
                 intensity => 1,
                 period => 5},

    Environment = nova:get_environment(),

    nova_pubsub:start(),

    ?LOG_NOTICE(#{msg => <<"Starting nova">>, environment => Environment}),

    Configuration = application:get_env(nova, cowboy_configuration, #{}),

    SessionManager = application:get_env(nova, session_manager, nova_session_ets),

    Children = [
                child(nova_handlers, nova_handlers),
                child(SessionManager, SessionManager),
                child(nova_watcher, nova_watcher)
               ],

    setup_cowboy(Configuration),


    {ok, {SupFlags, Children}}.




%%%===================================================================
%%% Internal functions
%%%===================================================================
child(Id, Type, Mod, Args) ->
    #{id => Id,
      start => {Mod, start_link, Args},
      restart => permanent,
      shutdown => 5000,
      type => Type,
      modules => [Mod]}.

child(Id, Type, Mod) ->
    child(Id, Type, Mod, []).

child(Id, Mod) ->
    child(Id, worker, Mod).

setup_cowboy(Configuration) ->
    case start_cowboy(Configuration) of
        {ok, App, Host, Port} ->
            Host0 = inet:ntoa(Host),
            CowboyVersion = get_version(cowboy),
            NovaVersion = get_version(nova),
            UseStacktrace = application:get_env(nova, use_stacktrace, false),
            persistent_term:put(nova_use_stacktrace, UseStacktrace),
            ?LOG_NOTICE(#{msg => <<"Nova is running">>,
                          url => unicode:characters_to_list(io_lib:format("http://~s:~B", [Host0, Port])),
                          cowboy_version => CowboyVersion, nova_version => NovaVersion, app => App});
        {error, Error} ->
            ?LOG_ERROR(#{msg => <<"Cowboy could not start">>, reason => Error})
    end.

-spec start_cowboy(Configuration :: map()) ->
          {ok, BootstrapApp :: atom(), Host :: string() | {integer(), integer(), integer(), integer()},
           Port :: integer()} | {error, Reason :: any()}.
start_cowboy(Configuration) ->
    Middlewares = [
                   nova_router, %% Lookup routes
                   nova_plugin_handler, %% Handle pre-request plugins
                   nova_security_handler, %% Handle security
                   nova_handler, %% Controller
                   nova_plugin_handler %% Handle post-request plugins
                  ],
    StreamHandlers = maps:get(stream_handlers, Configuration, [nova_stream_h, cowboy_compress_h, cowboy_stream_h]),
    Options = maps:get(options, Configuration, #{compress => true}),

    %% Build the options map
    CowboyOptions1 = Options#{middlewares => Middlewares,
                              stream_handlers => StreamHandlers},

    BootstrapApp = application:get_env(nova, bootstrap_application, undefined),
    %% Compile the routes
    Dispatch =
        case BootstrapApp of
            undefined ->
                ?LOG_ERROR(#{msg => <<"You need to define bootstrap_application option in configuration">>}),
                throw({error, no_nova_app_defined});
            App ->
                ExtraApps = application:get_env(App, nova_apps, []),
                nova_router:compile([nova|[App|ExtraApps]])
        end,

    CowboyOptions2 =
        case application:get_env(nova, use_persistent_term, true) of
            true ->
                CowboyOptions1;
            _ ->
                CowboyOptions1#{env => #{dispatch => Dispatch}}
        end,

    Host = maps:get(ip, Configuration, { 0, 0, 0, 0}),

    case maps:get(use_ssl, Configuration, false) of
        false ->
            Port = maps:get(port, Configuration, ?NOVA_STD_PORT),
            case cowboy:start_clear(
                   ?NOVA_LISTENER,
                   [{port, Port},
                    {ip, Host}],
                   CowboyOptions2) of
                {ok, _Pid} ->
                    {ok, BootstrapApp, Host, Port};
                Error ->
                    Error
            end;
        _ ->
            CACert = maps:get(ca_cert, Configuration),
            Cert = maps:get(cert, Configuration),
            Port = maps:get(ssl_port, Configuration, ?NOVA_STD_SSL_PORT),
            ?LOG_NOTICE(#{msg => <<"Nova starting SSL">>, port => Port}),
            case cowboy:start_tls(
              ?NOVA_LISTENER, [
                               {port, Port},
                               {ip, Host},
                               {certfile, Cert},
                               {cacertfile, CACert}
                              ],
              CowboyOptions2) of
                {ok, _Pid} ->
                    {ok, BootstrapApp, Host, Port};
                Error ->
                    Error
            end
    end.


get_version(Application) ->
    case lists:keyfind(Application, 1, application:loaded_applications()) of
        {_, _, Version} ->
            Version;
        false ->
            not_found
    end.