Merge branch 'dev' into 'master'

Dev

See merge request zxq9/zx!6
This commit is contained in:
Craig Everett 2018-06-05 03:05:28 +00:00
commit a1236630cd
9 changed files with 572 additions and 539 deletions

View File

@ -1,5 +0,0 @@
{realm,"otpr"}.
{username,"zxq9"}.
{realmname,"Craig Everett"}.
{contact_info,{"email","zxq9@zxq9.com"}}.
{keys,["zxq9-root"]}.

View File

@ -9,4 +9,4 @@
zx_conn, zx_conn,
zx_lib, zx_lib,
zx_net]}, zx_net]},
{mod, {zx, []}}]}. {mod, {zx, none}}]}.

View File

@ -64,7 +64,7 @@
type := app | lib}. type := app | lib}.
-type outcome() :: ok -type outcome() :: ok
| {error, Reason :: atom()} | {error, Reason :: term()}
| {error, Code :: non_neg_integer()} | {error, Code :: non_neg_integer()}
| {error, Info :: string(), Code :: non_neg_integer()}. | {error, Info :: string(), Code :: non_neg_integer()}.
@ -101,7 +101,8 @@ do(["list", "deps"]) ->
do(["list", "deps", PackageString]) -> do(["list", "deps", PackageString]) ->
done(zx_local:list_deps(PackageString)); done(zx_local:list_deps(PackageString));
do(["install", PackageFile]) -> do(["install", PackageFile]) ->
done(zx_local:assimilate(PackageFile)); ok = start(),
done(zx_daemon:install(PackageFile));
do(["set", "dep", PackageString]) -> do(["set", "dep", PackageString]) ->
done(zx_local:set_dep(PackageString)); done(zx_local:set_dep(PackageString));
do(["set", "version", VersionString]) -> do(["set", "version", VersionString]) ->
@ -229,7 +230,8 @@ compatibility_check(Platforms) ->
%% @equiv application:ensure_started(zx). %% @equiv application:ensure_started(zx).
start() -> start() ->
application:ensure_started(zx). ok = application:ensure_started(zx),
zx_daemon:init_connections().
-spec stop() -> ok | {error, Reason :: term()}. -spec stop() -> ok | {error, Reason :: term()}.
@ -249,14 +251,14 @@ stop() ->
-spec start(StartType, StartArgs) -> Result -spec start(StartType, StartArgs) -> Result
when StartType :: normal, when StartType :: normal,
StartArgs :: [], StartArgs :: none,
Result :: {ok, pid()}. Result :: {ok, pid()}.
%% @private %% @private
%% Application callback. Not to be called directly. %% Application callback. Not to be called directly.
start(normal, []) -> start(normal, none) ->
ok = application:ensure_started(inets), ok = application:ensure_started(inets),
zx_daemon:start_link(). zx_sup:start_link().
-spec stop(term()) -> ok. -spec stop(term()) -> ok.
@ -331,7 +333,7 @@ run(Identifier, RunArgs) ->
end, end,
{ok, PackageID} = ensure_installed(FuzzyID), {ok, PackageID} = ensure_installed(FuzzyID),
ok = build(PackageID), ok = build(PackageID),
Dir = zx_lib:path(lib, PackageID), Dir = zx_lib:ppath(lib, PackageID),
{ok, Meta} = zx_lib:read_project_meta(Dir), {ok, Meta} = zx_lib:read_project_meta(Dir),
prepare(PackageID, Meta, Dir, RunArgs). prepare(PackageID, Meta, Dir, RunArgs).
@ -371,34 +373,32 @@ prepare(PackageID, Meta, Dir, RunArgs) ->
Deps = maps:get(deps, Meta), Deps = maps:get(deps, Meta),
NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end, NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end,
Needed = lists:filter(NotInstalled, Deps), Needed = lists:filter(NotInstalled, Deps),
Pending = lists:map(fun zx_daemon:fetch/1, Needed),
case await_fetches(Pending) of
ok ->
ok = lists:foreach(fun install/1, Needed), ok = lists:foreach(fun install/1, Needed),
ok = lists:foreach(fun build/1, Needed), ok = lists:foreach(fun build/1, Needed),
execute(Type, PackageID, Meta, Dir, RunArgs); execute(Type, PackageID, Meta, Dir, RunArgs).
{error, Errors} ->
error_exit("Failed package fetches: ~tp", [Errors], ?LINE)
end.
await_fetches([]) -> ok; -spec install(PackageString :: string()) -> zx:outcome().
await_fetches(Pending) -> await_fetches(Pending, []). %% @private
%% Installs a package from upstream.
install(PackageString) ->
{ok, ID} = zx_daemon:install(PackageString),
install(PackageString, ID).
await_fetches([], []) -> install(PackageString, ID) ->
ok;
await_fetches([], Errors) ->
{error, Errors};
await_fetches(Pending, Errors) ->
{NewPending, NewErrors} =
receive receive
{z_reply, ID, ok} -> {z_result, ID, done} ->
{lists:delete(ID, Pending), Errors}; ok;
{z_reply, ID, {error, Package, Reason}} -> {z_result, ID, {hops, Count}} ->
{lists:delete(ID, Pending), [{Package, Reason} | Errors]} ok = log(info, "~ts ~w hops away.", [PackageString, Count]),
end, install(PackageString, ID);
await_fetches(NewPending, NewErrors). {z_result, ID, {error, Reason}} ->
{error, Reason, 1}
after 60000 ->
{error, timeout, 62}
end.
-spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return() -spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return()
@ -516,7 +516,7 @@ resolve_installed_version({Realm, Name, Version}) ->
resolve_installed_version(PackageDir, Version) -> resolve_installed_version(PackageDir, Version) ->
DirStrings = filelib:wildcard("*", PackageDir), DirStrings = filelib:wildcard("*", PackageDir),
Versions = lists:fold(fun tuplize/2, [], DirStrings), Versions = lists:foldl(fun tuplize/2, [], DirStrings),
zx_lib:find_latest_compatible(Version, Versions). zx_lib:find_latest_compatible(Version, Versions).
@ -527,58 +527,26 @@ tuplize(String, Acc) ->
end. end.
%%% Package utilities
-spec install(package_id()) -> ok.
%% @private
%% Install a package from the cache into the local system.
%% Before calling this function it must be known that:
%% - The zsp file is in the cache
%% - The zsp file is valid
%% - This function will only be called on startup by the launch process
%% - The package is not already installed
%% - If this function crashes it will completely halt the system
install(PackageID = {Realm, Name, _}) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Installing ~ts", [PackageString]),
ZrpFile = filename:join(zx_lib:path(zsp, Realm, Name), zx_lib:namify_zsp(PackageID)),
Files = zx_lib:extract_zsp_or_die(ZrpFile),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
ok = ensure_package_dirs(PackageID),
PackageDir = zx_lib:path(lib, PackageID),
ok = zx_lib:force_dir(PackageDir),
ok = zx_key:verify(TgzData, Signature, PubKey),
ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageDir}]),
log(info, "~ts installed", [PackageString]).
-spec build(package_id()) -> ok. -spec build(package_id()) -> ok.
%% @private %% @private
%% Given an AppID, build the project from source and add it to the current lib path. %% Given an AppID, build the project from source and add it to the current lib path.
build(PackageID) -> build(PackageID) ->
{ok, CWD} = file:get_cwd(), {ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:path(lib, PackageID)), ok = file:set_cwd(zx_lib:ppath(lib, PackageID)),
ok = zx_lib:build(), ok = zx_lib:build(),
file:set_cwd(CWD). file:set_cwd(CWD).
-spec ensure_package_dirs(package_id()) -> ok. %% FIXME
%% @private %-spec ensure_package_dirs(package_id()) -> ok.
%% Procedure to guarantee that directory locations necessary for the indicated app to %%% @private
%% run have been created or halt execution. %%% Procedure to guarantee that directory locations necessary for the indicated app to
%%% run have been created or halt execution.
ensure_package_dirs(PackageID) -> %
Dirs = [zx_lib:path(D, PackageID) || D <- [etc, var, tmp, log, lib]], %ensure_package_dirs(PackageID) ->
lists:foreach(fun zx_lib:force_dir/1, Dirs). % Dirs = [zx_lib:ppath(D, PackageID) || D <- [etc, var, tmp, log, lib]],
% lists:foreach(fun zx_lib:force_dir/1, Dirs).
@ -642,10 +610,9 @@ usage() ->
" zx reject PackageID~n" " zx reject PackageID~n"
" zx add key Realm KeyName~n" " zx add key Realm KeyName~n"
" zx get key Realm KeyName~n" " zx get key Realm KeyName~n"
" zx rem key Realm KeyName~n"
" zx create user~n" " zx create user~n"
" zx create userfiles Realm UserName~n" " zx create userfiles Realm UserName~n"
" zx create keypair Realm~n" " zx create keypair~n"
" zx export user UserID~n" " zx export user UserID~n"
" zx import user ZdufFile~n" " zx import user ZdufFile~n"
"~n" "~n"
@ -658,7 +625,6 @@ usage() ->
" zx accept PackageID~n" " zx accept PackageID~n"
" zx create realm~n" " zx create realm~n"
" zx create realmfile Realm~n" " zx create realmfile Realm~n"
" zx create sysop~n"
"~n" "~n"
"Where~n" "Where~n"
" PackageID :: A string of the form Realm-Name[-Version]~n" " PackageID :: A string of the form Realm-Name[-Version]~n"

