This commit is contained in:
Craig Everett 2017-11-21 07:51:35 +09:00
parent a1af4182ee
commit 684e4507fc

195
zx
View File

@ -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} ->
RealmMeta = realm_meta(Realm),
{prime, Prime} = lists:keyfind(prime, 1, RealmMeta),
Prime;
{error, enoent} ->
ok = log(error, "Missing realm file for ~tp (~tp).", [Realm, RealmFile]),
halt(1)
end.
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, []}].