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

197
zx
View File

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