View File

@ -91,26 +91,25 @@ list_resigns(Realm) ->
end. end.
-spec submit(PackageFile) -> no_return() -spec submit(ZspPath :: file:filename()) -> zx:outcome().
when PackageFile :: file:filename().
%% @private %% @private
%% Submit a package to the appropriate "prime" server for the given realm. %% Submit a package to the appropriate "prime" server for the given realm.
submit(PackageFile) -> submit(ZspPath) ->
Files = zx_lib:extract_zsp_or_die(PackageFile), {ok, ZspBin} = file:read_file(ZspPath),
{ok, PackageData} = file:read_file(PackageFile), <<SigSize:24, Sig:SigSize/binary, Signed/binary>> = ZspBin,
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), <<MetaSize:16, MetaBin:MetaSize/binary, _/binary>> = Signed,
Meta = binary_to_term(MetaBin), {ok, {PackageID, SigKeyName, _}} = zx_lib:b_to_ts(MetaBin),
{Realm, Package, Version} = maps:get(package_id, Meta), {ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
{ok, Socket} = connect_auth(Realm), true = zx_key:verify(Signed, Sig, PubKey),
ok = send(Socket, {submit, {Realm, Package, Version}}), {ok, Socket} = connect_auth(element(1, PackageID)),
ok = send(Socket, {submit, PackageID}),
ok = recv_or_die(Socket), ok = recv_or_die(Socket),
ok = gen_tcp:send(Socket, PackageData), ok = gen_tcp:send(Socket, ZspBin),
ok = log(info, "Done sending contents of ~tp", [PackageFile]), ok = log(info, "Done sending contents of ~tp", [ZspPath]),
Outcome = recv_or_die(Socket), Outcome = recv_or_die(Socket),
log(info, "Response: ~tp", [Outcome]), log(info, "Response: ~tp", [Outcome]),
ok = disconnect(Socket), disconnect(Socket).
halt(0).
review(PackageString) -> review(PackageString) ->

View File

@ -10,7 +10,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>"). -copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0"). -license("GPL-3.0").
-export([start_conn/2]). -export([start_conn/1]).
-export([start_link/0]). -export([start_link/0]).
-export([init/1]). -export([init/1]).
@ -18,9 +18,8 @@
%%% Interface Functions %%% Interface Functions
-spec start_conn(Host, Serial) -> Result -spec start_conn(Host) -> Result
when Host :: zx:host(), when Host :: zx:host(),
Serial :: zx:serial(),
Result :: {ok, pid()} Result :: {ok, pid()}
| {error, Reason}, | {error, Reason},
Reason :: term(). Reason :: term().
@ -28,8 +27,8 @@
%% Start an upstream connection handler. %% Start an upstream connection handler.
%% (Should only be called from zx_conn). %% (Should only be called from zx_conn).
start_conn(Host, Serial) -> start_conn(Host) ->
supervisor:start_child(?MODULE, [Host, Serial]). supervisor:start_child(?MODULE, [Host]).

View File

