12 |
7u83 |
1 |
%% @author Matthew Dempsky <matthew@mochimedia.com>
|
|
|
2 |
%% @copyright 2007 Mochi Media, Inc.
|
|
|
3 |
%%
|
|
|
4 |
%% Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
5 |
%% copy of this software and associated documentation files (the "Software"),
|
|
|
6 |
%% to deal in the Software without restriction, including without limitation
|
|
|
7 |
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
8 |
%% and/or sell copies of the Software, and to permit persons to whom the
|
|
|
9 |
%% Software is furnished to do so, subject to the following conditions:
|
|
|
10 |
%%
|
|
|
11 |
%% The above copyright notice and this permission notice shall be included in
|
|
|
12 |
%% all copies or substantial portions of the Software.
|
|
|
13 |
%%
|
|
|
14 |
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
15 |
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
16 |
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
17 |
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
18 |
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
19 |
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
20 |
%% DEALINGS IN THE SOFTWARE.
|
|
|
21 |
|
|
|
22 |
%% @doc Erlang module for automatically reloading modified modules
|
|
|
23 |
%% during development.
|
|
|
24 |
|
|
|
25 |
-module(reloader).
|
|
|
26 |
-author("Matthew Dempsky <matthew@mochimedia.com>").
|
|
|
27 |
|
|
|
28 |
-include_lib("kernel/include/file.hrl").
|
|
|
29 |
|
|
|
30 |
-behaviour(gen_server).
|
|
|
31 |
-export([start/0, start_link/0]).
|
|
|
32 |
-export([stop/0]).
|
|
|
33 |
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
|
|
34 |
-export([all_changed/0]).
|
|
|
35 |
-export([is_changed/1]).
|
|
|
36 |
-export([reload_modules/1]).
|
|
|
37 |
-record(state, {last, tref}).
|
|
|
38 |
|
|
|
39 |
%% External API
|
|
|
40 |
|
|
|
41 |
%% @spec start() -> ServerRet
|
|
|
42 |
%% @doc Start the reloader.
|
|
|
43 |
start() ->
|
|
|
44 |
gen_server:start({local, ?MODULE}, ?MODULE, [], []).
|
|
|
45 |
|
|
|
46 |
%% @spec start_link() -> ServerRet
|
|
|
47 |
%% @doc Start the reloader.
|
|
|
48 |
start_link() ->
|
|
|
49 |
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
50 |
|
|
|
51 |
%% @spec stop() -> ok
|
|
|
52 |
%% @doc Stop the reloader.
|
|
|
53 |
stop() ->
|
|
|
54 |
gen_server:call(?MODULE, stop).
|
|
|
55 |
|
|
|
56 |
%% gen_server callbacks
|
|
|
57 |
|
|
|
58 |
%% @spec init([]) -> {ok, State}
|
|
|
59 |
%% @doc gen_server init, opens the server in an initial state.
|
|
|
60 |
init([]) ->
|
|
|
61 |
{ok, TRef} = timer:send_interval(timer:seconds(1), doit),
|
|
|
62 |
{ok, #state{last = stamp(), tref = TRef}}.
|
|
|
63 |
|
|
|
64 |
%% @spec handle_call(Args, From, State) -> tuple()
|
|
|
65 |
%% @doc gen_server callback.
|
|
|
66 |
handle_call(stop, _From, State) ->
|
|
|
67 |
{stop, shutdown, stopped, State};
|
|
|
68 |
handle_call(_Req, _From, State) ->
|
|
|
69 |
{reply, {error, badrequest}, State}.
|
|
|
70 |
|
|
|
71 |
%% @spec handle_cast(Cast, State) -> tuple()
|
|
|
72 |
%% @doc gen_server callback.
|
|
|
73 |
handle_cast(_Req, State) ->
|
|
|
74 |
{noreply, State}.
|
|
|
75 |
|
|
|
76 |
%% @spec handle_info(Info, State) -> tuple()
|
|
|
77 |
%% @doc gen_server callback.
|
|
|
78 |
handle_info(doit, State) ->
|
|
|
79 |
Now = stamp(),
|
|
|
80 |
_ = doit(State#state.last, Now),
|
|
|
81 |
{noreply, State#state{last = Now}};
|
|
|
82 |
handle_info(_Info, State) ->
|
|
|
83 |
{noreply, State}.
|
|
|
84 |
|
|
|
85 |
%% @spec terminate(Reason, State) -> ok
|
|
|
86 |
%% @doc gen_server termination callback.
|
|
|
87 |
terminate(_Reason, State) ->
|
|
|
88 |
{ok, cancel} = timer:cancel(State#state.tref),
|
|
|
89 |
ok.
|
|
|
90 |
|
|
|
91 |
|
|
|
92 |
%% @spec code_change(_OldVsn, State, _Extra) -> State
|
|
|
93 |
%% @doc gen_server code_change callback (trivial).
|
|
|
94 |
code_change(_Vsn, State, _Extra) ->
|
|
|
95 |
{ok, State}.
|
|
|
96 |
|
|
|
97 |
%% @spec reload_modules([atom()]) -> [{module, atom()} | {error, term()}]
|
|
|
98 |
%% @doc code:purge/1 and code:load_file/1 the given list of modules in order,
|
|
|
99 |
%% return the results of code:load_file/1.
|
|
|
100 |
reload_modules(Modules) ->
|
|
|
101 |
[begin code:purge(M), code:load_file(M) end || M <- Modules].
|
|
|
102 |
|
|
|
103 |
%% @spec all_changed() -> [atom()]
|
|
|
104 |
%% @doc Return a list of beam modules that have changed.
|
|
|
105 |
all_changed() ->
|
|
|
106 |
[M || {M, Fn} <- code:all_loaded(), is_list(Fn), is_changed(M)].
|
|
|
107 |
|
|
|
108 |
%% @spec is_changed(atom()) -> boolean()
|
|
|
109 |
%% @doc true if the loaded module is a beam with a vsn attribute
|
|
|
110 |
%% and does not match the on-disk beam file, returns false otherwise.
|
|
|
111 |
is_changed(M) ->
|
|
|
112 |
try
|
|
|
113 |
module_vsn(M:module_info()) =/= module_vsn(code:get_object_code(M))
|
|
|
114 |
catch _:_ ->
|
|
|
115 |
false
|
|
|
116 |
end.
|
|
|
117 |
|
|
|
118 |
%% Internal API
|
|
|
119 |
|
|
|
120 |
module_vsn({M, Beam, _Fn}) ->
|
|
|
121 |
{ok, {M, Vsn}} = beam_lib:version(Beam),
|
|
|
122 |
Vsn;
|
|
|
123 |
module_vsn(L) when is_list(L) ->
|
|
|
124 |
{_, Attrs} = lists:keyfind(attributes, 1, L),
|
|
|
125 |
{_, Vsn} = lists:keyfind(vsn, 1, Attrs),
|
|
|
126 |
Vsn.
|
|
|
127 |
|
|
|
128 |
doit(From, To) ->
|
|
|
129 |
[case file:read_file_info(Filename) of
|
|
|
130 |
{ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To ->
|
|
|
131 |
reload(Module);
|
|
|
132 |
{ok, _} ->
|
|
|
133 |
unmodified;
|
|
|
134 |
{error, enoent} ->
|
|
|
135 |
%% The Erlang compiler deletes existing .beam files if
|
|
|
136 |
%% recompiling fails. Maybe it's worth spitting out a
|
|
|
137 |
%% warning here, but I'd want to limit it to just once.
|
|
|
138 |
gone;
|
|
|
139 |
{error, Reason} ->
|
|
|
140 |
io:format("Error reading ~s's file info: ~p~n",
|
|
|
141 |
[Filename, Reason]),
|
|
|
142 |
error
|
|
|
143 |
end || {Module, Filename} <- code:all_loaded(), is_list(Filename)].
|
|
|
144 |
|
|
|
145 |
reload(Module) ->
|
|
|
146 |
io:format("Reloading ~p ...", [Module]),
|
|
|
147 |
code:purge(Module),
|
|
|
148 |
case code:load_file(Module) of
|
|
|
149 |
{module, Module} ->
|
|
|
150 |
io:format(" ok.~n"),
|
|
|
151 |
case erlang:function_exported(Module, test, 0) of
|
|
|
152 |
true ->
|
|
|
153 |
io:format(" - Calling ~p:test() ...", [Module]),
|
|
|
154 |
case catch Module:test() of
|
|
|
155 |
ok ->
|
|
|
156 |
io:format(" ok.~n"),
|
|
|
157 |
reload;
|
|
|
158 |
Reason ->
|
|
|
159 |
io:format(" fail: ~p.~n", [Reason]),
|
|
|
160 |
reload_but_test_failed
|
|
|
161 |
end;
|
|
|
162 |
false ->
|
|
|
163 |
reload
|
|
|
164 |
end;
|
|
|
165 |
{error, Reason} ->
|
|
|
166 |
io:format(" fail: ~p.~n", [Reason]),
|
|
|
167 |
error
|
|
|
168 |
end.
|
|
|
169 |
|
|
|
170 |
|
|
|
171 |
stamp() ->
|
|
|
172 |
erlang:localtime().
|
|
|
173 |
|
|
|
174 |
%%
|
|
|
175 |
%% Tests
|
|
|
176 |
%%
|
|
|
177 |
-ifdef(TEST).
|
|
|
178 |
-include_lib("eunit/include/eunit.hrl").
|
|
|
179 |
-endif.
|