Packages

Sync - Automatic Code Reloader

Retired package: Deprecated - Obsolated

Current section

Files

Jump to
shiguredo_sync src sync_scanner.erl
Raw

src/sync_scanner.erl

%% vim: ts=4 sw=4 et
-module(sync_scanner).
-behaviour(gen_server).
-include_lib("kernel/include/file.hrl").
-compile([export_all, nowarn_export_all]).
%% API
-export([
start_link/0,
rescan/0,
info/0,
enable_patching/0,
pause/0,
unpause/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,
set_log/1,
get_log/0
]).
-define(SERVER, ?MODULE).
-define(PRINT(Var), io:format("DEBUG: ~p:~p - ~p~n~n ~p~n~n", [?MODULE, ?LINE, ??Var, Var])).
-define(LOG_OR_GROWL_ON(Val),Val==true;Val==all;Val==skip_success;is_list(Val),Val=/=[]).
-define(LOG_OR_GROWL_OFF(Val),Val==false;F==none;F==[]).
-type timestamp() :: file:date_time() | 0.
-record(state, {
modules = [] :: [module()],
src_dirs = [] :: [file:filename()],
src_files = [] :: [file:filename()],
hrl_dirs = [] :: [file:filename()],
hrl_files = [] :: [file:filename()],
beam_lastmod = undefined :: [{module(), timestamp()}] | undefined,
src_file_lastmod = [] :: [{file:filename(), timestamp()}],
hrl_file_lastmod = [] :: [{file:filename(), timestamp()}],
timers = [],
patching = false,
paused = false,
sync_method = scanner,
modified_files = [] :: [file:filename()],
fsevents_pids = []
}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
rescan() ->
gen_server:cast(?SERVER, discover_modules),
gen_server:cast(?SERVER, discover_src_dirs),
gen_server:cast(?SERVER, discover_src_files),
case sync_utils:get_env(sync_method, scanner) of
scanner ->
io:format("Scanning source files...~n"),
gen_server:cast(?SERVER, compare_src_files),
gen_server:cast(?SERVER, compare_beams),
gen_server:cast(?SERVER, compare_hrl_files),
ok;
_ ->
io:format("Listening on fsevents...~n"),
ok
end.
unpause() ->
gen_server:cast(?SERVER, unpause),
ok.
pause() ->
sync_notify:log_success("Pausing Sync. Call sync:go() to restart~n"),
sync_notify:growl_success("Pausing Sync"),
gen_server:cast(?SERVER, pause),
ok.
info() ->
io:format("Sync Info...~n"),
gen_server:cast(?SERVER, info),
ok.
set_growl(T) when ?LOG_OR_GROWL_ON(T) ->
sync_utils:set_env(growl,all),
sync_notify:growl_success("Sync","Desktop Notifications Enabled"),
sync_utils:set_env(growl,T),
ok;
set_growl(F) when ?LOG_OR_GROWL_OFF(F) ->
sync_utils:set_env(growl,all),
sync_notify:growl_success("Sync","Desktop Notifications Disabled"),
sync_utils:set_env(growl,none),
ok.
get_growl() ->
sync_utils:get_env(growl, all).
set_log(T) when ?LOG_OR_GROWL_ON(T) ->
sync_utils:set_env(log, T),
sync_notify:log_success("Console Notifications Enabled~n"),
ok;
set_log(F) when ?LOG_OR_GROWL_OFF(F) ->
sync_notify:log_success("Console Notifications Disabled~n"),
sync_utils:set_env(log, none),
ok.
get_log() ->
sync_utils:get_env(log, all).
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...
sync_notify:startup(get_growl()),
{ok, #state{
sync_method = sync_utils:get_env(sync_method, scanner)
}}.
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
handle_cast(pause, State) ->
{noreply, State#state {paused=true}};
handle_cast(unpause, State) ->
{noreply, State#state {paused=false}};
handle_cast(_, State) when State#state.paused==true ->
%% If paused, just absorb the request and do nothing
{noreply, State};
handle_cast(discover_modules, State) ->
%% Get a list of all loaded non-system modules.
Modules = (erlang:loaded() -- sync_utils:get_system_modules()),
%% Delete excluded modules/applications
FilteredModules = filter_modules_to_scan(Modules),
%% Schedule the next interval...
NewTimers = case State#state.sync_method of
scanner -> schedule_cast(discover_modules, 30000, State#state.timers);
_ -> proplists:delete(discover_modules, State#state.timers)
end,
%% Return with updated modules...
NewState = State#state { modules=FilteredModules, timers=NewTimers },
{noreply, NewState};
handle_cast(discover_src_dirs, State) ->
case application:get_env(sync, src_dirs) of
undefined ->
discover_source_dirs(State, [], []);
{ok, {add, DirsAndOpts}} ->
discover_source_dirs(State, dirs(DirsAndOpts), []);
{ok, {replace, DirsAndOpts}} ->
discover_source_dirs(State, [], dirs(DirsAndOpts))
end;
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|dtl|lfe|ex)$") ++ Acc
end,
ErlFiles = lists:usort(lists:foldl(F, [], State#state.src_dirs)),
%% For each include dir, get a list of hrl files...
Fhrl = fun(X, Acc) ->
sync_utils:wildcard(X, ".*\\.hrl$") ++ Acc
end,
HrlFiles = lists:usort(lists:foldl(Fhrl, [], State#state.hrl_dirs)),
%% Schedule the next interval...
NewTimers = case State#state.sync_method of
scanner -> schedule_cast(discover_src_files, 5000, State#state.timers);
_ -> proplists:delete(discover_src_files, State#state.timers)
end,
%% Return with updated files...
NewState = State#state { src_files=ErlFiles, hrl_files=HrlFiles, timers=NewTimers },
{noreply, NewState};
handle_cast(compare_beams, State) ->
%% Create a list of beam file lastmod times, but filter out modules not having
%% a valid beam file reference.
F = fun(X) ->
case code:which(X) of
Beam when is_list(Beam) ->
case filelib:last_modified(Beam) of
0 ->
false; %% file not found
LastMod ->
{true, {X, LastMod}}
end;
_Other ->
false %% non_existing | cover_compiled | preloaded
end
end,
NewBeamLastMod = lists:usort(lists:filtermap(F, 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 = case State#state.sync_method of
scanner -> schedule_cast(compare_beams, 2000, State#state.timers);
_ -> proplists:delete(compare_beams, State#state.timers)
end,
%% 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(compare_hrl_files, State) ->
%% Create a list of file lastmod times...
F = fun(X) ->
LastMod = filelib:last_modified(X),
{X, LastMod}
end,
NewHrlFileLastMod = lists:usort([F(X) || X <- State#state.hrl_files]),
%% Compare to previous results, if there are changes, then recompile src files that depends
process_hrl_file_lastmod(State#state.hrl_file_lastmod, NewHrlFileLastMod, State#state.src_files, State#state.patching),
%% Schedule the next interval...
NewTimers = schedule_cast(compare_hrl_files, 2000, State#state.timers),
%% Return with updated hrl_file lastmod...
NewState = State#state { hrl_file_lastmod=NewHrlFileLastMod, timers=NewTimers },
{noreply, NewState};
handle_cast(fsevents_modified_files,
#state{
modified_files = Files, patching = Patching, src_files = SrcFiles, modules = OldModules
} = State) when Files /= [] ->
Recompile = fun(F, P) ->
recompile_src_file(F, P),
{_, M} = determine_compile_fun_and_module_name(F),
M
end,
NewModules1 = lists:foldl(fun
(FileName, Acc) ->
case filename:extension(FileName) of
Ext when Ext == ".erl"; Ext == ".dtl"; Ext == ".lfe"; Ext == ".ex" ->
M = Recompile(FileName, Patching),
[M | Acc];
".hrl" ->
WhoInclude = who_include(FileName, SrcFiles),
[Recompile(SrcFile, Patching) || SrcFile <- WhoInclude] ++ Acc;
_ ->
Acc
end
end, [], Files),
%% It's possible that this module is not known to sync, if yes, add it to modules list
Filter = fun(M) ->
lists:member(M, OldModules) /= true
end,
NewModules = case lists:filter(Filter, NewModules1) of
[] -> OldModules;
R -> lists:usort(OldModules ++ R)
end,
{noreply, State#state{
modified_files = [],
modules = NewModules
}};
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}.
dirs(DirsAndOpts) ->
[begin
sync_options:set_options(Dir, Opts),
%% ensure module out path exists & in our code list
case proplists:get_value(outdir, Opts) of
undefined ->
true;
Path ->
ok = filelib:ensure_dir(filename:join(Path, "sample")),
true = code:add_pathz(Path)
end,
Dir
end || {Dir, Opts} <- DirsAndOpts].
handle_info({_Pid, {fs,file_event}, {FileName, _Events}},
#state{modified_files = OldModFiles} = State) ->
%% Process the modified files event about a second later. Just
%% to thaw all events happening on a file
{noreply, State#state{
modified_files = lists:usort([FileName | OldModFiles]),
timers = schedule_cast(fsevents_modified_files, 1000, State#state.timers)
}};
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, []}).
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...
Acc1 = case code:get_object_code(Module) of
error ->
Msg = io_lib:format("Error loading object code for ~p~n", [Module]),
sync_notify:log_errors(Msg),
sync_notify:growl_errors(Msg),
{FirstBeam, OtherBeams};
{Module, Binary, Filename} ->
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),
Msg = io_lib:format("~s: Reloaded on ~p nodes! (Beam changed.)~n", [Module, NumNodes]),
sync_notify:log_success(Msg);
false ->
%% Print a status message...
Msg = io_lib:format("~s: Reloaded! (Beam changed.)~n", [Module]),
sync_notify:log_success(Msg)
end,
case FirstBeam of
undefined -> {Module, OtherBeams};
_ -> {FirstBeam, [Module | OtherBeams] }
end
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(A, B, EnablePatching, Acc) when A =:= []; B =:= [] ->
MsgAdd = case EnablePatching of
true -> " on " ++ integer_to_list(length(get_nodes())) ++ " nodes.";
false -> "."
end,
%% Done.
case Acc of
{undefined, []} ->
nop; % nothing changed
{FirstBeam, []} ->
%% Print a status message...
sync_notify:growl_success("Reloaded " ++ atom_to_list(FirstBeam) ++ MsgAdd),
fire_onsync([FirstBeam]);
{FirstBeam, N} ->
%% Print a status message...
sync_notify:growl_success("Reloaded " ++ atom_to_list(FirstBeam) ++
" and " ++ integer_to_list(erlang:length(N)) ++ " other beam files" ++ MsgAdd),
fire_onsync([FirstBeam | N])
end,
ok;
process_beam_lastmod(undefined, _Other, _, _) ->
%% First load, do nothing.
ok.
fire_onsync(Modules) ->
case sync_options:get_onsync() of
undefined -> ok;
Funs when is_list(Funs) -> onsync_apply_list(Funs, Modules);
Fun -> onsync_apply(Fun, Modules)
end.
onsync_apply_list(Funs, Modules) ->
[onsync_apply(Fun, Modules) || Fun <- Funs].
onsync_apply({M, F}, Modules) ->
erlang:apply(M, F, [Modules]);
onsync_apply(Fun, Modules) when is_function(Fun) ->
Fun(Modules).
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]),
Msg = io_lib:format("Reloading '~s' on ~s.~n", [Module, Node]),
sync_notify:log_success(Msg),
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,
sync_notify: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 ->
maybe_recompile_src_file(File2, LastMod2, EnablePatching),
process_src_file_lastmod([{File1, LastMod1}|T1], T2, EnablePatching)
end;
process_src_file_lastmod([], [{File, LastMod}|T2], EnablePatching) ->
maybe_recompile_src_file(File, LastMod, EnablePatching),
process_src_file_lastmod([], T2, EnablePatching);
process_src_file_lastmod(_A, [], _) ->
%% All remaining files, if any, were removed.
ok;
process_src_file_lastmod(undefined, _Other, _) ->
%% First load, do nothing.
ok.
erlydtl_compile(SrcFile, Options) ->
F = fun({outdir, OutDir}, Acc) -> [{out_dir, OutDir} | Acc];
(OtherOption, Acc) -> [OtherOption | Acc]
end,
DtlOptions = lists:foldl(F, [], Options),
Module =
list_to_atom(
lists:flatten(filename:basename(SrcFile, ".dtl") ++ "_dtl")),
Compiler = erlydtl,
Compiler:compile(SrcFile, Module, DtlOptions).
elixir_compile(SrcFile, Options) ->
Outdir = proplists:get_value(outdir, Options),
Compiler = 'Elixir.Kernel.ParallelCompiler',
Modules = Compiler:files_to_path([list_to_binary(SrcFile)], list_to_binary(Outdir)),
Loader = fun(Module) ->
Outfile = code:which(Module),
Binary = file:read_file(Outfile),
{Module, Binary}
end,
Results = lists:map(Loader, Modules),
{ok, multiple, Results, []}.
lfe_compile(SrcFile, Options) ->
Compiler = lfe_comp,
Compiler:file(SrcFile, Options).
maybe_recompile_src_file(File, LastMod, EnablePatching) ->
Module = list_to_atom(filename:basename(File, ".erl")),
case code:which(Module) of
BeamFile when is_list(BeamFile) ->
%% check with beam file
case filelib:last_modified(BeamFile) of
BeamLastMod when LastMod > BeamLastMod ->
recompile_src_file(File, EnablePatching);
_ ->
ok
end;
_ ->
%% File is new, recompile...
recompile_src_file(File, EnablePatching)
end.
determine_compile_fun_and_module_name(SrcFile) ->
case sync_utils:get_filetype(SrcFile) of
erl ->
{fun compile:file/2,
list_to_atom(filename:basename(SrcFile, ".erl"))};
dtl ->
{fun erlydtl_compile/2,
list_to_atom(lists:flatten(filename:basename(SrcFile, ".dtl") ++ "_dtl"))};
lfe ->
{fun lfe_compile/2,
list_to_atom(filename:basename(SrcFile, ".lfe"))};
elixir ->
{fun elixir_compile/2,
list_to_atom(filename:basename(SrcFile, ".ex"))}
end.
get_object_code(Module) ->
case code:get_object_code(Module) of
{Module, B, _Filename} -> B;
_ -> undefined
end.
reload_if_necessary(_CompileFun, SrcFile, Module, Binary, Binary, _Options, Warnings) ->
%% Compiling didn't change the beam code. Don't reload...
print_results(Module, SrcFile, [], Warnings),
{ok, [], Warnings};
reload_if_necessary(CompileFun, SrcFile, Module, _OldBinary, _Binary, Options, Warnings) ->
%% Compiling changed the beam code. Compile and reload.
case CompileFun(SrcFile, [return | Options]) of
{ok, _} ->
ok;
{ok, _, _} ->
ok;
CompileResult ->
sync_notify:log_warnings("Compile failed in reload_if_necessary(): ~p~n",
[CompileResult])
end,
%% Try to load the module...
case code:ensure_loaded(Module) of
{module, Module} -> ok;
{error, nofile} -> error_no_file(Module);
{error, embedded} ->
%% Module is not yet loaded, load it.
case code:load_file(Module) of
{module, Module} -> ok;
{error, nofile} ->
error_no_file(Module)
end
end,
gen_server:cast(?SERVER, compare_beams),
%% Print the warnings...
print_results(Module, SrcFile, [], Warnings),
{ok, [], Warnings}.
error_no_file(Module) ->
Msg = io_lib:format("~p Couldn't load module: nofile~n", [Module]),
sync_notify:log_warnings([Msg]).
recompile_src_file(SrcFile, _EnablePatching) ->
%% Get the module, src dir, and options...
{ok, SrcDir} = sync_utils:get_src_dir(SrcFile),
{CompileFun, Module} = determine_compile_fun_and_module_name(SrcFile),
%% Get the old binary code...
OldBinary = get_object_code(Module),
case sync_options:get_options(SrcDir) of
{ok, Options0} ->
%% Event messages from fs may disrupt compile module, spawn it
Options = lists:delete(no_spawn_compiler_process, Options0),
case CompileFun(SrcFile, [binary, return|Options]) of
{ok, Module, Binary, Warnings} ->
reload_if_necessary(CompileFun, SrcFile, Module, OldBinary, Binary, Options, Warnings);
{ok, [{ok, Module, Binary, Warnings}], Warnings2} ->
reload_if_necessary(CompileFun, SrcFile, Module, OldBinary, Binary, Options, Warnings ++ Warnings2);
{ok, multiple, Results, Warnings} ->
Reloader = fun({CompiledModule, Binary}) ->
{ok, _, _} = reload_if_necessary(CompileFun, SrcFile, CompiledModule, OldBinary, Binary, Options, Warnings)
end,
lists:foreach(Reloader, Results),
{ok, [], Warnings};
{ok, OtherModule, _Binary, Warnings} ->
Desc = io_lib:format("Module definition (~p) differs from expected (~s)", [OtherModule, filename:rootname(filename:basename(SrcFile))]),
Errors = [{SrcFile, {0, Module, Desc}}],
print_results(Module, SrcFile, Errors, Warnings),
{ok, Errors, Warnings};
{error, Errors, Warnings} ->
%% Compiling failed. Print the warnings and errors...
print_results(Module, SrcFile, Errors, Warnings),
{ok, Errors, Warnings}
end;
undefined ->
Msg = io_lib:format("Unable to determine options for ~p", [SrcFile]),
sync_notify:log_errors(Msg)
end.
print_results(Module, SrcFile, [], []) ->
Msg = io_lib:format("~s: Recompiled.~n", [SrcFile]),
case code:is_loaded(Module) of
{file, _} ->
ok;
false ->
sync_notify:growl_success("Recompiled " ++ SrcFile ++ ".")
end,
sync_notify:log_success(lists:flatten(Msg));
print_results(_Module, SrcFile, [], Warnings) ->
Msg = [
format_errors(SrcFile, [], Warnings),
io_lib:format("~s Recompiled with ~p warnings~n", [SrcFile, length(Warnings)])
],
sync_notify:growl_warnings(growl_format_errors([], Warnings)),
sync_notify:log_warnings(Msg);
print_results(_Module, SrcFile, Errors, Warnings) ->
Msg = [
format_errors(SrcFile, Errors, Warnings)
],
sync_notify:growl_errors(growl_format_errors(Errors, Warnings)),
sync_notify:log_errors(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({Line0, Prefix, Module, ErrorDescription}) ->
Msg = format_error(Module, ErrorDescription),
Line = format_line(Line0),
io_lib:format("~s ~s: ~s: ~s~n", [File, Line, Prefix, Msg])
end,
[F(X) || X <- Everything].
format_error(Module, ErrorDescription) ->
case erlang:function_exported(Module, format_error, 1) of
true -> Module:format_error(ErrorDescription);
false -> io_lib:format("~s", [ErrorDescription])
end.
%% @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({Line0, Prefix, Module, ErrorDescription}) ->
Line = format_line(Line0),
Msg = format_error(Module, ErrorDescription),
io_lib:format("~s: ~s: ~s~n", [Line, Prefix, Msg])
end,
[F(X) || X <- Everything].
format_line(Line) when is_integer(Line) ->
"(Line " ++ integer_to_list(Line) ++ ")";
format_line({Line, Char}) ->
"(Line " ++ integer_to_list(Line) ++ ", Char " ++ integer_to_list(Char) ++ ")".
process_hrl_file_lastmod([{File, LastMod}|T1], [{File, LastMod}|T2], SrcFiles, Patching) ->
%% Hrl hasn't changed, do nothing...
process_hrl_file_lastmod(T1, T2, SrcFiles, Patching);
process_hrl_file_lastmod([{File, _}|T1], [{File, _}|T2], SrcFiles, Patching) ->
%% File has changed, recompile...
WhoInclude = who_include(File, SrcFiles),
[recompile_src_file(SrcFile, Patching) || SrcFile <- WhoInclude],
process_hrl_file_lastmod(T1, T2, SrcFiles, Patching);
process_hrl_file_lastmod([{File1, LastMod1}|T1], [{File2, LastMod2}|T2], SrcFiles, Patching) ->
%% Lists are different...
case File1 < File2 of
true ->
%% File was removed, do nothing...
warn_deleted_hrl_files(File1, SrcFiles),
process_hrl_file_lastmod(T1, [{File2, LastMod2}|T2], SrcFiles, Patching);
false ->
%% File is new, look for src that include it
WhoInclude = who_include(File2, SrcFiles),
[maybe_recompile_src_file(SrcFile, LastMod2, Patching) || SrcFile <- WhoInclude],
process_hrl_file_lastmod([{File1, LastMod1}|T1], T2, SrcFiles, Patching)
end;
process_hrl_file_lastmod([], [{File, LastMod}|T2], SrcFiles, Patching) ->
%% File is new, look for src that include it
WhoInclude = who_include(File, SrcFiles),
[maybe_recompile_src_file(SrcFile, LastMod, Patching) || SrcFile <- WhoInclude],
process_hrl_file_lastmod([], T2, SrcFiles, Patching);
process_hrl_file_lastmod([{File1, _LastMod1}|T1], [], SrcFiles, Patching) ->
%% Rest of file(s) removed, warn and process next
warn_deleted_hrl_files(File1, SrcFiles),
process_hrl_file_lastmod(T1, [], SrcFiles, Patching);
process_hrl_file_lastmod([], [], _, _) ->
%% Done
ok;
process_hrl_file_lastmod(undefined, _Other, _, _) ->
%% First load, do nothing
ok.
warn_deleted_hrl_files(HrlFile, SrcFiles) ->
WhoInclude = who_include(HrlFile, SrcFiles),
case WhoInclude of
[] -> ok;
_ -> io:format(
"Warning. Deleted ~p file included in existing src files: ~p~n",
[filename:basename(HrlFile), lists:map(fun(File) -> filename:basename(File) end, WhoInclude)])
end.
who_include(HrlFile, SrcFiles) ->
HrlFileBaseName = filename:basename(HrlFile),
Pred = fun(SrcFile) ->
{ok, Forms} = epp_dodger:parse_file(SrcFile),
is_include(HrlFileBaseName, Forms)
end,
lists:filter(Pred, SrcFiles).
is_include(_HrlFile, []) ->
false;
is_include(HrlFile, [{tree, attribute, _, {attribute, _, [{_, _, IncludeFile}]}} | Forms]) when is_list(IncludeFile) ->
IncludeFileBaseName = filename:basename(IncludeFile),
case IncludeFileBaseName of
HrlFile -> true;
_ -> is_include(HrlFile, Forms)
end;
is_include(HrlFile, [_SomeForm | Forms]) ->
is_include(HrlFile, Forms).
%% @private Filter the modules to be scanned.
filter_modules_to_scan(Modules) ->
exclude_modules_to_scan(whitelist_modules_to_scan(Modules)).
%% @private Filter the whitelisted modules.
whitelist_modules_to_scan(Modules) ->
case application:get_env(sync, whitelisted_modules) of
{ok, WhitelistedModules} when is_list(WhitelistedModules) andalso
WhitelistedModules =/= []->
lists:foldl(fun(Module, Acc) ->
case module_matches(Module, WhitelistedModules) of
true ->
[Module | Acc];
false ->
Acc
end
end, [], Modules);
_ ->
Modules
end.
%% @private Filter the excluded modules.
exclude_modules_to_scan(Modules) ->
case application:get_env(sync, excluded_modules) of
{ok, ExcludedModules} when is_list(ExcludedModules) andalso
ExcludedModules =/= []->
lists:foldl(fun(Module, Acc) ->
case module_matches(Module, ExcludedModules) of
true ->
Acc;
false ->
[Module | Acc]
end
end, [], Modules);
_ ->
Modules
end.
module_matches(_Module, []) ->
false;
module_matches(Module, [Module2|T]) when is_atom(Module2) ->
case Module =:= Module2 of
true -> true;
false -> module_matches(Module, T)
end;
module_matches(Module, [Pattern|T]) when is_list(Pattern) ->
case re:run(atom_to_list(Module), Pattern) of
{match, _} -> true;
nomatch -> module_matches(Module, T)
end.
discover_source_dirs(State, ExtraDirs, ReplaceDirs) ->
%% Extract the compile / options / source / dir from each module.
F = fun
(X, Acc = {SrcAcc, HrlAcc}) ->
%% Get the dir...
case sync_utils:get_src_dir_from_module(X) of
{ok, SrcDir} ->
case is_replace_dir(SrcDir, ReplaceDirs) of
true ->
%% Get the options, store under the dir...
{ok, Options} = sync_utils:get_options_from_module(X),
%% Store the options for later reference...
HrlDir = proplists:get_all_values(i, Options),
sync_options:set_options(SrcDir, Options),
%% Return the dir...
{[SrcDir|SrcAcc], HrlDir ++ HrlAcc};
_ ->
Acc
end;
undefined ->
Acc
end
end,
{SrcDirs, HrlDirs} = lists:foldl(F, {ExtraDirs, []}, State#state.modules),
USortedSrcDirs = lists:usort(SrcDirs),
USortedHrlDirs = lists:usort(HrlDirs),
%% InitialDirs = sync_utils:initial_src_dirs(),
%% Schedule the next interval...
case State#state.sync_method of
scanner ->
NewTimers = schedule_cast(discover_src_dirs, 30000, State#state.timers),
%% Return with updated dirs...
NewState = State#state { src_dirs=USortedSrcDirs, hrl_dirs=USortedHrlDirs, timers=NewTimers },
{noreply, NewState};
fsevents ->
%% Stop old processes
start_fsevents(SrcDirs++HrlDirs, ReplaceDirs, State#state{
src_dirs = USortedSrcDirs,
hrl_dirs = USortedHrlDirs
})
end.
start_fsevents(MonitorDirs, ReplaceDirs, State) ->
%% Stop existing fs processes if any
[erlang:exit(Pid, normal) || Pid <- State#state.fsevents_pids],
%% Start new fs processes based on the user inputs
NewPids = lists:foldl(fun
(Dir, Acc) ->
Name = erlang:list_to_atom(Dir),
case file:read_link_info(Dir) of
{ok, #file_info{type = symlink}} ->
Acc;
{ok, _} ->
case fs:start_link(Name, Dir) of
{ok, Pid} ->
fs:subscribe(Name),
[Pid | Acc];
_ ->
Acc
end;
_ ->
Acc
end
end, [], MonitorDirs),
%% It is possible that not all modules are discovered
%% by the time this process is ran. So, handle that
DirsNotDiscovered = lists:foldl(fun
(_, []) ->
[];
(Dir, Acc) ->
lists:filter(fun
(Match) ->
string:split(Dir, Match) == [Dir]
end, Acc)
end, ReplaceDirs, MonitorDirs),
%% Restart the discovery process if needed ...
NewTimers = case DirsNotDiscovered == [] of
true ->
proplists:delete(discover_src_dirs, State#state.timers);
_ ->
T1 = schedule_cast(discover_modules, 3000, State#state.timers),
T2 = schedule_cast(discover_src_dirs, 4000, T1),
schedule_cast(discover_src_files, 5000, T2)
end,
gen_server:cast(?SERVER, compare_beams),
{noreply, State#state{
fsevents_pids = NewPids,
timers = NewTimers
}}.
is_replace_dir(_, []) ->
true;
is_replace_dir(SrcDir, ReplaceDirs) ->
lists:foldl(fun
(Dir, false) ->
case re:run(SrcDir, Dir) of nomatch -> false; _ -> true end;
(_, Acc) -> Acc
end, false, ReplaceDirs).