@ -147,10 +147,10 @@
-export([pass_meta/3, -export([pass_meta/3,
subscribe/1, unsubscribe/1, subscribe/1, unsubscribe/1,
list/0, list/1, list/2, list/3, latest/1, list/0, list/1, list/2, list/3, latest/1,
fetch/1, verify_key/1, verify_key/1, fetch/1, install/1,
pending/1, packagers/1, maintainers/1, sysops/1]). pending/1, packagers/1, maintainers/1, sysops/1]).
-export([report/1, result/2, notify/2]). -export([report/1, result/2, notify/2]).
-export([start_link/0, stop/0]). -export([start_link/0, init_connections/0, stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]). code_change/3, terminate/2]).
@ -188,14 +188,12 @@
-record(rmeta, -record(rmeta,
{revision = 0 :: non_neg_integer(), {serial = 0 :: non_neg_integer(),
serial = 0 :: non_neg_integer(),
prime = {"zomp.tsuriai.jp", 11311} :: zx:host(), prime = {"zomp.tsuriai.jp", 11311} :: zx:host(),
private = [] :: [zx:host()], private = [] :: [zx:host()],
mirrors = queue:new() :: queue:queue(zx:host()), mirrors = queue:new() :: queue:queue(zx:host()),
realm_keys = [] :: [zx:key_meta()], key = [] :: zx:key_name(),
package_keys = [] :: [zx:key_meta()], sysop = none :: zx:user_name(),
sysops = [] :: [zx:sysop_meta()],
assigned = none :: none | pid(), assigned = none :: none | pid(),
available = [] :: [pid()]}). available = [] :: [pid()]}).
@ -449,25 +447,6 @@ latest({Realm, Name, Version}) ->
request({latest, Realm, Name, Version}). request({latest, Realm, Name, Version}).
-spec fetch(PackageID) -> {ok, RequestID}
when PackageID :: zx:package_id(),
RequestID :: integer().
%% @doc
%% Ensure a package is available locally, or queue it for download otherwise.
%% Returns a request ID which will be returned in a message with the result from an
%% upstream zomp node. Crashes the caller on an illegal realm name, package name or
%% version tuple.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `fetch_result()'.
fetch({Realm, Name, Version}) ->
true = zx_lib:valid_lower0_9(Realm),
true = zx_lib:valid_lower0_9(Name),
true = zx_lib:valid_version(Version),
request({fetch, Realm, Name, Version}).
-spec verify_key(KeyID) -> {ok, RequestID} -spec verify_key(KeyID) -> {ok, RequestID}
when KeyID :: zx:key_id(), when KeyID :: zx:key_id(),
RequestID :: id(). RequestID :: id().
@ -485,6 +464,28 @@ verify_key({Realm, KeyName}) ->
request({verify_key, Realm, KeyName}). request({verify_key, Realm, KeyName}).
-spec install(Path :: file:filename()) -> zx:outcome().
%% @doc
%% Install a package from a local file.
install(Path) ->
gen_server:call(?MODULE, {install, Path}).
-spec fetch(PackageString :: string()) -> {ok, id()}.
%% @doc
%% Install the specified package. This returns an id() that will be referenced
%% in a later response message.
fetch(PackageString) ->
case zx_lib:package_id(PackageString) of
{ok, PackageID} ->
gen_server:call(?MODULE, {fetch, PackageID});
{error, invalid_package_string} ->
{error, "Invalid package string.", 22}
end.
-spec pending(Package) -> {ok, RequestID} -spec pending(Package) -> {ok, RequestID}
when Package :: zx:package(), when Package :: zx:package(),
RequestID :: id(). RequestID :: id().
@ -557,7 +558,7 @@ sysops(Realm) ->
%% Private function to wrap the necessary bits up. %% Private function to wrap the necessary bits up.
request(Action) -> request(Action) ->
gen_server:call(?MODULE, {request, self(), Action}). gen_server:call(?MODULE, {request, Action}).
@ -610,63 +611,13 @@ start_link() ->
%% TODO: Implement lockfile checking and master lock acquisition. %% TODO: Implement lockfile checking and master lock acquisition.
init(none) -> init(none) ->
Blank = blank_state(), {ok, #s{}}.
{ok, MX, CX} = init_connections(),
State = Blank#s{mx = MX, cx = CX},
{ok, State}.
-spec blank_state() -> state(). -spec init_connections() -> ok.
%% @private
%% Used to generate a correct, but exactly empty state.
%% Useful mostly for testing and validation, though also actually used in the program.
blank_state() ->
#s{}.
-spec init_connections() -> {ok, MX, CX}
when MX :: monitor_index(),
CX :: conn_index().
%% @private
%% Starting from a stateless condition, recruit and resolve all realm relevant data,
%% populate host caches, and initiate connections to required realms. On completion
%% return a populated MX and CX to the caller. Should only ever be called by init/1.
%% Returns an `ok' tuple to disambiguate it from pure functions *and* to leave an
%% obvious place to populate error returns in the future if desired.
init_connections() -> init_connections() ->
CX = cx_load(), gen_server:cast(?MODULE, init_connections).
MX = mx_new(),
Realms = cx_realms(CX),
init_connections(Realms, MX, CX).
-spec init_connections(Realms, MX, CX) -> {ok, NewMX, NewCX}
when Realms :: [zx:realm()],
MX :: monitor_index(),
CX :: conn_index(),
NewMX :: monitor_index(),
NewCX :: conn_index().
init_connections([Realm | Realms], MX, CX) ->
{ok, Hosts, NextCX} = cx_next_hosts(3, Realm, CX),
MaybeAttempt =
fun(Host, {M, C}) ->
case cx_maybe_add_attempt(Host, Realm, C) of
not_connected ->
{ok, Pid} = zx_conn:start(Host),
NewM = mx_add_monitor(Pid, attempt, M),
NewC = cx_add_attempt(Pid, Host, Realm, C),
{NewM, NewC};
{ok, NewC} ->
{M, NewC}
end
end,
{NewMX, NewCX} = lists:foldl(MaybeAttempt, {MX, NextCX}, Hosts),
init_connections(Realms, NewMX, NewCX);
init_connections([], MX, CX) ->
{ok, MX, CX}.
@ -689,12 +640,24 @@ stop() ->
handle_call({request, list}, _, State = #s{cx = CX}) -> handle_call({request, list}, _, State = #s{cx = CX}) ->
Realms = cx_realms(CX), Realms = cx_realms(CX),
{reply, {ok, Realms}, State}; {reply, {ok, Realms}, State};
handle_call({request, Requestor, Action}, From, State = #s{id = ID}) -> handle_call({request, Action}, From, State = #s{id = ID}) ->
NewID = ID + 1, NewID = ID + 1,
_ = gen_server:reply(From, {ok, NewID}), _ = gen_server:reply(From, {ok, NewID}),
Requestor = element(1, From),
NextState = do_request(Requestor, Action, State#s{id = NewID}), NextState = do_request(Requestor, Action, State#s{id = NewID}),
NewState = eval_queue(NextState), NewState = eval_queue(NextState),
{noreply, NewState}; {noreply, NewState};
handle_call({fetch, PackageID}, From, State = #s{id = ID}) ->
NewID = ID + 1,
_ = gen_server:reply(From, {ok, NewID}),
Requestor = element(1, From),
NextState = do_fetch(PackageID, Requestor, State#s{id = NewID}),
NewState = eval_queue(NextState),
{noreply, NewState};
handle_call({install, Path}, _, State) ->
Result = do_import_zsp(Path),
NewState = eval_queue(State),
{reply, Result, NewState};
handle_call(Unexpected, From, State) -> handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]), ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]),
{noreply, State}. {noreply, State}.
@ -726,6 +689,9 @@ handle_cast({notify, Conn, Package, Update}, State) ->
ok = do_notify(Conn, Package, Update, State), ok = do_notify(Conn, Package, Update, State),
NewState = eval_queue(State), NewState = eval_queue(State),
{noreply, NewState}; {noreply, NewState};
handle_cast(init_connections, State) ->
NewState = init_connections(State),
{noreply, NewState};
handle_cast(stop, State) -> handle_cast(stop, State) ->
{stop, normal, State}; {stop, normal, State};
handle_cast(Unexpected, State) -> handle_cast(Unexpected, State) ->
@ -913,6 +879,49 @@ dequeue(Pending) ->
end. end.
-spec init_connections(State) -> NewState
when State :: state(),
NewState :: state().
%% @private
%% Starting from a stateless condition, recruit and resolve all realm relevant data,
%% populate host caches, and initiate connections to required realms. On completion
%% return a populated MX and CX to the caller. Should only ever be called by init/1.
%% Returns an `ok' tuple to disambiguate it from pure functions *and* to leave an
%% obvious place to populate error returns in the future if desired.
init_connections(State = #s{mx = MX, cx = CX}) ->
Realms = cx_realms(CX),
{ok, NewMX, NewCX} = init_connections(Realms, MX, CX),
State#s{mx = NewMX, cx = NewCX}.
-spec init_connections(Realms, MX, CX) -> {ok, NewMX, NewCX}
when Realms :: [zx:realm()],
MX :: monitor_index(),
CX :: conn_index(),
NewMX :: monitor_index(),
NewCX :: conn_index().
init_connections([Realm | Realms], MX, CX) ->
{ok, Hosts, NextCX} = cx_next_hosts(3, Realm, CX),
MaybeAttempt =
fun(Host, {M, C}) ->
case cx_maybe_add_attempt(Host, Realm, C) of
not_connected ->
{ok, Pid} = zx_conn:start(Host),
NewM = mx_add_monitor(Pid, attempt, M),
NewC = cx_add_attempt(Pid, Host, Realm, C),
{NewM, NewC};
{ok, NewC} ->
{M, NewC}
end
end,
{NewMX, NewCX} = lists:foldl(MaybeAttempt, {MX, NextCX}, Hosts),
init_connections(Realms, NewMX, NewCX);
init_connections([], MX, CX) ->
{ok, MX, CX}.
-spec ensure_connections(Realms, MX, CX) -> {NewMX, NewCX} -spec ensure_connections(Realms, MX, CX) -> {NewMX, NewCX}
when Realms :: [zx:realm()], when Realms :: [zx:realm()],
MX :: monitor_index(), MX :: monitor_index(),
@ -968,11 +977,14 @@ reassign_conns([], CX, Unassigned) ->
do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX}) -> do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX}) ->
{NewDropped, NewRequests, NewMX} = {NewDropped, NewRequests, NewMX} =
case maps:take(ID, Requests) of case maps:take(ID, Requests) of
{Request, NextRequests} -> {Request, Rest} when element(1, element(2, Request)) == fetch ->
{NextMX, NextR} = handle_fetch_result(ID, Result, Request, Rest, MX),
{Dropped, NextR, NextMX};
{Request, Rest} ->
Requestor = element(1, Request), Requestor = element(1, Request),
Requestor ! {z_result, ID, Result}, Requestor ! {z_result, ID, Result},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX), NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{Dropped, NextRequests, NextMX}; {Dropped, Rest, NextMX};
error -> error ->
NextDropped = handle_orphan_result(ID, Result, Dropped), NextDropped = handle_orphan_result(ID, Result, Dropped),
{NextDropped, Requests, MX} {NextDropped, Requests, MX}
@ -980,6 +992,24 @@ do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX
State#s{requests = NewRequests, dropped = NewDropped, mx = NewMX}. State#s{requests = NewRequests, dropped = NewDropped, mx = NewMX}.
handle_fetch_result(ID, {done, Bin}, {Requestor, _}, Requests, MX) ->
Result =
case do_import_package(Bin) of
ok -> done;
Error -> Error
end,
Requestor ! {z_result, ID, Result},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{NextMX, Requests};
handle_fetch_result(ID, Hops = {hops, _}, Request = {Requestor, _}, Requests, MX) ->
Requestor ! {z_result, ID, Hops},
{MX, maps:put(ID, Request, Requests)};
handle_fetch_result(ID, Outcome, {Requestor, _}, Requests, MX) ->
Requestor ! {z_result, ID, Outcome},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{NextMX, Requests}.
-spec handle_orphan_result(ID, Result, Dropped) -> NewDropped -spec handle_orphan_result(ID, Result, Dropped) -> NewDropped
when ID :: id(), when ID :: id(),
Result :: result(), Result :: result(),
@ -1208,6 +1238,112 @@ drop_requests(ReqIDs, Dropped, Requests) ->
lists:fold(Partition, {Dropped, Requests}, ReqIDs). lists:fold(Partition, {Dropped, Requests}, ReqIDs).
-spec do_fetch(PackageID, Requestor, State) -> NewState
when PackageID :: zx:package_id(),
Requestor :: pid(),
State :: state(),
NewState :: state().
%% @private
%% Provide a chance to bypass if the package is in cache.
do_fetch(PackageID, Requestor, State = #s{id = ID}) ->
Path = zx_lib:zsp_path(PackageID),
case file:read_file(Path) of
{ok, Bin} ->
case do_import_package(Bin) of
ok ->
Requestor ! {z_result, ID, ok},
{ok, State};
Error ->
Requestor ! {z_result, ID, Error},
{ok, State}
end;
{error, enoent} ->
{Realm, Name, Version} = PackageID,
Action = {fetch, Realm, Name, Version},
do_request(Requestor, Action, State);
Error ->
Requestor ! {z_result, ID, Error}
end.
-spec do_import_zsp(file:filename()) -> zx:outcome().
%% @private
%% Dealing with data from the (probably local) filesystem can fail in a bajillion ways
%% and spring memory leaks if one tries to get too clever. So I'm sidestepping all the
%% madness with a "try++" here by spawning a suicidal helper.
do_import_zsp(Path) ->
{Pid, Mon} = spawn_monitor(fun() -> import_from_path(Path) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info}
after 5000 ->
{error, timeout}
end.
do_import_package(Bin) ->
{Pid, Mon} = spawn_monitor(fun() -> import_package(Bin) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info}
after 5000 ->
{error, timeout}
end.
-spec import_from_path(ZspPath) -> no_return()
when ZspPath :: file:filename().
%% @private
%% The happy path of .zsp installation.
%% Must NEVER be executed by the zx_daemon directly.
%% More generally, there are a few phases:
%% 1- Loading the binary to extract the PackageID
%% 2- Checking the signature
%% 3- Moving the file to the cache
%% 4- Wiping the destination directory
%% 5- Extracting the TarGz to the destination
%% Some combination of functions should make these steps happen in a way that isn't
%% totally ridiculous, OR the bullet should just be bitten an allow for the
%% redundant lines here and there in different package management functions.
%%
%% Use cases are:
%% - Install a missing package from upstream
%% - Install a missing package from the local cache
%% - Reinstall a package from the local cache
%% - Import a package to the cache from the local filesystem and install it
%%
%% The Correct Approach as determine by The Royal Me is that I'm going to accept the
%% redundant code in the short-term because the data format is already decided.
%% If a place to get more fancy with the phases becomes really obvious after writing
%% identicalish segements of functions a few places then I'll break things apart.
import_from_path(ZspPath) ->
{ok, Bin} = file:read_file(ZspPath),
import_package(Bin).
import_package(Bin = <<Size:24, Sig:Size/binary, Signed/binary>>) ->
<<MetaSize:16, MetaBin:MetaSize/binary, TarGZ/binary>> = Signed,
{PackageID, SigKeyName, _} = zx_lib:b_to_ts(MetaBin),
{ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
true = zx_key:verify(Signed, Sig, PubKey),
ok = file:write_file(zx_lib:zsp_path(PackageID), Bin),
Destination = zx_lib:ppath(lib, PackageID),
ok = filelib:ensure_dir(Destination),
ok = zx_lib:rm_rf(Destination),
ok = file:make_dir(Destination),
Result = erl_tar:extract({binary, TarGZ}, [{cwd, Destination}, compressed]),
zx_daemon ! {self(), Result}.
%%% Monitor Index ADT Interface Functions %%% Monitor Index ADT Interface Functions
@ -1366,36 +1502,30 @@ cx_load() ->
%% where any number of wild things might be going on in the user's filesystem). %% where any number of wild things might be going on in the user's filesystem).
cx_populate() -> cx_populate() ->
Pattern = filename:join([zx_lib:path(etc), "*", "realm.conf"]), Realms = zx_lib:list_realms(),
case filelib:wildcard(Pattern) of CX = lists:foldl(fun cx_populate/2, [], Realms),
[] -> {error, no_realms}; {ok, CX}.
RealmFiles -> {ok, cx_populate(RealmFiles, [])}
end.
-spec cx_populate(RealmFiles, Realms) -> NewRealms -spec cx_populate(Realms, CX) -> NewCX
when RealmFiles :: file:filename(), when Realms :: [zx:realm()],
Realms :: [{zx:realm(), realm_meta()}], CX :: [{zx:realm(), realm_meta()}],
NewRealms :: [{zx:realm(), realm_meta()}]. NewCX :: [{zx:realm(), realm_meta()}].
%% @private %% @private
%% Pack an initially empty conn_index() with realm meta and host cache data. %% Pack an initially empty conn_index() with realm meta and host cache data.
%% Should not halt on a corrupted, missing, malformed, etc. realm file but will log %% Should not halt on a corrupted, missing, malformed, etc. realm file but will log
%% any loading errors. %% any loading errors.
cx_populate([File | Files], Realms) -> cx_populate(Realm, CX) ->
NewRealms = case zx_lib:load_realm_conf(Realm) of
case file:consult(File) of
{ok, Meta} -> {ok, Meta} ->
Realm = cx_load_realm_meta(Meta), Record = cx_load_realm_meta(Meta),
[Realm | Realms]; [{Realm, Record} | CX];
{error, Reason} -> {error, Reason} ->
Message = "Loading realm file ~tp failed with: ~tp. Skipping...", Message = "Loading realm ~tp failed with: ~tp. Skipping...",
ok = log(warning, Message, [File, Reason]), ok = log(warning, Message, [Realm, Reason]),
Realms CX
end, end.
cx_populate(Files, NewRealms);
cx_populate([], Realms) ->
Realms.
-spec cx_load_realm_meta(Meta) -> Result -spec cx_load_realm_meta(Meta) -> Result
@ -1405,20 +1535,12 @@ cx_populate([], Realms) ->
%% This function MUST adhere to the realmfile definition found at. %% This function MUST adhere to the realmfile definition found at.
cx_load_realm_meta(Meta) -> cx_load_realm_meta(Meta) ->
{realm, Realm} = lists:keyfind(realm, 1, Meta), Realm = maps:get(realm, Meta),
{revision, Revision} = lists:keyfind(revision, 1, Meta),
{prime, Prime} = lists:keyfind(prime, 1, Meta),
{realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, Meta),
{package_keys, PackageKeys} = lists:keyfind(packge_keys, 1, Meta),
{sysops, Sysops} = lists:keyfind(sysops, 1, Meta),
Basic = Basic =
#rmeta{revision = Revision, #rmeta{prime = maps:get(prime, Meta),
prime = Prime, sysop = maps:get(sysop, Meta),
realm_keys = RealmKeys, key = maps:get(key, Meta)},
package_keys = PackageKeys, cx_load_cache(Realm, Basic).
sysops = Sysops},
Complete = cx_load_cache(Realm, Basic),
{Realm, Complete}.
-spec cx_load_cache(Realm, Basic) -> Complete -spec cx_load_cache(Realm, Basic) -> Complete
@ -1542,7 +1664,7 @@ cx_next_host(Meta = #rmeta{prime = Prime, private = Private, mirrors = Mirrors})
{ok, Next, Meta#rmeta{mirrors = NewMirrors}}; {ok, Next, Meta#rmeta{mirrors = NewMirrors}};
{empty, Mirrors} -> {empty, Mirrors} ->
Enqueue = fun(H, Q) -> queue:in(H, Q) end, Enqueue = fun(H, Q) -> queue:in(H, Q) end,
NewMirrors = lists:foldl(Enqueue, Private, Mirrors), NewMirrors = lists:foldl(Enqueue, Mirrors, Private),
{prime, Prime, Meta#rmeta{mirrors = NewMirrors}} {prime, Prime, Meta#rmeta{mirrors = NewMirrors}}
end. end.

View File

@ -21,7 +21,7 @@
%%% Functions %%% Functions
-spec ensure_keypair(zx:key_id()) -> true | no_return(). -spec ensure_keypair(zx:key_id()) -> zx:outcome().
%% @private %% @private
%% Check if both the public and private key based on KeyID exists. %% Check if both the public and private key based on KeyID exists.
@ -30,17 +30,17 @@ ensure_keypair(KeyID = {Realm, KeyName}) ->
{true, true} -> {true, true} ->
true; true;
{false, true} -> {false, true} ->
Message = "Public key ~tp/~tp cannot be found", Format = "Public key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]), Message = io_lib:format(Format, [Realm, KeyName]),
halt(1); {error, Message, 2};
{true, false} -> {true, false} ->
Message = "Private key ~tp/~tp cannot be found", Format = "Private key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]), Message = io_lib:format(Format, [Realm, KeyName]),
halt(1); {error, Message, 2};
{false, false} -> {false, false} ->
Message = "Key pair ~tp/~tp cannot be found", Format = "Key pair ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]), Message = io_lib:format(Format, [Realm, KeyName]),
halt(1) {error, Message, 2}
end. end.
@ -76,31 +76,19 @@ path(private, {Realm, KeyName}) ->
prompt_keygen() -> prompt_keygen() ->
Message = Message =
"~n Enter a name for your new keys.~n~n" "~nKEY NAME~n"
" Valid names must start with a lower-case letter, and can include~n" "Enter a name for your new key pair.~n"
" only lower-case letters, numbers, and periods, but no series of~n" "Valid names must start with a lower-case letter, and can include "
" consecutive periods. (That is: [a-z0-9\\.])~n~n" "only lower-case letters, numbers, and underscores, but no series of "
" To designate the key as realm-specific, enter the realm name and~n" "consecutive underscores. (That is: [a-z0-9_])~n"
" key name separated by a space.~n~n" " Example: my_key~n",
" Example: some.realm my.key~n",
ok = io:format(Message), ok = io:format(Message),
Input = zx_tty:get_input(), Input = zx_tty:get_input(),
{Realm, KeyName} = case zx_lib:valid_lower0_9(Input) of
case string:lexemes(Input, " ") of true ->
[R, K] -> {R, K}; Input;
[K] -> {"otpr", K} false ->
end, ok = io:format("Bad key name ~tp. Try again.~n", [Input]),
case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_label(KeyName)} of
{true, true} ->
{Realm, KeyName};
{false, true} ->
ok = io:format("Bad realm name ~tp. Try again.~n", [Realm]),
prompt_keygen();
{true, false} ->
ok = io:format("Bad key name ~tp. Try again.~n", [KeyName]),
prompt_keygen();
{false, false} ->
ok = io:format("NUTS! Both key and realm names are illegal. Try again.~n"),
prompt_keygen() prompt_keygen()
end. end.
@ -108,19 +96,30 @@ prompt_keygen() ->
-spec generate_rsa(KeyID) -> Result -spec generate_rsa(KeyID) -> Result
when KeyID :: zx:key_id(), when KeyID :: zx:key_id(),
Result :: ok Result :: ok
| {error, keygen_fail}. | {error, Reason},
Reason :: keygen_fail
| exists.
%% @private %% @private
%% Generate an RSA keypair and write them in der format to the current directory, using %% Generate an RSA keypair and write them in der format to the current directory, using
%% filenames derived from Prefix. %% filenames derived from Prefix.
%% NOTE: The current version of this command is likely to only work on a unix system. %% NOTE: The current version of this command is likely to only work on a unix system.
generate_rsa(KeyID = {Realm, KeyName}) -> generate_rsa(KeyID = {Realm, KeyName}) ->
PemFile = filename:join(zx_lib:path(key, Realm), KeyName ++ ".pub.pem"), BaseName = filename:join(zx_lib:path(key, Realm), KeyName),
Pattern = BaseName ++ ".*.der",
case filelib:wildcard(Pattern) of
[] -> generate_rsa(KeyID, BaseName);
_ -> {error, exists}
end.
generate_rsa(KeyID, BaseName) ->
PemFile = BaseName ++ ".pub.pem",
KeyFile = path(private, KeyID), KeyFile = path(private, KeyID),
PubFile = path(public, KeyID), PubFile = path(public, KeyID),
ok = lists:foreach(fun zx_lib:halt_if_exists/1, [PemFile, KeyFile, PubFile]),
ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]), ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]),
ok = gen_p_key(KeyFile), case gen_p_key(KeyFile) of
ok ->
ok = der_to_pem(KeyFile, PemFile), ok = der_to_pem(KeyFile, PemFile),
{ok, PemBin} = file:read_file(PemFile), {ok, PemBin} = file:read_file(PemFile),
[PemData] = public_key:pem_decode(PemBin), [PemData] = public_key:pem_decode(PemBin),
@ -135,24 +134,35 @@ generate_rsa(KeyID = {Realm, KeyName}) ->
ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]), ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]),
ok = log(error, "Something has gone wrong."), ok = log(error, "Something has gone wrong."),
{error, keygen_fail} {error, keygen_fail}
end;
{error, no_ssl} ->
ok = log(error, "OpenSSL not found."),
{error, keygen_fail}
end. end.
-spec gen_p_key(KeyFile) -> ok -spec gen_p_key(KeyFile) -> Result
when KeyFile :: file:filename(). when KeyFile :: file:filename(),
Result :: ok
| {error, no_ssl}.
%% @private %% @private
%% Format an openssl shell command that will generate proper 16k RSA keys. %% Format an openssl shell command that will generate proper 16k RSA keys.
gen_p_key(KeyFile) -> gen_p_key(KeyFile) ->
case openssl() of
{ok, OpenSSL} ->
Command = Command =
io_lib:format("~ts genpkey" io_lib:format("~ts genpkey"
" -algorithm rsa" " -algorithm rsa"
" -out ~ts" " -out ~ts"
" -outform DER" " -outform DER"
" -pkeyopt rsa_keygen_bits:16384", " -pkeyopt rsa_keygen_bits:16384",
[openssl(), KeyFile]), [OpenSSL, KeyFile]),
Out = os:cmd(Command), Out = os:cmd(Command),
io:format(Out). io:format(Out);
Error ->
Error
end.
-spec der_to_pem(KeyFile, PemFile) -> ok -spec der_to_pem(KeyFile, PemFile) -> ok
@ -167,6 +177,8 @@ gen_p_key(KeyFile) ->
%% way. %% way.
der_to_pem(KeyFile, PemFile) -> der_to_pem(KeyFile, PemFile) ->
case openssl() of
{ok, OpenSSL} ->
Command = Command =
io_lib:format("~ts rsa" io_lib:format("~ts rsa"
" -inform DER" " -inform DER"
@ -174,9 +186,12 @@ der_to_pem(KeyFile, PemFile) ->
" -outform PEM" " -outform PEM"
" -pubout" " -pubout"
" -out ~ts", " -out ~ts",
[openssl(), KeyFile, PemFile]), [OpenSSL, KeyFile, PemFile]),
Out = os:cmd(Command), Out = os:cmd(Command),
io:format(Out). io:format(Out);
Error ->
Error
end.
-spec check_key(KeyFile, PubFile) -> Result -spec check_key(KeyFile, PubFile) -> Result
@ -196,11 +211,14 @@ check_key(KeyFile, PubFile) ->
public_key:verify(TestMessage, sha512, Signature, Pub). public_key:verify(TestMessage, sha512, Signature, Pub).
-spec openssl() -> Executable | no_return() -spec openssl() -> Result
when Executable :: file:filename(). when Result :: {ok, Executable}
| {error, no_ssl},
Executable :: file:filename().
%% @private %% @private
%% Attempt to locate the installed openssl executable for use in shell commands. %% Attempt to locate the installed openssl executable for use in shell commands.
%% Halts execution with an error message if the executable cannot be found. %% TODO: Determine whether it is even worth it to perform this check VS restricting
%% os:cmd/1 directed zx_key functions by platform.
openssl() -> openssl() ->
OpenSSL = OpenSSL =
@ -208,16 +226,15 @@ openssl() ->
{unix, _} -> "openssl"; {unix, _} -> "openssl";
{win32, _} -> "openssl.exe" {win32, _} -> "openssl.exe"
end, end,
ok =
case os:find_executable(OpenSSL) of case os:find_executable(OpenSSL) of
false -> false ->
ok = log(error, "OpenSSL could not be found in this system's PATH."), ok = log(error, "OpenSSL could not be found in this system's PATH."),
ok = log(error, "Install OpenSSL and then retry."), ok = log(error, "Install OpenSSL and then retry."),
error_exit("Missing system dependenct: OpenSSL", ?LINE); {error, no_ssl};
Path -> Path ->
log(info, "OpenSSL executable found at: ~ts", [Path]) log(info, "OpenSSL executable found at: ~ts", [Path]),
end, {ok, OpenSSL}
OpenSSL. end.
-spec load(Type, KeyID) -> Result -spec load(Type, KeyID) -> Result
@ -242,43 +259,13 @@ load(Type, KeyID) ->
end. end.
-spec verify(Data, Signature, PubKey) -> ok | no_return() -spec verify(Data, Signature, PubKey) -> boolean()
when Data :: binary(), when Data :: binary(),
Signature :: binary(), Signature :: binary(),
PubKey :: public_key:rsa_public_key(). PubKey :: public_key:rsa_public_key().
%% @private %% @private
%% Verify the RSA Signature of some Data against the given PubKey or halt execution. %% Curry out the choice of algorithm. This will probably disappear in a few more
%% This function always assumes sha512 is the algorithm being used. %% versions as the details of sha512 and RSA gradually give way to the Brave New World.
%% Should only ever be called by the initial launch process.
verify(Data, Signature, PubKey) -> verify(Data, Signature, PubKey) ->
case public_key:verify(Data, sha512, Signature, PubKey) of public_key:verify(Data, sha512, Signature, PubKey).
true -> ok;
false -> error_exit("Bad package signature!", ?LINE)
end.
%%% Error exits
-spec error_exit(Error, Line) -> no_return()
when Error :: term(),
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Error, Line) ->
error_exit(Error, [], Line).
-spec error_exit(Format, Args, Line) -> no_return()
when Format :: string(),
Args :: [term()],
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Format, Args, Line) ->
File = filename:basename(?FILE),
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
halt(1).

View File

@ -25,11 +25,9 @@
valid_lower0_9/1, valid_label/1, valid_version/1, valid_lower0_9/1, valid_label/1, valid_version/1,
string_to_version/1, version_to_string/1, string_to_version/1, version_to_string/1,
package_id/1, package_string/1, package_id/1, package_string/1,
namify_zsp/1, namify_tgz/1, namify_zsp/1, zsp_path/1,
zsp_path/1,
find_latest_compatible/2, installed/1, find_latest_compatible/2, installed/1,
realm_conf/1, load_realm_conf/1, realm_conf/1, load_realm_conf/1,
extract_zsp_or_die/1, halt_if_exists/1,
build/0, build/0,
rm_rf/1, rm/1, rm_rf/1, rm/1,
b_to_t/1, b_to_ts/1]). b_to_t/1, b_to_ts/1]).
@ -582,37 +580,17 @@ package_string(_) ->
when PackageID :: zx:package_id(), when PackageID :: zx:package_id(),
ZrpFileName :: file:filename(). ZrpFileName :: file:filename().
%% @private %% @private
%% Map an PackageID to its correct .zsp package file name. %% Map a PackageID to its correct .zsp package file name.
namify_zsp(PackageID) -> namify(PackageID, "zsp"). namify_zsp(PackageID) ->
-spec namify_tgz(PackageID) -> TgzFileName
when PackageID :: zx:package_id(),
TgzFileName :: file:filename().
%% @private
%% Map an PackageID to its correct gzipped tarball source bundle filename.
namify_tgz(PackageID) -> namify(PackageID, "tgz").
-spec namify(PackageID, Suffix) -> FileName
when PackageID :: zx:package_id(),
Suffix :: string(),
FileName :: file:filename().
%% @private
%% Converts an PackageID to a canonical string, then appends the provided
%% filename Suffix.
namify(PackageID, Suffix) ->
{ok, PackageString} = package_string(PackageID), {ok, PackageString} = package_string(PackageID),
PackageString ++ "." ++ Suffix. PackageString ++ ".zsp".
-spec zsp_path(zx:package_id()) -> file:filename(). -spec zsp_path(zx:package_id()) -> file:filename().
zsp_path(PackageID) -> zsp_path(PackageID = {Realm, _, _}) ->
filename:join(path(zsp, element(1, PackageID)), namify_zsp(PackageID)). filename:join(path(zsp, Realm), namify_zsp(PackageID)).
-spec find_latest_compatible(Version, Versions) -> Result -spec find_latest_compatible(Version, Versions) -> Result
@ -683,7 +661,7 @@ realm_conf(Realm) ->
| file:posix() | file:posix()
| {Line :: integer(), Mod :: module(), Cause :: term()}. | {Line :: integer(), Mod :: module(), Cause :: term()}.
%% @private %% @private
%% Load the config for the given realm or halt with an error. %% Load the config for the given realm.
load_realm_conf(Realm) -> load_realm_conf(Realm) ->
Path = realm_conf(Realm), Path = realm_conf(Realm),
@ -696,41 +674,6 @@ load_realm_conf(Realm) ->
end. end.
-spec extract_zsp_or_die(FileName) -> Files | no_return()
when FileName :: file:filename(),
Files :: [{file:filename(), binary()}].
%% @private
%% Extract a zsp archive, if possible. If not possible, halt execution with as accurate
%% an error message as can be managed.
extract_zsp_or_die(FileName) ->
case erl_tar:extract(FileName, [memory]) of
{ok, Files} ->
Files;
{error, {FileName, enoent}} ->
Message = "Can't find file ~ts.",
error_exit(Message, [FileName], ?LINE);
{error, invalid_tar_checksum} ->
Message = "~ts is not a valid zsp archive.",
error_exit(Message, [FileName], ?LINE);
{error, Reason} ->
Message = "Extracting package file failed with: ~160tp.",
error_exit(Message, [Reason], ?LINE)
end.
-spec halt_if_exists(file:filename()) -> ok | no_return().
%% @private
%% A helper function to guard against overwriting an existing file. Halts execution if
%% the file is found to exist.
halt_if_exists(Path) ->
case filelib:is_file(Path) of
true -> error_exit("~ts already exists! Halting.", [Path], ?LINE);
false -> ok
end.
-spec build() -> ok. -spec build() -> ok.
%% @private %% @private
%% Run any local `zxmake' script needed by the project for non-Erlang code (if present), %% Run any local `zxmake' script needed by the project for non-Erlang code (if present),
@ -755,6 +698,7 @@ build() ->
-spec rm_rf(file:filename()) -> ok | {error, file:posix()}. -spec rm_rf(file:filename()) -> ok | {error, file:posix()}.
%% @private %% @private
%% Recursively remove files and directories. Equivalent to `rm -rf'. %% Recursively remove files and directories. Equivalent to `rm -rf'.
%% Does not return an error on a nonexistant path.
rm_rf(Path) -> rm_rf(Path) ->
case filelib:is_dir(Path) of case filelib:is_dir(Path) of
@ -764,7 +708,10 @@ rm_rf(Path) ->
ok = lists:foreach(fun rm/1, Contents), ok = lists:foreach(fun rm/1, Contents),
file:del_dir(Path); file:del_dir(Path);
false -> false ->
file:delete(Path) case filelib:is_regular(Path) of
true -> file:delete(Path);
false -> ok
end
end. end.
@ -803,18 +750,3 @@ b_to_ts(Binary) ->
catch catch
error:badarg -> error error:badarg -> error
end. end.
%%% Error exits
-spec error_exit(Format, Args, Line) -> no_return()
when Format :: string(),
Args :: [term()],
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Format, Args, Line) ->
File = filename:basename(?FILE),
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
halt(1).

