You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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