%% Copyright 2014 Frank Hunleth %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%% @doc %%% This module is sent to the target device to scan files locally and %%% implement the updates as requested by the host device running relsync. %%% @end -module(target_syncer). -include_lib("kernel/include/file.hrl"). -behaviour(gen_server). %% API -export([ start_link/0, start_link/1, get_file_listing/2, set_hooks/2, get_local_file_listing/1, copy_file/4, rm_file/2, create_symlink_mirror/3, notify_presync/1, notify_postsync/1 ]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). -define(SERVER, ?MODULE). -record(state, { % hooks holds the module name that provides an alternative % implementation to the default synchronization hooks }). %%%=================================================================== %%% API %%%=================================================================== -spec get_file_listing(atom(), string()) -> [{string(), {integer(), binary()}}]. get_file_listing(Node, Path) -> gen_server:call({?SERVER, Node}, {get_file_listing, Path}). % Traverse the directory specified by path and return the list of files % with their info. -spec get_local_file_listing(string()) -> [{string(), {integer(), binary()}}]. get_local_file_listing(Path) -> PrefixLength = length(Path), filelib:fold_files( Path, ".*", true, fun(Y, Acc) -> [{lists:nthtail(PrefixLength, Y), file_info(Y)} | Acc] end, [] ). % Use the specified Module to customize the behavior of the % synchronization process. The object code for the Module % is sent to the remote Node as well. -spec set_hooks(atom(), atom()) -> ok. set_hooks(Node, undefined) -> gen_server:call({?SERVER, Node}, clear_hooks); set_hooks(Node, ModuleName) -> {Module, Bin, File} = maybe_compile(ModuleName), gen_server:call({?SERVER, Node}, {set_hooks, Module, Bin, File}). maybe_compile(ModuleName) -> Module = list_to_atom(ModuleName), case code:get_object_code(Module) of {Module, Bin, File} -> {Module, Bin, File}; _ -> {ok, CompiledModule, Bin} = compile:file(ModuleName, [binary]), {CompiledModule, Bin, ModuleName} end. % Copy the Contents to the file specified by Path on Node, and % then set the mode to Mode. -spec copy_file(atom(), string(), integer(), binary()) -> ok | {error, _}. copy_file(Node, Path, Mode, Contents) -> gen_server:call({?SERVER, Node}, {copy_file, Path, Mode, Contents}). % Remove the specified file from Node -spec rm_file(atom(), string()) -> ok | {error, _}. rm_file(Node, Path) -> gen_server:call({?SERVER, Node}, {rm_file, Path}). % Create a symlink mirror of all files in Path in NewPath, % but only if NewPath doesn't exist. -spec create_symlink_mirror(atom(), string(), string()) -> ok | {error, _}. create_symlink_mirror(Node, Path, NewPath) -> gen_server:call({?SERVER, Node}, {create_symlink_mirror, Path, NewPath}). % Called to let the remote node know that a synchronization % run is coming. -spec notify_presync(atom()) -> ok. notify_presync(Node) -> gen_server:call({?SERVER, Node}, notify_presync). % Called to let the remote node know that a synchronization % run has finished. -spec notify_postsync(atom()) -> ok. notify_postsync(Node) -> gen_server:call({?SERVER, Node}, notify_postsync). %%-------------------------------------------------------------------- %% @doc %% Starts the server locally (called by the supervisor) %% %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} %% @end %%-------------------------------------------------------------------- start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). %% Starts the server on the specified remote node. start_link(Node) -> Result = rpc:call(Node, gen_server, start, [{local, ?SERVER}, ?MODULE, [], []]), case Result of {ok, Pid} -> link(Pid) end, Result. %%%=================================================================== %%% gen_server callbacks %%%=================================================================== %%-------------------------------------------------------------------- %% @private %% @doc %% Initializes the server %% %% @spec init(Args) -> {ok, State} | %% {ok, State, Timeout} | %% ignore | %% {stop, Reason} %% @end %%-------------------------------------------------------------------- init([]) -> {ok, #state{}}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling call messages %% %% @spec handle_call(Request, From, State) -> %% {reply, Reply, State} | %% {reply, Reply, State, Timeout} | %% {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, Reply, State} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_call({get_file_listing, Path}, _From, State) -> Reply = get_local_file_listing(Path), {reply, Reply, State}; handle_call(clear_hooks, _From, State) -> NewState = State#state{hooks = undefined}, {reply, ok, NewState}; handle_call({set_hooks, Module, Bin, File}, _From, State) -> code:load_binary(Module, File, Bin), NewState = State#state{hooks = Module}, {reply, ok, NewState}; handle_call({copy_file, Path, Mode, Contents}, _From, State) -> ok = filelib:ensure_dir(Path), % delete the file first so that we write to a new inode. This is needed % for symlink mirrors, but also more gracefully handles the case where % someone else has the file opened. file:delete(Path), ok = file:write_file(Path, Contents), ok = file:change_mode(Path, Mode), maybe_update_beam(Path), {reply, ok, State}; handle_call({create_symlink_mirror, Path, NewPath}, _From, State) -> case filelib:is_dir(NewPath) of false -> ok = filelib:ensure_dir(NewPath), FromFiles = get_local_file_listing(Path), [ok = symlink_files(Path ++ File, NewPath ++ File) || {File, _} <- FromFiles], % Update Erlang's search paths to look in the mirror location now. ok = update_code_paths(Path, NewPath); true -> % Don't do anything, since the mirror already exists. ok end, {reply, ok, State}; handle_call({rm_file, Path}, _From, State) -> Reply = file:delete(Path), {reply, Reply, State}; handle_call(notify_presync, _From, State) -> #state{hooks = Hooks} = State, call_hook_or_not(Hooks, presync), {reply, ok, State}; handle_call(notify_postsync, _From, State) -> #state{hooks = Hooks} = State, call_hook_or_not(Hooks, postsync), {reply, ok, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling cast messages %% %% @spec handle_cast(Msg, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_cast(_Msg, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% Handling all non call/cast messages %% %% @spec handle_info(Info, State) -> {noreply, State} | %% {noreply, State, Timeout} | %% {stop, Reason, State} %% @end %%-------------------------------------------------------------------- handle_info(_Info, State) -> {noreply, State}. %%-------------------------------------------------------------------- %% @private %% @doc %% This function is called by a gen_server when it is about to %% terminate. It should be the opposite of Module:init/1 and do any %% necessary cleaning up. When it returns, the gen_server terminates %% with Reason. The return value is ignored. %% %% @spec terminate(Reason, State) -> void() %% @end %%-------------------------------------------------------------------- terminate(_Reason, _State) -> ok. %%-------------------------------------------------------------------- %% @private %% @doc %% Convert process state when code is changed %% %% @spec code_change(OldVsn, State, Extra) -> {ok, NewState} %% @end %%-------------------------------------------------------------------- code_change(_OldVsn, State, _Extra) -> {ok, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== -spec file_info(string()) -> {integer(), binary()}. file_info(Filename) -> {ok, Data} = file:read_file(Filename), Hash = crypto:hash(sha, Data), {ok, #file_info{mode = Mode}} = file:read_file_info(Filename), {Mode, Hash}. %%-spec call_hook_or_not(atom(), atom()) -> call_hook_or_not(undefined, presync) -> ok; call_hook_or_not(undefined, postsync) -> ok; call_hook_or_not(M, F) -> M:F(). maybe_update_beam(Path) -> case filename:extension(Path) of ".beam" -> update_beam(Path); _ -> ok end. update_beam(Path) -> Module = list_to_atom(filename:rootname(filename:basename(Path))), case code:which(Module) of non_existing -> % Code not loaded yet. Let the VM load it on demand. ok; _ -> % NOTE: we don't check whether the old path (from code:which/1) % is the same as the new Path. Symlink mirroring would fail this even % though it is ok, but in general, we don't police module naming collisions. case code:is_sticky(Module) of true -> io:format("Not reloading sticky module ~p.~n", [Module]); false -> % Updating code that has been loaded io:format("Reloading ~p...~n", [Module]), code:purge(Module), {module, Module} = code:load_file(Module) end end. symlink_files(From, To) -> filelib:ensure_dir(To), file:make_symlink(From, To). replace_prefix(Path, From, To) -> case lists:prefix(From, Path) of false -> % Not affected, so don't update. Path; true -> To ++ lists:nthtail(length(From), Path) end. % Update the Erlang VM's code search path to the new directory prefix. % This is called after mirroring the directory so that we can write to it. update_code_paths(From, To) -> NewPaths = [replace_prefix(Path, From, To) || Path <- code:get_path()], true = code:set_path(NewPaths), ok.