src/power_shell_default.erl (130 lines of code) (raw):

%%%------------------------------------------------------------------- %%% @author Maxim Fedorov <maximfca@gmail.com> %%% @copyright (c) WhatsApp Inc. and its affiliates. All rights reserved. %%% @doc %%% Allows to inject proxy functions into existing modules, compiled %%% with debug into. Added proxy functions are just redirecting %%% calls to another module. %%% %%% This could be useful for environments where both shell_default %%% and user_default are already used, and it's not possible to %%% replace any of these. %%% @end %%%------------------------------------------------------------------- -module(power_shell_default). -author("maximfca@gmail.com"). %% API -export([inject/2, eject/2]). %%==================================================================== %% API %% @doc Extracts abstract code from WhereTo module, adds all %% exports from the module Mod into WhereTo, compiles %% result and loads it, replacing current WhereTo. %% This sequence injects proxies for all functions exported %% from Mod into WhereTo (ignoring compiler-generated %% module_info/0 and module_info/1). %% For example, if Mod exports eval/3, after calling %% power_shell_default:inject(Mod) resulting WhereTo %% contains following code: %% ``` %% -export([eval/3]). %% eval(Arg1, Arg2, Arg3) -> %% Mod:eval(Arg1, Arg2, Arg3).''' %% %% If there is no WhereTo module loaded, proxy just %% creates one from scratch. %% @param WhereTo Module name to add proxy methods to %% @param Mod Module name to proxy calls to -spec inject(WhereTo :: module(), Mod :: module()) -> ok | {error, already_loaded} | {error, no_abstract_code} | {error, {badmatch, module()}} | {error, {beam_lib, beam_lib:chnk_rsn()}}. inject(WhereTo, Mod) when is_atom(WhereTo), is_atom(Mod) -> Filename = atom_to_list(Mod), case code:which(WhereTo) of Filename -> {error, already_loaded}; NewFile when is_list(NewFile) -> case beam_lib:chunks(NewFile, [abstract_code]) of {ok, {WhereTo, [{abstract_code, {_, Forms}}]}} -> inject_impl(Forms, Filename, WhereTo, Mod); {ok, {WhereTo, [{abstract_code,no_abstract_code}]}} -> {error, no_abstract_code}; {ok, {ActualMod, _}} -> {error, {badmatch, ActualMod}}; Error -> {error, {beam_lib, Error}} end; non_existing -> inject_impl([{attribute,20,module,WhereTo}], Filename, WhereTo, Mod) end. %% @doc Verifies that current WhereFrom has injected functions from %% Mod, and purges/deletes WhereFrom code from memory. Next %% time shell does ensure_loaded() for WhereFrom, an unmodified %% version is loaded from disk. %% @param WhereFrom original module name, e.g. shell_default %% @param Mod module that has been injected -spec eject(WhereFrom :: module(), Mod :: module()) -> ok | {error, not_loaded}. eject(WhereFrom, Mod) when is_atom(WhereFrom), is_atom(Mod) -> ModFile = atom_to_list(Mod), case code:is_loaded(WhereFrom) of {file, ModFile} -> % just purge/delete our version, so OTP loads % previous edition from dist code:purge(WhereFrom), true = code:delete(WhereFrom), ok; _ -> {error, not_loaded} end. %%==================================================================== %% Internal functions % Inserts an already reversed sequence of attributes and % functions in the middle of parsed AST. % This essentially adds a few more exported functions to the % file, pretty much like compiler adds module_info(). insert_funs(Exports, Funs, Forms) -> {Attrs, Functions} = lists:splitwith( fun ({function, _, _, _, _}) -> false; (_) -> true end, Forms), Attrs ++ [Exports | Funs] ++ Functions. % Generates abstract syntax tree for proxy function Mod:Fun/Arity. proxy_fun(Mod, Fun, Arity) -> ProxyArgs = [{var, 1, list_to_atom("Arg" ++ integer_to_list(N))} || N <- lists:seq(1, Arity)], % [{var,1,'T'}] {function, 1, Fun, Arity, [{clause, 1, ProxyArgs, [], [{call,1, {remote, 1, {atom, 1, Mod}, {atom, 1, Fun}}, ProxyArgs}]}]}. inject_impl(Forms, Filename, WhereTo, Mod) -> % pick exports from Mod (power_shell expected), removing OTP-added functions Forwards = Mod:module_info(exports) -- [{module_info, 0}, {module_info, 1}], % make AST out of exports Exports = {attribute, 1, export, Forwards}, Funs = [proxy_fun(Mod, Fun, Arity) || {Fun, Arity} <- Forwards], Forms1 = insert_funs(Exports, Funs, Forms), % compile to beam {ok, App, Bin} = compile:forms(Forms1), % unstick if necessary StickBack = case code:is_sticky(WhereTo) of true -> code:unstick_mod(WhereTo); false -> false end, % load augmented code {module, WhereTo} = code:load_binary(App, Filename, Bin), % stick back if was unstuck if StickBack -> code:stick_mod(WhereTo); true -> ok end, % and immediately purge old code (race conditions are unlikely) code:purge(WhereTo), ok.