Current section
Files
Jump to
Current section
Files
src/relsync.erl
%% 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).