diff --git a/zomp/etc/otpr/zxq9.user b/zomp/etc/otpr/zxq9.user deleted file mode 100644 index 58dfaf7..0000000 --- a/zomp/etc/otpr/zxq9.user +++ /dev/null @@ -1,5 +0,0 @@ -{realm,"otpr"}. -{username,"zxq9"}. -{realmname,"Craig Everett"}. -{contact_info,{"email","zxq9@zxq9.com"}}. -{keys,["zxq9-root"]}. diff --git a/zomp/lib/otpr/zx/0.1.0/ebin/zx.app b/zomp/lib/otpr/zx/0.1.0/ebin/zx.app index 23be553..3f56945 100644 --- a/zomp/lib/otpr/zx/0.1.0/ebin/zx.app +++ b/zomp/lib/otpr/zx/0.1.0/ebin/zx.app @@ -9,4 +9,4 @@ zx_conn, zx_lib, zx_net]}, - {mod, {zx, []}}]}. + {mod, {zx, none}}]}. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx.erl b/zomp/lib/otpr/zx/0.1.0/src/zx.erl index 5455d84..e4fc632 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx.erl @@ -64,7 +64,7 @@ type := app | lib}. -type outcome() :: ok - | {error, Reason :: atom()} + | {error, Reason :: term()} | {error, Code :: non_neg_integer()} | {error, Info :: string(), Code :: non_neg_integer()}. @@ -101,7 +101,8 @@ do(["list", "deps"]) -> do(["list", "deps", PackageString]) -> done(zx_local:list_deps(PackageString)); do(["install", PackageFile]) -> - done(zx_local:assimilate(PackageFile)); + ok = start(), + done(zx_daemon:install(PackageFile)); do(["set", "dep", PackageString]) -> done(zx_local:set_dep(PackageString)); do(["set", "version", VersionString]) -> @@ -229,7 +230,8 @@ compatibility_check(Platforms) -> %% @equiv application:ensure_started(zx). start() -> - application:ensure_started(zx). + ok = application:ensure_started(zx), + zx_daemon:init_connections(). -spec stop() -> ok | {error, Reason :: term()}. @@ -249,14 +251,14 @@ stop() -> -spec start(StartType, StartArgs) -> Result when StartType :: normal, - StartArgs :: [], + StartArgs :: none, Result :: {ok, pid()}. %% @private %% Application callback. Not to be called directly. -start(normal, []) -> +start(normal, none) -> ok = application:ensure_started(inets), - zx_daemon:start_link(). + zx_sup:start_link(). -spec stop(term()) -> ok. @@ -331,7 +333,7 @@ run(Identifier, RunArgs) -> end, {ok, PackageID} = ensure_installed(FuzzyID), ok = build(PackageID), - Dir = zx_lib:path(lib, PackageID), + Dir = zx_lib:ppath(lib, PackageID), {ok, Meta} = zx_lib:read_project_meta(Dir), prepare(PackageID, Meta, Dir, RunArgs). @@ -371,36 +373,34 @@ prepare(PackageID, Meta, Dir, RunArgs) -> Deps = maps:get(deps, Meta), NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end, 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 build/1, Needed), - execute(Type, PackageID, Meta, Dir, RunArgs); - {error, Errors} -> - error_exit("Failed package fetches: ~tp", [Errors], ?LINE) + ok = lists:foreach(fun install/1, Needed), + ok = lists:foreach(fun build/1, Needed), + execute(Type, PackageID, Meta, Dir, RunArgs). + + +-spec install(PackageString :: string()) -> zx:outcome(). +%% @private +%% Installs a package from upstream. + +install(PackageString) -> + {ok, ID} = zx_daemon:install(PackageString), + install(PackageString, ID). + + +install(PackageString, ID) -> + receive + {z_result, ID, done} -> + ok; + {z_result, ID, {hops, Count}} -> + ok = log(info, "~ts ~w hops away.", [PackageString, Count]), + install(PackageString, ID); + {z_result, ID, {error, Reason}} -> + {error, Reason, 1} + after 60000 -> + {error, timeout, 62} end. -await_fetches([]) -> ok; -await_fetches(Pending) -> await_fetches(Pending, []). - - -await_fetches([], []) -> - ok; -await_fetches([], Errors) -> - {error, Errors}; -await_fetches(Pending, Errors) -> - {NewPending, NewErrors} = - receive - {z_reply, ID, ok} -> - {lists:delete(ID, Pending), Errors}; - {z_reply, ID, {error, Package, Reason}} -> - {lists:delete(ID, Pending), [{Package, Reason} | Errors]} - end, - await_fetches(NewPending, NewErrors). - - -spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return() when Type :: app | lib, PackageID :: package_id(), @@ -516,7 +516,7 @@ resolve_installed_version({Realm, Name, Version}) -> resolve_installed_version(PackageDir, Version) -> 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). @@ -527,58 +527,26 @@ tuplize(String, Acc) -> 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. %% @private %% Given an AppID, build the project from source and add it to the current lib path. build(PackageID) -> {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(), file:set_cwd(CWD). --spec ensure_package_dirs(package_id()) -> ok. -%% @private -%% 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]], - lists:foreach(fun zx_lib:force_dir/1, Dirs). +%% FIXME +%-spec ensure_package_dirs(package_id()) -> ok. +%%% @private +%%% 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: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 add key Realm KeyName~n" " zx get key Realm KeyName~n" -" zx rem key Realm KeyName~n" " zx create user~n" " zx create userfiles Realm UserName~n" -" zx create keypair Realm~n" +" zx create keypair~n" " zx export user UserID~n" " zx import user ZdufFile~n" "~n" @@ -658,7 +625,6 @@ usage() -> " zx accept PackageID~n" " zx create realm~n" " zx create realmfile Realm~n" -" zx create sysop~n" "~n" "Where~n" " PackageID :: A string of the form Realm-Name[-Version]~n" diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl index 767de5c..1932681 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl @@ -91,26 +91,25 @@ list_resigns(Realm) -> end. --spec submit(PackageFile) -> no_return() - when PackageFile :: file:filename(). +-spec submit(ZspPath :: file:filename()) -> zx:outcome(). %% @private %% Submit a package to the appropriate "prime" server for the given realm. -submit(PackageFile) -> - Files = zx_lib:extract_zsp_or_die(PackageFile), - {ok, PackageData} = file:read_file(PackageFile), - {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), - Meta = binary_to_term(MetaBin), - {Realm, Package, Version} = maps:get(package_id, Meta), - {ok, Socket} = connect_auth(Realm), - ok = send(Socket, {submit, {Realm, Package, Version}}), +submit(ZspPath) -> + {ok, ZspBin} = file:read_file(ZspPath), + <> = ZspBin, + <> = Signed, + {ok, {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, Socket} = connect_auth(element(1, PackageID)), + ok = send(Socket, {submit, PackageID}), ok = recv_or_die(Socket), - ok = gen_tcp:send(Socket, PackageData), - ok = log(info, "Done sending contents of ~tp", [PackageFile]), + ok = gen_tcp:send(Socket, ZspBin), + ok = log(info, "Done sending contents of ~tp", [ZspPath]), Outcome = recv_or_die(Socket), log(info, "Response: ~tp", [Outcome]), - ok = disconnect(Socket), - halt(0). + disconnect(Socket). review(PackageString) -> diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_conn_sup.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_conn_sup.erl index 31e3572..f42ab01 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_conn_sup.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_conn_sup.erl @@ -10,7 +10,7 @@ -copyright("Craig Everett "). -license("GPL-3.0"). --export([start_conn/2]). +-export([start_conn/1]). -export([start_link/0]). -export([init/1]). @@ -18,9 +18,8 @@ %%% Interface Functions --spec start_conn(Host, Serial) -> Result +-spec start_conn(Host) -> Result when Host :: zx:host(), - Serial :: zx:serial(), Result :: {ok, pid()} | {error, Reason}, Reason :: term(). @@ -28,8 +27,8 @@ %% Start an upstream connection handler. %% (Should only be called from zx_conn). -start_conn(Host, Serial) -> - supervisor:start_child(?MODULE, [Host, Serial]). +start_conn(Host) -> + supervisor:start_child(?MODULE, [Host]). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl index 8428ef0..6963b74 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl @@ -147,10 +147,10 @@ -export([pass_meta/3, subscribe/1, unsubscribe/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]). -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, code_change/3, terminate/2]). @@ -188,14 +188,12 @@ -record(rmeta, - {revision = 0 :: non_neg_integer(), - serial = 0 :: non_neg_integer(), + {serial = 0 :: non_neg_integer(), prime = {"zomp.tsuriai.jp", 11311} :: zx:host(), private = [] :: [zx:host()], mirrors = queue:new() :: queue:queue(zx:host()), - realm_keys = [] :: [zx:key_meta()], - package_keys = [] :: [zx:key_meta()], - sysops = [] :: [zx:sysop_meta()], + key = [] :: zx:key_name(), + sysop = none :: zx:user_name(), assigned = none :: none | pid(), available = [] :: [pid()]}). @@ -449,25 +447,6 @@ 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} when KeyID :: zx:key_id(), RequestID :: id(). @@ -485,6 +464,28 @@ 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} when Package :: zx:package(), RequestID :: id(). @@ -557,7 +558,7 @@ sysops(Realm) -> %% Private function to wrap the necessary bits up. 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. init(none) -> - Blank = blank_state(), - {ok, MX, CX} = init_connections(), - State = Blank#s{mx = MX, cx = CX}, - {ok, State}. + {ok, #s{}}. --spec blank_state() -> state(). -%% @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. +-spec init_connections() -> ok. init_connections() -> - CX = cx_load(), - 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}. + gen_server:cast(?MODULE, init_connections). @@ -689,12 +640,24 @@ stop() -> handle_call({request, list}, _, State = #s{cx = CX}) -> Realms = cx_realms(CX), {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, _ = gen_server:reply(From, {ok, NewID}), + Requestor = element(1, From), NextState = do_request(Requestor, Action, State#s{id = NewID}), NewState = eval_queue(NextState), {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) -> ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]), {noreply, State}. @@ -726,6 +689,9 @@ handle_cast({notify, Conn, Package, Update}, State) -> ok = do_notify(Conn, Package, Update, State), NewState = eval_queue(State), {noreply, NewState}; +handle_cast(init_connections, State) -> + NewState = init_connections(State), + {noreply, NewState}; handle_cast(stop, State) -> {stop, normal, State}; handle_cast(Unexpected, State) -> @@ -913,6 +879,49 @@ dequeue(Pending) -> 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} when Realms :: [zx:realm()], MX :: monitor_index(), @@ -968,11 +977,14 @@ reassign_conns([], CX, Unassigned) -> do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX}) -> {NewDropped, NewRequests, NewMX} = 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 ! {z_result, ID, Result}, NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX), - {Dropped, NextRequests, NextMX}; + {Dropped, Rest, NextMX}; error -> NextDropped = handle_orphan_result(ID, Result, Dropped), {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}. +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 when ID :: id(), Result :: result(), @@ -1208,6 +1238,112 @@ drop_requests(ReqIDs, Dropped, Requests) -> 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 = <>) -> + <> = 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 @@ -1366,36 +1502,30 @@ cx_load() -> %% where any number of wild things might be going on in the user's filesystem). cx_populate() -> - Pattern = filename:join([zx_lib:path(etc), "*", "realm.conf"]), - case filelib:wildcard(Pattern) of - [] -> {error, no_realms}; - RealmFiles -> {ok, cx_populate(RealmFiles, [])} - end. + Realms = zx_lib:list_realms(), + CX = lists:foldl(fun cx_populate/2, [], Realms), + {ok, CX}. --spec cx_populate(RealmFiles, Realms) -> NewRealms - when RealmFiles :: file:filename(), - Realms :: [{zx:realm(), realm_meta()}], - NewRealms :: [{zx:realm(), realm_meta()}]. +-spec cx_populate(Realms, CX) -> NewCX + when Realms :: [zx:realm()], + CX :: [{zx:realm(), realm_meta()}], + NewCX :: [{zx:realm(), realm_meta()}]. %% @private %% 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 %% any loading errors. -cx_populate([File | Files], Realms) -> - NewRealms = - case file:consult(File) of - {ok, Meta} -> - Realm = cx_load_realm_meta(Meta), - [Realm | Realms]; - {error, Reason} -> - Message = "Loading realm file ~tp failed with: ~tp. Skipping...", - ok = log(warning, Message, [File, Reason]), - Realms - end, - cx_populate(Files, NewRealms); -cx_populate([], Realms) -> - Realms. +cx_populate(Realm, CX) -> + case zx_lib:load_realm_conf(Realm) of + {ok, Meta} -> + Record = cx_load_realm_meta(Meta), + [{Realm, Record} | CX]; + {error, Reason} -> + Message = "Loading realm ~tp failed with: ~tp. Skipping...", + ok = log(warning, Message, [Realm, Reason]), + CX + end. -spec cx_load_realm_meta(Meta) -> Result @@ -1405,20 +1535,12 @@ cx_populate([], Realms) -> %% This function MUST adhere to the realmfile definition found at. cx_load_realm_meta(Meta) -> - {realm, Realm} = lists:keyfind(realm, 1, 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), + Realm = maps:get(realm, Meta), Basic = - #rmeta{revision = Revision, - prime = Prime, - realm_keys = RealmKeys, - package_keys = PackageKeys, - sysops = Sysops}, - Complete = cx_load_cache(Realm, Basic), - {Realm, Complete}. + #rmeta{prime = maps:get(prime, Meta), + sysop = maps:get(sysop, Meta), + key = maps:get(key, Meta)}, + cx_load_cache(Realm, Basic). -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}}; {empty, Mirrors} -> 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}} end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl index 8eac4f2..a3cb119 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl @@ -21,7 +21,7 @@ %%% Functions --spec ensure_keypair(zx:key_id()) -> true | no_return(). +-spec ensure_keypair(zx:key_id()) -> zx:outcome(). %% @private %% Check if both the public and private key based on KeyID exists. @@ -30,17 +30,17 @@ ensure_keypair(KeyID = {Realm, KeyName}) -> {true, true} -> true; {false, true} -> - Message = "Public key ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1); + Format = "Public key ~tp/~tp cannot be found", + Message = io_lib:format(Format, [Realm, KeyName]), + {error, Message, 2}; {true, false} -> - Message = "Private key ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1); + Format = "Private key ~tp/~tp cannot be found", + Message = io_lib:format(Format, [Realm, KeyName]), + {error, Message, 2}; {false, false} -> - Message = "Key pair ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1) + Format = "Key pair ~tp/~tp cannot be found", + Message = io_lib:format(Format, [Realm, KeyName]), + {error, Message, 2} end. @@ -76,83 +76,93 @@ path(private, {Realm, KeyName}) -> prompt_keygen() -> Message = - "~n Enter a name for your new keys.~n~n" - " Valid names must start with a lower-case letter, and can include~n" - " only lower-case letters, numbers, and periods, but no series of~n" - " consecutive periods. (That is: [a-z0-9\\.])~n~n" - " To designate the key as realm-specific, enter the realm name and~n" - " key name separated by a space.~n~n" - " Example: some.realm my.key~n", + "~nKEY NAME~n" + "Enter a name for your new key pair.~n" + "Valid names must start with a lower-case letter, and can include " + "only lower-case letters, numbers, and underscores, but no series of " + "consecutive underscores. (That is: [a-z0-9_])~n" + " Example: my_key~n", ok = io:format(Message), Input = zx_tty:get_input(), - {Realm, KeyName} = - case string:lexemes(Input, " ") of - [R, K] -> {R, K}; - [K] -> {"otpr", K} - end, - 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"), + case zx_lib:valid_lower0_9(Input) of + true -> + Input; + false -> + ok = io:format("Bad key name ~tp. Try again.~n", [Input]), prompt_keygen() end. -spec generate_rsa(KeyID) -> Result - when KeyID :: zx:key_id(), - Result :: ok - | {error, keygen_fail}. + when KeyID :: zx:key_id(), + Result :: ok + | {error, Reason}, + Reason :: keygen_fail + | exists. %% @private %% Generate an RSA keypair and write them in der format to the current directory, using %% filenames derived from Prefix. %% NOTE: The current version of this command is likely to only work on a unix system. 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), 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 = gen_p_key(KeyFile), - ok = der_to_pem(KeyFile, PemFile), - {ok, PemBin} = file:read_file(PemFile), - [PemData] = public_key:pem_decode(PemBin), - Pub = public_key:pem_entry_decode(PemData), - PubDer = public_key:der_encode('RSAPublicKey', Pub), - ok = file:write_file(PubFile, PubDer), - case check_key(KeyFile, PubFile) of - true -> - ok = file:delete(PemFile), - log(info, "~ts and ~ts agree", [KeyFile, PubFile]); - false -> - ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]), - ok = log(error, "Something has gone wrong."), + case gen_p_key(KeyFile) of + ok -> + ok = der_to_pem(KeyFile, PemFile), + {ok, PemBin} = file:read_file(PemFile), + [PemData] = public_key:pem_decode(PemBin), + Pub = public_key:pem_entry_decode(PemData), + PubDer = public_key:der_encode('RSAPublicKey', Pub), + ok = file:write_file(PubFile, PubDer), + case check_key(KeyFile, PubFile) of + true -> + ok = file:delete(PemFile), + log(info, "~ts and ~ts agree", [KeyFile, PubFile]); + false -> + ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]), + ok = log(error, "Something has gone wrong."), + {error, keygen_fail} + end; + {error, no_ssl} -> + ok = log(error, "OpenSSL not found."), {error, keygen_fail} end. --spec gen_p_key(KeyFile) -> ok - when KeyFile :: file:filename(). +-spec gen_p_key(KeyFile) -> Result + when KeyFile :: file:filename(), + Result :: ok + | {error, no_ssl}. %% @private %% Format an openssl shell command that will generate proper 16k RSA keys. gen_p_key(KeyFile) -> - Command = - io_lib:format("~ts genpkey" - " -algorithm rsa" - " -out ~ts" - " -outform DER" - " -pkeyopt rsa_keygen_bits:16384", - [openssl(), KeyFile]), - Out = os:cmd(Command), - io:format(Out). + case openssl() of + {ok, OpenSSL} -> + Command = + io_lib:format("~ts genpkey" + " -algorithm rsa" + " -out ~ts" + " -outform DER" + " -pkeyopt rsa_keygen_bits:16384", + [OpenSSL, KeyFile]), + Out = os:cmd(Command), + io:format(Out); + Error -> + Error + end. -spec der_to_pem(KeyFile, PemFile) -> ok @@ -167,16 +177,21 @@ gen_p_key(KeyFile) -> %% way. der_to_pem(KeyFile, PemFile) -> - Command = - io_lib:format("~ts rsa" - " -inform DER" - " -in ~ts" - " -outform PEM" - " -pubout" - " -out ~ts", - [openssl(), KeyFile, PemFile]), - Out = os:cmd(Command), - io:format(Out). + case openssl() of + {ok, OpenSSL} -> + Command = + io_lib:format("~ts rsa" + " -inform DER" + " -in ~ts" + " -outform PEM" + " -pubout" + " -out ~ts", + [OpenSSL, KeyFile, PemFile]), + Out = os:cmd(Command), + io:format(Out); + Error -> + Error + end. -spec check_key(KeyFile, PubFile) -> Result @@ -196,11 +211,14 @@ check_key(KeyFile, PubFile) -> public_key:verify(TestMessage, sha512, Signature, Pub). --spec openssl() -> Executable | no_return() - when Executable :: file:filename(). +-spec openssl() -> Result + when Result :: {ok, Executable} + | {error, no_ssl}, + Executable :: file:filename(). %% @private %% 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 = @@ -208,16 +226,15 @@ openssl() -> {unix, _} -> "openssl"; {win32, _} -> "openssl.exe" end, - ok = - case os:find_executable(OpenSSL) of - false -> - ok = log(error, "OpenSSL could not be found in this system's PATH."), - ok = log(error, "Install OpenSSL and then retry."), - error_exit("Missing system dependenct: OpenSSL", ?LINE); - Path -> - log(info, "OpenSSL executable found at: ~ts", [Path]) - end, - OpenSSL. + case os:find_executable(OpenSSL) of + false -> + ok = log(error, "OpenSSL could not be found in this system's PATH."), + ok = log(error, "Install OpenSSL and then retry."), + {error, no_ssl}; + Path -> + log(info, "OpenSSL executable found at: ~ts", [Path]), + {ok, OpenSSL} + end. -spec load(Type, KeyID) -> Result @@ -242,43 +259,13 @@ load(Type, KeyID) -> end. --spec verify(Data, Signature, PubKey) -> ok | no_return() +-spec verify(Data, Signature, PubKey) -> boolean() when Data :: binary(), Signature :: binary(), PubKey :: public_key:rsa_public_key(). %% @private -%% Verify the RSA Signature of some Data against the given PubKey or halt execution. -%% This function always assumes sha512 is the algorithm being used. -%% Should only ever be called by the initial launch process. +%% Curry out the choice of algorithm. This will probably disappear in a few more +%% versions as the details of sha512 and RSA gradually give way to the Brave New World. verify(Data, Signature, PubKey) -> - case public_key:verify(Data, sha512, Signature, PubKey) of - 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). + public_key:verify(Data, sha512, Signature, PubKey). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl index a394168..c2b5651 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl @@ -25,11 +25,9 @@ valid_lower0_9/1, valid_label/1, valid_version/1, string_to_version/1, version_to_string/1, package_id/1, package_string/1, - namify_zsp/1, namify_tgz/1, - zsp_path/1, + namify_zsp/1, zsp_path/1, find_latest_compatible/2, installed/1, realm_conf/1, load_realm_conf/1, - extract_zsp_or_die/1, halt_if_exists/1, build/0, rm_rf/1, rm/1, b_to_t/1, b_to_ts/1]). @@ -582,37 +580,17 @@ package_string(_) -> when PackageID :: zx:package_id(), ZrpFileName :: file:filename(). %% @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"). - - --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) -> +namify_zsp(PackageID) -> {ok, PackageString} = package_string(PackageID), - PackageString ++ "." ++ Suffix. + PackageString ++ ".zsp". -spec zsp_path(zx:package_id()) -> file:filename(). -zsp_path(PackageID) -> - filename:join(path(zsp, element(1, PackageID)), namify_zsp(PackageID)). +zsp_path(PackageID = {Realm, _, _}) -> + filename:join(path(zsp, Realm), namify_zsp(PackageID)). -spec find_latest_compatible(Version, Versions) -> Result @@ -683,7 +661,7 @@ realm_conf(Realm) -> | file:posix() | {Line :: integer(), Mod :: module(), Cause :: term()}. %% @private -%% Load the config for the given realm or halt with an error. +%% Load the config for the given realm. load_realm_conf(Realm) -> Path = realm_conf(Realm), @@ -696,41 +674,6 @@ load_realm_conf(Realm) -> 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. %% @private %% 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()}. %% @private %% Recursively remove files and directories. Equivalent to `rm -rf'. +%% Does not return an error on a nonexistant path. rm_rf(Path) -> case filelib:is_dir(Path) of @@ -764,7 +708,10 @@ rm_rf(Path) -> ok = lists:foreach(fun rm/1, Contents), file:del_dir(Path); false -> - file:delete(Path) + case filelib:is_regular(Path) of + true -> file:delete(Path); + false -> ok + end end. @@ -803,18 +750,3 @@ b_to_ts(Binary) -> catch error:badarg -> error 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). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl index fbff670..45fbc88 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl @@ -10,7 +10,7 @@ -copyright("Craig Everett "). -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, set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1, add_realm/1, drop_realm/1, @@ -73,14 +73,14 @@ initialize3(Type, PackageID) -> case package_exists(PackageID) of false -> Prefix = ask_prefix(), - initialize(Type, PackageID, Prefix); + initialize4(Type, PackageID, Prefix); true -> Message = "Package already exists. Try another.", {error, Message, 17} end. -initialize(app, PackageID, Prefix) -> +initialize4(app, PackageID, Prefix) -> Instructions = "The OTP application controller has to know what module to call start/2 on to " "start your program.~n" @@ -93,23 +93,23 @@ initialize(app, PackageID, Prefix) -> ok = io:format(Instructions), case zx_tty:get_input() of "" -> - initialize(lib, PackageID, Prefix, none); + initialize5(lib, PackageID, Prefix, none); String -> case zx_lib:valid_lower0_9(String) of true -> AppStart = {list_to_atom(String), []}, - initialize(app, PackageID, Prefix, AppStart); + initialize5(app, PackageID, Prefix, AppStart); false -> Message = "The name \"~ts\" doesn't seem valid. Try \"[a-z_]*\".~n", ok = io:format(Message, String), - initialize(app, PackageID, Prefix) + initialize4(app, PackageID, Prefix) end end; -initialize(lib, PackageID, Prefix) -> - initialize(lib, PackageID, Prefix, none). +initialize4(lib, PackageID, Prefix) -> + initialize5(lib, PackageID, Prefix, none). -initialize(Type, PackageID, Prefix, AppStart) -> +initialize5(Type, PackageID, Prefix, AppStart) -> ok = update_source_vsn(element(3, PackageID)), ok = initialize_app_file(PackageID, AppStart), MetaList = @@ -268,39 +268,6 @@ initialize_app_file({_, Name, Version}, AppStart) -> 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() when VersionString :: string(). %% @private @@ -483,11 +450,11 @@ set_dep2(PackageID) -> Deps = maps:get(deps, Meta), case lists:member(PackageID, Deps) of true -> ok; - false -> set_dep(PackageID, Deps, Meta) + false -> set_dep3(PackageID, Deps, Meta) end. --spec set_dep(PackageID, Deps, Meta) -> ok +-spec set_dep3(PackageID, Deps, Meta) -> ok when PackageID :: zx:package_id(), Deps :: [zx:package_id()], Meta :: [term()]. @@ -497,7 +464,7 @@ set_dep2(PackageID) -> %% such a dependency is not already present. Then write the project meta back to its %% 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, NewDeps = case lists:partition(ExistingPackageIDs, Deps) of @@ -642,18 +609,35 @@ version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> %% @private %% Turn a target project directory into a package, prompting the user for appropriate %% 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) -> ok = log(info, "Packaging ~ts", [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), KeyDir = zx_lib:path(key, Realm), 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 [] -> - ok = log(info, "Need to generate key"), - KeyID = zx_key:prompt_keygen(), + ok = log(info, "No private keys found. Need to generate a keypair."), + KeyTag = zx_key:prompt_keygen(), + KeyName = UserName ++ "." ++ KeyTag, + KeyID = {Realm, KeyName}, ok = zx_key:generate_rsa(KeyID), package(KeyID, TargetDir); [KeyName] -> @@ -662,13 +646,15 @@ package(TargetDir) -> package(KeyID, TargetDir); KeyNames -> KeyName = zx_tty:select_string(KeyNames), - package({Realm, KeyName}, TargetDir) + package(TargetDir, Meta, UserName, {Realm, KeyName}) end. --spec package(KeyID, TargetDir) -> no_return() - when KeyID :: zx:key_id(), - TargetDir :: file:filename(). +-spec package(TargetDir, Meta, UserName, KeyID) -> zx:outcome() + when TargetDir :: file:filename(), + Meta :: zx:meta(), + UserName :: zx:user_name(), + KeyID :: zx:key_id(). %% @private %% 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. @@ -678,33 +664,42 @@ package(KeyID, TargetDir) -> PackageID = maps:get(package_id, Meta), true = element(1, PackageID) == element(1, KeyID), {ok, PackageString} = zx_lib:package_string(PackageID), - ZrpFile = PackageString ++ ".zsp", - TgzFile = PackageString ++ ".tgz", - ok = zx_lib:halt_if_exists(ZrpFile), + ZspFile = PackageString ++ ".zsp", + case filelib:is_regular(ZspFile) of + true -> {error, "Package file already exists. Aborting", 17}; + false -> package(KeyID, TargetDir, PackageID, ZspFile) + end. + + +package(KeyID, TargetDir, PackageID, ZspFile) -> 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), DotFiles = filelib:wildcard(".*", TargetDir), - Ignores = ["lib" | DotFiles], - Targets = lists:subtract(Everything, Ignores), + Targets = lists:subtract(Everything, DotFiles), {ok, CWD} = file:get_cwd(), ok = file:set_cwd(TargetDir), - ok = zx_lib:build(), - Modules = - [filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")], - ok = remove_binaries("."), - ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]), - ok = file:set_cwd(CWD), + Grep = "grep -oP '^-module\\(\\K[^)]+' src/* | cut -d: -f2", + Modules = string:lexemes(os:cmd(Grep), "\n"), + TarGzPath = filename:join(zx_lib:path(tmp), ZspFile ++ ".tgz"), + ok = erl_tar:create(TarGzPath, Targets, [compressed]), + {ok, TgzBin} = file:read_file(TarGzPath), + ok = file:delete(TarGzPath), + MetaBin = term_to_binary({PackageID, element(2, KeyID), Modules}), + MetaSize = byte_size(MetaBin), + SignMe = <>, {ok, Key} = zx_key:load(private, KeyID), - {ok, TgzBin} = file:read_file(TgzFile), - Sig = public_key:sign(TgzBin, sha512, Key), - Add = fun({K, V}, M) -> maps:put(K, V, M) end, - FinalMeta = lists:foldl(Add, Meta, [{modules, Modules}, {sig, {KeyID, Sig}}]), - ok = file:write_file("zomp.meta", term_to_binary(FinalMeta)), - ok = erl_tar:create(ZrpFile, ["zomp.meta", TgzFile]), - ok = file:delete(TgzFile), - ok = file:delete("zomp.meta"), - ok = log(info, "Wrote archive ~ts", [ZrpFile]), - halt(0). + Sig = public_key:sign(SignMe, sha512, Key), + SigSize = byte_size(Sig), + ZspData = <>, + ok = file:set_cwd(CWD), + ok = file:write_file(ZspFile, ZspData), + log(info, "Wrote archive ~ts", [ZspFile]). -spec remove_binaries(TargetDir) -> ok @@ -715,32 +710,18 @@ package(KeyID, TargetDir) -> remove_binaries(TargetDir) -> Beams = filelib:wildcard("**/*.{beam,ez}", TargetDir), - case [filename:join(TargetDir, Beam) || Beam <- Beams] of - [] -> - ok; - ToDelete -> - ok = log(info, "Removing: ~tp", [ToDelete]), - lists:foreach(fun file:delete/1, ToDelete) - end. + ToDelete = [filename:join(TargetDir, Beam) || Beam <- Beams], + lists:foreach(fun file:delete/1, ToDelete). --spec create_plt() -> no_return(). -%% @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. +-spec create_plt() -> ok. %% @private %% Build a general plt file for Dialyzer based on the core Erland distro. %% TODO: Make a per-package + dependencies version of this. +%% TODO: PLT build options. +%% TODO: Meaningful failure messages. -build_plt() -> +create_plt() -> PLT = default_plt(), Template = "dialyzer --build_plt" @@ -775,7 +756,7 @@ dialyze() -> ok = case filelib:is_regular(PLT) of true -> log(info, "Using PLT: ~tp", [PLT]); - false -> build_plt() + false -> create_plt() end, Me = escript:script_name(), EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")), @@ -799,8 +780,43 @@ dialyze() -> grow_a_pair() -> ok = file:set_cwd(zx_lib:zomp_dir()), - KeyID = zx_key:prompt_keygen(), - zx_key:generate_rsa(KeyID). + case zx_lib:list_realms() of + [] -> + {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. @@ -1043,13 +1059,14 @@ create_sysop(U = #user_data{username = UserName, end. --spec create_user() -> zx:outcome(). +-spec create_user() -> ok. 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}) -> 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", ok = io:format(Instructions, [Realm, UserName, RealName, Email]), case zx_tty:get_input() of - "1" -> create_user(U#user_data{realm = none}); - "2" -> create_user(U#user_data{username = none}); - "3" -> create_user(U#user_data{realname = none}); - "4" -> create_user(U#user_data{contact_info = none}); - "" -> store_user(U); + "1" -> + create_user(U#user_data{realm = none}); + "2" -> + create_user(U#user_data{username = none}); + "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"), create_user(U) @@ -1108,6 +1131,15 @@ store_user(#user_data{realm = Realm, 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 when Realm :: zx:realm(), KeyNames :: [zx:key_names()]. @@ -1229,16 +1261,17 @@ make_realm_dirs(Realm) -> lists:foreach(Make, Dirs). --spec configure_zomp() -> ok. - -configure_zomp() -> - ZompSettings = - [{node, 16}, - {vampire, 16}, - {leaf, 256}, - {listen_port, 11311}, - {public_port, 11311}], - io:format("~tp~n", [ZompSettings]). +%% FIXME +%-spec configure_zomp() -> ok. +% +%configure_zomp() -> +% ZompSettings = +% [{node, 16}, +% {vampire, 16}, +% {leaf, 256}, +% {listen_port, 11311}, +% {public_port, 11311}], +% io:format("~tp~n", [ZompSettings]). -spec create_realmfile(Realm, Dir) -> ok