Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Tree: c26e4e3af1
Fetching contributors…

Cannot retrieve contributors at this time

478 lines (401 sloc) 16.748 kB
%% -*- mode: nitrogen -*-
-module(sync_scanner).
-behaviour(gen_server).
%% API
-export([
start_link/0,
rescan/0,
info/0,
enable_patching/0
]).
%% gen_server callbacks
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3,
set_growl/1,
get_growl/0
]).
-define(SERVER, ?MODULE).
-define(PRINT(Var), io:format("DEBUG: ~p:~p - ~p~n~n ~p~n~n", [?MODULE, ?LINE, ??Var, Var])).
-record(state, {
modules,
src_dirs,
src_files,
beam_lastmod,
src_file_lastmod,
timers,
patching = false
}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
rescan() ->
io:format("Scanning source files...~n"),
gen_server:cast(?SERVER, discover_modules),
gen_server:cast(?SERVER, discover_src_dirs),
gen_server:cast(?SERVER, discover_src_files),
gen_server:cast(?SERVER, compare_beams),
gen_server:cast(?SERVER, compare_src_files),
ok.
info() ->
io:format("Sync Info...~n"),
gen_server:cast(?SERVER, info),
ok.
%% I know it's kinda sloppy to get and set env vars to determine if we should
%% be printing growl messages, but, hey, it works, and since growl/3 is called
%% from within the server, we can't call gen_server:call to get the value or it
%% will just hang. So env vars is the easy copout like using the process dict
%% TODO: make not use env_var for this :)
set_growl(true) ->
sync_utils:set_env(growl,true),
growl_success("Sync","Notifications Enabled"),
ok;
set_growl(skip_success) ->
growl_success("Sync","Notifications Enabled (skip success)"),
sync_utils:set_env(growl,skip_success),
ok;
set_growl(false) ->
growl_success("Sync","Notifications Disabled"),
sync_utils:set_env(growl,false),
ok.
get_growl() ->
case sync_utils:get_env(growl,true) of
Val when is_boolean(Val) -> Val;
_ -> true
end.
enable_patching() ->
gen_server:cast(?SERVER, enable_patching),
ok.
init([]) ->
%% Trap exits to catch failing processes...
erlang:process_flag(trap_exit, true),
%% Kick off the discovery process...
rescan(),
%% Display startup message...
case get_growl() of
true ->
growl_success("Sync", "The Sync utility is now running.");
false ->
io:format("Growl notifications disabled~n")
end,
%% Create the state and return...
State = #state {
modules = [],
src_dirs = [],
src_files = [],
beam_lastmod = undefined,
src_file_lastmod = undefined,
timers=[]
},
{ok, State}.
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
handle_cast(discover_modules, State) ->
%% Get a list of all loaded non-system modules.
Modules = erlang:loaded() -- sync_utils:get_system_modules(),
%% Schedule the next interval...
NewTimers = schedule_cast(discover_modules, 30000, State#state.timers),
%% Return with updated modules...
NewState = State#state { modules=Modules, timers=NewTimers },
{noreply, NewState};
handle_cast(discover_src_dirs, State) ->
%% Extract the compile / options / source / dir from each module.
F = fun(X, Acc) ->
%% Get the dir...
case sync_utils:get_src_dir_from_module(X) of
{ok, Dir} ->
%% Get the options, store under the dir...
{ok, Options1} = sync_utils:get_options_from_module(X),
Options2 = sync_utils:transform_options(Dir, Options1),
%% Store the options for later reference...
sync_options:set_options(Dir, Options2),
%% Return the dir...
[Dir|Acc];
undefined ->
Acc
end
end,
Dirs = lists:usort(lists:foldl(F, [], State#state.modules)),
%% Schedule the next interval...
NewTimers = schedule_cast(discover_src_dirs, 30000, State#state.timers),
%% Return with updated dirs...
NewState = State#state { src_dirs=Dirs, timers=NewTimers },
{noreply, NewState};
handle_cast(discover_src_files, State) ->
%% For each source dir, get a list of source files...
F = fun(X, Acc) ->
sync_utils:wildcard(X, ".*\.erl$") ++ Acc
end,
Files = lists:usort(lists:foldl(F, [], State#state.src_dirs)),
%% Schedule the next interval...
NewTimers = schedule_cast(discover_src_files, 5000, State#state.timers),
%% Return with updated files...
NewState = State#state { src_files=Files, timers=NewTimers },
{noreply, NewState};
handle_cast(compare_beams, State) ->
%% Create a list of beam file lastmod times...
F = fun(X) ->
Beam = code:which(X),
LastMod = filelib:last_modified(Beam),
{X, LastMod}
end,
NewBeamLastMod = lists:usort([F(X) || X <- State#state.modules]),
%% Compare to previous results, if there are changes, then reload
%% the beam...
process_beam_lastmod(State#state.beam_lastmod, NewBeamLastMod, State#state.patching),
%% Schedule the next interval...
NewTimers = schedule_cast(compare_beams, 2000, State#state.timers),
%% Return with updated beam lastmod...
NewState = State#state { beam_lastmod=NewBeamLastMod, timers=NewTimers },
{noreply, NewState};
handle_cast(compare_src_files, State) ->
%% Create a list of file lastmod times...
F = fun(X) ->
LastMod = filelib:last_modified(X),
{X, LastMod}
end,
NewSrcFileLastMod = lists:usort([F(X) || X <- State#state.src_files]),
%% Compare to previous results, if there are changes, then recompile the file...
process_src_file_lastmod(State#state.src_file_lastmod, NewSrcFileLastMod, State#state.patching),
%% Schedule the next interval...
NewTimers = schedule_cast(compare_src_files, 1000, State#state.timers),
%% Return with updated src_file lastmod...
NewState = State#state { src_file_lastmod=NewSrcFileLastMod, timers=NewTimers },
{noreply, NewState};
handle_cast(info, State) ->
io:format("Modules: ~p~n", [State#state.modules]),
io:format("Source Dirs: ~p~n", [State#state.src_dirs]),
io:format("Source Files: ~p~n", [State#state.src_files]),
{noreply, State};
handle_cast(enable_patching, State) ->
NewState = State#state { patching = true },
{noreply, NewState};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%% PRIVATE FUNCTIONS %%%
schedule_cast(Msg, Default, Timers) ->
%% Cancel the old timer...
TRef = proplists:get_value(Msg, Timers),
timer:cancel(TRef),
%% Lookup the interval...
IntervalKey = list_to_atom(atom_to_list(Msg) ++ "_interval"),
Interval = sync_utils:get_env(IntervalKey, Default),
%% Schedule the call...
{ok, NewTRef} = timer:apply_after(Interval, gen_server, cast, [?SERVER, Msg]),
%% Return the new timers structure...
lists:keystore(Msg, 1, Timers, {Msg, NewTRef}).
process_beam_lastmod(A, B, EnablePatching) ->
process_beam_lastmod(A, B, EnablePatching, {undefined, 0}).
process_beam_lastmod([{Module, LastMod}|T1], [{Module, LastMod}|T2], EnablePatching, Acc) ->
%% Beam hasn't changed, do nothing...
process_beam_lastmod(T1, T2, EnablePatching, Acc);
process_beam_lastmod([{Module, _}|T1], [{Module, _}|T2], EnablePatching, {FirstBeam, OtherBeams}) ->
%% Beam has changed, reload...
{Module, Binary, Filename} = code:get_object_code(Module),
code:load_binary(Module, Filename, Binary),
%% If patching is enabled, then reload the module across *all* connected
%% erlang VMs, and save the compiled beam to disk.
case EnablePatching of
true ->
{ok, _NumNodes} = load_module_on_all_nodes(Module);
false ->
ok
end,
Acc1 = case FirstBeam of
undefined -> {Module, OtherBeams};
_ -> {FirstBeam, OtherBeams+1}
end,
process_beam_lastmod(T1, T2, EnablePatching, Acc1);
process_beam_lastmod([{Module1, LastMod1}|T1], [{Module2, LastMod2}|T2], EnablePatching, Acc) ->
%% Lists are different, advance the smaller one...
case Module1 < Module2 of
true ->
process_beam_lastmod(T1, [{Module2, LastMod2}|T2], EnablePatching, Acc);
false ->
process_beam_lastmod([{Module1, LastMod1}|T1], T2, EnablePatching, Acc)
end;
process_beam_lastmod([], [], EnablePatching, Acc) ->
MsgAdd = case EnablePatching of
true -> " on " ++ integer_to_list(length(get_nodes())) ++ " nodes.";
false -> "."
end,
%% Done.
case Acc of
{undefined, 0} ->
nop; % nothing changed
{FirstBeam, 0} ->
%% Print a status message...
growl_success("Reloaded " ++ atom_to_list(FirstBeam) ++ MsgAdd);
{FirstBeam, N} ->
%% Print a status message...
growl_success("Reloaded " ++ atom_to_list(FirstBeam) ++
" and " ++ integer_to_list(N) ++ " other beam files" ++ MsgAdd)
end,
ok;
process_beam_lastmod(undefined, _Other, _, _) ->
%% First load, do nothing.
ok.
get_nodes() ->
lists:usort(lists:flatten(nodes() ++ [rpc:call(X, erlang, nodes, []) || X <- nodes()])) -- [node()].
load_module_on_all_nodes(Module) ->
%% Get a list of nodes known by this node, plus all attached
%% nodes.
Nodes = get_nodes(),
io:format("[~s:~p] DEBUG - Nodes: ~p~n", [?MODULE, ?LINE, Nodes]),
NumNodes = length(Nodes),
{Module, Binary, _} = code:get_object_code(Module),
F = fun(Node) ->
io:format("[~s:~p] DEBUG - Node: ~p~n", [?MODULE, ?LINE, Node]),
error_logger:info_msg("Reloading '~s' on ~s.~n", [Module, Node]),
rpc:call(Node, code, ensure_loaded, [Module]),
case rpc:call(Node, code, which, [Module]) of
Filename when is_binary(Filename) orelse is_list(Filename) ->
%% File exists, overwrite and load into VM.
ok = rpc:call(Node, file, write_file, [Filename, Binary]),
rpc:call(Node, code, purge, [Module]),
{module, Module} = rpc:call(Node, code, load_file, [Module]);
_ ->
%% File doesn't exist, just load into VM.
{module, Module} = rpc:call(Node, code, load_binary, [Module, undefined, Binary])
end,
growl_success("Reloaded " ++ atom_to_list(Module) ++ " on " ++ atom_to_list(Node) ++ ".")
end,
[F(X) || X <- Nodes],
{ok, NumNodes}.
process_src_file_lastmod([{File, LastMod}|T1], [{File, LastMod}|T2], EnablePatching) ->
%% Beam hasn't changed, do nothing...
process_src_file_lastmod(T1, T2, EnablePatching);
process_src_file_lastmod([{File, _}|T1], [{File, _}|T2], EnablePatching) ->
%% File has changed, recompile...
recompile_src_file(File, EnablePatching),
process_src_file_lastmod(T1, T2, EnablePatching);
process_src_file_lastmod([{File1, LastMod1}|T1], [{File2, LastMod2}|T2], EnablePatching) ->
%% Lists are different...
case File1 < File2 of
true ->
%% File was removed, do nothing...
process_src_file_lastmod(T1, [{File2, LastMod2}|T2], EnablePatching);
false ->
%% File is new, recompile...
recompile_src_file(File2, EnablePatching),
process_src_file_lastmod([{File1, LastMod1}|T1], T2, EnablePatching)
end;
process_src_file_lastmod([], [{File, _LastMod}|T2], EnablePatching) ->
%% File is new, recompile...
recompile_src_file(File, EnablePatching),
process_src_file_lastmod([], T2, EnablePatching);
process_src_file_lastmod([], [], _) ->
%% Done.
ok;
process_src_file_lastmod(undefined, _Other, _) ->
%% First load, do nothing.
ok.
recompile_src_file(SrcFile, EnablePatching) ->
%% Get the module, src dir, and options...
Module = list_to_atom(filename:basename(SrcFile, ".erl")),
{ok, SrcDir} = sync_utils:get_src_dir(SrcFile),
%% Get the old binary code...
OldBinary = case code:get_object_code(Module) of
{Module, B, _Filename} -> B;
_ -> undefined
end,
case sync_options:get_options(SrcDir) of
{ok, Options} ->
case compile:file(SrcFile, [binary, return|Options]) of
{ok, Module, OldBinary, Warnings} ->
%% Compiling didn't change the beam code. Don't reload...
print_results(Module, SrcFile, [], Warnings),
{ok, [], Warnings};
{ok, Module, _Binary, Warnings} ->
%% Compiling changed the beam code. Compile and reload.
compile:file(SrcFile, Options),
case EnablePatching of
true -> code:ensure_loaded(Module);
false -> ok
end,
gen_server:cast(?SERVER, compare_beams),
%% Print the warnings...
print_results(Module, SrcFile, [], Warnings),
{ok, [], Warnings};
{error, Errors, Warnings} ->
%% Compiling failed. Print the warnings and errors...
print_results(Module, SrcFile, Errors, Warnings),
{ok, Errors, Warnings}
end;
undefined ->
error_logger:error_msg("Unable to determine options for ~p", [SrcFile])
end.
print_results(_Module, _SrcFile, [], []) ->
%% Do not print message on successful compilation;
%% We already get a notification when the beam is reloaded.
ok;
print_results(_Module, SrcFile, [], Warnings) ->
Msg = [
format_errors(SrcFile, [], Warnings),
io_lib:format("~s:0: Recompiled with ~p warnings~n", [SrcFile, length(Warnings)])
],
growl_warnings(growl_format_errors([], Warnings)),
error_logger:info_msg(lists:flatten(Msg));
print_results(_Module, SrcFile, Errors, Warnings) ->
Msg = [
format_errors(SrcFile, Errors, Warnings)
],
growl_errors(growl_format_errors(Errors, Warnings)),
error_logger:info_msg(lists:flatten(Msg)).
%% @private Print error messages in a pretty and user readable way.
format_errors(File, Errors, Warnings) ->
AllErrors1 = lists:sort(lists:flatten([X || {_, X} <- Errors])),
AllErrors2 = [{Line, "Error", Module, Description} || {Line, Module, Description} <- AllErrors1],
AllWarnings1 = lists:sort(lists:flatten([X || {_, X} <- Warnings])),
AllWarnings2 = [{Line, "Warning", Module, Description} || {Line, Module, Description} <- AllWarnings1],
Everything = lists:sort(AllErrors2 ++ AllWarnings2),
F = fun({Line, Prefix, Module, ErrorDescription}) ->
Msg = Module:format_error(ErrorDescription),
io_lib:format("~s:~p: ~s: ~s~n", [File, Line, Prefix, Msg])
end,
[F(X) || X <- Everything].
%% @private Print error messages in a pretty and user readable way.
growl_format_errors(Errors, Warnings) ->
AllErrors1 = lists:sort(lists:flatten([X || {_, X} <- Errors])),
AllErrors2 = [{Line, "Error", Module, Description} || {Line, Module, Description} <- AllErrors1],
AllWarnings1 = lists:sort(lists:flatten([X || {_, X} <- Warnings])),
AllWarnings2 = [{Line, "Warning", Module, Description} || {Line, Module, Description} <- AllWarnings1],
Everything = lists:sort(AllErrors2 ++ AllWarnings2),
F = fun({Line, Prefix, Module, ErrorDescription}) ->
Msg = Module:format_error(ErrorDescription),
io_lib:format("~p: ~s: ~s~n", [Line, Prefix, Msg])
end,
[F(X) || X <- Everything].
growl(Image, Title, Message) ->
case get_growl() of
false -> ok;
true ->
ImagePath = filename:join([filename:dirname(code:which(sync)), "..", "icons", Image]) ++ ".png",
%% For OSX
GrowlMsg = io_lib:format("growlnotify -n \"Sync\" --image \"~s\" -m \"~s\" \"~s\"", [ImagePath, Message, Title]),
os:cmd(GrowlMsg),
%% For Linux.
NotifyMsg = io_lib:format("notify-send -i \"~s\" \"~s\" \"~s\" --expire-time=5000", [ImagePath, Title, Message]),
os:cmd(NotifyMsg)
end.
growl_success(Message) ->
growl_success("Success!", Message).
growl_success(Title, Message) ->
case sync_utils:get_env(growl,true) of
skip_success -> ok;
_ -> growl("success", Title, Message)
end.
growl_errors(Message) ->
growl("errors", "Errors...", Message).
growl_warnings(Message) ->
growl("warnings", "Warnings", Message).
Jump to Line
Something went wrong with that request. Please try again.