| %% @copyright 2007 Mochi Media, Inc. |
| %% @author Matthew Dempsky <matthew@mochimedia.com> |
| %% |
| %% @doc Erlang module for automatically reloading modified modules |
| %% during development. |
| |
| -module(reloader). |
| -author("Matthew Dempsky <matthew@mochimedia.com>"). |
| |
| -include_lib("kernel/include/file.hrl"). |
| |
| -behaviour(gen_server). |
| -export([start/0, start_link/0]). |
| -export([stop/0]). |
| -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). |
| |
| -record(state, {last, tref}). |
| |
| %% External API |
| |
| %% @spec start() -> ServerRet |
| %% @doc Start the reloader. |
| start() -> |
| gen_server:start({local, ?MODULE}, ?MODULE, [], []). |
| |
| %% @spec start_link() -> ServerRet |
| %% @doc Start the reloader. |
| start_link() -> |
| gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). |
| |
| %% @spec stop() -> ok |
| %% @doc Stop the reloader. |
| stop() -> |
| gen_server:call(?MODULE, stop). |
| |
| %% gen_server callbacks |
| |
| %% @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}}. |
| |
| %% @spec handle_call(Args, From, State) -> tuple() |
| %% @doc gen_server callback. |
| handle_call(stop, _From, State) -> |
| {stop, shutdown, stopped, State}; |
| 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) -> |
| Now = stamp(), |
| doit(State#state.last, Now), |
| {noreply, State#state{last = Now}}; |
| 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), |
| ok. |
| |
| |
| %% @spec code_change(_OldVsn, State, _Extra) -> State |
| %% @doc gen_server code_change callback (trivial). |
| code_change(_Vsn, State, _Extra) -> |
| {ok, State}. |
| |
| %% Internal API |
| |
| doit(From, To) -> |
| [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} -> |
| io:format("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]), |
| code:purge(Module), |
| case code:load_file(Module) of |
| {module, Module} -> |
| io:format(" ok.~n"), |
| case erlang:function_exported(Module, test, 0) of |
| true -> |
| io:format(" - Calling ~p:test() ...", [Module]), |
| case catch Module:test() of |
| ok -> |
| io:format(" ok.~n"), |
| reload; |
| Reason -> |
| io:format(" fail: ~p.~n", [Reason]), |
| reload_but_test_failed |
| end; |
| false -> |
| reload |
| end; |
| {error, Reason} -> |
| io:format(" fail: ~p.~n", [Reason]), |
| error |
| end. |
| |
| |
| stamp() -> |
| erlang:localtime(). |