Current section

Files

Jump to
kraft src kraft_instance.erl
Raw

src/kraft_instance.erl

-module(kraft_instance).
-behavior(gen_server).
% API
-export([start/1]).
-ignore_xref([{?MODULE, start_link, 1}]).
-export([start_link/1]).
-export([stop/1]).
% Callbacks
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-include_lib("kernel/include/logger.hrl").
%--- API -----------------------------------------------------------------------
start(Params) -> kraft_instance_sup:start_instance(Params).
start_link(Params) -> gen_server:start_link(?MODULE, Params, []).
stop(Ref) -> kraft_instance_sup:stop_instance(Ref).
%--- Callbacks -----------------------------------------------------------------
init(#{app := App, owner := Owner, opts := #{port := Port} = Opts} = Params) ->
% FIXME: Use monitor instead
link(Owner),
% Trap exits so terminate/2 gets called
% (see https://www.erlang.org/doc/man/gen_server.html#Module:terminate-2)
process_flag(trap_exit, true),
% Create routes
Dispatch = compile_cowboy(App, maps:get(routes, Params), Opts),
DispatchKey = {kraft_dispatch, App, make_ref()},
persistent_term:put(DispatchKey, Dispatch),
% Content Handlers
kraft_content:init_handlers(App),
% Start Cowboy
ListenerName = listener_name(App),
ProtocolOpts = #{
env => #{dispatch => {persistent_term, DispatchKey}},
stream_handlers => [
kraft_fallback_h,
cowboy_compress_h,
cowboy_stream_h
]
},
{ok, Listener} =
case Opts of
#{ssl_opts := SslOpts} ->
TransportOpts = [{port, Port} | SslOpts],
cowboy:start_tls(ListenerName, TransportOpts, ProtocolOpts);
_ ->
TransportOpts = [{port, Port}],
cowboy:start_clear(ListenerName, TransportOpts, ProtocolOpts)
end,
% FIXME: Use monitor instead
link(Listener),
?LOG_NOTICE(#{started => #{port => Port}}, #{kraft_app => kraft}),
{ok, #{
listener => ListenerName,
dispatch => DispatchKey
}}.
handle_call(Request, From, _State) -> error({unknown_request, Request, From}).
handle_cast(Request, _State) -> error({unknown_cast, Request}).
% TODO: Handler owner/Cowboy crashes here
handle_info(Info, _State) -> error({unknown_info, Info}).
terminate(_Reason, #{listener := ListenerName, dispatch := DispatchKey}) ->
cowboy:stop_listener(ListenerName),
% FIXME: Delete content handler persistent terms
persistent_term:erase(DispatchKey).
%--- Internal ------------------------------------------------------------------
compile_cowboy(App, Routes, #{ssl_opts := SslOpts}) ->
case proplists:is_defined(sni_hosts, SslOpts) of
true -> compile_cowboy_with_sni(App, Routes);
false -> compile_cowboy(App, Routes)
end;
compile_cowboy(App, Routes, _Opts) ->
compile_cowboy(App, Routes).
compile_cowboy(App, Routes) ->
InternalRoutes = {"/kraft", kraft_static, #{app => kraft}},
AllRoutes = routes(App, [InternalRoutes | Routes]),
cowboy_router:compile([{'_', lists:flatten(AllRoutes)}]).
compile_cowboy_with_sni(App, Routes) ->
InternalRoutes = {"/kraft", kraft_static, #{app => kraft}},
AllRoutes = sni_routes(App, Routes, InternalRoutes),
cowboy_router:compile(AllRoutes).
listener_name(App) -> {kraft_listener, App, make_ref()}.
sni_routes(App, Routes, InternalRoutes) ->
lists:map(
fun({Host, HostRoutes}) ->
{Host, routes(App, [InternalRoutes | HostRoutes])}
end,
Routes
).
routes(App, Routes) ->
lists:flatmap(
fun(Route) ->
Attr = element(3, Route),
AppName =
case Attr of
#{app := RouteApp} -> RouteApp;
_Else -> App
end,
kraft_dev:watch(AppName),
route(Route, AppName)
end,
Routes
).
route({Path, {ws, Handler}, MState}, App) ->
route({Path, {ws, Handler}, MState, #{}}, App);
route({Path, {ws, Handler}, MState, Opts}, App) ->
{Module, State} = kraft_ws_util:setup(Opts, App, Handler, MState),
[{Path, Module, State}];
route({Path, kraft_static, #{file := File} = State}, App) ->
StaticFile = filename:join("web/static/", File),
Handler = maps:get(handler, State, cowboy_static),
[{Path, Handler, {priv_file, App, StaticFile}}];
route({Path, kraft_static, State}, App) ->
Handler = maps:get(handler, State, cowboy_static),
static_routes(App, Path, Handler);
route({Path, {cowboy, Handler}, State}, _App) ->
[{Path, Handler, State}];
route({Path, Handler, State}, App) ->
route({Path, Handler, State, #{}}, App);
route({RootPath, Handler, State, Opts}, App) ->
{Mod, Path} =
case Opts of
#{type := rest} -> {kraft_rest, uri_join(RootPath, "[...]")};
#{type := controller} -> {kraft_controller, RootPath};
#{type := Type} -> error({invalid_handler_type, Type});
_ -> {kraft_controller, RootPath}
end,
[
{Path, kraft_handler, #{
mod => Mod,
handler => Handler,
app => App,
state => State,
route => iolist_to_binary(Path)
}}
].
static_routes(App, Path, Handler) ->
Default = [
{
uri_join(Path, "[...]"),
Handler,
{priv_dir, App, "web/static"}
}
],
Static = kraft_file:path(App, static),
Context = {Static, App, Path},
StaticRoute = fun(File, Acc) ->
static_route(File, Context, Handler, Acc)
end,
filelib:fold_files(Static, ".*", true, StaticRoute, Default).
static_route(File, {Static, App, Path}, Handler, Acc) ->
Prefix = string:trim(string:prefix(File, Static), leading, "/"),
case filename:basename(Prefix) of
"index.html" ->
PrivFile = {priv_file, App, filename:join("web/static/", Prefix)},
SubDir =
case filename:dirname(Prefix) of
"." -> "";
Dir -> Dir
end,
IndexPath = uri_join(Path, SubDir),
[{IndexPath, Handler, PrivFile} | Acc];
_ ->
Acc
end.
uri_join(Path, SubPath) ->
Prefix = string:trim(Path, trailing, "/"),
Suffix = string:trim(SubPath, leading, "/"),
string:join([Prefix, Suffix], "/").