Current section
Files
Jump to
Current section
Files
src/kraft_dev.erl
-module(kraft_dev).
-behavior(gen_server).
% API
-export([maybe_start/1]).
-export([start_link/0]).
-export([watch/1]).
% Callbacks
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-include_lib("kernel/include/logger.hrl").
-include_lib("kernel/include/file.hrl").
% -compile(export_all).
-record(s, {
watchexec = disabled,
dirs = #{},
monitors = #{}
}).
%--- API -----------------------------------------------------------------------
maybe_start(Opts) ->
case mode(Opts) of
dev ->
{ok, Pid} = kraft_dev:start_link(),
persistent_term:put(?MODULE, Pid);
_Other ->
ok
end.
start_link() ->
case gen_server:start_link({local, ?MODULE}, ?MODULE, undefined, []) of
{ok, Pid} -> {ok, Pid};
{error, {already_started, Pid}} -> {ok, Pid}
end.
watch(App) ->
Dir = filename:join(code:lib_dir(App), "priv/web/templates"),
case persistent_term:get(?MODULE, undefined) of
undefined -> ok;
Pid -> gen_server:call(Pid, {watch, App, resolve_symlinks(Dir)})
end.
%--- Callbacks -----------------------------------------------------------------
init(undefined) ->
State =
case os:find_executable("watchexec") of
false ->
?LOG_WARNING(#{
reason => watchexec_missing,
message => "Template reloading will be disabled"
}),
#s{};
Path ->
#s{watchexec = Path}
end,
{ok, State}.
handle_call({watch, App, Dir}, _From, S) when is_map_key(Dir, S#s.dirs) ->
?LOG_DEBUG(#{monitor => Dir, event => duplicate}, #{kraft_app => App}),
{reply, ok, S};
handle_call({watch, _App, _Dir}, _From, #s{watchexec = disabled} = S) ->
{reply, {error, watchexec_disabled}, S};
handle_call({watch, App, Dir}, _From, #s{dirs = Dirs} = S) ->
case filelib:is_dir(Dir) of
false ->
{reply, {error, enoent}, S};
true ->
Port = watchexec_start(Dir),
{reply, ok, S#s{
monitors = maps:put(
Port, #{app => App, dir => Dir}, S#s.monitors
),
dirs = maps:put(Dir, Port, Dirs)
}}
end;
handle_call(Request, From, _State) ->
error({unknown_request, Request, From}).
handle_cast(Request, _State) -> error({unknown_cast, Request}).
handle_info({Port, {data, {eol, Line}}}, S) ->
Event = watchexec_line_event(Line),
#{app := App, dir := Dir} = maps:get(Port, S#s.monitors),
handle(App, Dir, Event),
{noreply, S};
handle_info({Port, {exit_status, Status}}, S) ->
#{app := App, dir := Dir} = maps:get(Port, S#s.monitors),
?LOG_ERROR(
#{monitor => Dir, event => {watcher_error, Port, {exit, Status}}},
#{kraft_app => App}
),
{noreply, S#s{
monitors = maps:remove(Port, S#s.monitors),
dirs = maps:remove(Dir, S#s.dirs)
}};
handle_info(Info, _State) ->
error({unknown_info, Info}).
%--- Internal ------------------------------------------------------------------
handle(_App, _Dir, #{path := #{filetype := <<"dir">>}}) ->
ok;
handle(App, Dir, #{path := #{absolute := Path}}) ->
handle_file(App, Dir, Path).
handle_file(App, Dir, File) ->
Relative = string:prefix(File, Dir),
RootName = filename:rootname(Relative, <<".mustache">>),
try
case filelib:is_regular(File) of
false -> kraft_template:remove(App, RootName);
true -> kraft_template:reload(App, RootName)
end
catch
error:{missing_template, App, Path} ->
?LOG_DEBUG(#{
app => App,
message => "Ignoring unknown template",
file => File,
path => Path,
relative => Relative
});
Class:Reason:ST ->
?LOG_ERROR(#{
app => App,
class => Class,
reason => Reason,
file => File,
message => "Could not reload template",
stacktrace => ST
})
end.
mode(#{mode := Mode}) ->
Mode;
mode(_Opts) ->
case code:is_loaded(rebar3) of
{file, _File} -> dev;
_Else -> prod
end.
resolve_symlinks(Path) ->
resolve_symlinks(filename:split(Path), []).
resolve_symlinks([], Stack) ->
filename:join(Stack);
resolve_symlinks([Dir | Path], Stack) ->
Current = Stack ++ [Dir],
CurrentDir = filename:join(Current),
case file:read_link_info(CurrentDir) of
{ok, #file_info{type = symlink}} ->
{ok, Dest} = file:read_link(CurrentDir),
FullDest = filename:absname(
filename:join(Stack ++ filename:split(Dest))
),
Abs = absolute_path(filename:split(FullDest)),
resolve_symlinks(Abs ++ Path, []);
_ ->
resolve_symlinks(Path, Current)
end.
absolute_path(Path) -> absolute_path(Path, []).
absolute_path([], Acc) ->
lists:reverse(Acc);
absolute_path([".." | Path], [Prev | Acc]) when Prev =/= ".." ->
absolute_path(Path, Acc);
absolute_path([Dir | Path], Acc) ->
absolute_path(Path, [Dir | Acc]).
watchexec_start(Dir) ->
Command = lists:flatten(watchexec_cmd(Dir)),
?LOG_DEBUG("Starting watchexec: ~p", [Command]),
erlang:open_port(
{spawn, Command},
[{line, 1024}, exit_status, use_stdio, binary, stderr_to_stdout]
).
watchexec_cmd(Dir) ->
[
"watchexec",
" --no-discover-ignore",
" --no-meta",
" --only-emit-events",
" --emit-events-to json-stdio",
" --watch ",
Dir
].
watchexec_line_event(Line) ->
#{tags := Tags} = kraft_json:decode(Line),
#{
binary_to_atom(Kind) => maps:without([Kind], Tag)
|| #{kind := Kind} = Tag <- Tags
}.