diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx.erl b/zomp/lib/otpr-zx/0.1.0/src/zx.erl index 9d1fc3f..f08d944 100644 --- a/zomp/lib/otpr-zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr-zx/0.1.0/src/zx.erl @@ -369,6 +369,10 @@ ensure_all_started(AppMod) -> %% command line arguments provided. pass_argv(AppMod, Args) -> + case lists:member({accept_argv, 1}, AppMod:module_info(exports)) of + true -> AppMod:accept_argv(Args); + false -> ok +end. @@ -530,7 +534,7 @@ ensure_installed(PackageID = {Realm, Name, Version}) -> case resolve_installed_version(PackageID) of exact -> {ok, PackageID}; {ok, Installed} -> {ok, {Realm, Name, Installed}}; - not_found -> fetch({Realm, Name, Version}) + not_found -> fetch_one({Realm, Name, Version}) end. @@ -553,38 +557,6 @@ fetch_one(PackageID) -> end. --spec ensure_installed(Realm, Name, Version) -> Result | no_return() - when Realm :: realm(), - Name :: name(), - Version :: version(), - Result :: exact - | {ok, package_id()} - | not_found. -%% @private -%% Fetch and install the latest compatible version of the given package ID, whether -%% the version indicator is complete, partial or blank. - -ensure_installed(Realm, Name, Version) -> - case zx_daemon:query_latest({Realm, Name, Version}) of - {ok, LatestVersion} -> - LatestID = {Realm, Name, LatestVersion}, - ok = ensure_dep(Socket, LatestID), - {ok, LatestID}; - {error, bad_realm} -> - PackageString = package_string({Realm, Name, Version}), - ok = log(warning, "Bad realm: ~ts.", [PackageString]), - halt(1); - {error, bad_package} -> - PackageString = package_string({Realm, Name, Version}), - ok = log(warning, "Bad package: ~ts.", [PackageString]), - halt(1); - {error, bad_version} -> - PackageString = package_string({Realm, Name, Version}), - ok = log(warning, "Bad version: ~s.", [PackageString]), - halt(1) - end. - - -spec resolve_installed_version(PackageID) -> Result when PackageID :: package_id(), Result :: not_found @@ -629,59 +601,18 @@ tuplize(String, Acc) -> set_version(VersionString) -> NewVersion = case zx_lib:string_to_version(VersionString) of - {_, _, z} -> + {ok, {_, _, z}} -> Message = "'set version' arguments must be complete, ex: 1.2.3", - ok = log(error, Message), - halt(22); - Version -> - Version + error_exit(Message, ?LINE); + {ok, Version} -> + Version; + {error, invalid_version_string} -> + Message = "Invalid version string: ~tp", + error_exit(Message, [VersionString], ?LINE) end, - update_version(NewVersion). - - --spec update_version(Level) -> no_return() - when Level :: major - | minor - | patch - | VersionString, - VersionString :: string(). % Of the form "Major.Minor.Patch" -%% @private -%% Update a project's `zomp.meta' file by either incrementing the indicated component, -%% or setting the version number to the one specified in VersionString. -%% This part of the procedure guards for the case when the zomp.meta file cannot be -%% read for some reason. - -update_version(Arg) -> {ok, Meta} = zx_lib:read_project_meta(), - PackageID = maps:get(package_id, Meta), - update_version(Arg, PackageID, Meta). - - --spec update_version(Level, PackageID, Meta) -> no_return() - when Level :: major - | minor - | patch - | version(), - PackageID :: package_id(), - Meta :: [{atom(), term()}]. -%% @private -%% Update a project's `zomp.meta' file by either incrementing the indicated component, -%% or setting the version number to the one specified in VersionString. -%% This part of the procedure does the actual update calculation, to include calling to -%% convert the VersionString (if it is passed) to a `version()' type and check its -%% validity (or halt if it is a bad string). - -update_version(major, {Realm, Name, OldVersion = {Major, _, _}}, OldMeta) -> - NewVersion = {Major + 1, 0, 0}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta); -update_version(minor, {Realm, Name, OldVersion = {Major, Minor, _}}, OldMeta) -> - NewVersion = {Major, Minor + 1, 0}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta); -update_version(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> - NewVersion = {Major, Minor, Patch + 1}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta); -update_version(NewVersion, {Realm, Name, OldVersion}, OldMeta) -> - update_version(Realm, Name, OldVersion, NewVersion, OldMeta). + {Realm, Name, OldVersion} = maps:get(package_id, Meta), + update_version(Realm, Name, OldVersion, NewVersion, Meta). -spec update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> no_return() @@ -689,7 +620,7 @@ update_version(NewVersion, {Realm, Name, OldVersion}, OldMeta) -> Name :: name(), OldVersion :: version(), NewVersion :: version(), - OldMeta :: [{atom(), term()}]. + OldMeta :: project_meta(). %% @private %% Update a project's `zomp.meta' file by either incrementing the indicated component, %% or setting the version number to the one specified in VersionString. @@ -701,10 +632,9 @@ update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> PackageID = {Realm, Name, NewVersion}, NewMeta = maps:put(package_id, PackageID, OldMeta), ok = zx_lib:write_project_meta(NewMeta), - ok = log(info, - "Version changed from ~s to ~s.", - [zx_lib:version_to_string(OldVersion), - zx_lib:version_to_string(NewVersion)]), + OldVS = zx_lib:version_to_string(OldVersion), + NewVS = zx_lib:version_to_string(NewVersion), + ok = log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]), halt(0). @@ -714,7 +644,7 @@ update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> -spec list_realms() -> no_return(). %% @private %% List all currently configured realms. The definition of a "configured realm" is a -%% realm for which a .realm file exists in ~/zomp/. The realms will be printed to +%% realm for which a .realm file exists in $ZOMP_HOME. The realms will be printed to %% stdout and the program will exit. list_realms() -> @@ -731,6 +661,7 @@ list_realms() -> %% them to stdout. list_packages(Realm) -> + ok = start(), case zx_daemon:list_packages(Realm) of {ok, []} -> ok = log(info, "Realm ~tp has no packages available.", [Realm]), @@ -740,14 +671,12 @@ list_packages(Realm) -> ok = lists:foreach(Print, Packages), halt(0); {error, bad_realm} -> - ok = log(error, "Bad realm name."), - halt(1); + error_exit("Bad realm name.", ?LINE); {error, no_realm} -> - ok = log(error, "Realm \"~ts\" is not configured.", [Realm]), - halt(1); + error_exit("Realm \"~ts\" is not configured.", ?LINE); {error, network} -> - ok = log(error, "Network issues are preventing connection to the realm."), - halt(1) + Message = "Network issues are preventing connection to the realm.", + error_exit(Message, ?LINE); end. @@ -757,12 +686,10 @@ list_packages(Realm) -> %% package name (such as "otpr-zomp") and the return values will be full package strings %% of the form "otpr-zomp-1.2.3", one per line printed to stdout. -list_versions(Package = {Realm, Name}) -> - ok = valid_package(Package), - Socket = connect_user(Realm), - ok = send(Socket, {list, Realm, Name}), - case recv_or_die(Socket) of - {ok, []} -> +list_versions(Package) -> + ok = start(), + case zx_daemon:list_versions(Package) of + {ok, []} Message = "Package ~ts-~ts has no versions available.", ok = log(info, Message, [Realm, Name]), halt(0); @@ -773,21 +700,26 @@ list_versions(Package = {Realm, Name}) -> io:format("~ts~n", [PackageString]) end, ok = lists:foreach(Print, Versions), - halt(0) + halt(0); + {error, bad_realm} -> + error_exit("Bad realm name.", ?LINE); + {error, bad_package} -> + error_exit("Bad package name.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + error_exit(Message, ?LINE) end. -spec list_pending(package()) -> no_return(). %% @private -%% List the versions of a package that are pending review. The package name is input by the -%% user as a string of the form "otpr-zomp" and the output is a list of full package IDs, -%% printed one per line to stdout (like "otpr-zomp-3.2.2"). +%% List the versions of a package that are pending review. The package name is input by +%% the user as a string of the form "otpr-zomp" and the output is a list of full +%% package IDs, printed one per line to stdout (like "otpr-zomp-3.2.2"). -list_pending(Package = {Realm, Name}) -> - ok = valid_package(Package), - Socket = connect_user(Realm), - ok = send(Socket, {pending, Package}), - case recv_or_die(Socket) of +list_pending(Package) -> + ok = start(), + case zx_daemon:list_pending(Package) of {ok, []} -> Message = "Package ~ts-~ts has no versions pending.", ok = log(info, Message, [Realm, Name]), @@ -799,7 +731,14 @@ list_pending(Package = {Realm, Name}) -> io:format("~ts~n", [PackageString]) end, ok = lists:foreach(Print, Versions), - halt(0) + halt(0); + {error, bad_realm} -> + error_exit("Bad realm name.", ?LINE); + {error, bad_package} -> + error_exit("Bad package name.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + error_exit(Message, ?LINE) end. @@ -809,9 +748,8 @@ list_pending(Package = {Realm, Name}) -> %% printed to stdout one per line. list_resigns(Realm) -> - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {list_resigns, Realm}), - case recv_or_die(Socket) of + ok = start(), + case zx_daemon:list_resigns(Realm) of {ok, []} -> Message = "No packages pending signature in ~tp.", ok = log(info, Message, [Realm]), @@ -823,59 +761,14 @@ list_resigns(Realm) -> io:format("~ts~n", [PackageString]) end, ok = lists:foreach(Print, PackageIDs), - halt(0) - end. - - --spec valid_package(package()) -> ok | no_return(). -%% @private -%% Test whether a package() type is a valid value or not. If not, halt execution with -%% a non-zero error code, if so then return `ok'. - -valid_package({Realm, Name}) -> - case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_lower0_9(Name)} of - {true, true} -> - ok; - {false, true} -> - ok = log(error, "Invalid realm name: ~tp", [Realm]), - halt(1); - {true, false} -> - ok = log(error, "Invalid package name: ~tp", [Name]), - halt(1); - {false, false} -> - ok = log(error, "Invalid realm ~tp and package ~tp", [Realm, Name]), - halt(1) - end. - - --spec string_to_package(string()) -> package() | no_return(). -%% @private -%% Convert a string to a package() type if possible. If not then halt the system. - -string_to_package(String) -> - case string:lexemes(String, "-") of - [Realm, Name] -> - Package = {Realm, Name}, - ok = valid_package(Package), - Package; - _ -> - ok = log(error, "Bad package name."), - halt(1) - end. - - --spec valid_realm(realm()) -> ok | no_return(). -%% @private -%% Test whether a realm name is a valid realm() type (that is, a lower0_9()) or not. If not, -%% halt execution with a non-zero error code, if so then return `ok'. - -valid_realm(Realm) -> - case zx_lib:valid_lower0_9(Realm) of - true -> - ok; - false -> - ok = log(error, "Bad realm name."), - halt(1) + halt(0); + {error, bad_realm} -> + error_exit("Bad realm name.", ?LINE); + {error, no_realm} -> + error_exit("Realm \"~ts\" is not configured.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + error_exit(Message, ?LINE) end. @@ -884,6 +777,11 @@ valid_realm(Realm) -> -spec add_realm(Path) -> no_return() when Path :: file:filename(). +%% @private +%% Add a .realm file to $ZOMP_HOME from a location in the filesystem. +%% Print the SHA512 of the .realm file for the user so they can verify that the file +%% is authentic. This implies, of course, that .realm maintainers are going to +%% post SHA512 sums somewhere visible. add_realm(Path) -> case file:read_file(Path) of @@ -912,45 +810,30 @@ add_realm(Path, Data) -> ok = log(info, "Realm ~ts is now visible to this system.", [Realm]), halt(0); {error, invalid_tar_checksum} -> - ok = log(warning, "FAILED: ~ts is not a valid realm file.", [Path]), - halt(1); + error_exit("~ts is not a valid realm file.", [Path], ?LINE); {error, eof} -> - ok = log(warning, "FAILED: ~ts is not a valid realm file.", [Path]), - halt(1) + error_exit("~ts is not a valid realm file.", [Path], ?LINE) end. -spec add_package(PackageName) -> no_return() when PackageName :: package(). +%% @private +%% A sysop command that adds a package to a realm operated by the caller. add_package(PackageName) -> ok = file:set_cwd(zx_lib:zomp_home()), - case string:lexemes(PackageName, "-") of - [Realm, Name] -> - case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_lower0_9(Name)} of - {true, true} -> - add_package(Realm, Name); - {false, true} -> - ok = log(warning, "Invalid realm name: ~tp", [Realm]), - halt(1); - {true, false} -> - ok = log(warning, "Invalid package name: ~tp", [Name]), - halt(1); - {false, false} -> - ok = log(warning, "Invalid realm and package names."), - halt(1) - end; + case zx_lib:package_id(PackageName) of + {ok, {Realm, Name, {z, z, z}} -> + add_package(Realm, Name); _ -> - ok = log(warning, "Name ~tp is not a valid package name.", [PackageName]), - halt(1) + error_exit("~tp is not a valid package name.", [PackageName], ?LINE) end. -spec add_package(Realm, Name) -> no_return() when Realm :: realm(), Name :: name(). -%% @private -%% This sysop-only command can add a package to a realm operated by the caller. add_package(Realm, Name) -> Socket = connect_auth_or_die(Realm), @@ -1087,7 +970,8 @@ drop_dep(PackageID) -> drop_key({Realm, KeyName}) -> ok = file:set_cwd(zx_lib:zomp_home()), - Pattern = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".{key,pub}.der"]), + KeyGlob = KeyName ++ ".{key,pub},der", + Pattern = filename:join([zx_lib:zomp_home(), "key", Realm, KeyGlob]), case filelib:wildcard(Pattern) of [] -> ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), @@ -1105,7 +989,7 @@ drop_key({Realm, KeyName}) -> drop_realm(Realm) -> ok = file:set_cwd(zx_lib:zomp_home()), - RealmConf = realm_conf(Realm), + RealmConf = zx_lib:realm_conf(Realm), case filelib:is_regular(RealmConf) of true -> Message = @@ -1129,6 +1013,7 @@ drop_realm(Realm) -> clear_keys(Realm) end. + -spec drop_prime(realm()) -> ok. drop_prime(Realm) -> @@ -1150,15 +1035,8 @@ drop_prime(Realm) -> clear_keys(Realm) -> KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), case filelib:is_dir(KeyDir) of - true -> - ok = log(info, "Wiping key dir ~ts", [KeyDir]), - Keys = filelib:wildcard(KeyDir ++ "/**"), - Delete = fun(K) -> file:delete(K) end, - ok = lists:foreach(Delete, Keys), - ok = file:del_dir(KeyDir), - log(info, "Done!"); - false -> - log(warning, "Keydir ~ts not found", [KeyDir]) + true -> rm_rf(KeyDir); + false -> log(warning, "Keydir ~ts not found", [KeyDir]) end. @@ -1170,12 +1048,53 @@ clear_keys(Realm) -> %% @private %% Convert input string arguments to acceptable atoms for use in update_version/1. -verup("major") -> update_version(major); -verup("minor") -> update_version(minor); -verup("patch") -> update_version(patch); +verup("major") -> version_up(major); +verup("minor") -> version_up(minor); +verup("patch") -> version_up(patch); verup(_) -> usage_exit(22). +-spec version_up(Level) -> no_return() + when Level :: major + | minor + | patch. +%% @private +%% Update a project's `zomp.meta' file by either incrementing the indicated component, +%% or setting the version number to the one specified in VersionString. +%% This part of the procedure guards for the case when the zomp.meta file cannot be +%% read for some reason. + +version_up(Arg) -> + {ok, Meta} = zx_lib:read_project_meta(), + PackageID = maps:get(package_id, Meta), + version_up(Arg, PackageID, Meta). + + +-spec version_up(Level, PackageID, Meta) -> no_return() + when Level :: major + | minor + | patch + | version(), + PackageID :: package_id(), + Meta :: [{atom(), term()}]. +%% @private +%% Update a project's `zomp.meta' file by either incrementing the indicated component, +%% or setting the version number to the one specified in VersionString. +%% This part of the procedure does the actual update calculation, to include calling to +%% convert the VersionString (if it is passed) to a `version()' type and check its +%% validity (or halt if it is a bad string). + +version_up(major, {Realm, Name, OldVersion = {Major, _, _}}, OldMeta) -> + NewVersion = {Major + 1, 0, 0}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +version_up(minor, {Realm, Name, OldVersion = {Major, Minor, _}}, OldMeta) -> + NewVersion = {Major, Minor + 1, 0}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> + NewVersion = {Major, Minor, Patch + 1}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta). + + %%% Package generation @@ -1231,7 +1150,8 @@ package(KeyID, TargetDir) -> {ok, CWD} = file:get_cwd(), ok = file:set_cwd(TargetDir), ok = build(), - Modules = [filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")], + Modules = + [filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")], ok = remove_binaries("."), ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]), ok = file:set_cwd(CWD), @@ -1290,6 +1210,9 @@ submit(PackageFile) -> halt(0). + +%%% Authenticated communication with prime + -spec send(Socket, Message) -> ok when Socket :: gen_tcp:socket(), Message :: term(). @@ -1313,175 +1236,15 @@ recv_or_die(Socket) -> ok; {ok, Response} -> {ok, Response}; - {error, bad_realm} -> - ok = log(warning, "No such realm at the connected node."), - halt(1); - {error, bad_package} -> - ok = log(warning, "No such package."), - halt(1); - {error, bad_version} -> - ok = log(warning, "No such version."), - halt(1); - {error, not_in_queue} -> - ok = log(warning, "Version is not queued."), - halt(1); - {error, bad_message} -> - ok = log(error, "Oh noes! zx sent an illegal message!"), - halt(1); - {error, already_exists} -> - ok = log(warning, "Server refuses: already_exists"), - halt(1); - {error, {system, Reason}} -> - Message = "Node experienced system error: ~160tp", - ok = log(warning, Message, [{error, Reason}]), - halt(1); + {error, Reason} -> + error_exit("Action failed with: ~tp", [Reason], ?LINE); Unexpected -> - ok = log(warning, "Unexpected message: ~tp", [Unexpected]), - halt(1) + error_exit("Unexpected message: ~tp", [Unexpected], ?LINE) end; {tcp_closed, Socket} -> - ok = log(warning, "Lost connection to node unexpectedly."), - halt(1) + error_exit("Lost connection to node unexpectedly.", ?LINE) after 5000 -> - ok = log(warning, "Node timed out."), - halt(1) - end. - - --spec halt_on_unexpected_close() -> no_return(). - -halt_on_unexpected_close() -> - ok = log(warning, "Socket closed unexpectedly."), - halt(1). - - --spec connect_user(realm()) -> gen_tcp:socket() | no_return(). -%% @private -%% Connect to a given realm, whatever method is required. - -connect_user(Realm) -> - ok = log(info, "Connecting to realm ~ts...", [Realm]), - Hosts = - case file:consult(zx_lib:hosts_cache_file(Realm)) of - {ok, Cached} -> Cached; - {error, enoent} -> [] - end, - connect_user(Realm, Hosts). - - --spec connect_user(realm(), [host()]) -> gen_tcp:socket() | no_return(). -%% @private -%% Try to connect to a subordinate host, if there are none then connect to prime. - -connect_user(Realm, []) -> - {Host, Port} = zx_lib:get_prime(Realm), - HostString = - case io_lib:printable_unicode_list(Host) of - true -> Host; - false -> inet:ntoa(Host) - end, - ok = log(info, "Trying prime at ~ts:~160tp", [HostString, Port]), - case gen_tcp:connect(Host, Port, connect_options(), 5000) of - {ok, Socket} -> - confirm_user(Realm, Socket, []); - {error, Error} -> - ok = log(warning, "Connection problem with prime: ~tp", [Error]), - halt(0) - end; -connect_user(Realm, Hosts = [Node = {Host, Port} | Rest]) -> - ok = log(info, "Trying node at ~ts:~tp", [inet:ntoa(Host), Port]), - case gen_tcp:connect(Host, Port, connect_options(), 5000) of - {ok, Socket} -> - confirm_user(Realm, Socket, Hosts); - {error, Error} -> - ok = log(warning, "Connection problem with ~tp: ~tp", [Node, Error]), - connect_user(Realm, Rest) - end. - - --spec confirm_user(Realm, Socket, Hosts) -> Socket | no_return() - when Realm :: realm(), - Socket :: gen_tcp:socket(), - Hosts :: [host()]. -%% @private -%% Confirm the zomp node can handle "OTPR USER 1" and is accepting connections or try -%% another node. - -confirm_user(Realm, Socket, Hosts) -> - {ok, {Addr, Port}} = inet:peername(Socket), - Host = inet:ntoa(Addr), - ok = gen_tcp:send(Socket, <<"OTPR USER 1">>), - receive - {tcp, Socket, Bin} -> - case binary_to_term(Bin) of - ok -> - ok = log(info, "Connected to ~ts:~p", [Host, Port]), - confirm_serial(Realm, Socket, Hosts); - {redirect, Next} -> - ok = log(info, "Redirected..."), - ok = disconnect(Socket), - connect_user(Realm, Next ++ Hosts) - end; - {tcp_closed, Socket} -> - halt_on_unexpected_close() - after 5000 -> - ok = log(warning, "Host ~ts:~p timed out.", [Host, Port]), - ok = disconnect(Socket), - connect_user(Realm, Hosts) - end. - - --spec confirm_serial(Realm, Socket, Hosts) -> Socket | no_return() - when Realm :: realm(), - Socket :: gen_tcp:socket(), - Hosts :: [host()]. -%% @private -%% Confirm that the connected host has a valid serial for the realm zx is trying to -%% reach, and if not retry on another node. - -confirm_serial(Realm, Socket, Hosts) -> - SerialFile = filename:join(zx_lib:zomp_home(), "realm.serials"), - Serials = - case file:consult(SerialFile) of - {ok, Ss} -> Ss; - {error, enoent} -> [] - end, - Serial = - case lists:keyfind(Realm, 1, Serials) of - false -> 0; - {Realm, S} -> S - end, - ok = send(Socket, {latest, Realm}), - receive - {tcp, Socket, Bin} -> - case binary_to_term(Bin) of - {ok, Serial} -> - ok = log(info, "Node's serial same as ours."), - Socket; - {ok, Current} when Current > Serial -> - ok = log(info, "Node's serial newer than ours. Storing."), - NewSerials = lists:keystore(Realm, 1, Serials, {Realm, Current}), - {ok, Host} = inet:peername(Socket), - CacheFile = zx_lib:hosts_cache_file(Realm), - ok = zx_lib:write_terms(CacheFile, [Host | Hosts]), - ok = zx_lib:write_terms(SerialFile, NewSerials), - Socket; - {ok, Current} when Current < Serial -> - log(info, "Our serial: ~tp, node serial: ~tp.", [Serial, Current]), - ok = log(info, "Node's serial older than ours. Trying another."), - ok = disconnect(Socket), - connect_user(Realm, Hosts); - {error, bad_realm} -> - ok = log(info, "Node is no longer serving realm. Trying another."), - ok = disconnect(Socket), - connect_user(Realm, Hosts) - end; - {tcp_closed, Socket} -> - halt_on_unexpected_close() - after 5000 -> - ok = log(info, "Host timed out on confirm_serial. Trying another."), - ok = disconnect(Socket), - connect_user(Realm, Hosts) + error_exit("Connection timed out.", ?LINE) end. @@ -1509,7 +1272,8 @@ connect_auth(Realm) -> RealmConf = load_realm_conf(Realm), {User, KeyID, Key} = prep_auth(Realm, RealmConf), {prime, {Host, Port}} = lists:keyfind(prime, 1, RealmConf), - case gen_tcp:connect(Host, Port, connect_options(), 5000) of + Options = [{packet, 4}, {mode, binary}, {active, true}], + case gen_tcp:connect(Host, Port, Options, 5000) of {ok, Socket} -> ok = log(info, "Connected to ~tp prime.", [Realm]), connect_auth(Socket, Realm, User, KeyID, Key); @@ -1644,14 +1408,6 @@ prep_auth(Realm, RealmConf) -> {User, KeyID, Key}. --spec connect_options() -> [gen_tcp:connect_option()]. -%% @private -%% Hide away the default options used for TCP connections. - -connect_options() -> - [{packet, 4}, {mode, binary}, {active, true}]. - - -spec disconnect(gen_tcp:socket()) -> ok. %% @private %% Gracefully shut down a socket, logging (but sidestepping) the case when the socket @@ -1667,6 +1423,9 @@ disconnect(Socket) -> end. + +%%% Key utilities + -spec ensure_keypair(key_id()) -> true | no_return(). %% @private %% Check if both the public and private key based on KeyID exists. @@ -1808,11 +1567,8 @@ generate_rsa({Realm, KeyName}) -> halt_if_exists(Path) -> case filelib:is_file(Path) of - true -> - ok = log(error, "~ts already exists! Halting.", [Path]), - halt(1); - false -> - ok + true -> error_exit("~ts already exists! Halting.", [Path], ?LINE); + false -> ok end. @@ -1910,10 +1666,12 @@ loadkey(Type, {Realm, KeyName}) -> {DerType, Path} = case Type of private -> - P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".key.der"]), + KeyDer = KeyName ++ ".key.der", + P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyDer]), {'RSAPrivateKey', P}; public -> - P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".pub.der"]), + PubDer = KeyName ++ ".pub.der" + P = filename:join([zx_lib:zomp_home(), "key", Realm, PubDer]), {'RSAPublicKey', P} end, ok = log(info, "Loading key from file ~ts", [Path]), @@ -1937,13 +1695,18 @@ create_plt() -> halt(0). +-spec build_plt() -> ok. +%% @private +%% Build a general plt file for Dialyzer based on the core Erland distro. +%% TODO: Make a per-package + dependencies version of this. + build_plt() -> PLT = default_plt(), Template = "dialyzer --build_plt" - " --output_plt ~ts" - " --apps asn1 reltool wx common_test crypto erts eunit inets" - " kernel mnesia public_key sasl ssh ssl stdlib", + " --output_plt ~ts" + " --apps asn1 reltool wx common_test crypto erts eunit inets" + " kernel mnesia public_key sasl ssh ssl stdlib", Command = io_lib:format(Template, [PLT]), Message = "Generating PLT file and writing to: ~tp~n" @@ -1955,6 +1718,8 @@ build_plt() -> log(info, Out). +-spec default_plt() -> file:filename(). + default_plt() -> filename:join(zx_lib:zomp_home(), "basic.plt"). @@ -1965,6 +1730,8 @@ default_plt() -> -spec dialyze() -> no_return(). %% @private %% Preps a copy of this script for typechecking with Dialyzer. +%% TODO: Create a package_id() based version of this to handle dialyzation of complex +%% projects. dialyze() -> PLT = default_plt(), @@ -2270,7 +2037,9 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email) -> create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealName). --spec create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> +-spec create_realm(ZompConf, Realm, + ExAddress, ExPort, InPort, + UserName, Email, RealName) -> no_return() when ZompConf :: [{Key :: atom(), Value :: term()}], Realm :: realm(), @@ -2362,7 +2131,7 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa ok = rm_rf(TempDir), Message = - "============================================================================~n" + "===========================================================================~n" "DONE!~n" "~n" "The realm ~ts has been created and is accessible from the current system.~n" @@ -2373,8 +2142,8 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa "node. It includes the your (the sysop's) public key.~n" "~n" " 2. ~ts ~n" - "This file is the PUBLIC realm file other zomp nodes and zx users will need to~n" - "access the realm. It does not include your (the sysop's) public key.~n" + "This file is the PUBLIC realm file other zomp nodes and zx users will need~n" + "to access the realm. It does not include your (the sysop's) public key.~n" "~n" " 3. ~ts ~n" "This is the bundle of ALL KEYS that are defined in this realm at the moment.~n" @@ -2391,7 +2160,7 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa "~n" "Public & Private key installation (if you need to recover them or perform~n" "sysop functions from another computer) is `zx add keybundle ~ts`.~n" - "============================================================================~n", + "===========================================================================~n", Substitutions = [Realm, PrimeZRF, PublicZRF, KeyBundle, @@ -2427,7 +2196,7 @@ create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end, RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs), PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs), - Targets = [realm_conf(Realm) | RealmKeyPaths ++ PackageKeyPaths], + Targets = [zx_lib:realm_conf(Realm) | RealmKeyPaths ++ PackageKeyPaths], OutFile = filename:join(CWD, Realm ++ "." ++ integer_to_list(Revision) ++ ".zrf"), ok = erl_tar:create(OutFile, Targets, [compressed]), ok = log(info, "Realm conf file written to ~ts", [OutFile]), @@ -2442,7 +2211,7 @@ create_sysop() -> -%%% Network operations and package utilities +%%% Package utilities -spec install(package_id()) -> ok. @@ -2490,86 +2259,6 @@ verify(Data, Signature, PubKey) -> end. --spec fetch(Socket, PackageID) -> Result - when Socket :: gen_tcp:socket(), - PackageID :: package_id(), - Result :: ok. -%% @private -%% Download a package to the local cache. - -fetch(Socket, PackageID) -> - {ok, LatestID} = request_zrp(Socket, PackageID), - ok = receive_zrp(Socket, LatestID), - log(info, "Fetched ~ts", [package_string(LatestID)]). - - --spec request_zrp(Socket, PackageID) -> Result - when Socket :: gen_tcp:socket(), - PackageID :: package_id(), - Result :: {ok, Latest :: package_id()} - | {error, Reason :: timeout | term()}. - -request_zrp(Socket, PackageID) -> - ok = send(Socket, {fetch, PackageID}), - receive - {tcp, Socket, Bin} -> - case binary_to_term(Bin) of - {sending, LatestID} -> - {ok, LatestID}; - Error = {error, Reason} -> - PackageString = package_string(PackageID), - Message = "Error receiving package ~ts: ~tp", - ok = log(info, Message, [PackageString, Reason]), - Error - end; - {tcp_closed, Socket} -> - halt_on_unexpected_close() - after 60000 -> - {error, timeout} - end. - - --spec receive_zrp(Socket, PackageID) -> Result - when Socket :: gen_tcp:socket(), - PackageID :: package_id(), - Result :: ok | {error, timeout}. - -receive_zrp(Socket, PackageID) -> - receive - {tcp, Socket, Bin} -> - ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), - ok = file:write_file(ZrpPath, Bin), - ok = send(Socket, ok), - log(info, "Wrote ~ts", [ZrpPath]); - {tcp_closed, Socket} -> - halt_on_unexpected_close() - after 60000 -> - ok = log(error, "Timeout in socket receive for ~tp", [PackageID]), - {error, timeout} - end. - - --spec mktemp_dir(Prefix) -> Result - when Prefix :: string(), - Result :: {ok, TempDir :: file:filename()} - | {error, Reason :: file:posix()}. - -mktemp_dir(Prefix) -> - Rand = integer_to_list(binary:decode_unsigned(crypto:strong_rand_bytes(8)), 36), - TempPath = filename:basedir(user_cache, Prefix), - TempDir = filename:join(TempPath, Rand), - Result1 = filelib:ensure_dir(TempDir), - Result2 = file:make_dir(TempDir), - case {Result1, Result2} of - {ok, ok} -> {ok, TempDir}; - {ok, Error} -> Error; - {Error, _} -> Error - end. - - -%%% Utility functions - - -spec build(package_id()) -> ok. %% @private %% Given an AppID, build the project from source and add it to the current lib path. @@ -2602,22 +2291,6 @@ build() -> ok. --spec rm_rf(file:filename()) -> ok | {error, file:posix()}. -%% @private -%% Recursively remove files and directories, equivalent to `rm -rf' on unix. - -rm_rf(Path) -> - case filelib:is_dir(Path) of - true -> - Pattern = filename:join(Path, "**"), - Contents = lists:reverse(lists:sort(filelib:wildcard(Pattern))), - ok = lists:foreach(fun rm/1, Contents), - file:del_dir(Path); - false -> - file:delete(Path) - end. - - %%% User menu interface (terminal) @@ -2705,6 +2378,40 @@ hurr() -> io:format("That isn't an option.~n"). %%% Directory & File Management +-spec mktemp_dir(Prefix) -> Result + when Prefix :: string(), + Result :: {ok, TempDir :: file:filename()} + | {error, Reason :: file:posix()}. + +mktemp_dir(Prefix) -> + Rand = integer_to_list(binary:decode_unsigned(crypto:strong_rand_bytes(8)), 36), + TempPath = filename:basedir(user_cache, Prefix), + TempDir = filename:join(TempPath, Rand), + Result1 = filelib:ensure_dir(TempDir), + Result2 = file:make_dir(TempDir), + case {Result1, Result2} of + {ok, ok} -> {ok, TempDir}; + {ok, Error} -> Error; + {Error, _} -> Error + end. + + +-spec rm_rf(file:filename()) -> ok | {error, file:posix()}. +%% @private +%% Recursively remove files and directories. Equivalent to `rm -rf'. + +rm_rf(Path) -> + case filelib:is_dir(Path) of + true -> + Pattern = filename:join(Path, "**"), + Contents = lists:reverse(lists:sort(filelib:wildcard(Pattern))), + ok = lists:foreach(fun rm/1, Contents), + file:del_dir(Path); + false -> + file:delete(Path) + end. + + -spec ensure_zomp_home() -> ok. %% @private %% Ensure the zomp home directory exists and is populated. @@ -2767,33 +2474,6 @@ force_dir(Path) -> end. --spec realm_conf(Realm) -> RealmFileName - when Realm :: string(), - RealmFileName :: file:filename(). -%% @private -%% Take a realm name, and return the name of the realm filename that would result. - -realm_conf(Realm) -> - Realm ++ ".realm". - - --spec load_realm_conf(Realm) -> RealmConf | no_return() - when Realm :: realm(), - RealmConf :: list(). -%% @private -%% Load the config for the given realm or halt with an error. - -load_realm_conf(Realm) -> - Path = filename:join(zx_lib:zomp_home(), realm_conf(Realm)), - case file:consult(Path) of - {ok, C} -> - C; - {error, enoent} -> - ok = log(warning, "Realm ~tp is not configured.", [Realm]), - halt(1) - end. - - -spec extract_zrp_or_die(FileName) -> Files | no_return() when FileName :: file:filename(), Files :: [{file:filename(), binary()}]. @@ -2817,6 +2497,23 @@ extract_zrp_or_die(FileName) -> end. +-spec load_realm_conf(Realm) -> RealmConf | no_return() + when Realm :: realm(), + RealmConf :: list(). +%% @private +%% Load the config for the given realm or halt with an error. + +load_realm_conf(Realm) -> + case zx_lib:load_realm_conf(Realm) of + {ok, C} -> + C; + {error, enoent} -> + ok = log(warning, "Realm ~tp is not configured.", [Realm]), + halt(1) + end. + + + %%% Usage diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx_conn.erl b/zomp/lib/otpr-zx/0.1.0/src/zx_conn.erl index f585bc7..ccd3ab4 100644 --- a/zomp/lib/otpr-zx/0.1.0/src/zx_conn.erl +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_conn.erl @@ -68,4 +68,204 @@ connect(Parent, Debug, {Host, Port}) -> confirm(Parent, Debug, Socket) -> + + +-spec connect_user(realm()) -> gen_tcp:socket() | no_return(). +%% @private +%% Connect to a given realm, whatever method is required. + +connect_user(Realm) -> + ok = log(info, "Connecting to realm ~ts...", [Realm]), + Hosts = + case file:consult(zx_lib:hosts_cache_file(Realm)) of + {ok, Cached} -> Cached; + {error, enoent} -> [] + end, + connect_user(Realm, Hosts). + + +-spec connect_user(realm(), [host()]) -> gen_tcp:socket() | no_return(). +%% @private +%% Try to connect to a subordinate host, if there are none then connect to prime. + +connect_user(Realm, []) -> + {Host, Port} = zx_lib:get_prime(Realm), + HostString = + case io_lib:printable_unicode_list(Host) of + true -> Host; + false -> inet:ntoa(Host) + end, + ok = log(info, "Trying prime at ~ts:~160tp", [HostString, Port]), + case gen_tcp:connect(Host, Port, connect_options(), 5000) of + {ok, Socket} -> + confirm_user(Realm, Socket, []); + {error, Error} -> + ok = log(warning, "Connection problem with prime: ~tp", [Error]), + halt(0) + end; +connect_user(Realm, Hosts = [Node = {Host, Port} | Rest]) -> + ok = log(info, "Trying node at ~ts:~tp", [inet:ntoa(Host), Port]), + case gen_tcp:connect(Host, Port, connect_options(), 5000) of + {ok, Socket} -> + confirm_user(Realm, Socket, Hosts); + {error, Error} -> + ok = log(warning, "Connection problem with ~tp: ~tp", [Node, Error]), + connect_user(Realm, Rest) + end. + + +-spec confirm_user(Realm, Socket, Hosts) -> Socket | no_return() + when Realm :: realm(), + Socket :: gen_tcp:socket(), + Hosts :: [host()]. +%% @private +%% Confirm the zomp node can handle "OTPR USER 1" and is accepting connections or try +%% another node. + +confirm_user(Realm, Socket, Hosts) -> + {ok, {Addr, Port}} = inet:peername(Socket), + Host = inet:ntoa(Addr), + ok = gen_tcp:send(Socket, <<"OTPR USER 1">>), + receive + {tcp, Socket, Bin} -> + case binary_to_term(Bin) of + ok -> + ok = log(info, "Connected to ~ts:~p", [Host, Port]), + confirm_serial(Realm, Socket, Hosts); + {redirect, Next} -> + ok = log(info, "Redirected..."), + ok = disconnect(Socket), + connect_user(Realm, Next ++ Hosts) + end; + {tcp_closed, Socket} -> + halt_on_unexpected_close() + after 5000 -> + ok = log(warning, "Host ~ts:~p timed out.", [Host, Port]), + ok = disconnect(Socket), + connect_user(Realm, Hosts) + end. + + +-spec confirm_serial(Realm, Socket, Hosts) -> Socket | no_return() + when Realm :: realm(), + Socket :: gen_tcp:socket(), + Hosts :: [host()]. +%% @private +%% Confirm that the connected host has a valid serial for the realm zx is trying to +%% reach, and if not retry on another node. + +confirm_serial(Realm, Socket, Hosts) -> + SerialFile = filename:join(zx_lib:zomp_home(), "realm.serials"), + Serials = + case file:consult(SerialFile) of + {ok, Ss} -> Ss; + {error, enoent} -> [] + end, + Serial = + case lists:keyfind(Realm, 1, Serials) of + false -> 0; + {Realm, S} -> S + end, + ok = send(Socket, {latest, Realm}), + receive + {tcp, Socket, Bin} -> + case binary_to_term(Bin) of + {ok, Serial} -> + ok = log(info, "Node's serial same as ours."), + Socket; + {ok, Current} when Current > Serial -> + ok = log(info, "Node's serial newer than ours. Storing."), + NewSerials = lists:keystore(Realm, 1, Serials, {Realm, Current}), + {ok, Host} = inet:peername(Socket), + CacheFile = zx_lib:hosts_cache_file(Realm), + ok = zx_lib:write_terms(CacheFile, [Host | Hosts]), + ok = zx_lib:write_terms(SerialFile, NewSerials), + Socket; + {ok, Current} when Current < Serial -> + log(info, "Our serial: ~tp, node serial: ~tp.", [Serial, Current]), + ok = log(info, "Node's serial older than ours. Trying another."), + ok = disconnect(Socket), + connect_user(Realm, Hosts); + {error, bad_realm} -> + ok = log(info, "Node is no longer serving realm. Trying another."), + ok = disconnect(Socket), + connect_user(Realm, Hosts) + end; + {tcp_closed, Socket} -> + halt_on_unexpected_close() + after 5000 -> + ok = log(info, "Host timed out on confirm_serial. Trying another."), + ok = disconnect(Socket), + connect_user(Realm, Hosts) + end. + + +-spec halt_on_unexpected_close() -> no_return(). + +halt_on_unexpected_close() -> + ok = log(warning, "Socket closed unexpectedly."), + halt(1). + + + + + + +-spec fetch(Socket, PackageID) -> Result + when Socket :: gen_tcp:socket(), + PackageID :: package_id(), + Result :: ok. +%% @private +%% Download a package to the local cache. + +fetch(Socket, PackageID) -> + {ok, LatestID} = request_zrp(Socket, PackageID), + ok = receive_zrp(Socket, LatestID), + log(info, "Fetched ~ts", [package_string(LatestID)]). + + +-spec request_zrp(Socket, PackageID) -> Result + when Socket :: gen_tcp:socket(), + PackageID :: package_id(), + Result :: {ok, Latest :: package_id()} + | {error, Reason :: timeout | term()}. + +request_zrp(Socket, PackageID) -> + ok = send(Socket, {fetch, PackageID}), + receive + {tcp, Socket, Bin} -> + case binary_to_term(Bin) of + {sending, LatestID} -> + {ok, LatestID}; + Error = {error, Reason} -> + PackageString = package_string(PackageID), + Message = "Error receiving package ~ts: ~tp", + ok = log(info, Message, [PackageString, Reason]), + Error + end; + {tcp_closed, Socket} -> + halt_on_unexpected_close() + after 60000 -> + {error, timeout} + end. + + +-spec receive_zrp(Socket, PackageID) -> Result + when Socket :: gen_tcp:socket(), + PackageID :: package_id(), + Result :: ok | {error, timeout}. + +receive_zrp(Socket, PackageID) -> + receive + {tcp, Socket, Bin} -> + ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), + ok = file:write_file(ZrpPath, Bin), + ok = send(Socket, ok), + log(info, "Wrote ~ts", [ZrpPath]); + {tcp_closed, Socket} -> + halt_on_unexpected_close() + after 60000 -> + ok = log(error, "Timeout in socket receive for ~tp", [PackageID]), + {error, timeout} + end. diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx_lib.erl b/zomp/lib/otpr-zx/0.1.0/src/zx_lib.erl index 2f0b39a..315a2fb 100644 --- a/zomp/lib/otpr-zx/0.1.0/src/zx_lib.erl +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_lib.erl @@ -328,26 +328,25 @@ version_to_string(_) -> package_id(String) -> case dash_split(String) of - [Realm, Name, VersionString] -> + {ok, [Realm, Name, VersionString]} -> package_id(Realm, Name, VersionString); - [A, B] -> + {ok, [A, B]} -> case valid_lower0_9(B) of true -> package_id(A, B, ""); false -> package_id("otpr", A, B) end; - [Name] -> + {ok, [Name]} -> package_id("otpr", Name, ""); - _ -> + error -> {error, invalid_package_string} end. --spec dash_split(string()) -> [string()] | error. +-spec dash_split(string()) -> {ok, [string()]} | error. %% @private %% An explicit, strict token split that ensures invalid names with leading, trailing or %% double dashes don't slip through (a problem discovered with using string:tokens/2 %% and string:lexemes/2. -%% Intended only as a helper function for package_id/1 dash_split(String) -> dash_split(String, "", []). @@ -360,7 +359,7 @@ dash_split([Char | Rest], Acc, Elements) -> dash_split(Rest, [Char | Acc], Elements); dash_split("", Acc, Elements) -> Element = lists:reverse(Acc), - lists:reverse([Element | Elements]); + {ok, lists:reverse([Element | Elements])}; dash_split(_, _, _) -> error. @@ -521,3 +520,33 @@ latest_compatible(Version, Versions) -> installed(PackageID) -> filelib:is_dir(package_dir(PackageID)). + + + + +-spec realm_conf(Realm) -> RealmFileName + when Realm :: string(), + RealmFileName :: file:filename(). +%% @private +%% Take a realm name, and return the name of the realm filename that would result. + +realm_conf(Realm) -> + Realm ++ ".realm". + + +-spec load_realm_conf(Realm) -> Result + when Realm :: realm(), + Result :: {ok, RealmConf} + | {error, Reason}, + RealmConf :: list(), + Reason :: badarg + | terminated + | system_limit + | file:posix() + | {Line :: integer(), Mod :: module(), Cause :: term()}. +%% @private +%% Load the config for the given realm or halt with an error. + +load_realm_conf(Realm) -> + Path = filename:join(zx_lib:zomp_home(), realm_conf(Realm)), + file:consult(Path).