|
|
@ -1,45 +1,18 @@ |
|
|
|
%% The MIT License (MIT) |
|
|
|
%% |
|
|
|
%% Copyright (c) 2014-2024 |
|
|
|
%% Savin Max <mafei.198@gmail.com> |
|
|
|
%% |
|
|
|
%% Permission is hereby granted, free of charge, to any person obtaining a copy |
|
|
|
%% of this software and associated documentation files (the "Software"), to deal |
|
|
|
%% in the Software without restriction, including without limitation the rights |
|
|
|
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
|
|
%% copies of the Software, and to permit persons to whom the Software is |
|
|
|
%% furnished to do so, subject to the following conditions: |
|
|
|
%% |
|
|
|
%% The above copyright notice and this permission notice shall be included in all |
|
|
|
%% copies or substantial portions of the Software. |
|
|
|
%% |
|
|
|
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
|
|
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
|
|
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
|
|
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
|
|
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
|
|
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
|
|
%% SOFTWARE. |
|
|
|
%% |
|
|
|
%% @doc Erlang module for automatically reloading modified modules |
|
|
|
%% during development. |
|
|
|
|
|
|
|
-module(reloader). |
|
|
|
|
|
|
|
-include_lib("kernel/include/file.hrl"). |
|
|
|
|
|
|
|
-behaviour(gen_server). |
|
|
|
|
|
|
|
-export([start/0, start_link/0]). |
|
|
|
-export([register_after_reload/1]). |
|
|
|
-export([stop/0]). |
|
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). |
|
|
|
-export([all_changed/0]). |
|
|
|
-export([is_changed/1]). |
|
|
|
-export([reload_modules/1]). |
|
|
|
-export([reload_all/0]). |
|
|
|
|
|
|
|
-record(state, {last, |
|
|
|
tref, |
|
|
|
after_reload_callback}). |
|
|
|
-record(state, {last, tref}). |
|
|
|
|
|
|
|
%% External API |
|
|
|
|
|
|
@ -58,52 +31,58 @@ start_link() -> |
|
|
|
stop() -> |
|
|
|
gen_server:call(?MODULE, stop). |
|
|
|
|
|
|
|
register_after_reload(Fun) -> |
|
|
|
gen_server:call(?MODULE, {register_after_reload, Fun}). |
|
|
|
|
|
|
|
%% gen_server callbacks |
|
|
|
%% -define(RERODER_CHECK_TIME, 5000). |
|
|
|
|
|
|
|
%% @spec init([]) -> {ok, State} |
|
|
|
%% @doc gen_server init, opens the server in an initial state. |
|
|
|
init([]) -> |
|
|
|
{ok, TRef} = timer:send_interval(timer:seconds(1), doit), |
|
|
|
{ok, #state{last = stamp(), tref = TRef}}. |
|
|
|
%% {ok, TRef} = timer:send_interval(timer:seconds(1), doit), |
|
|
|
%% TimerRef = erlang:send_after(?RERODER_CHECK_TIME, self(), doit), |
|
|
|
%% tref = TimerRef}}. |
|
|
|
{ok, #state{last = stamp()}}. |
|
|
|
|
|
|
|
|
|
|
|
%% @spec handle_call(Args, From, State) -> tuple() |
|
|
|
%% @doc gen_server callback. |
|
|
|
handle_call(stop, _From, State) -> |
|
|
|
{stop, shutdown, stopped, State}; |
|
|
|
handle_call({register_after_reload, Fun}, _From, State) -> |
|
|
|
{reply, ok, State#state{after_reload_callback = Fun}}; |
|
|
|
handle_call(_Req, _From, State) -> |
|
|
|
{reply, {error, badrequest}, State}. |
|
|
|
|
|
|
|
%% @spec handle_cast(Cast, State) -> tuple() |
|
|
|
%% @doc gen_server callback. |
|
|
|
handle_cast(_Req, State) -> |
|
|
|
{noreply, State}. |
|
|
|
|
|
|
|
%% @spec handle_info(Info, State) -> tuple() |
|
|
|
%% @doc gen_server callback. |
|
|
|
handle_info(doit, #state{after_reload_callback = Fun} = State) -> |
|
|
|
handle_cast(doit, State) -> |
|
|
|
error_logger:info_msg("reloader do reload ... ~n", []), |
|
|
|
%% TimerRef = erlang:send_after(?RERODER_CHECK_TIME, self(), doit), |
|
|
|
Now = stamp(), |
|
|
|
ReloadedModules = doit(State#state.last, Now), |
|
|
|
if |
|
|
|
ReloadedModules =/= [] andalso Fun =/= undefined -> |
|
|
|
Fun(ReloadedModules); |
|
|
|
true -> ok |
|
|
|
end, |
|
|
|
{noreply, State#state{last = Now}}; |
|
|
|
try |
|
|
|
_ = doit(State#state.last, Now), |
|
|
|
%% tref = TimerRef |
|
|
|
error_logger:info_msg("reloader done ... ~n", []), |
|
|
|
{noreply, State#state{last = Now}} |
|
|
|
catch |
|
|
|
_:R -> |
|
|
|
error_logger:error_msg( |
|
|
|
"reload failed R:~w Stack:~p~n", [R, erlang:get_stacktrace()]), |
|
|
|
%% reloader failed, no state update |
|
|
|
{noreply, State} |
|
|
|
end; |
|
|
|
handle_cast(_Req, State) -> |
|
|
|
{noreply, State}. |
|
|
|
|
|
|
|
handle_info(_Info, State) -> |
|
|
|
{noreply, State}. |
|
|
|
|
|
|
|
%% @spec terminate(Reason, State) -> ok |
|
|
|
%% @doc gen_server termination callback. |
|
|
|
terminate(_Reason, State) -> |
|
|
|
{ok, cancel} = timer:cancel(State#state.tref), |
|
|
|
terminate(_Reason, _State) -> |
|
|
|
%% erlang:cancel_timer(State#state.tref), |
|
|
|
%% {ok, cancel} = timer:cancel(State#state.tref), |
|
|
|
ok. |
|
|
|
|
|
|
|
|
|
|
|
%% @spec code_change(_OldVsn, State, _Extra) -> State |
|
|
|
%% @doc gen_server code_change callback (trivial). |
|
|
|
code_change(_Vsn, State, _Extra) -> |
|
|
@ -120,6 +99,10 @@ reload_modules(Modules) -> |
|
|
|
all_changed() -> |
|
|
|
[M || {M, Fn} <- code:all_loaded(), is_list(Fn), is_changed(M)]. |
|
|
|
|
|
|
|
%% @spec reload_all() -> [atom()] |
|
|
|
reload_all() -> |
|
|
|
gen_server:cast(?MODULE, doit). |
|
|
|
|
|
|
|
%% @spec is_changed(atom()) -> boolean() |
|
|
|
%% @doc true if the loaded module is a beam with a vsn attribute |
|
|
|
%% and does not match the on-disk beam file, returns false otherwise. |
|
|
@ -141,46 +124,35 @@ module_vsn(L) when is_list(L) -> |
|
|
|
Vsn. |
|
|
|
|
|
|
|
doit(From, To) -> |
|
|
|
lists:foldl(fun({Module, Filename}, Acc) -> |
|
|
|
case is_list(Filename) of |
|
|
|
false -> Acc; |
|
|
|
true -> |
|
|
|
case file:read_file_info(Filename) of |
|
|
|
{ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To -> |
|
|
|
case reload(Module) of |
|
|
|
error -> Acc; |
|
|
|
_ -> [Module | Acc] |
|
|
|
end; |
|
|
|
{ok, _} -> |
|
|
|
% unmodified; |
|
|
|
Acc; |
|
|
|
{error, enoent} -> |
|
|
|
%% The Erlang compiler deletes existing .beam files if |
|
|
|
%% recompiling fails. Maybe it's worth spitting out a |
|
|
|
%% warning here, but I'd want to limit it to just once. |
|
|
|
% gone; |
|
|
|
Acc; |
|
|
|
{error, Reason} -> |
|
|
|
io:format("Error reading ~s's file info: ~p~n", |
|
|
|
[Filename, Reason]), |
|
|
|
% error |
|
|
|
Acc |
|
|
|
end |
|
|
|
end |
|
|
|
end, [], code:all_loaded()). |
|
|
|
[case file:read_file_info(Filename) of |
|
|
|
{ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To -> |
|
|
|
reload(Module); |
|
|
|
{ok, _} -> |
|
|
|
unmodified; |
|
|
|
{error, enoent} -> |
|
|
|
%% The Erlang compiler deletes existing .beam files if |
|
|
|
%% recompiling fails. Maybe it's worth spitting out a |
|
|
|
%% warning here, but I'd want to limit it to just once. |
|
|
|
gone; |
|
|
|
{error, Reason} -> |
|
|
|
error_logger:error_msg("Error reading ~s's file info: ~p~n", |
|
|
|
[Filename, Reason]), |
|
|
|
error |
|
|
|
end || {Module, Filename} <- code:all_loaded(), is_list(Filename)]. |
|
|
|
|
|
|
|
reload(Module) -> |
|
|
|
io:format("Reloading ~p ...", [Module]), |
|
|
|
error_logger:info_msg("Reloading ~p ...", [Module]), |
|
|
|
code:purge(Module), |
|
|
|
case code:load_file(Module) of |
|
|
|
{module, Module} -> |
|
|
|
io:format(" ok.~n"), |
|
|
|
error_logger:info_msg("reload ~w ok.~n", [Module]), |
|
|
|
reload; |
|
|
|
{error, Reason} -> |
|
|
|
io:format(" fail: ~p.~n", [Reason]), |
|
|
|
error_logger:error_msg("reload fail: ~p.~n", [Reason]), |
|
|
|
error |
|
|
|
end. |
|
|
|
|
|
|
|
|
|
|
|
stamp() -> |
|
|
|
erlang:localtime(). |
|
|
|
|