%% 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 is the script that runs the relsync logic on the host. %%% @end -module(relsync). -export([ main/1, init/1, update_nodes/2, setup_local_node/1 ]). -spec main([string()]) -> no_return(). main(CmdLine) -> Opts = relsync_cli:opts(), case getopt:parse(Opts, CmdLine) of {ok, {Options, []}} -> relsync_cli:do("relsync", Options, []); {error, Error} -> io:put_chars(standard_error, [getopt:format_error(Opts, Error), "\n\n"]), getopt:usage(relsync_cli:opts(), "relsync") end. %% rebar3 plugin entry point -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> rebar3_relsync_prv:init(State). %% Use the command line parameters to setup the local %% Erlang instance to be able to talk to remote nodes. -spec setup_local_node([term()]) -> ok. setup_local_node(Options) -> % First, make sure that Epmd is running case net_adm:names() of %% Epmd is running {ok, _} -> ok; {error, address} -> Epmd = os:find_executable("epmd"), os:cmd(Epmd ++ " -daemon") end, % Next, start up net_kernel case net_kernel:start(options_to_netkernel(Options)) of {ok, _} -> case is_alive() of true -> Cookie = proplists:get_value(cookie, Options), erlang:set_cookie(node(), list_to_atom(Cookie)), ok; false -> {error, set_cookie} end; {error, Reason} -> {error, net_kernel, Reason} end. -spec options_to_netkernel([term()]) -> [atom()]. options_to_netkernel(Options) -> case proplists:get_value(sname, Options) of undefined -> case proplists:get_value(name, Options) of undefined -> exit({badargs, "Specify --sname or --name"}); Name -> [list_to_atom(Name), longnames] end; SName -> [list_to_atom(SName), shortnames] end. update_nodes(ssh, Options) -> ok = ssh:start(), DestNode = proplists:get_value(destnode, Options), update_node(ssh, [DestNode], Options); update_nodes(erl, Options) -> % Only support one node for now. In theory, we could update % a swarm of devices. That would be wild. DestNode = proplists:get_value(destnode, Options), update_node(erl, [list_to_atom(DestNode)], Options). update_node(ssh, [], _Options) -> ok; update_node(ssh, [Node | T], Options) -> io:format("Updating ~p...~n", [Node]), % Gather our options {ok, Cwd} = file:get_cwd(), UserDir = proplists:get_value(user_dir, Options, Cwd), DestPath = normalize_path(proplists:get_value(destpath, Options)), DestRwPath = normalize_path(proplists:get_value(destrwpath, Options)), LocalPath = normalize_path(proplists:get_value(localpath, Options)), Hooks = proplists:get_value(hooks, Options), Port = proplists:get_value(port, Options), {ok, ConRef} = relsync_ssh_client:connect(Node, Port, UserDir), {ok, Client} = relsync_ssh_client:start_link(ConRef), % Start syncing ok = relsync_ssh_client:set_hooks(Client, Hooks), ok = relsync_ssh_client:notify_presync(Client), DestPathToUse = case DestRwPath of "" -> DestPath; _ -> %ok = target_syncer:create_symlink_mirror(Node, DestPath, DestRwPath), DestRwPath end, {ok, DestFileInfos} = relsync_ssh_client:get_file_listing(Client, DestPathToUse), LocalFileInfos = relsync_lib:get_file_listing(LocalPath), {ok, FileList} = synchronize_node( {relsync_ssh_client, Client}, LocalPath, LocalFileInfos, DestPathToUse, DestFileInfos ), {ok, FilesReloaded} = relsync_ssh_client:reload(Client, FileList), [io:format("~-10s ~s", [Status, Path]) || {Path, Status} <- FilesReloaded], ok = relsync_ssh_client:notify_postsync(Client), ok = relsync_ssh_client:stop(Client), ok = relsync_ssh_client:close(ConRef), % Do the next node. update_node(erl, T, Options); update_node(erl, [], _Options) -> ok; update_node(erl, [Node | T], Options) -> % Ping the node to make sure that it is connected io:format("Updating ~p...~n", [Node]), pong = net_adm:ping(Node), % Start up the remote syncer {ok, _} = target_syncer_sup:start_child(Node), % Gather our options DestPath = normalize_path(proplists:get_value(destpath, Options)), DestRwPath = normalize_path(proplists:get_value(destrwpath, Options)), LocalPath = normalize_path(proplists:get_value(localpath, Options)), Hooks = proplists:get_value(hooks, Options), % Start syncing ok = target_syncer:set_hooks(Node, Hooks), ok = target_syncer:notify_presync(Node), DestPathToUse = case DestRwPath of "" -> DestPath; _ -> ok = target_syncer:create_symlink_mirror(Node, DestPath, DestRwPath), DestRwPath end, DestFileInfos = target_syncer:get_file_listing(Node, DestPathToUse), LocalFileInfos = relsync_lib:get_file_listing(LocalPath), synchronize_node( {target_syncer, Node}, LocalPath, LocalFileInfos, DestPathToUse, DestFileInfos ), ok = target_syncer:notify_postsync(Node), % Do the next node. update_node(erl, T, Options). -spec normalize_path(string()) -> string(). normalize_path("") -> ""; normalize_path(Path) -> case lists:last(Path) of $/ -> Path; _ -> Path ++ "/" end. % Return true if this is a safe file to synchronize -spec safe_file({string(), {integer(), binary()}}) -> true | false. safe_file({Filename, _Info}) -> filename:extension(Filename) =/= ".so". % Synchronize the nodes by taking the local and remote file % lists, filtering and sorting them, and then comparing them % one by one to make sure that both sides are in sync. -spec synchronize_node(Node, LocalPath, LocalFileInfos, DestPath, DestFileInfos) -> ok when Node :: atom() | {module(), any()}, LocalPath :: string(), LocalFileInfos :: [{string(), {integer(), binary()}}], DestPath :: string(), DestFileInfos :: [{string(), {integer(), binary()}}]. synchronize_node(Node, LocalPath, LocalFileInfos, DestPath, DestFileInfos) -> FilteredLocalInfos = lists:filter(fun safe_file/1, LocalFileInfos), SortedLocalInfos = lists:sort(FilteredLocalInfos), FilteredDestInfos = lists:filter(fun safe_file/1, DestFileInfos), SortedDestInfos = lists:sort(FilteredDestInfos), sync_files( Node, normalize_path(LocalPath), SortedLocalInfos, normalize_path(DestPath), SortedDestInfos, [] ). -spec sync_files(Node, LocalPath, LocalFiles, DestPath, DestFiles, AccFiles) -> ok when Node :: atom(), LocalPath :: string(), LocalFiles :: [{string(), {integer(), binary()}}], DestPath :: string(), DestFiles :: [{string(), {integer(), binary()}}], AccFiles :: [string()]. sync_files(_Node, _LocalPath, [], _DestPath, [], AccFiles) -> {ok, AccFiles}; sync_files(Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [], AccFiles) -> io:format("Creating ~p on ~p...~n", [LocalFile, Node]), {ok, Contents} = file:read_file(LocalPath ++ LocalFile), {Mode, _} = LocalInfo, Path = DestPath ++ LocalFile, ok = copy_file(Node, Path, Mode, Contents), sync_files(Node, LocalPath, LTail, DestPath, [], [Path | AccFiles]); sync_files(Node, LocalPath, [], DestPath, [{DestFile, _DestInfo} | DTail], AccFiles) -> io:format("Deleting ~p from ~p...~n", [DestFile, Node]), Path = DestPath ++ DestFile, rm_file(Node, Path), sync_files(Node, LocalPath, [], DestPath, DTail, [Path | AccFiles]); sync_files( Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [{DestFile, DestInfo} | DTail], AccFiles ) when LocalFile =:= DestFile, LocalInfo =:= DestInfo -> sync_files(Node, LocalPath, LTail, DestPath, DTail, AccFiles); sync_files( Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [{DestFile, DestInfo} | DTail], AccFiles ) when LocalFile =:= DestFile, LocalInfo =/= DestInfo -> io:format("Updating ~p on ~p...~n", [LocalFile, Node]), Path = LocalPath ++ LocalFile, {ok, Contents} = file:read_file(Path), {Mode, _} = LocalInfo, ok = copy_file(Node, DestPath ++ LocalFile, Mode, Contents), sync_files(Node, LocalPath, LTail, DestPath, DTail, [Path | AccFiles]); sync_files( Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [{DestFile, _DestInfo} | DTail], AccFiles ) when LocalFile > DestFile -> io:format("Deleting ~p from ~p...~n", [DestFile, Node]), Path = DestPath ++ DestFile, rm_file(Node, Path), sync_files(Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, DTail, [Path | AccFiles]); sync_files( Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [{DestFile, DestInfo} | DTail], AccFiles ) when LocalFile < DestFile -> io:format("Creating ~p on ~p...~n", [LocalFile, Node]), Path = LocalPath ++ LocalFile, {ok, Contents} = file:read_file(Path), {Mode, _} = LocalInfo, ok = copy_file(Node, DestPath ++ LocalFile, Mode, Contents), sync_files(Node, LocalPath, LTail, DestPath, [{DestFile, DestInfo} | DTail], [Path | AccFiles]). copy_file({Module, Node}, FilePath, Mode, Contents) -> Module:copy_file(Node, FilePath, Mode, Contents). rm_file({Module, Node}, FilePath) -> Module:rm_file(Node, FilePath).