summaryrefslogtreecommitdiff
path: root/src/rebar_agent.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/rebar_agent.erl')
-rw-r--r--src/rebar_agent.erl256
1 files changed, 224 insertions, 32 deletions
diff --git a/src/rebar_agent.erl b/src/rebar_agent.erl
index 95818d8..b4734f1 100644
--- a/src/rebar_agent.erl
+++ b/src/rebar_agent.erl
@@ -1,5 +1,8 @@
+%%% @doc Runs a process that holds a rebar3 state and can be used
+%%% to statefully maintain loaded project state into a running VM.
-module(rebar_agent).
--export([start_link/1, do/1, do/2]).
+-export([start_link/1, do/1, do/2, async_do/1, async_do/2]).
+-export(['$handle_undefined_function'/2]).
-export([init/1,
handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
@@ -10,47 +13,125 @@
cwd,
show_warning=true}).
+%% @doc boots an agent server; requires a full rebar3 state already.
+%% By default (within rebar3), this isn't called; `rebar_prv_shell'
+%% enters and transforms into this module
+-spec start_link(rebar_state:t()) -> {ok, pid()}.
start_link(State) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, State, []).
+%% @doc runs a given command in the agent's context.
+-spec do(atom()) -> ok | {error, term()}.
do(Command) when is_atom(Command) ->
- gen_server:call(?MODULE, {cmd, Command}, infinity).
+ gen_server:call(?MODULE, {cmd, Command}, infinity);
+do(Args) when is_list(Args) ->
+ gen_server:call(?MODULE, {cmd, default, do, Args}, infinity).
+%% @doc runs a given command in the agent's context, under a given
+%% namespace.
+-spec do(atom(), atom()) -> ok | {error, term()}.
do(Namespace, Command) when is_atom(Namespace), is_atom(Command) ->
- gen_server:call(?MODULE, {cmd, Namespace, Command}, infinity).
+ gen_server:call(?MODULE, {cmd, Namespace, Command}, infinity);
+do(Namespace, Args) when is_atom(Namespace), is_list(Args) ->
+ gen_server:call(?MODULE, {cmd, Namespace, do, Args}, infinity).
+-spec async_do(atom()) -> ok | {error, term()}.
+async_do(Command) when is_atom(Command) ->
+ gen_server:cast(?MODULE, {cmd, Command});
+async_do(Args) when is_list(Args) ->
+ gen_server:cast(?MODULE, {cmd, default, do, Args}).
+
+-spec async_do(atom(), atom()) -> ok.
+async_do(Namespace, Command) when is_atom(Namespace), is_atom(Command) ->
+ gen_server:cast(?MODULE, {cmd, Namespace, Command});
+async_do(Namespace, Args) when is_atom(Namespace), is_list(Args) ->
+ gen_server:cast(?MODULE, {cmd, Namespace, do, Args}).
+
+'$handle_undefined_function'(Cmd, [Namespace, Args]) ->
+ gen_server:call(?MODULE, {cmd, Namespace, Cmd, Args}, infinity);
+'$handle_undefined_function'(Cmd, [Args]) ->
+ gen_server:call(?MODULE, {cmd, default, Cmd, Args}, infinity);
+'$handle_undefined_function'(Cmd, []) ->
+ gen_server:call(?MODULE, {cmd, default, Cmd}, infinity).
+
+%%%%%%%%%%%%%%%%%
+%%% CALLBACKS %%%
+%%%%%%%%%%%%%%%%%
+
+%% @private
init(State) ->
Cwd = rebar_dir:get_cwd(),
{ok, #state{state=State, cwd=Cwd}}.
+%% @private
handle_call({cmd, Command}, _From, State=#state{state=RState, cwd=Cwd}) ->
MidState = maybe_show_warning(State),
- {Res, NewRState} = run(default, Command, RState, Cwd),
+ put(cmd_type, sync),
+ {Res, NewRState} = run(default, Command, "", RState, Cwd),
+ put(cmd_type, undefined),
{reply, Res, MidState#state{state=NewRState}, hibernate};
handle_call({cmd, Namespace, Command}, _From, State = #state{state=RState, cwd=Cwd}) ->
MidState = maybe_show_warning(State),
- {Res, NewRState} = run(Namespace, Command, RState, Cwd),
+ put(cmd_type, sync),
+ {Res, NewRState} = run(Namespace, Command, "", RState, Cwd),
+ put(cmd_type, undefined),
+ {reply, Res, MidState#state{state=NewRState}, hibernate};
+handle_call({cmd, Namespace, Command, Args}, _From, State = #state{state=RState, cwd=Cwd}) ->
+ MidState = maybe_show_warning(State),
+ put(cmd_type, sync),
+ {Res, NewRState} = run(Namespace, Command, Args, RState, Cwd),
+ put(cmd_type, undefined),
{reply, Res, MidState#state{state=NewRState}, hibernate};
handle_call(_Call, _From, State) ->
{noreply, State}.
+%% @private
+handle_cast({cmd, Command}, State=#state{state=RState, cwd=Cwd}) ->
+ MidState = maybe_show_warning(State),
+ put(cmd_type, async),
+ {_, NewRState} = run(default, Command, "", RState, Cwd),
+ put(cmd_type, undefined),
+ {noreply, MidState#state{state=NewRState}, hibernate};
+handle_cast({cmd, Namespace, Command}, State = #state{state=RState, cwd=Cwd}) ->
+ MidState = maybe_show_warning(State),
+ put(cmd_type, async),
+ {_, NewRState} = run(Namespace, Command, "", RState, Cwd),
+ put(cmd_type, undefined),
+ {noreply, MidState#state{state=NewRState}, hibernate};
+handle_cast({cmd, Namespace, Command, Args}, State = #state{state=RState, cwd=Cwd}) ->
+ MidState = maybe_show_warning(State),
+ put(cmd_type, async),
+ {_, NewRState} = run(Namespace, Command, Args, RState, Cwd),
+ put(cmd_type, undefined),
+ {noreply, MidState#state{state=NewRState}, hibernate};
handle_cast(_Cast, State) ->
{noreply, State}.
+%% @private
handle_info(_Info, State) ->
{noreply, State}.
+%% @private
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
+%% @private
terminate(_Reason, _State) ->
ok.
-run(Namespace, Command, RState, Cwd) ->
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+%% @private runs the actual command and maintains the state changes
+-spec run(atom(), atom(), string(), rebar_state:t(), file:filename()) ->
+ {ok, rebar_state:t()} | {{error, term()}, rebar_state:t()}.
+run(Namespace, Command, StrArgs, RState, Cwd) ->
try
case rebar_dir:get_cwd() of
Cwd ->
- Args = [atom_to_list(Namespace), atom_to_list(Command)],
+ PArgs = getopt:tokenize(StrArgs),
+ Args = [atom_to_list(Namespace), atom_to_list(Command)] ++ PArgs,
CmdState0 = refresh_state(RState, Cwd),
CmdState1 = rebar_state:set(CmdState0, task, atom_to_list(Command)),
CmdState = rebar_state:set(CmdState1, caller, api),
@@ -69,57 +150,168 @@ run(Namespace, Command, RState, Cwd) ->
{{error, cwd_changed}, RState}
end
catch
- Type:Reason ->
- ?DEBUG("Agent Stacktrace: ~p", [erlang:get_stacktrace()]),
+ ?WITH_STACKTRACE(Type, Reason, Stacktrace)
+ ?DEBUG("Agent Stacktrace: ~p", [Stacktrace]),
{{error, {Type, Reason}}, RState}
end.
+%% @private function to display a warning for the feature only once
+-spec maybe_show_warning(#state{}) -> #state{}.
maybe_show_warning(S=#state{show_warning=true}) ->
?WARN("This feature is experimental and may be modified or removed at any time.", []),
S#state{show_warning=false};
maybe_show_warning(State) ->
State.
+%% @private based on a rebar3 state term, reload paths in a way
+%% that makes sense.
+-spec refresh_paths(rebar_state:t()) -> ok.
refresh_paths(RState) ->
- ToRefresh = (rebar_state:code_paths(RState, all_deps)
- ++ [filename:join([rebar_app_info:out_dir(App), "test"])
- || App <- rebar_state:project_apps(RState)]
- %% make sure to never reload self; halt()s the VM
- ) -- [filename:dirname(code:which(?MODULE))],
+ RefreshPaths = application:get_env(rebar, refresh_paths, [all_deps, test]),
+ ToRefresh = parse_refresh_paths(RefreshPaths, RState, []),
%% Modules from apps we can't reload without breaking functionality
- Blacklist = [ec_cmd_log, providers, cf, cth_readable],
+ Blacklist = lists:usort(
+ application:get_env(rebar, refresh_paths_blacklist, [])
+ ++ [rebar, erlware_commons, providers, cf, cth_readable]),
%% Similar to rebar_utils:update_code/1, but also forces a reload
%% of used modules. Also forces to reload all of ebin/ instead
%% of just the modules in the .app file, because 'extra_src_dirs'
%% allows to load and compile files that are not to be kept
%% in the app file.
- lists:foreach(fun(Path) ->
- Name = filename:basename(Path, "/ebin"),
- Files = filelib:wildcard(filename:join([Path, "*.beam"])),
- Modules = [list_to_atom(filename:basename(F, ".beam"))
- || F <- Files],
- App = list_to_atom(Name),
+ [refresh_path(Path, Blacklist) || Path <- ToRefresh],
+ ok.
+
+refresh_path(Path, Blacklist) ->
+ Name = filename:basename(Path, "/ebin"),
+ App = list_to_atom(Name),
+ case App of
+ test -> % skip
+ code:add_patha(Path),
+ ok;
+ _ ->
application:load(App),
case application:get_key(App, modules) of
undefined ->
- code:add_patha(Path),
- ok;
- {ok, Mods} ->
- case {length(Mods), length(Mods -- Blacklist)} of
- {X,X} ->
- ?DEBUG("reloading ~p from ~s", [Modules, Path]),
- code:replace_path(App, Path),
- [begin code:purge(M), code:delete(M), code:load_file(M) end
- || M <- Modules];
- {_,_} ->
+ code:add_patha(Path);
+ {ok, _Mods} ->
+ case lists:member(App, Blacklist) of
+ false ->
+ refresh_path_do(Path, App);
+ true ->
?DEBUG("skipping app ~p, stable copy required", [App])
end
end
- end, ToRefresh).
+ end.
+refresh_path_do(Path, App) ->
+ Files = filelib:wildcard(filename:join([Path, "*.beam"])),
+ Modules = [list_to_atom(filename:basename(F, ".beam"))
+ || F <- Files],
+ ?DEBUG("reloading ~p from ~ts", [Modules, Path]),
+ code:replace_path(App, Path),
+ reload_modules(Modules).
+
+%% @private parse refresh_paths option
+%% no_deps means only project_apps's ebin path
+%% no_test means no test path
+%% OtherPath.
+parse_refresh_paths([all_deps | RefreshPaths], RState, Acc) ->
+ Paths = rebar_state:code_paths(RState, all_deps),
+ parse_refresh_paths(RefreshPaths, RState, Paths ++ Acc);
+parse_refresh_paths([project_apps | RefreshPaths], RState, Acc) ->
+ Paths = [filename:join([rebar_app_info:out_dir(App), "ebin"])
+ || App <- rebar_state:project_apps(RState)],
+ parse_refresh_paths(RefreshPaths, RState, Paths ++ Acc);
+parse_refresh_paths([test | RefreshPaths], RState, Acc) ->
+ Paths = [filename:join([rebar_app_info:out_dir(App), "test"])
+ || App <- rebar_state:project_apps(RState)],
+ parse_refresh_paths(RefreshPaths, RState, Paths ++ Acc);
+parse_refresh_paths([RefreshPath0 | RefreshPaths], RState, Acc) when is_list(RefreshPath0) ->
+ case filelib:is_dir(RefreshPath0) of
+ true ->
+ RefreshPath0 =
+ case filename:basename(RefreshPath0) of
+ "ebin" -> RefreshPath0;
+ _ -> filename:join([RefreshPath0, "ebin"])
+ end,
+ parse_refresh_paths(RefreshPaths, RState, [RefreshPath0 | Acc]);
+ false ->
+ parse_refresh_paths(RefreshPaths, RState, Acc)
+ end;
+parse_refresh_paths([_ | RefreshPaths], RState, Acc) ->
+ parse_refresh_paths(RefreshPaths, RState, Acc);
+parse_refresh_paths([], _RState, Acc) ->
+ lists:usort(Acc).
+%% @private from a disk config, reload and reapply with the current
+%% profiles; used to find changes in the config from a prior run.
+-spec refresh_state(rebar_state:t(), file:filename()) -> rebar_state:t().
refresh_state(RState, _Dir) ->
lists:foldl(
fun(F, State) -> F(State) end,
rebar3:init_config(),
[fun(S) -> rebar_state:apply_profiles(S, rebar_state:current_profiles(RState)) end]
).
+
+%% @private takes a list of modules and reloads them
+-spec reload_modules([module()]) -> term().
+reload_modules([]) -> noop;
+reload_modules(Modules0) ->
+ Modules = [M || M <- Modules0, is_changed(M)],
+ reload_modules(Modules, erlang:function_exported(code, prepare_loading, 1)).
+
+%% @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.
+is_changed(M) ->
+ try
+ module_vsn(M:module_info(attributes)) =/= module_vsn(code:get_object_code(M))
+ catch _:_ ->
+ false
+ end.
+
+module_vsn({M, Beam, _Fn}) ->
+ % Because the vsn can set by -vsn(X) in module.
+ % So didn't use beam_lib:version/1 to get the vsn.
+ % So if set -vsn(X) in module, it will always reload the module.
+ {ok, {M, <<Vsn:128>>}} = beam_lib:md5(Beam),
+ Vsn;
+module_vsn(Attrs) when is_list(Attrs) ->
+ {_, Vsn} = lists:keyfind(vsn, 1, Attrs),
+ Vsn.
+
+%% @private reloading modules, when there are modules to actually reload
+reload_modules(Modules, true) ->
+ %% OTP 19 and later -- use atomic loading and ignore unloadable mods
+ case code:prepare_loading(Modules) of
+ {ok, Prepared} ->
+ [code:purge(M) || M <- Modules],
+ code:finish_loading(Prepared);
+ {error, ModRsns} ->
+ Blacklist =
+ lists:foldr(fun({ModError, Error}, Acc) ->
+ case Error of
+ % perhaps cover other cases of failure?
+ on_load_not_allowed ->
+ reload_modules([ModError], false),
+ [ModError|Acc];
+ _ ->
+ ?DEBUG("Module ~p failed to atomic load because ~p", [ModError, Error]),
+ [ModError|Acc]
+ end
+ end,
+ [], ModRsns
+ ),
+ reload_modules(Modules -- Blacklist, true)
+ end;
+reload_modules(Modules, false) ->
+ %% Older versions, use a more ad-hoc mechanism.
+ lists:foreach(fun(M) ->
+ code:delete(M),
+ code:purge(M),
+ case code:load_file(M) of
+ {module, M} -> ok;
+ {error, Error} ->
+ ?DEBUG("Module ~p failed to load because ~p", [M, Error])
+ end
+ end, Modules
+ ).