diff --git a/zx b/zx index 0f21ff0..b7d768b 100755 --- a/zx +++ b/zx @@ -18,7 +18,7 @@ -record(s, - {realm = "otpr" :: name(), + {realm = "otpr" :: realm(), name = none :: none | name(), version = {z, z, z} :: version(), type = app :: app | lib, @@ -47,6 +47,7 @@ -type key_name() :: lower0_9(). -type lower0_9() :: [$a..$z | $0..$9 | $_]. %-type label() :: [$a..$z | $0..$9 | $_ | $- | $.]. +-type package_meta() :: #{}. @@ -79,7 +80,8 @@ start(["init", "lib", PackageString]) -> start(["install", PackageFile]) -> assimilate(PackageFile); start(["set", "dep", PackageString]) -> - set_dep(PackageString); + PackageID = package_id(PackageString), + set_dep(PackageID); start(["set", "version", VersionString]) -> set_version(VersionString); start(["drop", "dep", PackageString]) -> @@ -133,24 +135,25 @@ start(_) -> %% %% Once the target program is running, this process, (which will run with the registered %% name `zx') will sit in an `exec_wait' state, waiting for either a direct message from -%% a child program or for calls made via vx_lib to assist in environment discovery. +%% a child program or for calls made via zx_lib to assist in environment discovery. %% -%% If there is a problem anywhere in the locationg, discovery, building, and loading +%% If there is a problem anywhere in the locating, discovery, building, and loading %% procedure the runtime will halt with an error message. run(Identifier, Args) -> - {Realm, Name, Version} = package_id(Identifier), + MaybeID = package_id(Identifier), + {ok, PackageID = {Realm, Name, Version}} = ensure_installed(MaybeID), ok = file:set_cwd(zomp_dir()), - State = #s{realm = Realm, - name = Name, - version = Version}, - NewState = #s{version = Installed} = ensure_installed(State), - PackageID = {Realm, Name, Installed}, Dir = filename:join("lib", package_string(PackageID)), Meta = read_meta(Dir), Deps = maps:get(deps, Meta), ok = ensure_deps(Deps), - execute(NewState#s{dir = Dir, deps = Deps}). + State = #s{realm = Realm, + name = Name, + version = Version, + dir = Dir, + deps = Deps}, + execute(State, Args). @@ -227,8 +230,7 @@ assimilate(PackageFile) -> %%% Set dependency --spec set_dep(PackageString) -> no_return() - when PackageString :: string(). +-spec set_dep(package_id()) -> no_return(). %% @private %% Set a specific dependency in the current project. If the project currently has a %% dependency on the same package then the version of that dependency is updated to @@ -236,17 +238,27 @@ assimilate(PackageFile) -> %% incomplete. Incomplete elements of the VersionString (if included) will default to %% the latest version available at the indicated level. -set_dep(PackageString) -> - PackageID = package_id(PackageString), +set_dep(PackageID = {_, _, {X, Y, Z}}) + when is_integer(X), is_integer(Y), is_integer(Z) -> Meta = read_meta(), Deps = maps:get(deps, Meta), case lists:member(PackageID, Deps) of true -> - ok = log(info, "~ts is already a dependency", [PackageString]), + ok = log(info, "~ts is already a dependency", [package_string(PackageID)]), halt(0); false -> set_dep(PackageID, Deps, Meta) - end. + end; +set_dep({Realm, Name, {z, z, z}}) -> + Socket = connect_user(Realm), + {ok, Version} = query_latest(Socket, {Realm, Name}), + ok = disconnect(Socket), + set_dep({Realm, Name, Version}); +set_dep({Realm, Name, Version}) -> + Socket = connect_user(Realm), + {ok, Latest} = query_latest(Socket, {Realm, Name, Version}), + ok = disconnect(Socket), + set_dep({Realm, Name, Latest}). -spec set_dep(PackageID, Deps, Meta) -> no_return() @@ -259,9 +271,7 @@ set_dep(PackageString) -> %% such a dependency is not already present. Then write the project meta back to its %% file and exit. -set_dep(PackageID = {Realm, Name, Version}, Deps, Meta) -> - {ok, CWD} = file:get_cwd(), - LatestVersion = latest_version(PackageID), +set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) -> ExistingPackageIDs = fun ({R, N, _}) -> {R, N} == {Realm, Name} end, NewDeps = case lists:partition(ExistingPackageIDs, Deps) of @@ -280,34 +290,42 @@ set_dep(PackageID = {Realm, Name, Version}, Deps, Meta) -> halt(0). --spec latest_version(package_id()) -> version(). -%% @private -%% Query the relevant realm for the latest version of a given package. - -latest_version({Realm, Name, Version}) -> - Socket = connect(Realm), - ok = send(Socket, {latest, Realm, Name, Version}), - Response = - receive - {tcp, Socket, Bin} -> binary_to_term(Bin, [safe]) - after 5000 -> {error, timeout} - end, - ok = disconnect(Socket). - - --spec ensure_installed(State) -> NewState | no_return() - when State :: state(), - NewState :: state(). +-spec ensure_installed(PackageID) -> Result | no_return() + when PackageID :: package_id(), + Result :: {ok, ActualID :: package_id()}. %% @private %% Given a PackageID, check whether it is installed on the system, and if not, ensure %% that the package is either in the cache or can be downloaded. If all attempts at %% locating or acquiring the package fail, then exit with an error. -ensure_installed(State = #s{realm = Realm, name = Name, version = Version}) -> - case resolve_installed_version({Realm, Name, Version}) of - exact -> State; - {ok, Installed} -> State#s{version = Installed}; - not_found -> ensure_dep(State) +ensure_installed(PackageID = {Realm, Name, Version}) -> + case resolve_installed_version(PackageID) of + exact -> {ok, PackageID}; + {ok, Installed} -> {ok, {Realm, Name, Installed}}; + not_found -> ensure_installed(Realm, Name, Version) + end. + + +ensure_installed(Realm, Name, Version) -> + Socket = connect_user(Realm), + {ok, LatestVersion} = query_latest(Socket, {Realm, Name, Version}), + LatestID = {Realm, Name, LatestVersion}, + ok = ensure_dep(Socket, LatestID), + ok = disconnect(Socket), + {ok, LatestID}. + + +query_latest(Socket, {Realm, Name}) -> + ok = send(Socket, {latest, Realm, Name}), + receive + {tcp, Socket, Bin} -> binary_to_term(Bin, [safe]) + after 5000 -> {error, timeout} + end; +query_latest(Socket, {Realm, Name, Version}) -> + ok = send(Socket, {latest, Realm, Name, Version}), + receive + {tcp, Socket, Bin} -> binary_to_term(Bin, [safe]) + after 5000 -> {error, timeout} end. @@ -332,7 +350,7 @@ resolve_installed_version(PackageID) -> exact; [Dir] -> {_, _, Version} = package_id(Dir), - {ok, Versoin}; + {ok, Version}; Dirs -> Dir = lists:last(lists:sort(Dirs)), {_, _, Version} = package_id(Dir), @@ -351,15 +369,14 @@ ensure_deps(Deps) -> Socket = connect_user(Realm), ok = ensure_deps(Socket, Realm, Packages), ok = disconnect(Socket), - log(info, "Disconnecting from realm: ~ts", [Realm]), + log(info, "Disconnecting from realm: ~ts", [Realm]) end, lists:foreach(EnsureDeps, Partitioned) end. partition_by_realm(PackageIDs) -> - Sorted = lists:sort(Needed), - PartitionMap = lists:foldl(fun partition_by_realm/2, #{}, Needed), + PartitionMap = lists:foldl(fun partition_by_realm/2, #{}, PackageIDs), maps:to_list(PartitionMap). @@ -387,13 +404,13 @@ ensure_dep(Socket, PackageID) -> true -> ok; false -> fetch(Socket, PackageID) end, - install(PackageID). + ok = install(PackageID), + build(PackageID). %%% Set version - -spec set_version(VersionString) -> no_return() when VersionString :: string(). %% @private @@ -576,26 +593,21 @@ run_local(Args) -> dir = Dir}, ok = ensure_deps(Deps), ok = file:set_cwd(Dir), - execute(State). + execute(State, Args). -execute(State = #s{type = app}) -> +execute(State = #s{type = app, realm = Realm, name = Name, version = Version}, Args) -> true = register(zx, self()), ok = inets:start(), - ok = log(info, "Starting ~ts", [package_string(PackageID)]), + ok = log(info, "Starting ~ts", [package_string({Realm, Name, Version})]), AppMod = list_to_atom(Name), {ok, Pid} = AppMod:start(normal, Args), Mon = monitor(process, Pid), Shell = spawn(shell, start, []), ok = log(info, "Your shell is ~p, application is: ~p", [Shell, Pid]), - State = #s{realm = Realm, - name = Name, - version = Version, - pid = Pid, - mon = Mon}, - exec_wait(State); -execute(State = #s{type = lib, realm = Realm, name = Name, version = Version}) -> - Message = "Lib ~ts is available on the system, but is not a standalone app", + exec_wait(State#s{pid = Pid, mon = Mon}); +execute(#s{type = lib, realm = Realm, name = Name, version = Version}, _) -> + Message = "Lib ~ts is available on the system, but is not a standalone app.", PackageString = package_string({Realm, Name, Version}), ok = log(info, Message, [PackageString]), halt(0). @@ -807,6 +819,7 @@ send(Socket, Message) -> %% Connect to a given realm, whatever method is required. connect_user(Realm) -> + ok = log(info, "Connecting to realm ~ts...", [Realm]), Hosts = case file:consult(hosts_cache_file(Realm)) of {ok, Cached} -> Cached; @@ -821,6 +834,7 @@ connect_user(Realm) -> connect_user(Realm, []) -> {Host, Port} = get_prime(Realm), + ok = log(info, "Realm host at ~tp:~tp", [Host, Port]), case gen_tcp:connect(Host, Port, connect_options(), 5000) of {ok, Socket} -> confirm_user(Realm, Socket, []); @@ -854,7 +868,7 @@ confirm_user(Realm, Socket, Hosts) -> case binary_to_term(Bin, [safe]) of ok -> ok = log(info, "Connected to ~s:~p", [Host, Port]), - confirm_serial(Realm, Socket, Hosts, user); + confirm_serial(Realm, Socket, Hosts); {redirect, Next} -> ok = log(info, "Redirected..."), ok = disconnect(Socket), @@ -863,7 +877,7 @@ confirm_user(Realm, Socket, Hosts) -> after 5000 -> ok = log(warning, "Host ~s:~p timed out.", [Host, Port]), ok = disconnect(Socket), - connect_user(Realm, tl(Hosts)) + connect_user(Realm, Hosts) end. @@ -879,7 +893,7 @@ confirm_serial(Realm, Socket, Hosts) -> SerialFile = filename:join(zomp_dir(), "realm.serials"), Serials = case file:consult(SerialFile) of - {ok, Serials} -> Serials; + {ok, Ss} -> Ss; {error, enoent} -> [] end, Serial = @@ -897,22 +911,24 @@ confirm_serial(Realm, Socket, Hosts) -> {ok, Current} when Current > Serial -> ok = log(info, "Node's serial newer than ours. Storing."), NewSerials = lists:keystore(Realm, 1, Current, Serials), - ok = write_terms(hosts_cache_file(Realm), Hosts), + {ok, Host} = inet:peername(Socket), + ok = write_terms(hosts_cache_file(Realm), [Host | Hosts]), ok = 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, tl(Hosts)); + connect_user(Realm, Hosts); {error, bad_realm} -> ok = log(info, "Node is no longer serving realm. Trying another."), ok = disconnect(Socket), - connect_user(Realm, tl(Hosts)) + connect_user(Realm, Hosts) end after 5000 -> ok = log(info, "Host timed out on confirm_serial. Trying another."), ok = disconnect(Socket), - connect_user(Realm, tl(Hosts)) + connect_user(Realm, Hosts) end. @@ -949,7 +965,7 @@ connect_auth(Realm, KeyName) -> confirm_auth(Socket, Key) -> ok = log(info, "Would be using key ~tp now", [Key]), - {ok, {Addr, Port}} = inet:peername(Socket), + {ok, {Host, Port}} = inet:peername(Socket), ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>), receive {tcp, Socket, <<"OK">>} -> @@ -973,15 +989,9 @@ connect_options() -> %% Check the given Realm's config file for the current prime node and return it. get_prime(Realm) -> - RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), - case file:consult(RealmFile) of - {ok, RealmMeta} -> - {prime, Prime} = lists:keyfind(prime, 1, RealmMeta), - Prime; - {error, enoent} -> - ok = log(error, "Missing realm file for ~tp (~tp).", [Realm, RealmFile]), - halt(1) - end. + RealmMeta = realm_meta(Realm), + {prime, Prime} = lists:keyfind(prime, 1, RealmMeta), + Prime. -spec hosts_cache_file(realm()) -> file:filename(). @@ -1050,19 +1060,19 @@ have_private_key({Realm, KeyName}) -> filelib:is_regular(PrivateKeyPath). --spec realm_data(Realm) -> Data | no_return() +-spec realm_meta(Realm) -> Meta | no_return() when Realm :: string(), - Data :: [{atom(), term()}]. + Meta :: [{atom(), term()}]. %% @private %% Given a realm name, try to locate and read the realm's configuration file if it %% exists, exiting with an appropriate error message if there is a problem reading %% the file. -realm_data(Realm) -> +realm_meta(Realm) -> RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), case file:consult(RealmFile) of - {ok, Data} -> - Data; + {ok, Meta} -> + Meta; {error, enoent} -> ok = log(error, "No realm file for ~ts", [Realm]), halt(1); @@ -1415,28 +1425,32 @@ verify(Data, Signature, PubKey) -> end. --spec fetch(gen_tcp:socket(), package_id()) -> ok. +-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 = send(Socket, {fetch, PackageID}), - ok = await_zrp(Socket, PackageID), + ok = request_zrp(Socket, PackageID), ok = receive_zrp(Socket, PackageID), log(info, "Fetched ~ts", [package_string(PackageID)]). -await_zrp(Socket, PackageID) -> +request_zrp(Socket, PackageID) -> + ok = send(Socket, {fetch, PackageID}), receive {tcp, Socket, Bin} -> case binary_to_term(Bin, [safe]) of - sending -> - ok; + {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 after 60000 -> {error, timeout} end. @@ -1487,8 +1501,8 @@ read_meta(Dir) -> %% @private %% @equiv write_meta(".") -write_meta() -> - write_meta("."). +write_meta(Meta) -> + write_meta(".", Meta). -spec write_meta(Dir, Meta) -> ok @@ -1984,7 +1998,8 @@ realm_file(Realm) -> default_realm() -> [{name, "otpr"}, - {prime, {"repo.psychobitch.party", 11311}}, +% {prime, {"repo.psychobitch.party", 11311}}, + {prime, {"localhost", 11311}}, {pubkey, default_pubkey_file()}, {serial, 0}, {mirrors, []}].