Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

220 linhas
8.6 KiB

  1. %%% @doc Runs a process that holds a rebar3 state and can be used
  2. %%% to statefully maintain loaded project state into a running VM.
  3. -module(rebar_agent).
  4. -export([start_link/1, do/1, do/2]).
  5. -export(['$handle_undefined_function'/2]).
  6. -export([init/1,
  7. handle_call/3, handle_cast/2, handle_info/2,
  8. code_change/3, terminate/2]).
  9. -include("rebar.hrl").
  10. -record(state, {state,
  11. cwd,
  12. show_warning=true}).
  13. %% @doc boots an agent server; requires a full rebar3 state already.
  14. %% By default (within rebar3), this isn't called; `rebar_prv_shell'
  15. %% enters and transforms into this module
  16. -spec start_link(rebar_state:t()) -> {ok, pid()}.
  17. start_link(State) ->
  18. gen_server:start_link({local, ?MODULE}, ?MODULE, State, []).
  19. %% @doc runs a given command in the agent's context.
  20. -spec do(atom()) -> ok | {error, term()}.
  21. do(Command) when is_atom(Command) ->
  22. gen_server:call(?MODULE, {cmd, Command}, infinity);
  23. do(Args) when is_list(Args) ->
  24. gen_server:call(?MODULE, {cmd, default, do, Args}, infinity).
  25. %% @doc runs a given command in the agent's context, under a given
  26. %% namespace.
  27. -spec do(atom(), atom()) -> ok | {error, term()}.
  28. do(Namespace, Command) when is_atom(Namespace), is_atom(Command) ->
  29. gen_server:call(?MODULE, {cmd, Namespace, Command}, infinity);
  30. do(Namespace, Args) when is_atom(Namespace), is_list(Args) ->
  31. gen_server:call(?MODULE, {cmd, Namespace, do, Args}, infinity).
  32. '$handle_undefined_function'(Cmd, [Namespace, Args]) ->
  33. gen_server:call(?MODULE, {cmd, Namespace, Cmd, Args}, infinity);
  34. '$handle_undefined_function'(Cmd, [Args]) ->
  35. gen_server:call(?MODULE, {cmd, default, Cmd, Args}, infinity);
  36. '$handle_undefined_function'(Cmd, []) ->
  37. gen_server:call(?MODULE, {cmd, default, Cmd}, infinity).
  38. %%%%%%%%%%%%%%%%%
  39. %%% CALLBACKS %%%
  40. %%%%%%%%%%%%%%%%%
  41. %% @private
  42. init(State) ->
  43. Cwd = rebar_dir:get_cwd(),
  44. {ok, #state{state=State, cwd=Cwd}}.
  45. %% @private
  46. handle_call({cmd, Command}, _From, State=#state{state=RState, cwd=Cwd}) ->
  47. MidState = maybe_show_warning(State),
  48. {Res, NewRState} = run(default, Command, "", RState, Cwd),
  49. {reply, Res, MidState#state{state=NewRState}, hibernate};
  50. handle_call({cmd, Namespace, Command}, _From, State = #state{state=RState, cwd=Cwd}) ->
  51. MidState = maybe_show_warning(State),
  52. {Res, NewRState} = run(Namespace, Command, "", RState, Cwd),
  53. {reply, Res, MidState#state{state=NewRState}, hibernate};
  54. handle_call({cmd, Namespace, Command, Args}, _From, State = #state{state=RState, cwd=Cwd}) ->
  55. MidState = maybe_show_warning(State),
  56. {Res, NewRState} = run(Namespace, Command, Args, RState, Cwd),
  57. {reply, Res, MidState#state{state=NewRState}, hibernate};
  58. handle_call(_Call, _From, State) ->
  59. {noreply, State}.
  60. %% @private
  61. handle_cast(_Cast, State) ->
  62. {noreply, State}.
  63. %% @private
  64. handle_info(_Info, State) ->
  65. {noreply, State}.
  66. %% @private
  67. code_change(_OldVsn, State, _Extra) ->
  68. {ok, State}.
  69. %% @private
  70. terminate(_Reason, _State) ->
  71. ok.
  72. %%%%%%%%%%%%%%%
  73. %%% PRIVATE %%%
  74. %%%%%%%%%%%%%%%
  75. %% @private runs the actual command and maintains the state changes
  76. -spec run(atom(), atom(), string(), rebar_state:t(), file:filename()) ->
  77. {ok, rebar_state:t()} | {{error, term()}, rebar_state:t()}.
  78. run(Namespace, Command, StrArgs, RState, Cwd) ->
  79. try
  80. case rebar_dir:get_cwd() of
  81. Cwd ->
  82. PArgs = getopt:tokenize(StrArgs),
  83. Args = [atom_to_list(Namespace), atom_to_list(Command)] ++ PArgs,
  84. CmdState0 = refresh_state(RState, Cwd),
  85. CmdState1 = rebar_state:set(CmdState0, task, atom_to_list(Command)),
  86. CmdState = rebar_state:set(CmdState1, caller, api),
  87. case rebar3:run(CmdState, Args) of
  88. {ok, TmpState} ->
  89. refresh_paths(TmpState),
  90. {ok, CmdState};
  91. {error, Err} when is_list(Err) ->
  92. refresh_paths(CmdState),
  93. {{error, lists:flatten(Err)}, CmdState};
  94. {error, Err} ->
  95. refresh_paths(CmdState),
  96. {{error, Err}, CmdState}
  97. end;
  98. _ ->
  99. {{error, cwd_changed}, RState}
  100. end
  101. catch
  102. ?WITH_STACKTRACE(Type, Reason, Stacktrace)
  103. ?DEBUG("Agent Stacktrace: ~p", [Stacktrace]),
  104. {{error, {Type, Reason}}, RState}
  105. end.
  106. %% @private function to display a warning for the feature only once
  107. -spec maybe_show_warning(#state{}) -> #state{}.
  108. maybe_show_warning(S=#state{show_warning=true}) ->
  109. ?WARN("This feature is experimental and may be modified or removed at any time.", []),
  110. S#state{show_warning=false};
  111. maybe_show_warning(State) ->
  112. State.
  113. %% @private based on a rebar3 state term, reload paths in a way
  114. %% that makes sense.
  115. -spec refresh_paths(rebar_state:t()) -> ok.
  116. refresh_paths(RState) ->
  117. ToRefresh = (rebar_state:code_paths(RState, all_deps)
  118. ++ [filename:join([rebar_app_info:out_dir(App), "test"])
  119. || App <- rebar_state:project_apps(RState)]
  120. %% make sure to never reload self; halt()s the VM
  121. ) -- [filename:dirname(code:which(?MODULE))],
  122. %% Modules from apps we can't reload without breaking functionality
  123. Blacklist = [ec_cmd_log, providers, cf, cth_readable],
  124. %% Similar to rebar_utils:update_code/1, but also forces a reload
  125. %% of used modules. Also forces to reload all of ebin/ instead
  126. %% of just the modules in the .app file, because 'extra_src_dirs'
  127. %% allows to load and compile files that are not to be kept
  128. %% in the app file.
  129. lists:foreach(fun(Path) ->
  130. Name = filename:basename(Path, "/ebin"),
  131. Files = filelib:wildcard(filename:join([Path, "*.beam"])),
  132. Modules = [list_to_atom(filename:basename(F, ".beam"))
  133. || F <- Files],
  134. App = list_to_atom(Name),
  135. application:load(App),
  136. case application:get_key(App, modules) of
  137. undefined ->
  138. code:add_patha(Path),
  139. ok;
  140. {ok, Mods} ->
  141. case {length(Mods), length(Mods -- Blacklist)} of
  142. {X,X} ->
  143. ?DEBUG("reloading ~p from ~ts", [Modules, Path]),
  144. code:replace_path(App, Path),
  145. reload_modules(Modules);
  146. {_,_} ->
  147. ?DEBUG("skipping app ~p, stable copy required", [App])
  148. end
  149. end
  150. end, ToRefresh).
  151. %% @private from a disk config, reload and reapply with the current
  152. %% profiles; used to find changes in the config from a prior run.
  153. -spec refresh_state(rebar_state:t(), file:filename()) -> rebar_state:t().
  154. refresh_state(RState, _Dir) ->
  155. lists:foldl(
  156. fun(F, State) -> F(State) end,
  157. rebar3:init_config(),
  158. [fun(S) -> rebar_state:apply_profiles(S, rebar_state:current_profiles(RState)) end]
  159. ).
  160. %% @private takes a list of modules and reloads them
  161. -spec reload_modules([module()]) -> term().
  162. reload_modules([]) -> noop;
  163. reload_modules(Modules) ->
  164. reload_modules(Modules, erlang:function_exported(code, prepare_loading, 1)).
  165. %% @private reloading modules, when there are modules to actually reload
  166. reload_modules(Modules, true) ->
  167. %% OTP 19 and later -- use atomic loading and ignore unloadable mods
  168. case code:prepare_loading(Modules) of
  169. {ok, Prepared} ->
  170. [code:purge(M) || M <- Modules],
  171. code:finish_loading(Prepared);
  172. {error, ModRsns} ->
  173. Blacklist =
  174. lists:foldr(fun({ModError, Error}, Acc) ->
  175. case Error of
  176. % perhaps cover other cases of failure?
  177. on_load_not_allowed ->
  178. reload_modules([ModError], false),
  179. [ModError|Acc];
  180. _ ->
  181. ?DEBUG("Module ~p failed to atomic load because ~p", [ModError, Error]),
  182. [ModError|Acc]
  183. end
  184. end,
  185. [], ModRsns
  186. ),
  187. reload_modules(Modules -- Blacklist, true)
  188. end;
  189. reload_modules(Modules, false) ->
  190. %% Older versions, use a more ad-hoc mechanism.
  191. lists:foreach(fun(M) ->
  192. code:delete(M),
  193. code:purge(M),
  194. case code:load_file(M) of
  195. {module, M} -> ok;
  196. {error, Error} ->
  197. ?DEBUG("Module ~p failed to load because ~p", [M, Error])
  198. end
  199. end, Modules
  200. ).