View File

@ -10,7 +10,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>"). -copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0"). -license("GPL-3.0").
-export([initialize/2, assimilate/1, set_version/1, -export([initialize/2, set_version/1,
list_realms/0, list_packages/1, list_versions/1, list_realms/0, list_packages/1, list_versions/1,
set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1, set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1,
add_realm/1, drop_realm/1, add_realm/1, drop_realm/1,
@ -73,14 +73,14 @@ initialize3(Type, PackageID) ->
case package_exists(PackageID) of case package_exists(PackageID) of
false -> false ->
Prefix = ask_prefix(), Prefix = ask_prefix(),
initialize(Type, PackageID, Prefix); initialize4(Type, PackageID, Prefix);
true -> true ->
Message = "Package already exists. Try another.", Message = "Package already exists. Try another.",
{error, Message, 17} {error, Message, 17}
end. end.
initialize(app, PackageID, Prefix) -> initialize4(app, PackageID, Prefix) ->
Instructions = Instructions =
"The OTP application controller has to know what module to call start/2 on to " "The OTP application controller has to know what module to call start/2 on to "
"start your program.~n" "start your program.~n"
@ -93,23 +93,23 @@ initialize(app, PackageID, Prefix) ->
ok = io:format(Instructions), ok = io:format(Instructions),
case zx_tty:get_input() of case zx_tty:get_input() of
"" -> "" ->
initialize(lib, PackageID, Prefix, none); initialize5(lib, PackageID, Prefix, none);
String -> String ->
case zx_lib:valid_lower0_9(String) of case zx_lib:valid_lower0_9(String) of
true -> true ->
AppStart = {list_to_atom(String), []}, AppStart = {list_to_atom(String), []},
initialize(app, PackageID, Prefix, AppStart); initialize5(app, PackageID, Prefix, AppStart);
false -> false ->
Message = "The name \"~ts\" doesn't seem valid. Try \"[a-z_]*\".~n", Message = "The name \"~ts\" doesn't seem valid. Try \"[a-z_]*\".~n",
ok = io:format(Message, String), ok = io:format(Message, String),
initialize(app, PackageID, Prefix) initialize4(app, PackageID, Prefix)
end end
end; end;
initialize(lib, PackageID, Prefix) -> initialize4(lib, PackageID, Prefix) ->
initialize(lib, PackageID, Prefix, none). initialize5(lib, PackageID, Prefix, none).
initialize(Type, PackageID, Prefix, AppStart) -> initialize5(Type, PackageID, Prefix, AppStart) ->
ok = update_source_vsn(element(3, PackageID)), ok = update_source_vsn(element(3, PackageID)),
ok = initialize_app_file(PackageID, AppStart), ok = initialize_app_file(PackageID, AppStart),
MetaList = MetaList =
@ -268,39 +268,6 @@ initialize_app_file({_, Name, Version}, AppStart) ->
zx_lib:write_terms(AppFile, [AppProfile]). zx_lib:write_terms(AppFile, [AppProfile]).
-spec assimilate(PackageFile) -> zx:outcome()
when PackageFile :: file:filename().
%% @private
%% Receives a path to a file containing package data, examines it, and copies it to a
%% canonical location under a canonical name.
assimilate(PackageFile) ->
Files = zx_lib:extract_zsp_or_die(PackageFile),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:zomp_dir()),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
PackageID = maps:get(package_id, Meta),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
case public_key:verify(TgzData, sha512, Signature, PubKey) of
true ->
ok = file:copy(PackageFile, zx_lib:zsp_path(PackageID)),
assimilate2(CWD, PackageID);
false ->
{error, "Bad package signature.", 1}
end.
assimilate2(CWD, PackageID) ->
ok = file:set_cwd(CWD),
Message = "~ts is now locally available.",
{ok, PackageString} = zx_lib:package_string(PackageID),
log(info, Message, [PackageString]).
-spec set_version(VersionString) -> zx:outcome() -spec set_version(VersionString) -> zx:outcome()
when VersionString :: string(). when VersionString :: string().
%% @private %% @private
@ -483,11 +450,11 @@ set_dep2(PackageID) ->
Deps = maps:get(deps, Meta), Deps = maps:get(deps, Meta),
case lists:member(PackageID, Deps) of case lists:member(PackageID, Deps) of
true -> ok; true -> ok;
false -> set_dep(PackageID, Deps, Meta) false -> set_dep3(PackageID, Deps, Meta)
end. end.
-spec set_dep(PackageID, Deps, Meta) -> ok -spec set_dep3(PackageID, Deps, Meta) -> ok
when PackageID :: zx:package_id(), when PackageID :: zx:package_id(),
Deps :: [zx:package_id()], Deps :: [zx:package_id()],
Meta :: [term()]. Meta :: [term()].
@ -497,7 +464,7 @@ set_dep2(PackageID) ->
%% 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, NewVersion}, Deps, Meta) -> set_dep3(PackageID = {Realm, Name, NewVersion}, Deps, Meta) ->
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
@ -642,18 +609,35 @@ version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) ->
%% @private %% @private
%% Turn a target project directory into a package, prompting the user for appropriate %% Turn a target project directory into a package, prompting the user for appropriate
%% key selection or generation actions along the way. %% key selection or generation actions along the way.
%% TODO: Add user selection in the case more than one user with private keys exists.
package(TargetDir) -> package(TargetDir) ->
ok = log(info, "Packaging ~ts", [TargetDir]), ok = log(info, "Packaging ~ts", [TargetDir]),
{ok, Meta} = zx_lib:read_project_meta(TargetDir), {ok, Meta} = zx_lib:read_project_meta(TargetDir),
{Realm, _, _} = maps:get(package_id, Meta),
case list_users(Realm) of
[] ->
UserName = create_user(#user_data{realm = Realm}),
package(TargetDir, Meta, UserName);
[UserName] ->
ok = log(info, "Signing as user ~tp", [UserName]),
package(TargetDir, Meta, UserName);
UserNames ->
UserName = zx_tty:select_string(UserNames),
package(TargetDir, Meta, UserName)
end.
package(TargetDir, Meta, UserName) ->
{Realm, _, _} = maps:get(package_id, Meta), {Realm, _, _} = maps:get(package_id, Meta),
KeyDir = zx_lib:path(key, Realm), KeyDir = zx_lib:path(key, Realm),
ok = zx_lib:force_dir(KeyDir), ok = zx_lib:force_dir(KeyDir),
Pattern = KeyDir ++ "/*.key.der", Pattern = filename:join(KeyDir, UserName ++ ".*.key.der"),
case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of
[] -> [] ->
ok = log(info, "Need to generate key"), ok = log(info, "No private keys found. Need to generate a keypair."),
KeyID = zx_key:prompt_keygen(), KeyTag = zx_key:prompt_keygen(),
KeyName = UserName ++ "." ++ KeyTag,
KeyID = {Realm, KeyName},
ok = zx_key:generate_rsa(KeyID), ok = zx_key:generate_rsa(KeyID),
package(KeyID, TargetDir); package(KeyID, TargetDir);
[KeyName] -> [KeyName] ->
@ -662,13 +646,15 @@ package(TargetDir) ->
package(KeyID, TargetDir); package(KeyID, TargetDir);
KeyNames -> KeyNames ->
KeyName = zx_tty:select_string(KeyNames), KeyName = zx_tty:select_string(KeyNames),
package({Realm, KeyName}, TargetDir) package(TargetDir, Meta, UserName, {Realm, KeyName})
end. end.
-spec package(KeyID, TargetDir) -> no_return() -spec package(TargetDir, Meta, UserName, KeyID) -> zx:outcome()
when KeyID :: zx:key_id(), when TargetDir :: file:filename(),
TargetDir :: file:filename(). Meta :: zx:meta(),
UserName :: zx:user_name(),
KeyID :: zx:key_id().
%% @private %% @private
%% Accept a KeyPrefix for signing and a TargetDir containing a project to package and %% Accept a KeyPrefix for signing and a TargetDir containing a project to package and
%% build a zsp package file ready to be submitted to a repository. %% build a zsp package file ready to be submitted to a repository.
@ -678,33 +664,42 @@ package(KeyID, TargetDir) ->
PackageID = maps:get(package_id, Meta), PackageID = maps:get(package_id, Meta),
true = element(1, PackageID) == element(1, KeyID), true = element(1, PackageID) == element(1, KeyID),
{ok, PackageString} = zx_lib:package_string(PackageID), {ok, PackageString} = zx_lib:package_string(PackageID),
ZrpFile = PackageString ++ ".zsp", ZspFile = PackageString ++ ".zsp",
TgzFile = PackageString ++ ".tgz", case filelib:is_regular(ZspFile) of
ok = zx_lib:halt_if_exists(ZrpFile), true -> {error, "Package file already exists. Aborting", 17};
false -> package(KeyID, TargetDir, PackageID, ZspFile)
end.
package(KeyID, TargetDir, PackageID, ZspFile) ->
ok = remove_binaries(TargetDir), ok = remove_binaries(TargetDir),
CrashDump = filename:join(TargetDir, "erl_crash.dump"),
ok =
case filelib:is_regular(CrashDump) of
true -> file:delete(CrashDump);
false -> ok
end,
{ok, Everything} = file:list_dir(TargetDir), {ok, Everything} = file:list_dir(TargetDir),
DotFiles = filelib:wildcard(".*", TargetDir), DotFiles = filelib:wildcard(".*", TargetDir),
Ignores = ["lib" | DotFiles], Targets = lists:subtract(Everything, DotFiles),
Targets = lists:subtract(Everything, Ignores),
{ok, CWD} = file:get_cwd(), {ok, CWD} = file:get_cwd(),
ok = file:set_cwd(TargetDir), ok = file:set_cwd(TargetDir),
ok = zx_lib:build(), Grep = "grep -oP '^-module\\(\\K[^)]+' src/* | cut -d: -f2",
Modules = Modules = string:lexemes(os:cmd(Grep), "\n"),
[filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")], TarGzPath = filename:join(zx_lib:path(tmp), ZspFile ++ ".tgz"),
ok = remove_binaries("."), ok = erl_tar:create(TarGzPath, Targets, [compressed]),
ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]), {ok, TgzBin} = file:read_file(TarGzPath),
ok = file:set_cwd(CWD), ok = file:delete(TarGzPath),
MetaBin = term_to_binary({PackageID, element(2, KeyID), Modules}),
MetaSize = byte_size(MetaBin),
SignMe = <<MetaSize:16, MetaBin:MetaSize/binary, TgzBin/binary>>,
{ok, Key} = zx_key:load(private, KeyID), {ok, Key} = zx_key:load(private, KeyID),
{ok, TgzBin} = file:read_file(TgzFile), Sig = public_key:sign(SignMe, sha512, Key),
Sig = public_key:sign(TgzBin, sha512, Key), SigSize = byte_size(Sig),
Add = fun({K, V}, M) -> maps:put(K, V, M) end, ZspData = <<SigSize:24, Sig:SigSize/binary, SignMe/binary>>,
FinalMeta = lists:foldl(Add, Meta, [{modules, Modules}, {sig, {KeyID, Sig}}]), ok = file:set_cwd(CWD),
ok = file:write_file("zomp.meta", term_to_binary(FinalMeta)), ok = file:write_file(ZspFile, ZspData),
ok = erl_tar:create(ZrpFile, ["zomp.meta", TgzFile]), log(info, "Wrote archive ~ts", [ZspFile]).
ok = file:delete(TgzFile),
ok = file:delete("zomp.meta"),
ok = log(info, "Wrote archive ~ts", [ZrpFile]),
halt(0).
-spec remove_binaries(TargetDir) -> ok -spec remove_binaries(TargetDir) -> ok
@ -715,32 +710,18 @@ package(KeyID, TargetDir) ->
remove_binaries(TargetDir) -> remove_binaries(TargetDir) ->
Beams = filelib:wildcard("**/*.{beam,ez}", TargetDir), Beams = filelib:wildcard("**/*.{beam,ez}", TargetDir),
case [filename:join(TargetDir, Beam) || Beam <- Beams] of ToDelete = [filename:join(TargetDir, Beam) || Beam <- Beams],
[] -> lists:foreach(fun file:delete/1, ToDelete).
ok;
ToDelete ->
ok = log(info, "Removing: ~tp", [ToDelete]),
lists:foreach(fun file:delete/1, ToDelete)
end.
-spec create_plt() -> no_return(). -spec create_plt() -> ok.
%% @private
%% Generate a fresh PLT file that includes most basic core applications needed to
%% make a resonable estimate of a type system, write the name of the PLT to stdout,
%% and exit.
create_plt() ->
ok = build_plt(),
halt(0).
-spec build_plt() -> ok.
%% @private %% @private
%% Build a general plt file for Dialyzer based on the core Erland distro. %% Build a general plt file for Dialyzer based on the core Erland distro.
%% TODO: Make a per-package + dependencies version of this. %% TODO: Make a per-package + dependencies version of this.
%% TODO: PLT build options.
%% TODO: Meaningful failure messages.
build_plt() -> create_plt() ->
PLT = default_plt(), PLT = default_plt(),
Template = Template =
"dialyzer --build_plt" "dialyzer --build_plt"
@ -775,7 +756,7 @@ dialyze() ->
ok = ok =
case filelib:is_regular(PLT) of case filelib:is_regular(PLT) of
true -> log(info, "Using PLT: ~tp", [PLT]); true -> log(info, "Using PLT: ~tp", [PLT]);
false -> build_plt() false -> create_plt()
end, end,
Me = escript:script_name(), Me = escript:script_name(),
EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")), EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")),
@ -799,8 +780,43 @@ dialyze() ->
grow_a_pair() -> grow_a_pair() ->
ok = file:set_cwd(zx_lib:zomp_dir()), ok = file:set_cwd(zx_lib:zomp_dir()),
KeyID = zx_key:prompt_keygen(), case zx_lib:list_realms() of
zx_key:generate_rsa(KeyID). [] ->
{error, "No realms configured.", 61};
[Realm] ->
grow_a_pair(Realm);
Realms ->
Realm = zx_tty:select_string(Realms),
grow_a_pair(Realm)
end.
grow_a_pair(Realm) ->
Pattern = zx_lib:path(etc, Realm) ++ "*.user",
case [filename:basename(F, ".user") || F <- filelib:wildcard(Pattern)] of
[] ->
{ok, UserName} = create_user(#user_data{realm = Realm}),
grow_a_pair(UserName);
[UserName] ->
grow_a_pair(Realm, UserName);
UserNames ->
UserName = zx_tty:select_string(UserNames),
grow_a_pair(Realm, UserName)
end.
grow_a_pair(Realm, UserName) ->
KeyTag = zx_key:prompt_keygen(),
KeyName = UserName ++ "." ++ KeyTag,
case zx_key:generate_rsa({Realm, KeyName}) of
ok ->
ok;
{error, exists} ->
ok = io:format("That key name already exists. Try something different.~n"),
grow_a_pair(Realm, UserName);
Error ->
Error
end.
-spec drop_key(zx:key_id()) -> ok. -spec drop_key(zx:key_id()) -> ok.
@ -1043,13 +1059,14 @@ create_sysop(U = #user_data{username = UserName,
end. end.
-spec create_user() -> zx:outcome(). -spec create_user() -> ok.
create_user() -> create_user() ->
create_user(#user_data{}). UserName = create_user(#user_data{}),
log(info, "User ~ts created.", [UserName]).
-spec create_user(#user_data{}) -> zx:outcome(). -spec create_user(#user_data{}) -> zx:user_name().
create_user(U = #user_data{realm = none}) -> create_user(U = #user_data{realm = none}) ->
case pick_realm() of case pick_realm() of
@ -1078,11 +1095,17 @@ create_user(U = #user_data{realm = Realm,
"Press a number to select something to change, or [ENTER] to accept.~n", "Press a number to select something to change, or [ENTER] to accept.~n",
ok = io:format(Instructions, [Realm, UserName, RealName, Email]), ok = io:format(Instructions, [Realm, UserName, RealName, Email]),
case zx_tty:get_input() of case zx_tty:get_input() of
"1" -> create_user(U#user_data{realm = none}); "1" ->
"2" -> create_user(U#user_data{username = none}); create_user(U#user_data{realm = none});
"3" -> create_user(U#user_data{realname = none}); "2" ->
"4" -> create_user(U#user_data{contact_info = none}); create_user(U#user_data{username = none});
"" -> store_user(U); "3" ->
create_user(U#user_data{realname = none});
"4" ->
create_user(U#user_data{contact_info = none});
"" ->
ok = store_user(U),
{ok, UserName};
_ -> _ ->
ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), ok = io:format("~nArglebargle, glop-glyf!?!~n~n"),
create_user(U) create_user(U)
@ -1108,6 +1131,15 @@ store_user(#user_data{realm = Realm,
log(info, "User ~tp created.", [{Realm, UserName}]). log(info, "User ~tp created.", [{Realm, UserName}]).
-spec list_users(Realm) -> UserNames
when Realm :: zx:realm(),
UserNames :: [zx:user_name()].
list_users(Realm) ->
Pattern = filename:join(zx_lib:path(etc, Realm), "*.user"),
[filename:basename(UN, ".user") || UN <- filelib:wildcard(Pattern)].
-spec gen_keys(Realm, KeyNames) -> ok -spec gen_keys(Realm, KeyNames) -> ok
when Realm :: zx:realm(), when Realm :: zx:realm(),
KeyNames :: [zx:key_names()]. KeyNames :: [zx:key_names()].
@ -1229,16 +1261,17 @@ make_realm_dirs(Realm) ->
lists:foreach(Make, Dirs). lists:foreach(Make, Dirs).
-spec configure_zomp() -> ok. %% FIXME
%-spec configure_zomp() -> ok.
configure_zomp() -> %
ZompSettings = %configure_zomp() ->
[{node, 16}, % ZompSettings =
{vampire, 16}, % [{node, 16},
{leaf, 256}, % {vampire, 16},
{listen_port, 11311}, % {leaf, 256},
{public_port, 11311}], % {listen_port, 11311},
io:format("~tp~n", [ZompSettings]). % {public_port, 11311}],
% io:format("~tp~n", [ZompSettings]).
-spec create_realmfile(Realm, Dir) -> ok -spec create_realmfile(Realm, Dir) -> ok