Merge branch 'dev' into 'master'

Half un-borked

See merge request zxq9/zx!13
This commit is contained in:
Craig Everett 2018-08-03 14:55:33 +00:00
commit 0bf9620eb3
25 changed files with 2988 additions and 1459 deletions

View File

@ -1,5 +1,9 @@
#!/bin/bash
# This script prepares a general test environment.
# If it is invoked as $(path/to/script) the first time it is run the necessary
# environment variable will be exported.
pushd $(dirname $BASH_SOURCE) > /dev/null
ZX_DEV_ROOT=$PWD
popd > /dev/null
@ -7,4 +11,4 @@ ZOMP_DIR="$ZX_DEV_ROOT/tester"
rm -rf "$ZOMP_DIR"
cp -r "$ZX_DEV_ROOT/zomp" "$ZOMP_DIR"
echo "Done. Make sure to export \"export ZOMP_DIR=$ZOMP_DIR\" before running zx"
echo "export ZOMP_DIR=$ZOMP_DIR"

View File

@ -1,4 +1,4 @@
{realm, "otpr"}.
{prime, {"otpr.psychobitch.party",11311}}.
{prime, {"zomp.psychobitch.party",11311}}.
{sysop, "zxq9"}.
{key, "zxq9-root"}.

View File

@ -1,12 +1,7 @@
{application, zx,
[{description, "Zomp client program"},
{vsn, "0.1.0"},
{applications, [stdlib, kernel]},
{modules, [zx,
zx_sup,
zx_daemon,
zx_conn_sup,
zx_conn,
zx_lib,
zx_net]},
{mod, {zx, none}}]}.
{application,zx,
[{description,"Zomp client program"},
{vsn,"0.1.0"},
{applications,[stdlib,kernel]},
{modules,[zx,zx_auth,zx_conn,zx_conn_sup,zx_daemon,zx_key,
zx_lib,zx_local,zx_net,zx_sup,zx_sys_conf,zx_tty]},
{mod,{zx,none}}]}.

View File

@ -31,6 +31,6 @@ log(Level, Format, Args) ->
warning -> "[WARNING]";
error -> "[ERROR]"
end,
Out = io_lib:format("~s ~p: " ++ Format ++ "~n", [Tag, self() | Args]),
Out = io_lib:format("~s ~w ~w: " ++ Format ++ "~n", [Tag, ?MODULE, self() | Args]),
UTF8 = unicode:characters_to_binary(Out),
io:format(UTF8).

View File

@ -4,9 +4,14 @@
main(Args) ->
true = code:add_patha("ebin"),
up_to_date = make:all(),
ok = lists:foreach(fun dispatch/1, Args),
halt(0).
case make:all() of
up_to_date ->
lists:foreach(fun dispatch/1, Args),
halt(0);
error ->
ok = io:format("Build error. Halting.~n"),
halt(1)
end.
dispatch("edoc") ->

View File

@ -1,15 +1,26 @@
%%% @doc
%%% ZX
%%% ZX: A suite of tools for Erlang development and deployment.
%%%
%%% A general dependency and packaging tool that works together with the zomp
%%% package manager. Given a project directory with a standard layout, zx can:
%%% - Initialize your project for packaging and semver tracking under zomp.
%%% - Add dependencies (recursively) defined in any zomp repository realm.
%%% - Update dependencies (recursively) defined in any zomp repository realm.
%%% - Remove dependencies.
%%% - Update, upgrade or run any application from source that zomp tracks.
%%% ZX can:
%%% - Create project templates for applications, libraries and escripts.
%%% - Initialize existing projects for packaging and management.
%%% - Create and manage zomp realms, users, keys, etc.
%%% - Manage dependencies hosted in any zomp realm.
%%% - Package, submit, pull-for-review, and resign-to-accept packages.
%%% - Update, upgrade, and run any application from source that zomp tracks.
%%% - Locally install packages from files and locally stored public keys.
%%% - Build and run a local project from source using zomp dependencies.
%%% - Start an anonymous zomp distribution node.
%%% - Act as a unified code launcher for any projects (Erlang + ZX = deployed).
%%%
%%% ZX is currently limited in one specific way:
%%% - Can only launch pure Erlang code.
%%%
%%% In the works:
%%% - Support for LFE
%%% - Support for Rust (cross-platform)
%%% - Support for Elixir (as a peer language)
%%% - Unified Windows installer to deploy Erlang, Rust, LFE, Elixir and ZX
%%% @end
-module(zx).
@ -30,7 +41,7 @@
key_id/0, key_name/0,
user_id/0, user_name/0, contact_info/0, user_data/0,
lower0_9/0, label/0,
package_meta/0,
package_meta/0, ss_tag/0,
outcome/0]).
-include("zx_logger.hrl").
@ -48,20 +59,24 @@
Minor :: non_neg_integer() | z,
Patch :: non_neg_integer() | z}.
-type host() :: {string() | inet:ip_address(), inet:port_number()}.
-type key_data() :: {Name :: key_name(),
Public :: none | {SHA512 :: binary(), DER :: binary()},
Private :: none | {SHA512 :: binary(), DER :: binary()}}.
-type key_id() :: {realm(), key_name()}.
-type key_name() :: lower0_9().
-type user_id() :: {realm(), user_name()}.
-type user_name() :: label().
-type contact_info() :: {Type :: string(), Data :: string()}.
-type user_data() :: {ID :: user_id(),
RealName :: string(),
Contact :: [contact_info()],
Keys :: [key_name()]}.
Keys :: [key_data()]}.
-type user_id() :: {realm(), user_name()}.
-type user_name() :: label().
-type contact_info() :: {Type :: string(), Data :: string()}.
-type lower0_9() :: [$a..$z | $0..$9 | $_].
-type label() :: [$a..$z | $0..$9 | $_ | $- | $.].
-type package_meta() :: #{package_id := package_id(),
deps := [package_id()],
type := app | lib}.
-type ss_tag() :: {serial(), calendar:timestamp()}.
-type outcome() :: ok
| {error, Reason :: term()}
@ -75,6 +90,7 @@
-spec do() -> no_return().
do() ->
ok = io:setopts([{encoding, unicode}]),
do([]).
@ -83,44 +99,16 @@ do() ->
%% Dispatch work functions based on the nature of the input arguments.
do(["help"]) ->
usage_exit(0);
do(["run", PackageString | Args]) ->
done(help(top));
do(["help", "user"]) ->
done(help(user));
do(["help", "dev"]) ->
done(help(dev));
do(["help", "sysop"]) ->
done(help(sysop));
do(["run", PackageString | ArgV]) ->
ok = start(),
run(PackageString, Args);
do(["runlocal" | ArgV]) ->
ok = start(),
run_local(ArgV);
do(["init", "app", PackageString]) ->
ok = compatibility_check([unix]),
done(zx_local:initialize(app, PackageString));
do(["init", "lib", PackageString]) ->
ok = compatibility_check([unix]),
done(zx_local:initialize(lib, PackageString));
do(["list", "deps"]) ->
done(zx_local:list_deps());
do(["list", "deps", PackageString]) ->
done(zx_local:list_deps(PackageString));
do(["install", PackageFile]) ->
ok = start(),
done(zx_daemon:install(PackageFile));
do(["set", "timeout", String]) ->
done(zx_local:set_timeout(String));
do(["add", "mirror"]) ->
done(zx_local:add_mirror());
do(["drop", "mirror"]) ->
done(zx_local:drop_mirror());
do(["status"]) ->
done(zx_local:status());
do(["set", "dep", PackageString]) ->
done(zx_local:set_dep(PackageString));
do(["set", "version", VersionString]) ->
ok = compatibility_check([unix]),
done(zx_local:set_version(VersionString));
do(["verup", Level]) ->
ok = compatibility_check([unix]),
done(zx_local:verup(Level));
do(["update", ".app"]) ->
done(zx_local:update_app_file());
not_done(run(PackageString, ArgV));
do(["list", "realms"]) ->
done(zx_local:list_realms());
do(["list", "packages", Realm]) ->
@ -134,67 +122,121 @@ do(["latest", PackageString]) ->
done(zx_local:latest(PackageString));
do(["import", "realm", RealmFile]) ->
done(zx_local:import_realm(RealmFile));
do(["drop", "realm", Realm]) ->
done(zx_local:drop_realm(Realm));
do(["logpath", Package, Run]) ->
done(zx_local:logpath(Package, Run));
do(["status"]) ->
done(zx_local:status());
do(["set", "timeout", String]) ->
done(zx_local:set_timeout(String));
do(["add", "mirror"]) ->
done(zx_local:add_mirror());
do(["drop", "mirror"]) ->
done(zx_local:drop_mirror());
do(["create", "project"]) ->
done(zx_local:create_project());
do(["runlocal" | ArgV]) ->
ok = start(),
not_done(run_local(ArgV));
do(["init"]) ->
ok = compatibility_check([unix]),
done(zx_local:initialize());
do(["list", "deps"]) ->
done(zx_local:list_deps());
do(["list", "deps", PackageString]) ->
done(zx_local:list_deps(PackageString));
do(["set", "dep", PackageString]) ->
done(zx_local:set_dep(PackageString));
do(["drop", "dep", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
done(zx_local:drop_dep(PackageID));
do(["verup", Level]) ->
ok = compatibility_check([unix]),
done(zx_local:verup(Level));
do(["set", "version", VersionString]) ->
ok = compatibility_check([unix]),
done(zx_local:set_version(VersionString));
do(["update", ".app"]) ->
done(zx_local:update_app_file());
do(["create", "plt"]) ->
done(zx_local:create_plt());
do(["dialyze"]) ->
done(zx_local:dialyze());
do(["package"]) ->
{ok, TargetDir} = file:get_cwd(),
zx_local:package(TargetDir);
done(zx_local:package(TargetDir));
do(["package", TargetDir]) ->
case filelib:is_dir(TargetDir) of
true -> done(zx_local:package(TargetDir));
false -> done({error, "Target directory does not exist", 22})
end;
do(["dialyze"]) ->
done(zx_local:dialyze());
do(["submit", PackageFile]) ->
done(zx_auth:submit(PackageFile));
do(["list", "pending", PackageName]) ->
done(zx_auth:list_pending(PackageName));
do(["list", "approved", Realm]) ->
done(zx_auth:list_approved(Realm));
do(["review", PackageString]) ->
done(zx_auth:review(PackageString));
do(["approve", PackageString]) ->
done(zx_auth:approve(PackageString));
do(["reject", PackageString]) ->
done(zx_auth:reject(PackageString));
do(["add", "key"]) ->
done(zx_auth:add_key());
do(["create", "user"]) ->
done(zx_local:create_user());
do(["create", "userfile"]) ->
done(zx_local:create_userfile());
do(["create", "keypair"]) ->
done(zx_local:grow_a_pair());
do(["export", "user"]) ->
done(zx_local:export_user());
do(["import", "user", ZdufFile]) ->
done(zx_local:import_user(ZdufFile));
do(["create", "keypair"]) ->
done(zx_local:grow_a_pair());
do(["drop", "key", Realm, KeyName]) ->
done(zx_local:drop_key({Realm, KeyName}));
do(["create", "plt"]) ->
done(zx_local:create_plt());
do(["create", "realm"]) ->
done(zx_local:create_realm());
do(["list", "packagers", PackageName]) ->
done(zx_auth:list_packagers(PackageName));
do(["list", "maintainers", PackageName]) ->
done(zx_auth:list_maintainers(PackageName));
do(["list", "sysops", Realm]) ->
done(zx_auth:list_sysops(Realm));
do(["create", "realmfile"]) ->
done(zx_local:create_realmfile());
do(["install", PackageFile]) ->
case filelib:is_regular(PackageFile) of
true ->
ok = start(),
done(zx_daemon:install(PackageFile));
false ->
done({error, "Target directory does not exist", 22})
end;
do(["accept", PackageString]) ->
done(zx_auth:accept(PackageString));
do(["add", "package", PackageName]) ->
done(zx_auth:add_package(PackageName));
do(["list", "users", Realm]) ->
done(zx_auth:list_users(Realm));
do(["add", "user", ZpuFile]) ->
done(zx_auth:add_user(ZpuFile));
do(["rem", "user", ZpuFile]) ->
done(zx_auth:rem_user(ZpuFile));
do(["add", "packager", Package, UserName]) ->
done(zx_auth:add_packager(Package, UserName));
do(["rem", "packager", Package, UserName]) ->
done(zx_auth:rem_packager(Package, UserName));
do(["add", "maintainer", Package, UserName]) ->
done(zx_auth:add_maintainer(Package, UserName));
do(["rem", "maintainer", Package, UserName]) ->
done(zx_auth:rem_maintainer(Package, UserName));
do(["add", "sysop", Package, UserName]) ->
done(zx_auth:add_sysop(Package, UserName));
do(["create", "realm"]) ->
done(zx_local:create_realm());
do(["takeover", Realm]) ->
done(zx_local:takeover(Realm));
do(["abdicate", Realm]) ->
done(zx_local:abdicate(Realm));
do(["drop", "realm", Realm]) ->
done(zx_local:drop_realm(Realm));
do(["list", "pending", PackageName]) ->
done(zx_auth:list_pending(PackageName));
do(["list", "resigns", Realm]) ->
done(zx_auth:list_resigns(Realm));
do(["submit", PackageFile]) ->
done(zx_auth:submit(PackageFile));
do(["review", PackageString]) ->
done(zx_auth:review(PackageString));
do(["approve", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
done(zx_auth:approve(PackageID));
do(["reject", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
done(zx_auth:reject(PackageID));
do(["accept", PackageString]) ->
done(zx_auth:accept(PackageString));
do(["add", "packager", Package, UserName]) ->
done(zx_auth:add_packager(Package, UserName));
do(["add", "maintainer", Package, UserName]) ->
done(zx_auth:add_maintainer(Package, UserName));
do(["add", "sysop", Package, UserName]) ->
done(zx_auth:add_sysop(Package, UserName));
do(["add", "package", PackageName]) ->
done(zx_auth:add_package(PackageName));
do(_) ->
usage_exit(22).
@ -203,16 +245,23 @@ do(_) ->
done(ok) ->
halt(0);
done({error, Reason}) when is_atom(Reason) ->
ok = log(error, "Operation failed with: ~tp", [Reason]),
halt(1);
done({error, Code}) when is_integer(Code) ->
ok = log(error, "Operation failed with code: ~w", [Code]),
halt(Code);
done({error, Reason}) ->
ok = log(error, "Operation failed with: ~160tp", [Reason]),
halt(1);
done({error, Info, Code}) ->
ok = log(error, Info),
halt(Code).
-spec not_done(outcome()) -> ok | no_return().
not_done(ok) -> ok;
not_done(Error) -> done(Error).
-spec compatibility_check(Platforms) -> ok | no_return()
when Platforms :: unix | win32.
%% @private
@ -229,7 +278,7 @@ compatibility_check(Platforms) ->
true ->
ok;
false ->
Message = "Unfortunately this command is not available on ~tp ~tp",
Message = "Unfortunately this command is not available on ~tw ~tw",
ok = log(error, Message, [Family, Name]),
halt(0)
end.
@ -249,6 +298,7 @@ compatibility_check(Platforms) ->
%% @equiv application:ensure_started(zx).
start() ->
% ok = application:ensure_started(sasl),
ok = application:ensure_started(zx),
zx_daemon:init_connections().
@ -321,8 +371,8 @@ unsubscribe() ->
%%% Execution of application
-spec run(Identifier, RunArgs) -> no_return()
when Identifier :: string(),
-spec run(PackageString, RunArgs) -> no_return()
when PackageString :: string(),
RunArgs :: [string()].
%% @private
%% Given a program Identifier and a list of Args, attempt to locate the program and its
@ -341,20 +391,47 @@ unsubscribe() ->
%% If there is a problem anywhere in the locating, discovery, building, and loading
%% procedure the runtime will halt with an error message.
run(Identifier, RunArgs) ->
ok = file:set_cwd(zx_lib:zomp_dir()),
FuzzyID =
case zx_lib:package_id(Identifier) of
{ok, Fuzzy} ->
Fuzzy;
{error, invalid_package_string} ->
error_exit("Bad package string: ~ts", [Identifier], ?LINE)
end,
{ok, PackageID} = ensure_installed(FuzzyID),
ok = build(PackageID),
run(PackageString, RunArgs) ->
case zx_lib:package_id(PackageString) of
{ok, FuzzyID} -> run2(FuzzyID, RunArgs);
Error -> log(info, "run/2 got ~tp", [Error]), Error
end.
run2(FuzzyID = {Realm, Name, _}, RunArgs) ->
case resolve_installed_version(FuzzyID) of
exact -> run3(FuzzyID, RunArgs);
{ok, Installed} -> run3({Realm, Name, Installed}, RunArgs);
not_found -> run3_maybe(FuzzyID, RunArgs)
end.
run3_maybe(PackageID, RunArgs) ->
{ok, ID} = zx_daemon:latest(PackageID),
case wait_result(ID) of
{ok, Version} ->
NewID = setelement(3, PackageID, Version),
{ok, PackageString} = zx_lib:package_string(NewID),
ok = log(info, "Fetching ~ts", [PackageString]),
run3_maybe2(NewID, RunArgs);
Error ->
Error
end.
run3_maybe2(PackageID, RunArgs) ->
case fetch(PackageID) of
ok -> run3(PackageID, RunArgs);
Error -> Error
end.
run3(PackageID, RunArgs) ->
Dir = zx_lib:ppath(lib, PackageID),
{ok, Meta} = zx_lib:read_project_meta(Dir),
prepare(PackageID, Meta, Dir, RunArgs).
Type = maps:get(type, Meta),
Deps = maps:get(deps, Meta),
ok = prepare([PackageID | Deps]),
execute(Type, PackageID, Meta, Dir, RunArgs).
-spec run_local(RunArgs) -> no_return()
@ -370,52 +447,72 @@ run(Identifier, RunArgs) ->
run_local(RunArgs) ->
{ok, Meta} = zx_lib:read_project_meta(),
PackageID = maps:get(package_id, Meta),
ok = zx_lib:build(),
PackageID = {_, Name, _} = maps:get(package_id, Meta),
Type = maps:get(type, Meta),
Deps = maps:get(deps, Meta),
{ok, Dir} = file:get_cwd(),
true = os:putenv(Name ++ "_include", filename:join(Dir, "include")),
case prepare(Deps) of
ok ->
ok = file:set_cwd(Dir),
ok = zx_lib:build(),
ok = file:set_cwd(zx_lib:zomp_dir()),
prepare(PackageID, Meta, Dir, RunArgs).
execute(Type, PackageID, Meta, Dir, RunArgs);
Error ->
Error
end.
-spec prepare(PackageID, Meta, Dir, RunArgs) -> no_return()
when PackageID :: package_id(),
Meta :: package_meta(),
Dir :: file:filename(),
RunArgs :: [string()].
-spec prepare([zx:package_id()]) -> ok.
%% @private
%% Execution prep common to all packages.
prepare(PackageID, Meta, Dir, RunArgs) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Preparing ~ts...", [PackageString]),
Type = maps:get(type, Meta),
Deps = maps:get(deps, Meta),
prepare(Deps) ->
true = os:putenv("zx_include", filename:join(os:getenv("ZX_DIR"), "include")),
ok = lists:foreach(fun include_env/1, Deps),
NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end,
Needed = lists:filter(NotInstalled, Deps),
ok = lists:foreach(fun install/1, Needed),
ok = lists:foreach(fun build/1, Needed),
execute(Type, PackageID, Meta, Dir, RunArgs).
acquire(Needed, Deps).
acquire([Dep | Rest], Deps) ->
case fetch(Dep) of
ok -> acquire(Rest, Deps);
Error -> Error
end;
acquire([], Deps) ->
make(Deps).
make([Dep | Rest]) ->
case zx_daemon:build(Dep) of
ok -> make(Rest);
Error -> Error
end;
make([]) ->
log(info, "Deps prepared.").
-spec install(PackageString :: string()) -> zx:outcome().
%% @private
%% Installs a package from upstream.
install(PackageString) ->
{ok, ID} = zx_daemon:install(PackageString),
install(PackageString, ID).
include_env(PackageID = {_, Name, _}) ->
Path = filename:join(zx_lib:ppath(lib, PackageID), "include"),
os:putenv(Name ++ "_include", Path).
install(PackageString, ID) ->
-spec fetch(zx:package_id()) -> zx:outcome().
fetch(PackageID) ->
{ok, ID} = zx_daemon:fetch(PackageID),
fetch2(ID).
fetch2(ID) ->
receive
{result, ID, done} ->
ok;
{result, ID, {hops, Count}} ->
ok = log(info, "~ts ~w hops away.", [PackageString, Count]),
install(PackageString, ID);
ok = log(info, "Inbound; ~w hops away.", [Count]),
fetch2(ID);
{result, ID, {error, Reason}} ->
{error, Reason, 1}
after 60000 ->
after 10000 ->
{error, timeout, 62}
end.
@ -434,11 +531,9 @@ execute(app, PackageID, Meta, Dir, RunArgs) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Starting ~ts.", [PackageString]),
Name = element(2, PackageID),
AppTag = list_to_atom(Name),
{AppMod, _} = maps:get(appmod, Meta),
ok = zx_daemon:pass_meta(Meta, Dir, RunArgs),
AppTag = list_to_atom(Name),
ok = ensure_all_started(AppTag),
ok = pass_argv(AppMod, RunArgs),
log(info, "Launcher complete.");
execute(lib, PackageID, _, _, _) ->
Message = "Lib ~ts is available on the system, but is not a standalone app.",
@ -459,58 +554,7 @@ execute(lib, PackageID, _, _, _) ->
ensure_all_started(AppMod) ->
case application:ensure_all_started(AppMod) of
{ok, []} -> ok;
{ok, Apps} -> log(info, "Started ~tp", [Apps])
end.
-spec pass_argv(AppMod, Args) -> ok
when AppMod :: module(),
Args :: [string()].
%% @private
%% Check whether the AppMod:accept_argv/1 is implemented. If so, pass in the
%% command line arguments provided.
pass_argv(AppMod, Args) ->
case lists:member({accept_argv, 1}, AppMod:module_info(exports)) of
true -> AppMod:accept_argv(Args);
false -> ok
end.
-spec ensure_installed(PackageID) -> Result | no_return()
when PackageID :: package_id(),
Result :: {ok, ActualID :: package_id()}.
%% @private
%% Given a PackageID, check whether it is installed on the system, and if not, ensure
%% that the package is either in the cache or can be downloaded. If all attempts at
%% locating or acquiring the package fail, then exit with an error.
ensure_installed(PackageID = {Realm, Name, Version}) ->
case resolve_installed_version(PackageID) of
exact -> {ok, PackageID};
{ok, Installed} -> {ok, {Realm, Name, Installed}};
not_found -> fetch_one({Realm, Name, Version})
end.
-spec fetch_one(PackageID) -> {ok, ActualID} | no_return()
when PackageID :: package_id(),
ActualID :: package_id().
%% @private
%% A helper function to deal with the special case of downloading and installing a
%% single primary application package with a possibly incomplete version designator.
%% All other fetches are for arbitrarily long lists of package IDs with complete
%% version numbers (dependency fetches).
fetch_one(PackageID) ->
case zx_daemon:fetch([PackageID]) of
{{ok, [ActualID]}, {error, []}} ->
ok = install(ActualID),
{ok, ActualID};
{{ok, []}, {error, [{PackageID, Reason}]}} ->
error_exit("Package fetch failed with: ~tp", [Reason], ?LINE)
{ok, Apps} -> log(info, "Started ~160tp", [Apps])
end.
@ -546,26 +590,11 @@ tuplize(String, Acc) ->
end.
-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:ppath(lib, PackageID)),
ok = zx_lib:build(),
file:set_cwd(CWD).
%% 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).
wait_result(ID) ->
receive
{result, ID, Result} -> Result
after 5000 -> {error, timeout}
end.
@ -578,100 +607,105 @@ build(PackageID) ->
%% with the provided exit code.
usage_exit(Code) ->
ok = usage(),
ok = lists:foreach(fun io:format/1, usage_all()),
halt(Code).
-spec usage() -> ok.
%% @private
%% Display the zx command line usage message.
help(top) -> show_help();
help(user) -> show_help([usage_header(), usage_user(), usage_spec()]);
help(dev) -> show_help([usage_header(), usage_dev(), usage_spec()]);
help(sysop) -> show_help([usage_header(), usage_sysop(), usage_spec()]).
usage() ->
show_help() ->
T =
"ZX usage: zx [command] [object] [args]~n"
"~n"
"User Actions:~n"
" zx help~n"
" zx run PackageID [Args]~n"
" zx runlocal [Args]~n"
" zx list realms~n"
" zx list packages Realm~n"
" zx list versions PackageID~n"
" zx latest PackageID~n"
" zx import realm RealmFile~n"
" zx drop realm Realm~n"
" zx install PackageID~n"
" zx logpath [Package [1-10]]~n"
" zx status~n"
" zx set timeout Value~n"
" zx add mirror Realm Host:Port~n"
" zx drop mirror Realm Host:Port~n"
"~n"
"Developer/Packager/Maintainer Actions:~n"
" zx create project [app | lib] PackageID~n"
" zx init Type PackageID~n"
" zx list deps [PackageID]~n"
" zx set dep PackageID~n"
" zx drop dep PackageID~n"
" zx verup Level~n"
" zx set version Version~n"
" zx update .app~n"
" zx create plt~n"
" zx dialyze~n"
" zx package Path~n"
" zx submit ZspFile~n"
" zx list pending PackageName~n"
" zx list resigns Realm~n"
" zx list packagers PackageName~n"
" zx list maintainers PackageName~n"
" zx list sysops Realm~n"
" zx review PackageID~n"
" zx approve PackageID~n"
" zx reject PackageID~n"
" zx add key Realm KeyName~n"
" zx get key Realm KeyName~n"
" zx create user~n"
" zx create userfiles Realm UserName~n"
" zx create keypair~n"
" zx export user UserID~n"
" zx import user ZdufFile~n"
"~n"
"Sysop Actions:~n"
" zx add user ZpufFile~n"
" zx add package PackageName~n"
" zx add packager PackageName UserID~n"
" zx add maintainer PackageName UserID~n"
" zx add sysop UserID~n"
" zx accept PackageID~n"
" zx create realm~n"
" zx create realmfile Realm~n"
" zx takeover Realm~n"
" zx abdicate Realm~n"
"~n"
"Where~n"
" PackageID :: A string of the form Realm-Name[-Version]~n"
" Args :: Arguments to pass to the application~n"
" Type :: The project type: a standalone \"app\" or a \"lib\"~n"
" Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n"
" RealmFile :: Path to a valid .zrf realm file~n"
" Realm :: The name of a realm as a string [:a-z:]~n"
" KeyName :: The prefix of a keypair to drop~n"
" Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n"
" Path :: Path to a valid project directory or .zsp file~n",
"ZX help has three forms, one for each category of commands:~n"
" zx help [user | dev | sysop]~n"
"The user manual is also available online at: http://zxq9.com/projects/zomp/~n",
io:format(T).
show_help(Info) -> lists:foreach(fun io:format/1, Info).
%%% 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.
usage_all() ->
[usage_header(), usage_user(), usage_dev(), usage_sysop(), usage_spec()].
error_exit(Format, Args, Line) ->
File = filename:basename(?FILE),
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
halt(1).
usage_header() ->
"ZX usage: zx [command] [object] [args]~n~n".
usage_user() ->
"User Actions:~n"
" zx help~n"
" zx run PackageID [Args]~n"
" zx list realms~n"
" zx list packages Realm~n"
" zx list versions PackageID~n"
" zx latest PackageID~n"
" zx import realm RealmFile~n"
" zx drop realm Realm~n"
" zx logpath [Package [1-10]]~n"
" zx status~n"
" zx set timeout Value~n"
" zx add mirror Realm Host:Port~n"
" zx drop mirror Realm Host:Port~n~n".
usage_dev() ->
"Developer/Packager/Maintainer Actions:~n"
" zx create project~n"
" zx runlocal [Args]~n"
" zx init~n"
" zx list deps [PackageID]~n"
" zx set dep PackageID~n"
" zx drop dep PackageID~n"
" zx verup Level~n"
" zx set version Version~n"
" zx update .app~n"
" zx create plt~n"
" zx dialyze~n"
" zx package Path~n"
" zx submit ZSP~n"
" zx list pending PackageName~n"
" zx review PackageID~n"
" zx approve PackageID~n"
" zx reject PackageID~n"
" zx add key~n"
" zx create user~n"
" zx create userfile~n"
" zx create keypair~n"
" zx export user~n"
" zx import user [ZPUF | ZDUF]~n"
" zx list packagers PackageName~n"
" zx list maintainers PackageName~n"
" zx list sysops Realm~n"
" zx install ZSP~n~n".
usage_sysop() ->
"Sysop Actions:~n"
" zx list approved Realm~n"
" zx accept PackageID~n"
" zx add package PackageName~n"
" zx list users Realm~n"
" zx add user ZPUF~n"
" zx add packager PackageName UserID~n"
" zx add maintainer PackageName UserID~n"
" zx add sysop UserID~n"
" zx create realm~n"
" zx create realmfile~n"
" zx takeover Realm~n"
" zx abdicate Realm~n~n".
usage_spec() ->
"Where~n"
" PackageID :: A string of the form Realm-Name[-Version]~n"
" Args :: Arguments to pass to the application~n"
" Type :: The project type: a standalone \"app\" or a \"lib\"~n"
" Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n"
" RealmFile :: Path to a valid .zrf realm file~n"
" Realm :: The name of a realm as a string [:a-z:]~n"
" KeyName :: The prefix of a keypair to drop~n"
" Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n"
" Path :: Path or filename.~n"
" ZSP :: Path to a .zsp file (Zomp Source Package).~n"
" ZPUF :: Path to a .zpuf file (Zomp Public User File).~n"
" ZDUF :: Path to a .zduf file (Zomp DANGEROUS User File).~n".

View File

@ -13,9 +13,13 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([list_pending/1, list_resigns/1,
-export([list_pending/1, list_approved/1,
submit/1, review/1, approve/1, reject/1, accept/1,
add_packager/2, add_maintainer/2, add_sysop/1,
list_users/1, list_packagers/1, list_maintainers/1, list_sysops/1,
add_user/1,
add_packager/2, rem_packager/2,
add_maintainer/2, rem_maintainer/2,
add_sysop/1,
add_package/1]).
-include("zx_logger.hrl").
@ -25,70 +29,86 @@
%%% Functions
-spec list_pending(PackageName :: string()) -> no_return().
-spec list_pending(PackageName :: string()) -> zx:outcome().
%% @private
%% List the versions of a package that are pending review. The package name is input by
%% the user as a string of the form "otpr-zomp" and the output is a list of full
%% package IDs, printed one per line to stdout (like "otpr-zomp-3.2.2").
list_pending(PackageName) ->
Package = {Realm, Name} =
case zx_lib:package_id(PackageName) of
{ok, {R, N, {z, z, z}}} ->
{R, N};
{error, invalid_package_string} ->
error_exit("~tp is not a valid package name.", [PackageName], ?LINE)
end,
case zx_daemon:list_pending(Package) of
{ok, []} ->
Message = "Package ~ts has no versions pending.",
ok = log(info, Message, [PackageName]),
halt(0);
{ok, Versions} ->
Print =
fun(Version) ->
{ok, PackageString} = zx_lib:package_string({Realm, Name, Version}),
io:format("~ts~n", [PackageString])
end,
ok = lists:foreach(Print, Versions),
halt(0);
{error, bad_realm} ->
error_exit("Bad realm name.", ?LINE);
{error, bad_package} ->
error_exit("Bad package name.", ?LINE);
{error, network} ->
Message = "Network issues are preventing connection to the realm.",
error_exit(Message, ?LINE)
{ok, {Realm, Name, {z, z, z}}} -> list_pending2(Realm, Name);
Error -> Error
end.
list_pending2(Realm, Name) ->
case connect(Realm) of
{ok, Socket} -> list_pending3(Realm, Name, Socket);
Error -> Error
end.
-spec list_resigns(zx:realm()) -> no_return().
list_pending3(Realm, Name, Socket) ->
Message = <<"ZOMP AUTH 1:", 0:24, 5:8, (term_to_binary({Realm, Name}))/binary>>,
ok = gen_tcp:send(Socket, Message),
receive
{tcp, Socket, <<0:8, Bin/binary>>} -> list_pending4(Realm, Name, Socket, Bin);
{tcp, Socket, Error} -> done(Socket, Error);
{tcp_closed, Socket} -> {error, "Socket closed unexpectedly."}
after 5000 -> done(Socket, timeout)
end.
list_pending4(Realm, Name, Socket, Bin) ->
ok = zx_net:disconnect(Socket),
case zx_lib:b_to_ts(Bin) of
{ok, Versions} -> list_pending5(Realm, Name, Versions);
Error -> Error
end.
list_pending5(Realm, Name, Versions) ->
Print =
fun(Version) ->
PackageID = {Realm, Name, Version},
{ok, PackageString} = zx_lib:package_string(PackageID),
io:format("~ts~n", [PackageString])
end,
lists:foreach(Print, Versions).
-spec list_approved(zx:realm()) -> zx:outcome().
%% @private
%% List the package ids of all packages waiting in the resign queue for the given realm,
%% printed to stdout one per line.
list_resigns(Realm) ->
case zx_daemon:list_resigns(Realm) of
{ok, []} ->
Message = "No packages pending signature in ~tp.",
ok = log(info, Message, [Realm]),
halt(0);
{ok, PackageIDs} ->
list_approved(Realm) ->
case connect(Realm) of
{ok, Socket} -> list_approved2(Realm, Socket);
Error -> Error
end.
list_approved2(Realm, Socket) ->
Message = <<"ZOMP AUTH 1:", 0:24, 7:8, (term_to_binary(Realm))/binary>>,
ok = gen_tcp:send(Socket, Message),
receive
{tcp, Socket, <<0:8, Bin/binary>>} -> list_approved3(Socket, Bin, Realm);
{tcp, Socket, Error} -> done(Socket, Error);
{tcp_closed, Socket} -> {error, "Socket closed unexpectedly."}
after 5000 -> done(Socket, timeout)
end.
list_approved3(Socket, Bin, Realm) ->
ok = zx_net:disconnect(Socket),
case zx_lib:b_to_ts(Bin) of
{ok, PackageIDs} -> list_approved4(Realm, PackageIDs);
Error -> Error
end.
list_approved4(Realm, PackageIDs) ->
Print =
fun(PackageID) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
fun({Name, Version}) ->
{ok, PackageString} = zx_lib:package_string({Realm, Name, Version}),
io:format("~ts~n", [PackageString])
end,
ok = lists:foreach(Print, PackageIDs),
halt(0);
{error, bad_realm} ->
error_exit("Bad realm name.", ?LINE);
{error, no_realm} ->
error_exit("Realm \"~ts\" is not configured.", ?LINE);
{error, network} ->
Message = "Network issues are preventing connection to the realm.",
error_exit(Message, ?LINE)
end.
lists:foreach(Print, PackageIDs).
-spec submit(ZspPath :: file:filename()) -> zx:outcome().
@ -96,382 +116,483 @@ list_resigns(Realm) ->
%% Submit a package to the appropriate "prime" server for the given realm.
submit(ZspPath) ->
{ok, ZspBin} = file:read_file(ZspPath),
<<SigSize:24, Sig:SigSize/binary, Signed/binary>> = ZspBin,
<<MetaSize:16, MetaBin:MetaSize/binary, _/binary>> = 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, ZspBin),
ok = log(info, "Done sending contents of ~tp", [ZspPath]),
Outcome = recv_or_die(Socket),
log(info, "Response: ~tp", [Outcome]),
disconnect(Socket).
case file:read_file(ZspPath) of
{ok, ZspBin} -> submit2(ZspBin);
Error -> Error
end.
submit2(ZspBin = <<SigSize:24, Sig:SigSize/binary, Signed/binary>>) ->
<<MetaSize:16, MetaBin:MetaSize/binary, _/binary>> = Signed,
{ok, {PackageID = {Realm, _, _}, KeyName, _, _}} = zx_lib:b_to_ts(MetaBin),
UserName = zx_local:select_user(Realm),
KeyID = {Realm, KeyName},
case zx_key:ensure_keypair(KeyID) of
true ->
{ok, PKey} = zx_key:load(public, KeyID),
{ok, DKey} = zx_key:load(private, KeyID),
case zx_key:verify(Signed, Sig, PKey) of
true -> submit3(PackageID, ZspBin, UserName, KeyName, DKey);
false -> {error, bad_sig}
end;
Error ->
Error
end.
submit3({Realm, Name, Ver}, ZspBin, UserName, KeyName, Key) ->
Payload = {Realm, UserName, KeyName, {Name, Ver}},
case connect(Realm) of
{ok, Socket} -> submit4(Socket, Payload, ZspBin, Key);
Error -> Error
end.
submit4(Socket, Payload, ZspBin, Key) ->
Command = 1,
Request = pack_and_sign(Command, Payload, Key),
ok = gen_tcp:send(Socket, <<"ZOMP AUTH 1:", Request/binary>>),
case zx_net:tx(Socket, ZspBin) of
ok -> done(Socket);
Error -> done(Socket, Error)
end.
-spec review(PackageString :: string()) -> zx:outcome().
review(PackageString) ->
PackageID = {Realm, _, _} = zx_lib:package_id(PackageString),
Socket = connect_auth_or_die(Realm),
ok = send(Socket, {review, PackageID}),
{ok, ZrpBin} = recv_or_die(Socket),
ok = send(Socket, ok),
ok = disconnect(Socket),
{ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin, [safe]),
PackageID = maps:get(package_id, Meta),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
TgzFile = PackageString ++ ".tgz",
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
ok = zx_key:verify(TgzData, Signature, PubKey),
ok =
case zx_lib:package_id(PackageString) of
{ok, PackageID} -> review2(PackageID);
Error -> Error
end.
review2(PackageID = {Realm, _, _}) ->
case connect(Realm) of
{ok, Socket} -> review3(PackageID, Socket);
Error -> Error
end.
review3(PackageID, Socket) ->
Command = 8,
TermBin = term_to_binary(PackageID),
Request = <<0:24, Command:8, TermBin/binary>>,
ok = gen_tcp:send(Socket, <<"ZOMP AUTH 1:", Request/binary>>),
receive
{tcp, Socket, <<0:1, 0:7>>} -> review4(PackageID, Socket);
{tcp, Socket, Bin} -> done(Socket, Bin);
{tcp_closed, Socket} -> {error, tcp_closed}
after 5000 -> done(Socket, timeout)
end.
review4(PackageID, Socket) ->
case zx_net:rx(Socket) of
{ok, <<Size:24, Sig:Size/binary, Signed/binary>>} ->
ok = zx_net:disconnect(Socket),
review5(PackageID, Sig, Signed);
Error ->
done(Socket, Error)
end.
review5(PackageID, Sig, Signed = <<Size:16, MetaBin:Size/binary, TgzBin/binary>>) ->
case zx_lib:b_to_ts(MetaBin) of
{ok, {PackageID = {Realm, _, _}, KeyName, _, _}} ->
review6(PackageID, {Realm, KeyName}, Signed, Sig, TgzBin);
{ok, {UnexpectedID, _, _, _}} ->
{ok, Requested} = zx_lib:package_string(PackageID),
{ok, Unexpected} = zx_lib:package_string(UnexpectedID),
Message = "Requested ~ts, but inside was ~ts! Aborting.",
ok = log(warning, Message, [Requested, Unexpected]),
{error, "Wrong package received.", 29};
error ->
{error, bad_response}
end.
review6(PackageID, KeyID, Signed, Sig, TgzBin) ->
case zx_key:load(public, KeyID) of
{ok, Key} -> review7(PackageID, Key, Signed, Sig, TgzBin);
Error -> Error % TODO: Fetch unknown keys
end.
review7(PackageID, SigKey, Signed, Sig, TgzBin) ->
case zx_key:verify(Signed, Sig, SigKey) of
true -> review8(PackageID, TgzBin);
false -> {error, bad_sig}
end.
review8(PackageID, TgzBin) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
case file:make_dir(PackageString) of
ok ->
log(info, "Will unpack to directory ./~ts", [PackageString]);
review9(PackageString, TgzBin);
{error, Error} ->
Message = "Creating dir ./~ts failed with ~ts. Aborting.",
ok = log(error, Message, [PackageString, Error]),
halt(1)
end,
ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageString}]),
ok = log(info, "Unpacked and awaiting inspection."),
halt(0).
{error, Error}
end.
review9(PackageString, TgzBin) ->
ok = erl_tar:extract({binary, TgzBin}, [compressed, {cwd, PackageString}]),
log(info, "Sources unpacked to ./~ts", [PackageString]).
approve(PackageID = {Realm, _, _}) ->
Socket = connect_auth_or_die(Realm),
ok = send(Socket, {approve, PackageID}),
ok = recv_or_die(Socket),
ok = log(info, "ok"),
halt(0).
approve(Package) -> package_operation(9, Package).
reject(Package) -> package_operation(10, Package).
reject(PackageID = {Realm, _, _}) ->
Socket = connect_auth_or_die(Realm),
ok = send(Socket, {reject, PackageID}),
ok = recv_or_die(Socket),
ok = log(info, "ok"),
halt(0).
-spec package_operation(Code :: 9 | 10, Package :: string()) -> zx:outcome().
package_operation(Code, Package) ->
case zx_lib:package_id(Package) of
{ok, {Realm, Name, Version}} -> make_su_request(Code, Realm, {Name, Version});
Error -> Error
end.
-spec accept(PackageString :: string()) -> zx:outcome().
accept(PackageString) ->
PackageID = {Realm, _, _} = zx_lib:package_id(PackageString),
RealmConf = zx_lib:load_realm_conf(Realm),
{package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf),
KeySelection = [{K, {R, K}} || {R, K} <- [element(1, K) || K <- PackageKeys]],
PackageKeyID = zx_tty:select(KeySelection),
{ok, PackageKey} = zx_key:load(private, PackageKeyID),
Socket = connect_auth_or_die(Realm),
ok = send(Socket, {accept, PackageID}),
{ok, ZrpBin} = recv_or_die(Socket),
{ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin, [safe]),
PackageID = maps:get(package_id, Meta),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
TgzFile = PackageString ++ ".tgz",
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
ok = zx_key:verify(TgzData, Signature, PubKey),
ReSignature = public_key:sign(TgzData, sha512, PackageKey),
FinalMeta = maps:put(sig, {PackageKeyID, ReSignature}, Meta),
NewMetaBin = term_to_binary(FinalMeta),
NewFiles = lists:keystore("zomp.meta", 1, Files, {"zomp.meta", NewMetaBin}),
ResignedZrp = PackageString ++ ".zsp.resign",
ok = erl_tar:create(ResignedZrp, NewFiles),
{ok, ResignedBin} = file:read_file(ResignedZrp),
ok = gen_tcp:send(Socket, ResignedBin),
ok = recv_or_die(Socket),
ok = file:delete(ResignedZrp),
ok = recv_or_die(Socket),
ok = disconnect(Socket),
ok = log(info, "Resigned ~ts", [PackageString]),
halt(0).
case zx_lib:package_id(PackageString) of
{ok, PackageID} -> accept2(PackageID);
Error -> Error
end.
accept2(PackageID = {Realm, Name, Version}) ->
case connect_auth(Realm) of
{ok, AuthConn} -> accept3(PackageID, {Name, Version}, AuthConn);
Error -> Error
end.
accept3(PackageID, PV, {UserName, KeyName, Key, Tag, Socket}) ->
Command = 11,
Payload = {Tag, UserName, KeyName, PV},
Request = pack_and_sign(Command, Payload, Key),
ok = gen_tcp:send(Socket, <<0:8, Request/binary>>),
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:1, 0:7>>} -> accept4(PackageID, KeyName, Key, Socket);
{tcp, Socket, Bin} -> done(Socket, Bin);
{tcp_closed, Socket} -> {error, tcp_closed}
after 5000 -> done(Socket, timeout)
end.
accept4(PackageID, KeyName, Key, Socket) ->
case zx_net:rx(Socket) of
{ok, <<Size:24, Sig:Size/binary, Signed/binary>>} ->
accept5(PackageID, {KeyName, Key, Socket}, Sig, Signed);
Error ->
done(Socket, Error)
end.
accept5(PackageID,
Auth,
Sig,
Signed = <<Size:16, MetaBin:Size/binary, TgzBin/binary>>) ->
case zx_lib:b_to_ts(MetaBin) of
{ok, Meta = {PackageID = {Realm, _, _}, SigKeyName, _, _}} ->
accept6(Meta, Auth, {Realm, SigKeyName}, Signed, Sig, TgzBin);
{ok, {UnexpectedID, _, _, _}} ->
{ok, Requested} = zx_lib:package_string(PackageID),
{ok, Unexpected} = zx_lib:package_string(UnexpectedID),
Message = "Requested ~ts, but inside was ~ts! Aborting.",
ok = log(warning, Message, [Requested, Unexpected]),
{error, "Wrong package received.", 29};
error ->
{error, bad_response}
end.
accept6(Meta, Auth, SigKeyID, Signed, Sig, TgzBin) ->
case zx_key:load(public, SigKeyID) of
{ok, SigKey} -> accept7(Meta, Auth, SigKey, Signed, Sig, TgzBin);
Error -> Error % TODO: Fetch unknown keys
end.
accept7(Meta, Auth, SigKey, Signed, Sig, TgzBin) ->
case zx_key:verify(Signed, Sig, SigKey) of
true -> accept8(Meta, Auth, TgzBin);
false -> {error, bad_sig}
end.
accept8({PackageID, _, Deps, Modules}, {KeyName, Key, Socket}, TgzBin) ->
MetaBin = term_to_binary({PackageID, KeyName, Deps, Modules}),
MetaSize = byte_size(MetaBin),
SignMe = <<MetaSize:16, MetaBin:MetaSize/binary, TgzBin/binary>>,
Sig = public_key:sign(SignMe, sha512, Key),
SigSize = byte_size(Sig),
ZspData = <<SigSize:24, Sig:SigSize/binary, SignMe/binary>>,
ok = zx_net:tx(Socket, ZspData),
done(Socket).
add_packager(Package, UserFile) ->
ok = log(info, "Would add ~ts to packagers for ~160tp now.", [UserFile, Package]),
halt(0).
list_users(Realm) ->
list_users(2, Realm).
list_packagers(Package) ->
{ok, {Realm, Name, {z, z, z}}} = zx_lib:package_id(Package),
list_users(3, {Realm, Name}).
list_maintainers(Package) ->
{ok, {Realm, Name, {z, z, z}}} = zx_lib:package_id(Package),
list_users(4, {Realm, Name}).
list_sysops(Realm) ->
list_users(5, Realm).
add_maintainer(Package, UserFile) ->
ok = log(info, "Would add ~ts to maintainer for ~160tp now.", [UserFile, Package]),
halt(0).
-spec list_users(Command, Target) -> zx:outcome()
when Command :: 2..5,
Target :: zx:realm() | zx:package().
list_users(Command, Target) ->
case make_uu_request(Command, Target) of
{ok, Users} -> lists:foreach(fun print_user/1, Users);
Error -> Error
end.
print_user({UserName, RealName, [{"email", Email}]}) ->
io:format("~ts (~ts <~ts>) ~n", [UserName, RealName, Email]).
-spec add_user(file:filename()) -> zx:outcome().
add_user(ZPUF) ->
case file:read_file(ZPUF) of
{ok, Bin} -> add_user2(Bin);
Error -> Error
end.
add_user2(Bin) ->
case zx_lib:b_to_t(Bin) of
{ok, {UserInfo, KeyData}} ->
Realm = proplists:get_value(realm, UserInfo),
UserData = {proplists:get_value(username, UserInfo),
proplists:get_value(realname, UserInfo),
proplists:get_value(contact_info, UserInfo),
[setelement(2, KD, none) || KD <- KeyData]},
Command = 13,
make_su_request(Command, Realm, UserData);
error ->
{error, "Bad user file.", 1}
end.
add_packager(Package, User) -> user_auth_operation(15, Package, User).
rem_packager(Package, User) -> user_auth_operation(16, Package, User).
add_maintainer(Package, User) -> user_auth_operation(17, Package, User).
rem_maintainer(Package, User) -> user_auth_operation(18, Package, User).
-spec user_auth_operation(Code, Package, User)-> zx:outcome()
when Code :: 15..18,
Package :: string(),
User :: zx:user_name().
user_auth_operation(Code, Package, User) ->
case zx_lib:package_id(Package) of
{ok, {Realm, Name, {z, z, z}}} -> make_su_request(Code, Realm, {Name, User});
Error -> Error
end.
-spec add_sysop(file:filename()) -> zx:outcome().
add_sysop(UserFile) ->
ok = log(info, "Would add ~ts to sysop list.", [UserFile]),
halt(0).
{error, "Not yet implemented", 1}.
-spec add_package(PackageName) -> no_return()
when PackageName :: zx:package().
%% @private
%% A sysop command that adds a package to a realm operated by the caller.
-spec add_package(zx:package()) -> zx:outcome().
add_package(PackageName) ->
ok = file:set_cwd(zx_lib:zomp_dir()),
case zx_lib:package_id(PackageName) of
{ok, {Realm, Name, {z, z, z}}} ->
add_package(Realm, Name);
_ ->
error_exit("~tp is not a valid package name.", [PackageName], ?LINE)
{ok, {Realm, Name, {z, z, z}}} -> add_package2(Realm, Name);
Error -> Error
end.
-spec add_package(Realm, Name) -> no_return()
when Realm :: zx:realm(),
Name :: zx:name().
add_package(Realm, Name) ->
Socket = connect_auth_or_die(Realm),
ok = send(Socket, {add_package, {Realm, Name}}),
ok = recv_or_die(Socket),
ok = log(info, "\"~ts-~ts\" added successfully.", [Realm, Name]),
halt(0).
%%% Authenticated communication with prime
-spec send(Socket, Message) -> ok
when Socket :: gen_tcp:socket(),
Message :: term().
%% @private
%% Wrapper for the procedure necessary to send an internal message over the wire.
send(Socket, Message) ->
Bin = term_to_binary(Message),
gen_tcp:send(Socket, Bin).
-spec recv_or_die(Socket) -> Result | no_return()
when Socket :: gen_tcp:socket(),
Result :: ok | {ok, term()}.
recv_or_die(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin) of
ok ->
ok;
{ok, Response} ->
{ok, Response};
{error, Reason} ->
error_exit("Action failed with: ~tp", [Reason], ?LINE);
Unexpected ->
error_exit("Unexpected message: ~tp", [Unexpected], ?LINE)
end;
{tcp_closed, Socket} ->
error_exit("Lost connection to node unexpectedly.", ?LINE)
after 5000 ->
error_exit("Connection timed out.", ?LINE)
end.
-spec connect_auth_or_die(zx:realm()) -> gen_tcp:socket() | no_return().
connect_auth_or_die(Realm) ->
add_package2(Realm, Name) ->
case connect_auth(Realm) of
{ok, Socket} ->
Socket;
Error ->
M1 = "Connection failed to realm prime with ~160tp.",
ok = log(warning, M1, [Error]),
halt(1)
{ok, AuthConn} -> add_package3(Realm, Name, AuthConn);
Error -> Error
end.
add_package3(Realm, Name, {UserName, KeyName, Key, Tag, Socket}) ->
Command = 12,
Package = {Realm, Name},
Payload = {Tag, UserName, KeyName, Package},
Request = pack_and_sign(Command, Payload, Key),
ok = gen_tcp:send(Socket, <<0:8, Request/binary>>),
done(Socket).
%%% Generic Request Forms
-spec make_uu_request(Command, Target) -> zx:outcome()
when Command :: pos_integer(),
Target :: zx:realm() | zx:package() | zx:package_id().
make_uu_request(Command, Target) when is_tuple(Target) ->
make_uu_request2(Command, element(1, Target), Target);
make_uu_request(Command, Target) when is_list(Target) ->
make_uu_request2(Command, Target, Target).
make_uu_request2(Command, Realm, Target) ->
case connect(Realm) of
{ok, Socket} -> make_uu_request3(Command, Target, Socket);
Error -> Error
end.
make_uu_request3(Command, Target, Socket) ->
TermBin = term_to_binary(Target),
Request = <<"ZOMP AUTH 1:", 0:24, Command:8, TermBin/binary>>,
ok = gen_tcp:send(Socket, Request),
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:1, 0:7, Bin/binary>>} -> make_uu_request4(Bin);
{tcp, Socket, Bin} -> done(Socket, Bin);
{tcp_closed, Socket} -> {error, tcp_closed}
after 5000 -> done(Socket, timeout)
end.
make_uu_request4(Bin) ->
case zx_lib:b_to_ts(Bin) of
error -> {error, bad_response};
Term -> Term
end.
-spec make_su_request(Command, Realm, Data) -> zx:outcome()
when Command :: 1 | 9 | 10 | 13..20,
Realm :: zx:realm(),
Data :: term().
make_su_request(Command, Realm, Data) ->
AuthData = prep_auth(Realm),
case connect(Realm) of
{ok, Socket} -> make_su_request2(Command, Realm, Data, AuthData, Socket);
Error -> Error
end.
make_su_request2(Command, Realm, Data, {Signatory, KeyName, Key}, Socket) ->
Payload = {Realm, Signatory, KeyName, Data},
Request = pack_and_sign(Command, Payload, Key),
ok = gen_tcp:send(Socket, <<"ZOMP AUTH 1:", Request/binary>>),
done(Socket).
%%% Connectiness with prime
-spec connect_auth(Realm) -> Result
when Realm :: zx:realm(),
Result :: {ok, gen_tcp:socket()}
| {error, Reason :: term()}.
Result :: {ok, AuthConn}
| {error, Reason},
AuthConn :: {UserName :: zx:user_name(),
KeyName :: zx:key_name(),
Key :: term(),
SSTag :: zx:ss_tag(),
Socket :: gen_tcp:socket()},
Reason :: term().
%% @private
%% Connect to one of the servers in the realm constellation.
connect_auth(Realm) ->
RealmConf = zx_lib:load_realm_conf(Realm),
{User, KeyID, Key} = prep_auth(Realm, RealmConf),
{prime, {Host, Port}} = lists:keyfind(prime, 1, RealmConf),
Options = [{packet, 4}, {mode, binary}, {active, true}],
UserData = prep_auth(Realm),
case zx_lib:load_realm_conf(Realm) of
{ok, RealmConf} ->
connect_auth2(Realm, RealmConf, UserData);
Error ->
ok = log(error, "Realm ~160tp is not configured.", [Realm]),
Error
end.
connect_auth2(Realm, RealmConf, UserData) ->
{Host, Port} = maps:get(prime, RealmConf),
Options = [{packet, 4}, {mode, binary}, {active, once}],
case gen_tcp:connect(Host, Port, Options, 5000) of
{ok, Socket} ->
ok = log(info, "Connected to ~tp prime.", [Realm]),
connect_auth(Socket, Realm, User, KeyID, Key);
connect_auth3(Socket, Realm, UserData);
Error = {error, E} ->
ok = log(warning, "Connection problem: ~tp", [E]),
ok = log(warning, "Connection problem: ~160tp", [E]),
{error, Error}
end.
-spec connect_auth(Socket, Realm, User, KeyID, Key) -> Result
when Socket :: gen_tcp:socket(),
Realm :: zx:realm(),
User :: zx:user_id(),
KeyID :: zx:key_id(),
Key :: term(),
Result :: {ok, gen_tcp:socket()}
| {error, timeout}.
%% @private
%% Send a protocol ID string to notify the server what we're up to, disconnect
%% if it does not return an "OK" response within 5 seconds.
connect_auth(Socket, Realm, User, KeyID, Key) ->
ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>),
connect_auth3(Socket, Realm, UD = {UserName, KeyName, Key}) ->
Null = 0,
Timestamp = calendar:universal_time(),
Payload = {Realm, Timestamp, UserName, KeyName},
NullRequest = pack_and_sign(Null, Payload, Key),
ok = gen_tcp:send(Socket, <<"ZOMP AUTH 1:", NullRequest/binary>>),
receive
{tcp, Socket, Bin} ->
ok = binary_to_term(Bin, [safe]),
confirm_auth(Socket, Realm, User, KeyID, Key);
{tcp_closed, Socket} ->
ok = log(warning, "Socket closed unexpectedly."),
halt(1)
after 5000 ->
ok = log(warning, "Host realm ~160tp prime timed out.", [Realm]),
{error, auth_timeout}
{tcp, Socket, <<0:8, Bin/binary>>} -> connect_auth4(Socket, UD, Bin);
{tcp, Socket, Bin} -> done(Socket, Bin);
{tcp_closed, Socket} -> {error, tcp_closed}
after 5000 -> done(Socket, timeout)
end.
connect_auth4(Socket, {UserName, KeyName, Key}, Bin) ->
case zx_lib:b_to_ts(Bin) of
{ok, Tag} -> {ok, {UserName, KeyName, Key, Tag, Socket}};
error -> done(Socket, bad_response)
end.
confirm_auth(Socket, Realm, User, KeyID, Key) ->
ok = send(Socket, {User, KeyID}),
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin, [safe]) of
{sign, Blob} ->
Sig = public_key:sign(Blob, sha512, Key),
ok = send(Socket, {signed, Sig}),
confirm_auth(Socket);
{error, not_prime} ->
M1 = "Connected node is not prime for realm ~160tp",
ok = log(warning, M1, [Realm]),
ok = disconnect(Socket),
{error, not_prime};
{error, bad_user} ->
M2 = "Bad user record ~160tp",
ok = log(warning, M2, [User]),
ok = disconnect(Socket),
{error, bad_user};
{error, unauthorized_key} ->
M3 = "Unauthorized user key ~160tp",
ok = log(warning, M3, [KeyID]),
ok = disconnect(Socket),
{error, unauthorized_key};
{error, Reason} ->
Message = "Could not begin auth exchange. Failed with ~160tp",
ok = log(warning, Message, [Reason]),
ok = disconnect(Socket),
{error, Reason}
end;
{tcp_closed, Socket} ->
ok = log(warning, "Socket closed unexpectedly."),
halt(1)
after 5000 ->
ok = log(warning, "Host realm ~tp prime timed out.", [Realm]),
{error, auth_timeout}
connect(Realm) ->
case zx_lib:load_realm_conf(Realm) of
{ok, RealmConf} ->
{Host, Port} = maps:get(prime, RealmConf),
Options = [{packet, 4}, {mode, binary}, {nodelay, true}, {active, once}],
gen_tcp:connect(Host, Port, Options, 5000);
Error ->
ok = log(error, "Realm ~160tp is not configured.", [Realm]),
Error
end.
confirm_auth(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin, [safe]) of
ok -> {ok, Socket};
Other -> {error, Other}
end;
{tcp_closed, Socket} ->
ok = log(warning, "Socket closed unexpectedly."),
halt(1)
after 5000 ->
{error, timeout}
end.
-spec prep_auth(Realm, RealmConf) -> {User, KeyID, Key} | no_return()
-spec prep_auth(Realm) -> {User, KeyName, Key}
when Realm :: zx:realm(),
RealmConf :: [term()],
User :: zx:user_id(),
KeyID :: zx:key_id(),
KeyName :: zx:key_id(),
Key :: term().
%% @private
%% Loads the appropriate User, KeyID and reads in a registered key for use in
%% connect_auth/4.
prep_auth(Realm, RealmConf) ->
UsersFile = filename:join(zx_lib:zomp_dir(), "zomp.users"),
Users =
case file:consult(UsersFile) of
{ok, U} ->
U;
{error, enoent} ->
ok = log(warning, "You do not have any users configured."),
halt(1)
end,
{User, KeyIDs} =
case lists:keyfind(Realm, 1, Users) of
{Realm, UserName, []} ->
W = "User ~tp does not have any keys registered for realm ~tp.",
ok = log(warning, W, [UserName, Realm]),
ok = log(info, "Contact the following sysop(s) to register a key:"),
{sysops, Sysops} = lists:keyfind(sysops, 1, RealmConf),
PrintContact =
fun({_, _, Email, Name, _, _}) ->
log(info, "Sysop: ~ts Email: ~ts", [Name, Email])
end,
ok = lists:foreach(PrintContact, Sysops),
halt(1);
{Realm, UserName, KeyNames} ->
KIDs = [{Realm, KeyName} || KeyName <- KeyNames],
{{Realm, UserName}, KIDs};
false ->
Message = "You are not a user of the given realm: ~160tp.",
ok = log(warning, Message, [Realm]),
halt(1)
end,
KeyID = hd(KeyIDs),
true = zx_key:ensure_keypair(KeyID),
{ok, Key} = zx_key:load(private, KeyID),
{User, KeyID, Key}.
prep_auth(Realm) ->
UserName = zx_local:select_user(Realm),
KeyName = zx_local:select_private_key({Realm, UserName}),
{ok, Key} = zx_key:load(private, {Realm, KeyName}),
{UserName, KeyName, Key}.
-spec disconnect(gen_tcp:socket()) -> ok.
%% @private
%% Gracefully shut down a socket, logging (but sidestepping) the case when the socket
%% has already been closed by the other side.
pack_and_sign(Command, Payload, Key) ->
Bin = term_to_binary(Payload),
Signed = <<Command:8, Bin/binary>>,
Sig = public_key:sign(Signed, sha512, Key),
SSize = byte_size(Sig),
<<SSize:24, Sig/binary, Signed/binary>>.
disconnect(Socket) ->
case gen_tcp:shutdown(Socket, read_write) of
ok ->
log(info, "Disconnected from ~tp", [Socket]);
{error, Error} ->
Message = "Shutdown connection ~p failed with: ~p",
log(warning, Message, [Socket, Error])
done(Socket) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:1, 0:7>>} ->
zx_net:disconnect(Socket);
{tcp, Socket, Bin} ->
ok = zx_net:disconnect(Socket),
{error, zx_net:err_in(Bin)};
{tcp_closed, Socket} ->
{error, tcp_closed}
after 5000 ->
done(Socket, timeout)
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).
done(Socket, Reason) ->
ok = zx_net:disconnect(Socket),
case is_binary(Reason) of
true -> {error, zx_net:err_in(Reason)};
false -> {error, Reason}
end.

View File

@ -11,7 +11,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([subscribe/2, unsubscribe/2, fetch/3, query/3]).
-export([subscribe/2, unsubscribe/2, request/3]).
-export([start/1, stop/1]).
-export([start_link/1, init/2]).
@ -38,31 +38,17 @@ unsubscribe(Conn, Package) ->
ok.
-spec fetch(Conn, ID, Object) -> ok
when Conn :: pid(),
ID :: zx_daemon:id(),
Object :: zx_daemon:object().
%% @doc
%% This requests a package be fetched from upstream.
%% Only to be called by zx_daemon.
%% Results must be returned with the ID via zx_daemon:result/2.
fetch(Conn, ID, Object) ->
Conn ! {fetch, ID, Object},
ok.
-spec query(Conn, ID, Action) -> ok
-spec request(Conn, ID, Action) -> ok
when Conn :: pid(),
ID :: zx_daemon:id(),
Action :: zx_daemon:action().
%% @doc
%% Wraps any legal query.
%% Wraps any legal request.
%% Only to be called by zx_daemon.
%% Results must be returned with the ID via zx_daemon:result/2.
query(Conn, ID, Action) ->
Conn ! {query, ID, Action},
request(Conn, ID, Action) ->
Conn ! {request, ID, Action},
ok.
@ -109,7 +95,6 @@ start_link(Target) ->
%% gen_server callback. For more information refer to the OTP documentation.
init(Parent, Target) ->
ok = log(info, "Connecting to ~tp", [Target]),
Debug = sys:debug_options([]),
ok = proc_lib:init_ack(Parent, {ok, self()}),
connect(Parent, Debug, Target).
@ -124,12 +109,12 @@ init(Parent, Target) ->
Target :: zx:host().
connect(Parent, Debug, {Host, Port}) ->
Options = [{packet, 4}, {mode, binary}, {nodelay, true}, {active, true}],
Options = [{packet, 4}, {mode, binary}, {nodelay, true}, {active, once}],
case gen_tcp:connect(Host, Port, Options, 5000) of
{ok, Socket} ->
confirm_service(Parent, Debug, Socket);
{error, Error} ->
ok = log(warning, "Connection problem with ~tp: ~tp", [Host, Error]),
ok = log(warning, "Connection problem with ~160tp: ~160tp", [Host, Error]),
ok = zx_daemon:report(failed),
terminate()
end.
@ -144,11 +129,13 @@ connect(Parent, Debug, {Host, Port}) ->
%% another node.
confirm_service(Parent, Debug, Socket) ->
ok = gen_tcp:send(Socket, <<"LEAF 1:">>),
Realms = zx_lib:list_realms(),
Message = <<"ZOMP LEAF 1:", (term_to_binary(Realms))/binary>>,
ok = gen_tcp:send(Socket, Message),
receive
{tcp, Socket, <<0:8, RealmsBin/binary>>} ->
{ok, Realms} = zx_lib:b_to_ts(RealmsBin),
ok = zx_daemon:report({connected, Realms}),
{ok, Available} = zx_lib:b_to_ts(RealmsBin),
ok = zx_daemon:report({connected, Available}),
loop(Parent, Debug, Socket);
{tcp, Socket, <<1:8, HostsBin/binary>>} ->
{ok, Hosts} = zx_lib:b_to_ts(HostsBin),
@ -198,29 +185,26 @@ loop(Parent, Debug, Socket) ->
ok = zx_daemon:report({serial_update, Realm, Serial}),
loop(Parent, Debug, Socket);
{tcp, Socket, Unexpected} ->
ok = log(warning, "Funky data from node: ~tp", [Unexpected]),
ok = log(warning, "Funky data from node: ~160tp", [Unexpected]),
ok = zx_net:disconnect(Socket),
terminate();
{request, ID, Action} ->
Result = dispatch(Socket, ID, Action),
ok = zx_daemon:result(ID, Result),
loop(Parent, Debug, Socket);
{subscribe, Package} ->
ok = do_subscribe(Socket, Package),
loop(Parent, Debug, Socket);
{unsubscribe, Package} ->
ok = do_unsubscribe(Socket, Package),
loop(Parent, Debug, Socket);
{query, ID, Action} ->
Result = do_query(Socket, Action),
ok = zx_daemon:result(ID, Result),
loop(Parent, Debug, Socket);
{fetch, ID, PackageID} ->
ok = do_fetch(Socket, ID, PackageID),
loop(Parent, Debug, Socket);
stop ->
ok = zx_net:disconnect(Socket),
terminate();
{tcp_closed, Socket} ->
handle_unexpected_close();
Unexpected ->
ok = log(warning, "Unexpected message: ~tp", [Unexpected]),
ok = log(warning, "Unexpected message: ~160tp", [Unexpected]),
loop(Parent, Debug, Socket)
end.
@ -235,7 +219,7 @@ handle_package_update(Sig, Bin) ->
true ->
zx_daemon:notify(Package, {update, PackageID});
false ->
ok = log(error, "Received an unverified update message: ~tp", [Message]),
ok = log(error, "Received an unverified update message: ~160tp", [Message]),
terminate()
end.
@ -260,26 +244,33 @@ do_unsubscribe(Socket, Package) ->
wait_ok(Socket).
do_query(Socket, {list, Realm}) ->
Reference = term_to_binary(Realm),
Message = <<0:1, 3:7, Reference/binary>>,
send_query(Socket, Message);
do_query(Socket, {list, Realm, Name}) ->
Reference = term_to_binary({Realm, Name}),
Message = <<0:1, 4:7, Reference/binary>>,
send_query(Socket, Message);
do_query(Socket, {list, Realm, Name, Version}) ->
Reference = term_to_binary({Realm, Name, Version}),
Message = <<0:1, 4:7, Reference/binary>>,
send_query(Socket, Message);
do_query(Socket, {latest, Realm, Name}) ->
Reference = term_to_binary({Realm, Name}),
Message = <<0:1, 6:7, Reference/binary>>,
send_query(Socket, Message);
do_query(Socket, {latest, Realm, Name, Version}) ->
Reference = term_to_binary({Realm, Name, Version}),
Message = <<0:1, 6:7, Reference/binary>>,
send_query(Socket, Message).
-spec wait_ok(gen_tcp:socket()) -> ok | no_return().
wait_ok(Socket) ->
receive
{tcp, Socket, <<0:1, 0:7>>} -> ok
after 5000 -> handle_timeout(Socket)
end.
dispatch(Socket, ID, Action) ->
case Action of
{list, R} ->
send_query(Socket, <<0:1, 3:7, (term_to_binary(R))/binary>>);
{list, R, N} ->
send_query(Socket, <<0:1, 4:7, (term_to_binary({R, N}))/binary>>);
{list, R, N, V} ->
send_query(Socket, <<0:1, 4:7, (term_to_binary({R, N, V}))/binary>>);
{latest, R, N} ->
send_query(Socket, <<0:1, 5:7, (term_to_binary({R, N}))/binary>>);
{latest, R, N, V} ->
send_query(Socket, <<0:1, 5:7, (term_to_binary({R, N, V}))/binary>>);
{fetch, R, N, V} ->
make_fetch(Socket, ID, {R, N, V});
_ ->
Message = "Received unexpected request action. ID: ~tp, Action: ~200tp",
log(warning, Message, [ID, Action])
end.
send_query(Socket, Message) ->
@ -290,105 +281,67 @@ send_query(Socket, Message) ->
wait_query(Socket) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<1:1, 0:7>>} ->
ok = pong(Socket),
wait_query(Socket);
{tcp, Socket, <<0:1, 0:7, Bin/binary>>} ->
{ok, Result} = zx_lib:b_to_ts(Bin),
Result;
{tcp, Socket, <<0:1, 1:7>>} ->
ok = zx_daemon:report(failed),
terminate();
{tcp, Socket, <<0:1, 2:7>>} ->
{error, bad_realm};
{tcp, Socket, <<0:1, 3:7>>} ->
{error, bad_package};
{tcp, Socket, <<0:1, 4:7>>} ->
{error, bad_version}
after 5000 ->
handle_timeout(Socket)
{tcp, Socket, <<1:1, 0:7>>} -> wait_pong(Socket);
{tcp, Socket, <<0:1, 0:7>>} -> ok;
{tcp, Socket, <<0:1, 0:7, Bin/binary>>} -> zx_lib:b_to_ts(Bin);
{tcp, Socket, Bin} -> {error, zx_net:err_in(Bin)}
after 5000 -> handle_timeout(Socket)
end.
wait_pong(Socket) ->
ok = pong(Socket),
wait_query(Socket).
pong(Socket) ->
gen_tcp:send(Socket, <<1:1, 0:7>>).
-spec do_fetch(Socket, ID, PackageID) -> Result
-spec make_fetch(Socket, ID, PackageID) -> Result
when Socket :: gen_tcp:socket(),
ID :: zx_daemon:id(),
PackageID :: zx:package_id(),
Result :: ok.
Result :: {done, binary()}.
%% @private
%% Download a package to the local cache.
do_fetch(Socket, ID, PackageID) ->
Reference = binary_to_term(PackageID),
Message = <<0:1, 7:7, Reference/binary>>,
make_fetch(Socket, ID, PackageID) ->
TermBin = term_to_binary(PackageID),
Message = <<0:1, 6:7, TermBin/binary>>,
ok = gen_tcp:send(Socket, Message),
ok = wait_hops(Socket, ID),
{ok, Bin} = receive_zsp(Socket),
zx_daemon:result(ID, {done, Bin}).
{ok, Bin} = zx_net:rx(Socket),
{done, Bin}.
wait_hops(Socket, ID) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:1, 0:7, 0:8>>} ->
ok = inet:setopts(Socket, [{packet, 0}]),
gen_tcp:send(Socket, <<0:1, 0:7>>);
ok;
{tcp, Socket, <<0:1, 0:7, Distance:8>>} ->
ok = zx_daemon:result(ID, {hops, Distance}),
wait_hops(Socket, ID);
{tcp, Socket, <<0:1, 1:7>>} ->
{error, bad_message};
{tcp, Socket, <<0:1, 2:7>>} ->
{error, bad_realm};
{tcp, Socket, <<0:1, 3:7>>} ->
{error, bad_package};
{tcp, Socket, <<0:1, 4:7>>} ->
{error, bad_version};
{tcp, Socket, <<0:1, 5:7>>} ->
handle_timeout(Socket)
handle_timeout(Socket);
{tcp, Socket, Bin} ->
Reason = zx_net:err_in(Bin),
ok = log(error, "Failed in wait_hops/2 with reason: ~tp", [Reason]),
ok = zx_net:disconnect(Socket),
terminate()
after 60000 ->
handle_timeout(Socket)
end.
receive_zsp(Socket) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<Size:32, Bin:Size/binary>>} ->
{ok, Bin};
{tcp, Socket, <<Total:32, Bin/binary>>} ->
Size = byte_size(Bin),
receive_zsp(Socket, Total, Size, Bin)
after 5000 ->
handle_timeout(Socket)
end.
receive_zsp(Socket, Total, SoFar, Bin) when Total > SoFar ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, New} ->
Size = byte_size(New),
NewBin = <<Bin/binary, New/binary>>,
receive_zsp(Socket, Total, Size + SoFar, NewBin)
after 5000 ->
handle_timeout(Socket)
end;
receive_zsp(Socket, Total, Total, Bin) ->
ok = inet:setopts(Socket, [{packet, 4}]),
ok = gen_tcp:send(Socket, <<0:1, 0:7>>),
{ok, Bin}.
%%% Terminal handlers
-spec handle_unexpected_close() -> no_return().
handle_unexpected_close() ->
ok = log(info, "Connection closed unexpectedly."),
ok = zx_daemon:report(disconnected),
terminate().
@ -401,15 +354,6 @@ handle_timeout(Socket) ->
terminate().
-spec wait_ok(gen_tcp:socket()) -> ok | no_return().
wait_ok(Socket) ->
receive
{tcp, Socket, <<0:1, 0:7>>} -> ok
after 5000 -> handle_timeout(Socket)
end.
-spec terminate() -> no_return().
%% @private
%% Convenience wrapper around the suicide call.

View File

@ -144,11 +144,11 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([declare_proxy/0]).
-export([pass_meta/3,
subscribe/1, unsubscribe/1,
list/0, list/1, list/2, list/3, latest/1,
verify_key/1, fetch/1, install/1,
pending/1, packagers/1, maintainers/1, sysops/1]).
verify_key/1, fetch/1, install/1, build/1]).
-export([report/1, result/2, notify/2]).
-export([start_link/0, init_connections/0, stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@ -158,7 +158,6 @@
-export_type([id/0, result/0,
realm_list/0, package_list/0, version_list/0, latest_result/0,
fetch_result/0, key_result/0,
pending_result/0, pack_result/0, maint_result/0, sysop_result/0,
sub_message/0]).
@ -199,11 +198,11 @@
-record(conn,
{pid :: pid(),
host :: zx:host(),
realms :: [zx:realm()],
requests :: [id()],
subs :: [{pid(), zx:package()}]}).
{pid = none :: none | pid(),
host = none :: none | zx:host(),
realms = [] :: [zx:realm()],
requests = [] :: [id()],
subs = [] :: [{pid(), zx:package()}]}).
%% State Types
@ -237,6 +236,8 @@
| {list, zx:realm(), zx:name(), zx:version()}
| {latest, zx:realm(), zx:name()}
| {latest, zx:realm(), zx:name(), zx:version()}
| {pending, zx:realm(), zx:name()}
| {approved, zx:realm()}
| {fetch, zx:realm(), zx:name(), zx:version()}
| {fetchkey, zx:realm(), zx:key_name()}.
@ -254,11 +255,7 @@
| version_list()
| latest_result()
| fetch_result()
| key_result()
| pending_result()
| pack_result()
| maint_result()
| sysop_result()}.
| key_result()}.
-type realm_list() :: [zx:realm()].
-type package_list() :: {ok, [zx:name()]}
@ -274,7 +271,7 @@
| bad_version
| timeout}.
-type fetch_result() :: {hops, non_neg_integer()}
| done
| {done, zx:package_id()}
| {error, bad_realm
| bad_package
| bad_version
@ -283,21 +280,6 @@
| {error, bad_realm
| bad_key
| timeout}.
-type pending_result() :: {ok, [zx:version()]}
| {error, bad_realm
| bad_package
| timeout}.
-type pack_result() :: {ok, [zx:user()]}
| {error, bad_realm
| bad_package
| timeout}.
-type maint_result() :: {ok, [zx:user()]}
| {error, bad_realm
| bad_package
| timeout}.
-type sysop_result() :: {ok, [zx:user()]}
| {error, bad_host
| timeout}.
% Subscription Results
@ -306,6 +288,13 @@
Message :: {update, zx:package_id()}
| {error, bad_realm | bad_package}}.
%%% Zomp Interface
-spec declare_proxy() -> ok.
declare_proxy() ->
log(info, "Would be claiming this node for myself right now...").
%%% Requestor Interface
@ -320,9 +309,6 @@
%% references.
pass_meta(Meta, Dir, ArgV) ->
ok = log(info, "Meta: ~tp", [Meta]),
ok = log(info, "Dir : ~tp", [Dir]),
ok = log(info, "ArgV: ~tp", [ArgV]),
gen_server:cast(?MODULE, {pass_meta, Meta, Dir, ArgV}).
@ -460,6 +446,15 @@ verify_key({Realm, KeyName}) ->
request({verify_key, Realm, KeyName}).
-spec fetch(zx:package_id()) -> {ok, id()}.
%% @doc
%% Install the specified package. This returns an id() that will be referenced
%% in a later response message.
fetch(PackageID) ->
gen_server:call(?MODULE, {fetch, PackageID}).
-spec install(Path :: file:filename()) -> zx:outcome().
%% @doc
%% Install a package from a local file.
@ -468,83 +463,10 @@ 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.
-spec build(zx:package_id()) -> zx:outcome().
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().
%% @doc
%% Request the list of versions of a given package that have been submitted but not
%% signed and included in their relevant realm.
%% Crashes the caller if either component of the Package is illegal.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `pending_result()'.
pending({Realm, Name}) ->
true = zx_lib:valid_lower0_9(Realm),
true = zx_lib:valid_lower0_9(Name),
request({pending, Realm, Name}).
-spec packagers(Package) -> {ok, RequestID}
when Package :: zx:package(),
RequestID :: id().
%% @doc
%% Request a list of packagers assigned to work on a given package.
%% Crashes the caller if either component of the Package is illegal.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `pack_result()'.
packagers({Realm, Name}) ->
true = zx_lib:valid_lower0_9(Realm),
true = zx_lib:valid_lower0_9(Name),
request({packagers, Realm, Name}).
-spec maintainers(Package) -> {ok, RequestID}
when Package :: zx:package(),
RequestID :: id().
%% @doc
%% Request a list of maintainers assigned to work on a given package.
%% Crashes the caller if either component of the Package is illegal.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `maint_result()'.
maintainers({Realm, Name}) ->
true = zx_lib:valid_lower0_9(Realm),
true = zx_lib:valid_lower0_9(Name),
request({maintainers, Realm, Name}).
-spec sysops(Realm) -> {ok, RequestID}
when Realm :: zx:realm(),
RequestID :: id().
%% @doc
%% Request a list of sysops in charge of maintaining a given realm. What this
%% effectively does is request the sysops of the prime host of the given realm.
%% Crashes the caller if the Realm string is illegal.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `sysops_result()'.
sysops(Realm) ->
true = zx_lib:valid_lower0_9(Realm),
request({sysops, Realm}).
build(PackageID) ->
gen_server:call(?MODULE, {build, PackageID}).
%% Request Caster
@ -655,8 +577,12 @@ handle_call({install, Path}, _, State) ->
Result = do_import_zsp(Path),
NewState = eval_queue(State),
{reply, Result, NewState};
handle_call({build, PackageID}, _, State) ->
Result = do_build(PackageID),
NewState = eval_queue(State),
{reply, Result, NewState};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]),
ok = log(warning, "Unexpected call ~160tp: ~160tp", [From, Unexpected]),
{noreply, State}.
@ -692,7 +618,7 @@ handle_cast(init_connections, State) ->
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp", [Unexpected]),
ok = log(warning, "Unexpected cast: ~160tp", [Unexpected]),
{noreply, State}.
@ -703,7 +629,7 @@ handle_info({'DOWN', Ref, process, Pid, Reason}, State) ->
NewState = clear_monitor(Pid, Ref, Reason, State),
{noreply, NewState};
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp", [Unexpected]),
ok = log(warning, "Unexpected info: ~160tp", [Unexpected]),
{noreply, State}.
@ -726,7 +652,7 @@ terminate(normal, #s{cx = CX}) ->
ok ->
log(info, "Cache written.");
{error, Reason} ->
Message = "Cache write failed with ~tp",
Message = "Cache write failed with ~160tp",
log(error, Message, [Reason])
end.
@ -742,9 +668,6 @@ terminate(normal, #s{cx = CX}) ->
NewState :: state().
do_pass_meta(Meta, Home, ArgV, State) ->
PackageID = maps:get(package_id, Meta),
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Received meta for ~tp.", [PackageString]),
State#s{meta = Meta, home = Home, argv = ArgV}.
@ -756,9 +679,10 @@ do_pass_meta(Meta, Home, ArgV, State) ->
%% @private
%% Enqueue a subscription request.
do_subscribe(Pid, Package, State = #s{actions = Actions}) ->
do_subscribe(Pid, Package, State = #s{actions = Actions, mx = MX}) ->
NewActions = [{subscribe, Pid, Package} | Actions],
State#s{actions = NewActions}.
NewMX = mx_add_monitor(Pid, {subscriber, Package}, MX),
State#s{actions = NewActions, mx = NewMX}.
-spec do_unsubscribe(Pid, Package, State) -> NextState
@ -769,9 +693,10 @@ do_subscribe(Pid, Package, State = #s{actions = Actions}) ->
%% @private
%% Clear or dequeue a subscription request.
do_unsubscribe(Pid, Package, State = #s{actions = Actions}) ->
do_unsubscribe(Pid, Package, State = #s{actions = Actions, mx = MX}) ->
{ok, NewMX} = mx_del_monitor(Pid, {subscription, Package}, MX),
NewActions = [{unsubscribe, Pid, Package} | Actions],
State#s{actions = NewActions}.
State#s{actions = NewActions, mx = NewMX}.
-spec do_request(Requestor, Action, State) -> NextState
@ -782,9 +707,10 @@ do_unsubscribe(Pid, Package, State = #s{actions = Actions}) ->
%% @private
%% Enqueue requests and update relevant index.
do_request(Requestor, Action, State = #s{id = ID, actions = Actions}) ->
do_request(Requestor, Action, State = #s{id = ID, actions = Actions, mx = MX}) ->
NewActions = [{request, Requestor, ID, Action} | Actions],
State#s{actions = NewActions}.
NewMX = mx_add_monitor(Requestor, {requestor, ID}, MX),
State#s{actions = NewActions, mx = NewMX}.
-spec do_report(Conn, Message, State) -> NewState
@ -820,7 +746,7 @@ do_report(Conn, disconnected, State = #s{mx = MX}) ->
NewMX = mx_del_monitor(Conn, conn, MX),
disconnected(Conn, State#s{mx = NewMX});
do_report(Conn, timeout, State = #s{mx = MX}) ->
ok = log(warning, "Connection ~tp timed out.", [Conn]),
ok = log(warning, "Connection ~160tp timed out.", [Conn]),
NewMX = mx_del_monitor(Conn, conn, MX),
disconnected(Conn, State#s{mx = NewMX}).
@ -1024,11 +950,11 @@ handle_fetch_result(ID, Outcome, {Requestor, _}, Requests, MX) ->
handle_orphan_result(ID, Result, Dropped) ->
case maps:take(ID, Dropped) of
{Request, NewDropped} ->
Message = "Received orphan result for ~tp, ~tp: ~tp",
Message = "Received orphan result for ~160tp, ~160tp: ~160tp",
ok = log(info, Message, [ID, Request, Result]),
NewDropped;
error ->
Message = "Received untracked request result ~tp: ~tp",
Message = "Received untracked request result ~160tp: ~160tp",
ok = log(warning, Message, [ID, Result]),
Dropped
end.
@ -1080,49 +1006,38 @@ eval_queue(State = #s{actions = Actions}) ->
eval_queue([], State) ->
State;
eval_queue([Action = {request, Pid, ID, Message} | Rest],
State = #s{actions = Actions, requests = Requests, mx = MX, cx = CX}) ->
{NewActions, NewRequests, NewMX, NewCX} =
State = #s{actions = Actions, requests = Requests, cx = CX}) ->
{NewActions, NewRequests, NewCX} =
case dispatch_request(Message, ID, CX) of
{dispatched, NextCX} ->
NextRequests = maps:put(ID, {Pid, Message}, Requests),
NextMX = mx_add_monitor(Pid, requestor, MX),
{Actions, NextRequests, NextMX, NextCX};
{result, Response} ->
Pid ! Response,
{Actions, Requests, MX, CX};
{Actions, NextRequests, NextCX};
wait ->
NextActions = [Action | Actions],
NextMX = mx_add_monitor(Pid, requestor, MX),
{NextActions, Requests, NextMX, CX}
{NextActions, Requests, CX};
Result ->
Pid ! Result,
{Actions, Requests, CX}
end,
NewState =
State#s{actions = NewActions,
requests = NewRequests,
mx = NewMX,
cx = NewCX},
NewState = State#s{actions = NewActions, requests = NewRequests, cx = NewCX},
eval_queue(Rest, NewState);
eval_queue([Action = {subscribe, Pid, Package} | Rest],
State = #s{actions = Actions, mx = MX, cx = CX}) ->
{NewActions, NewMX, NewCX} =
State = #s{actions = Actions, cx = CX}) ->
{NewActions, NewCX} =
case cx_add_sub(Pid, Package, CX) of
{need_sub, Conn, NextCX} ->
ok = zx_conn:subscribe(Conn, Package),
NextMX = mx_add_monitor(Pid, subscriber, MX),
{Actions, NextMX, NextCX};
{Actions, NextCX};
{have_sub, NextCX} ->
NextMX = mx_add_monitor(Pid, subscriber, MX),
{Actions, NextMX, NextCX};
{Actions, NextCX};
unassigned ->
NextMX = mx_add_monitor(Pid, subscriber, MX),
{[Action | Actions], NextMX, CX};
{[Action | Actions], CX};
unconfigured ->
Pid ! {z_sub, Package, {error, bad_realm}},
{Actions, MX, CX}
{Actions, CX}
end,
eval_queue(Rest, State#s{actions = NewActions, mx = NewMX, cx = NewCX});
eval_queue([{unsubscribe, Pid, Package} | Rest],
State = #s{mx = MX, cx = CX}) ->
{ok, NewMX} = mx_del_monitor(Pid, {subscription, Package}, MX),
eval_queue(Rest, State#s{actions = NewActions, cx = NewCX});
eval_queue([{unsubscribe, Pid, Package} | Rest], State = #s{cx = CX}) ->
NewCX =
case cx_del_sub(Pid, Package, CX) of
{{drop_sub, ConnPid}, NextCX} ->
@ -1133,11 +1048,11 @@ eval_queue([{unsubscribe, Pid, Package} | Rest],
unassigned ->
CX;
unconfigured ->
Message = "Received 'unsubscribe' request for unconfigured realm: ~tp",
ok = log(warning, Message, [Package]),
M = "Received 'unsubscribe' request for unconfigured realm: ~160tp",
ok = log(warning, M, [Package]),
CX
end,
eval_queue(Rest, State#s{mx = NewMX, cx = NewCX}).
eval_queue(Rest, State#s{cx = NewCX}).
-spec dispatch_request(Action, ID, CX) -> Result
@ -1194,7 +1109,7 @@ clear_monitor(Pid,
disconnected(Pid, State#s{mx = NewMX});
{{Reqs, Subs}, NewMX} ->
NewActions = drop_actions(Pid, Actions),
{NewDropped, NewRequests} = drop_requests(Pid, Dropped, Requests),
{NewDropped, NewRequests} = drop_requests(Reqs, Dropped, Requests),
NewCX = cx_clear_client(Pid, Reqs, Subs, CX),
State#s{actions = NewActions,
requests = NewRequests,
@ -1203,7 +1118,7 @@ clear_monitor(Pid,
cx = NewCX};
unknown ->
Unexpected = {'DOWN', Ref, process, Pid, Reason},
ok = log(warning, "Unexpected info: ~tp", [Unexpected]),
ok = log(warning, "Unexpected info: ~160tp", [Unexpected]),
State
end.
@ -1237,7 +1152,7 @@ drop_requests(ReqIDs, Dropped, Requests) ->
NewDrop = maps:put(K, V, Drop),
{NewDrop, NewKeep}
end,
lists:fold(Partition, {Dropped, Requests}, ReqIDs).
lists:foldl(Partition, {Dropped, Requests}, ReqIDs).
-spec do_fetch(PackageID, Requestor, State) -> NewState
@ -1252,14 +1167,8 @@ 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 ! {result, ID, ok},
ok = do_fetch2(Bin, Requestor, ID),
{ok, State};
Error ->
Requestor ! {result, ID, Error},
{ok, State}
end;
{error, enoent} ->
{Realm, Name, Version} = PackageID,
Action = {fetch, Realm, Name, Version},
@ -1268,6 +1177,15 @@ do_fetch(PackageID, Requestor, State = #s{id = ID}) ->
Requestor ! {result, ID, Error}
end.
do_fetch2(Bin, Requestor, ID) ->
Result =
case do_import_package(Bin) of
ok -> done;
Error -> Error
end,
Requestor ! {result, ID, Result},
ok.
-spec do_import_zsp(file:filename()) -> zx:outcome().
%% @private
@ -1276,15 +1194,9 @@ do_fetch(PackageID, Requestor, State = #s{id = ID}) ->
%% 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}
case file:read_file(Path) of
{ok, Bin} -> do_import_package(Bin);
Error -> Error
end.
@ -1301,8 +1213,7 @@ do_import_package(Bin) ->
end.
-spec import_from_path(ZspPath) -> no_return()
when ZspPath :: file:filename().
-spec import_package(binary()) -> no_return().
%% @private
%% The happy path of .zsp installation.
%% Must NEVER be executed by the zx_daemon directly.
@ -1328,13 +1239,9 @@ do_import_package(Bin) ->
%% 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, {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),
@ -1346,6 +1253,35 @@ import_package(Bin = <<Size:24, Sig:Size/binary, Signed/binary>>) ->
zx_daemon ! {self(), Result}.
-spec do_build(zx:package_id()) -> zx:outcome().
%% @private
%% Build a project from source.
do_build(PackageID) ->
{Pid, Mon} = spawn_monitor(fun() -> make(PackageID) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info}
after 5000 ->
{error, timeout}
end.
-spec make(zx:package_id()) -> no_return().
%% @private
%% Keep (the highly uncertain) build procedure separate from the zx_daemon, but
%% still sequentialized by it.
make(PackageID) ->
Dirs = [zx_lib:ppath(D, PackageID) || D <- [etc, var, tmp, log, lib]],
ok = lists:foreach(fun zx_lib:force_dir/1, Dirs),
ok = file:set_cwd(zx_lib:ppath(lib, PackageID)),
Result = zx_lib:build(),
zx_daemon ! {self(), Result}.
%%% Monitor Index ADT Interface Functions
@ -1359,29 +1295,29 @@ mx_new() ->
-spec mx_add_monitor(Pid, Category, MX) -> NewMX
when Pid :: pid(),
Category :: subscriber
| requestor
Category :: {requestor, id()}
| {subscriber, Sub :: tuple()}
| attempt,
MX :: monitor_index(),
NewMX :: monitor_index().
%% @private
%% Begin monitoring the given Pid, keeping track of its category.
mx_add_monitor(Pid, subscriber, MX) ->
mx_add_monitor(Pid, {subscriber, Sub}, MX) ->
case maps:take(Pid, MX) of
{{Ref, {Subs, Reqs}}, NextMX} ->
maps:put(Pid, {Ref, {Subs + 1, Reqs}}, NextMX);
{{Ref, {Reqs, Subs}}, NextMX} ->
maps:put(Pid, {Ref, {Reqs, [Sub | Subs]}}, NextMX);
error ->
Ref = monitor(process, Pid),
maps:put(Pid, {Ref, {1, 0}}, MX)
maps:put(Pid, {Ref, {[], [Sub]}}, MX)
end;
mx_add_monitor(Pid, requestor, MX) ->
mx_add_monitor(Pid, {requestor, Req}, MX) ->
case maps:take(Pid, MX) of
{{Ref, {Subs, Reqs}}, NextMX} ->
maps:put(Pid, {Ref, {Subs, Reqs + 1}}, NextMX);
{{Ref, {Reqs, Subs}}, NextMX} ->
maps:put(Pid, {Ref, {[Req | Reqs], Subs}}, NextMX);
error ->
Ref = monitor(process, Pid),
maps:put(Pid, {Ref, {0, 1}}, MX)
maps:put(Pid, {Ref, {[Req], []}}, MX)
end;
mx_add_monitor(Pid, attempt, MX) ->
false = maps:is_key(Pid, MX),
@ -1422,13 +1358,13 @@ mx_del_monitor(Pid, conn, MX) ->
{{Ref, conn}, NewMX} = maps:take(Pid, MX),
true = demonitor(Ref, [flush]),
NewMX;
mx_del_monitor(Pid, {requestor, ID}, MX) ->
mx_del_monitor(Pid, {requestor, Req}, MX) ->
case maps:take(Pid, MX) of
{{Ref, {[ID], []}}, NextMX} ->
{{Ref, {[Req], []}}, NextMX} ->
true = demonitor(Ref, [flush]),
NextMX;
{{Ref, {Reqs, Subs}}, NextMX} when Reqs > 0 ->
NewReqs = lists:delete(ID, Reqs),
{{Ref, {Reqs, Subs}}, NextMX} ->
NewReqs = lists:delete(Req, Reqs),
maps:put(Pid, {Ref, {NewReqs, Subs}}, NextMX)
end;
mx_del_monitor(Pid, {subscriber, Sub}, MX) ->
@ -1436,7 +1372,7 @@ mx_del_monitor(Pid, {subscriber, Sub}, MX) ->
{{Ref, {[], [Sub]}}, NextMX} ->
true = demonitor(Ref, [flush]),
NextMX;
{{Ref, {Reqs, Subs}}, NextMX} when Subs > 0 ->
{{Ref, {Reqs, Subs}}, NextMX} ->
NewSubs = lists:delete(Sub, Subs),
maps:put(Pid, {Ref, {Reqs, NewSubs}}, NextMX)
end.
@ -1484,7 +1420,7 @@ cx_load() ->
{ok, Realms} ->
#cx{realms = maps:from_list(Realms)};
{error, Reason} ->
Message = "Realm data and host cache load failed with : ~tp",
Message = "Realm data and host cache load failed with : ~160tp",
ok = log(error, Message, [Reason]),
ok = log(warning, "No realms configured."),
#cx{}
@ -1524,7 +1460,7 @@ cx_populate(Realm, CX) ->
Record = cx_load_realm_meta(Meta),
[{Realm, Record} | CX];
{error, Reason} ->
Message = "Loading realm ~tp failed with: ~tp. Skipping...",
Message = "Loading realm ~160tp failed with: ~160tp. Skipping...",
ok = log(warning, Message, [Realm, Reason]),
CX
end.

View File

@ -13,7 +13,7 @@
-license("GPL-3.0").
-export([ensure_keypair/1, have_key/2, path/2,
prompt_keygen/0, generate_rsa/1,
prompt_keygen/1, generate_rsa/1,
load/2, verify/3]).
-include("zx_logger.hrl").
@ -30,15 +30,15 @@ ensure_keypair(KeyID = {Realm, KeyName}) ->
{true, true} ->
true;
{false, true} ->
Format = "Public key ~tp/~tp cannot be found",
Format = "Public key ~ts/~ts cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2};
{true, false} ->
Format = "Private key ~tp/~tp cannot be found",
Format = "Private key ~ts/~ts cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2};
{false, false} ->
Format = "Key pair ~tp/~tp cannot be found",
Format = "Key pair ~ts/~ts cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2}
end.
@ -70,11 +70,11 @@ path(private, {Realm, KeyName}) ->
%%% Key generation
-spec prompt_keygen() -> zx:key_id().
-spec prompt_keygen(zx:user_id()) -> zx:key_name().
%% @private
%% Prompt the user for a valid KeyPrefix to use for naming a new RSA keypair.
prompt_keygen() ->
prompt_keygen(UserID = {Realm, UserName}) ->
Message =
"~nKEY NAME~n"
"Enter a name for your new key pair.~n"
@ -83,13 +83,15 @@ prompt_keygen() ->
"consecutive underscores. (That is: [a-z0-9_])~n"
" Example: my_key~n",
ok = io:format(Message),
Input = zx_tty:get_input(),
case zx_lib:valid_lower0_9(Input) of
KeyTag = zx_tty:get_input(),
case zx_lib:valid_lower0_9(KeyTag) of
true ->
Input;
KeyName = UserName ++ "-" ++ KeyTag,
ok = zx_key:generate_rsa({Realm, KeyName}),
KeyName;
false ->
ok = io:format("Bad key name ~tp. Try again.~n", [Input]),
prompt_keygen()
ok = io:format("Bad key name ~ts. Try again.~n", [KeyTag]),
prompt_keygen(UserID)
end.
@ -117,7 +119,7 @@ generate_rsa(KeyID, BaseName) ->
PemFile = BaseName ++ ".pub.pem",
KeyFile = path(private, KeyID),
PubFile = path(public, KeyID),
ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]),
ok = log(info, "Generating ~s and ~s. Please be patient...", [KeyFile, PubFile]),
case gen_p_key(KeyFile) of
ok ->
ok = der_to_pem(KeyFile, PemFile),
@ -252,7 +254,6 @@ load(Type, KeyID) ->
public -> 'RSAPublicKey'
end,
Path = path(Type, KeyID),
ok = log(info, "Loading key from file ~ts", [Path]),
case file:read_file(Path) of
{ok, Bin} -> {ok, public_key:der_decode(DerType, Bin)};
Error -> Error

View File

@ -25,16 +25,19 @@
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, zsp_path/1,
zsp_name/1, zsp_path/1,
find_latest_compatible/2, installed/1,
realm_conf/1, load_realm_conf/1,
build/0,
rm_rf/1, rm/1,
time_diff/2, elapsed_time/1,
b_to_t/1, b_to_ts/1]).
-include("zx_logger.hrl").
-type core_dir() :: etc | var | tmp | log | key | zsp | lib.
%%% Functions
@ -70,13 +73,7 @@ find_zomp_dir() ->
end.
-spec path(Type) -> Path
when Type :: etc
| var
| tmp
| log
| lib,
Path :: file:filename().
-spec path(core_dir()) -> file:filename().
%% @private
%% Return the top-level path of the given type in the Zomp/ZX system.
@ -89,14 +86,7 @@ path(zsp) -> filename:join(zomp_dir(), "zsp");
path(lib) -> filename:join(zomp_dir(), "lib").
-spec path(Type, Realm) -> Path
when Type :: etc
| var
| tmp
| log
| lib,
Realm :: zx:realm(),
Path :: file:filename().
-spec path(core_dir(), zx:realm()) -> file:filename().
%% @private
%% Return the realm-level path of the given type in the Zomp/ZX system.
@ -104,15 +94,7 @@ path(Type, Realm) ->
filename:join(path(Type), Realm).
-spec path(Type, Realm, Name) -> Path
when Type :: etc
| var
| tmp
| log
| lib,
Realm :: zx:realm(),
Name :: zx:name(),
Path :: file:filename().
-spec path(core_dir(), zx:realm(), zx:name()) -> file:filename().
%% @private
%% Return the package-level path of the given type in the Zomp/ZX system.
@ -120,16 +102,7 @@ path(Type, Realm, Name) ->
filename:join([path(Type), Realm, Name]).
-spec path(Type, Realm, Name, Version) -> Path
when Type :: etc
| var
| tmp
| log
| lib,
Realm :: zx:realm(),
Name :: zx:name(),
Version :: zx:version(),
Path :: file:filename().
-spec path(core_dir(), zx:realm(), zx:name(), zx:version()) -> file:filename().
%% @private
%% Return the version-specific level path of the given type in the Zomp/ZX system.
@ -138,14 +111,7 @@ path(Type, Realm, Name, Version) ->
filename:join([path(Type), Realm, Name, VersionString]).
-spec ppath(Type, PackageID) -> Path
when Type :: etc
| var
| tmp
| log
| lib,
PackageID :: zx:package_id(),
Path :: file:filename().
-spec ppath(core_dir(), zx:package_id()) -> file:filename().
%% @private
%% An alias for path/4, but more convenient when needing a path from a closed
%% package_id().
@ -257,7 +223,7 @@ read_project_meta(Dir) ->
{error, enoent} ->
{error, "No project zomp.meta file. Wrong directory? Not initialized?", 2};
Error ->
ok = log(error, "Read from zomp.meta failed with: ~tp", [Error]),
ok = log(error, "Read from zomp.meta failed with: ~tw", [Error]),
Error
end.
@ -327,7 +293,7 @@ exec_shell(CMD) ->
ok;
Out ->
Trimmed = string:trim(Out, trailing, "\r\n"),
log(info, "os:cmd(~tp) -> ~ts", [CMD, Trimmed])
log(info, "os:cmd(~tw) -> ~ts", [CMD, Trimmed])
end.
@ -335,8 +301,7 @@ exec_shell(CMD) ->
%% @private
%% Check whether a provided string is a valid lower0_9.
valid_lower0_9([Char | Rest])
when $a =< Char, Char =< $z ->
valid_lower0_9([Char | Rest]) when $a =< Char, Char =< $z ->
valid_lower0_9(Rest, Char);
valid_lower0_9(_) ->
false.
@ -363,8 +328,7 @@ valid_lower0_9(_, _) ->
%% @private
%% Check whether a provided string is a valid label.
valid_label([Char | Rest])
when $a =< Char, Char =< $z ->
valid_label([Char | Rest]) when $a =< Char, Char =< $z ->
valid_label(Rest, Char);
valid_label(_) ->
false.
@ -582,13 +546,13 @@ package_string(_) ->
{error, invalid_package_id}.
-spec namify_zsp(PackageID) -> ZrpFileName
-spec zsp_name(PackageID) -> ZrpFileName
when PackageID :: zx:package_id(),
ZrpFileName :: file:filename().
%% @private
%% Map a PackageID to its correct .zsp package file name.
namify_zsp(PackageID) ->
zsp_name(PackageID) ->
{ok, PackageString} = package_string(PackageID),
PackageString ++ ".zsp".
@ -596,7 +560,7 @@ namify_zsp(PackageID) ->
-spec zsp_path(zx:package_id()) -> file:filename().
zsp_path(PackageID = {Realm, _, _}) ->
filename:join(path(zsp, Realm), namify_zsp(PackageID)).
filename:join(path(zsp, Realm), zsp_name(PackageID)).
-spec find_latest_compatible(Version, Versions) -> Result
@ -675,7 +639,7 @@ load_realm_conf(Realm) ->
{ok, C} ->
{ok, maps:from_list(C)};
Error ->
ok = log(warning, "Loading realm conf ~ts failed with: ~tp", [Path, Error]),
ok = log(warning, "Loading realm conf ~ts failed with: ~tw", [Path, Error]),
Error
end.
@ -732,6 +696,25 @@ rm(Path) ->
end.
-spec time_diff(Before, After) -> Diff
when Before :: calendar:datetime(),
After :: calendar:datetime(),
Diff :: integer().
time_diff(Before, After) ->
Early = calendar:datetime_to_gregorian_seconds(Before),
Late = calendar:datetime_to_gregorian_seconds(After),
Late - Early.
-spec elapsed_time(Timestamp) -> Diff
when Timestamp :: calendar:datetime(),
Diff :: integer().
elapsed_time(Timestamp) ->
time_diff(Timestamp, calendar:universal_time()).
-spec b_to_t(binary()) -> {ok, term()} | error.
%% @private
%% A wrapper for the binary_to_term/1 BIF to hide the try..catch mess in the places we
@ -752,7 +735,7 @@ b_to_t(Binary) ->
b_to_ts(Binary) ->
try
binary_to_term(Binary, [safe])
{ok, binary_to_term(Binary, [safe])}
catch
error:badarg -> error
end.

File diff suppressed because it is too large Load Diff

View File

@ -9,11 +9,48 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([disconnect/1]).
-export([peername/1,
host_string/1,
disconnect/1,
tx/2, tx/3,
rx/1, rx/2, rx/3,
err_ex/1, err_in/1]).
-include("zx_logger.hrl").
%%% Library Functions
-spec peername(Socket) -> Result
when Socket :: gen_tcp:socket(),
Result :: {ok, zx:host()}
| {error, inet:posix()}.
%% @doc
%% Returns the IPv4 or IPv6 peer-side address of a connected socket, translating the
%% address to IPv4 in the case that it is a translated IPv4 -> IPv6 address.
peername(Socket) ->
case inet:peername(Socket) of
{ok, {{0, 0, 0, 0, 0, 65535, X, Y}, Port}} ->
<<A:8, B:8, C:8, D:8>> = <<X:16, Y:16>>,
{ok, {{A, B, C, D}, Port}};
Other ->
Other
end.
-spec host_string(zx:host()) -> string().
host_string({Address, Port}) when is_list(Address) ->
PortS = integer_to_list(Port),
Address ++ ":" ++ PortS;
host_string({Address, Port}) ->
AddressS = inet:ntoa(Address),
PortS = integer_to_list(Port),
AddressS ++ ":" ++ PortS.
-spec disconnect(Socket) -> ok
when Socket :: gen_tcp:socket().
%% @doc
@ -21,12 +58,12 @@
%% disconnected on the other side.
disconnect(Socket) ->
case zomp:peername(Socket) of
case peername(Socket) of
{ok, {Addr, Port}} ->
Host = inet:ntoa(Addr),
disconnect(Socket, Host, Port);
{error, Reason} ->
log(warning, "Disconnect failed with: ~p", [Reason])
log(warning, "Disconnect failed with: ~w", [Reason])
end.
@ -38,17 +75,182 @@ disconnect(Socket) ->
disconnect(Socket, Host, Port) ->
case gen_tcp:shutdown(Socket, read_write) of
ok ->
log(info, "~ts:~w disconnected", [Host, Port]);
ok;
{error, enotconn} ->
log(info, "~ts:~w disconnected", [Host, Port]),
receive
{tcp_closed, Socket} -> ok
after 0 -> ok
end;
{error, E} ->
log(warning, "~ts:~w disconnect failed with: ~p", [Host, Port, E]),
log(warning, "~ts:~w disconnect failed with: ~w", [Host, Port, E]),
receive
{tcp_closed, Socket} -> ok
after 0 -> ok
end
end.
-spec tx(Socket, Bytes) -> Result
when Socket :: gen_tcp:socket(),
Bytes :: iodata(),
Result :: ok
| {error, Reason},
Reason :: tcp_closed
| timeout
| inet:posix().
tx(Socket, Bytes) ->
tx(Socket, Bytes, 5000).
-spec tx(Socket, Bytes, Timeout) -> Result
when Socket :: gen_tcp:socket(),
Bytes :: iodata(),
Timeout :: pos_integer(),
Result :: ok
| {error, Reason},
Reason :: tcp_closed
| timeout
| inet:posix().
tx(Socket, Bytes, Timeout) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:8>>} -> tx2(Socket, Bytes, Timeout);
{tcp, Socket, Bin} -> {error, Bin};
{tcp_closed, Socket} -> {error, tcp_closed}
after Timeout -> {error, timeout}
end.
tx2(Socket, Bytes, Timeout) ->
case gen_tcp:send(Socket, Bytes) of
ok ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<0:8>>} -> ok;
{tcp, Socket, Bin} -> {error, Bin};
{tcp_closed, Socket} -> {error, tcp_closed}
after Timeout -> {error, timeout}
end;
Error ->
Error
end.
-spec rx(Socket) -> Result
when Socket :: gen_tcp:socket(),
Result :: {ok, binary()}
| {error, timeout | tcp_closed}.
%% @doc
%% Abstract large receives with a fixed timeout of 5 seconds between segments.
rx(Socket) ->
rx(Socket, 5000, []).
-spec rx(Socket, Timeout) -> Result
when Socket :: gen_tcp:socket(),
Timeout :: pos_integer(),
Result :: {ok, binary()}
| {error, timeout | tcp_closed}.
%% @doc
%% Abstract large receives with a fixed timeout between segments.
rx(Socket, Timeout) ->
rx(Socket, Timeout, []).
-spec rx(Socket, Timeout, Watchers) -> Result
when Socket :: gen_tcp:socket(),
Timeout :: pos_integer(),
Watchers :: [pid()],
Result :: {ok, binary()}
| {error, timeout | tcp_closed}.
%% @doc
%% Abstract large receives with a fixed timeout between segments and progress update
%% messages to listening processes.
rx(Socket, Timeout, Watchers) ->
ok = inet:setopts(Socket, [{active, once}, {packet, 0}]),
ok = gen_tcp:send(Socket, <<1:32, 0:8>>),
receive
{tcp, Socket, <<Size:32, Bin/binary>>} ->
ok = broadcast({rx, Socket, start, Size}, Watchers),
Left = Size - byte_size(Bin),
rx(Socket, Watchers, Left, Bin, Timeout);
{tcp_closed, Socket} ->
{error, tcp_closed}
after Timeout ->
{error, timeout}
end.
rx(Socket, Watchers, 0, Bin, _) ->
ok = inet:setopts(Socket, [{packet, 4}]),
ok = gen_tcp:send(Socket, <<0:8>>),
ok = broadcast({rx, Socket, done}, Watchers),
{ok, Bin};
rx(Socket, Watchers, Left, Buffer, Timeout) when Left > 0 ->
ok = broadcast({rx, Socket, pending, Left}, Watchers),
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, Bin} ->
NewLeft = Left - byte_size(Bin),
rx(Socket, Watchers, NewLeft, <<Buffer/binary, Bin/binary>>, Timeout);
{tcp_closed, Socket} ->
{error, tcp_closed}
after Timeout ->
{error, timeout}
end;
rx(Socket, Watchers, Left, _, _) when Left < 0 ->
ok = broadcast({rx, Socket, overflow}, Watchers),
{error, bad_message}.
broadcast(Message, Pids) ->
Notify = fun(P) -> P ! Message end,
lists:foreach(Notify, Pids).
err_ex(bad_message) -> <<0:1, 1:7>>;
err_ex(timeout) -> <<0:1, 2:7>>;
err_ex(bad_realm) -> <<0:1, 3:7>>;
err_ex(bad_package) -> <<0:1, 4:7>>;
err_ex(bad_version) -> <<0:1, 5:7>>;
err_ex(bad_serial) -> <<0:1, 6:7>>;
err_ex(bad_user) -> <<0:1, 7:7>>;
err_ex(bad_key) -> <<0:1, 8:7>>;
err_ex(bad_sig) -> <<0:1, 9:7>>;
err_ex(no_permission) -> <<0:1, 10:7>>;
err_ex(unknown_user) -> <<0:1, 11:7>>;
err_ex(bad_request) -> <<0:1, 12:7>>;
err_ex(realm_mismatch) -> <<0:1, 13:7>>;
err_ex(not_prime) -> <<0:1, 14:7>>;
err_ex(bad_auth) -> <<0:1, 15:7>>;
err_ex(unauthorized_key) -> <<0:1, 16:7>>;
err_ex(already_exists) -> <<0:1, 17:7>>;
err_ex(busy) -> <<0:1, 18:7>>;
err_ex({retry, Seconds}) -> <<0:1, 19:7, Seconds:24>>;
err_ex(Reason) -> [<<0:1, 127:7>>, io_lib:format("~tw", [Reason])].
err_in(<<0:1, 1:7>>) -> bad_message;
err_in(<<0:1, 2:7>>) -> timeout;
err_in(<<0:1, 3:7>>) -> bad_realm;
err_in(<<0:1, 4:7>>) -> bad_package;
err_in(<<0:1, 5:7>>) -> bad_version;
err_in(<<0:1, 6:7>>) -> bad_serial;
err_in(<<0:1, 7:7>>) -> bad_user;
err_in(<<0:1, 8:7>>) -> bad_key;
err_in(<<0:1, 9:7>>) -> bad_sig;
err_in(<<0:1, 10:7>>) -> no_permission;
err_in(<<0:1, 11:7>>) -> unknown_user;
err_in(<<0:1, 12:7>>) -> bad_request;
err_in(<<0:1, 13:7>>) -> realm_mismatch;
err_in(<<0:1, 14:7>>) -> not_prime;
err_in(<<0:1, 15:7>>) -> bad_auth;
err_in(<<0:1, 16:7>>) -> unauthorized_key;
err_in(<<0:1, 17:7>>) -> already_exists;
err_in(<<0:1, 18:7>>) -> busy;
err_in(<<0:1, 19:7, Seconds:24>>) -> {retry, Seconds};
err_in(<<0:1, 127:7, Reason/binary>>) -> unicode:characters_to_list(Reason).

View File

@ -62,7 +62,7 @@ load() ->
{ok, List} ->
populate_data(List);
{error, Reason} ->
ok = log(error, "Load ~tp failed with: ~tp", [Path, Reason]),
ok = log(error, "Load ~160tp failed with: ~160tp", [Path, Reason]),
Data = #d{},
ok = save(Data),
Data
@ -203,7 +203,7 @@ maxconn(Value, Data) when is_integer(Value) and Value > 0 ->
Data#d{maxconn = Value}.
-spec managed(data()) -> list(zx:realm()).
-spec managed(data()) -> [zx:realm()].
%% @doc
%% Return the list of realms managed by the current node.
@ -241,10 +241,10 @@ add_managed(Realm, Data = #d{managed = Managed}) ->
case zx_lib:realm_exists(Realm) of
true ->
NewData = Data#d{managed = sets:add_element(Realm, Managed)},
ok = log(info, "Now managing realm: ~tp", [Realm]),
ok = log(info, "Now managing realm: ~160tp", [Realm]),
{ok, NewData};
false ->
ok = log(warning, "Cannot manage unconfigured realm: ~tp", [Realm]),
ok = log(warning, "Cannot manage unconfigured realm: ~160tp", [Realm]),
{error, unconfigured}
end.
@ -262,10 +262,10 @@ rem_managed(Realm, Data = #d{managed = Managed}) ->
case sets:is_element(Realm, Managed) of
true ->
NewData = Data#d{managed = sets:del_element(Realm, Managed)},
ok = log(info, "No longer managing realm: ~tp", [Realm]),
ok = log(info, "No longer managing realm: ~160tp", [Realm]),
{ok, NewData};
false ->
ok = log(warning, "Cannot stop managing unmanaged realm: ~tp", [Realm]),
ok = log(warning, "Cannot stop managing unmanaged realm: ~160tp", [Realm]),
{error, unmanaged}
end.

View File

@ -10,7 +10,10 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([get_input/0, get_input/1, get_input/2, select/1, select_string/1]).
-export([get_input/0, get_input/1, get_input/2, get_input/3,
get_lower0_9/1, get_lower0_9/2, get_lower0_9/3,
select/1, select_string/1,
derp/0]).
%%% Type Definitions
@ -32,12 +35,12 @@ get_input() ->
end.
-spec get_input(Prompt :: string()) -> string().
-spec get_input(Info :: string()) -> string().
%% @private
%% Introduce
%% Prompt the user for input.
get_input(Prompt) ->
get_input(Prompt, []).
get_input(Info) ->
get_input(Info, []).
-spec get_input(Format, Args) -> string()
@ -46,14 +49,55 @@ get_input(Prompt) ->
%% @private
%% Allow the caller to use io format strings and args to create a prompt.
get_input(Format, Args) ->
Prompt = io_lib:format(Format, Args),
case string:trim(io:get_line(["(or \"QUIT\") ", Prompt, ": "])) of
get_input(Format, Args, []).
-spec get_input(Format, Args, Prompt) -> string()
when Format :: string(),
Args :: [term()],
Prompt :: string().
%% @private
%% Similar to get_input/2, but allows a default prompt to be inserted after the
%% "QUIT" message (useful for things like bracketed default values).
get_input(Format, Args, Prompt) ->
Info = io_lib:format(Format, Args),
case string:trim(io:get_line([Info, "(or \"QUIT\") ", Prompt, ": "])) of
"QUIT" -> what_a_quitter();
String -> String
end.
-spec get_lower0_9(Prompt :: string()) -> zx:lower0_9().
%% @private
%% Prompt the user for input, constraining the input to valid zx:lower0_9 strings.
get_lower0_9(Prompt) ->
get_lower0_9(Prompt, []).
-spec get_lower0_9(Format, Args) -> zx:lower0_9()
when Format :: string(),
Args :: [term()].
get_lower0_9(Format, Args) ->
get_lower0_9(Format, Args, "").
get_lower0_9(Format, Args, Prompt) ->
String = get_input(Format, Args, Prompt),
case zx_lib:valid_lower0_9(String) of
true ->
String;
false ->
Message = "The string \"~ts\" isn't valid. Try \"[a-z0-9_]*\".~n",
ok = io:format(Message, [String]),
get_lower0_9(Format, Args, Prompt)
end.
-spec select(Options) -> Selected
when Options :: [option()],
Selected :: term().
@ -139,3 +183,9 @@ hurr() -> io:format("That isn't an option.~n").
what_a_quitter() ->
ok = io:format("User abort: \"QUIT\".~nHalting.~n"),
halt(0).
-spec derp() -> ok.
derp() ->
io:format("~nArglebargle, glop-glyf!?!~n~n").

View File

@ -0,0 +1 @@
{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}.

View File

@ -0,0 +1,22 @@
%%% @doc
%%% *PROJECT NAME*: *MODULE*
%%%
%%% This module is currently named `*MODULE*', but you may want to change that.
%%% Remember that changing the name in `-module()' below requires renaming
%%% this file, and it is recommended to run `zx update .app` in the main
%%% project directory to make sure the ebin/*MODULE*.app file stays in
%%% sync with the project whenever you add, remove or rename a module.
%%% @end
-module(*MODULE*).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([hello/0]).
-spec hello() -> ok.
hello() ->
io:format("~p (~p) says \"Hello!\"~n", [self(), ?MODULE]).

View File

@ -0,0 +1,17 @@
#! /usr/bin/env escript
%%% 〘*PROJECT NAME*〙
-mode(compile).
〘*SCRIPT*〙
〘*AUTHOR*〙
〘*COPYRIGHT*〙
〘*LICENSE*〙
-spec main(Args :: [term()]) -> no_return().
main(Args) ->
ok = io:setopts([{encoding, unicode}]),
ok = io:format("Hello, world!~n"),
io:format("I received the args: ~tp~n", [Args]).

View File

@ -0,0 +1,75 @@
%%% @doc
%%% *PROJECT NAME*
%%% @end
-module(*APP MOD*).
-behavior(application).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([listen/1, ignore/0]).
-export([start/0, start/1]).
-export([start/2, stop/1]).
-spec listen(PortNum) -> Result
when PortNum :: inet:port_num(),
Result :: ok
| {error, {listening, inet:port_num()}}.
%% @doc
%% Make the server start listening on a port.
%% Returns an {error, Reason} tuple if it is already listening.
listen(PortNum) ->
*PREFIX*client_man:listen(PortNum).
-spec ignore() -> ok.
%% @doc
%% Make the server stop listening if it is, or continue to do nothing if it isn't.
ignore() ->
*PREFIX*client_man:ignore().
-spec start() -> ok.
%% @doc
%% Start the server in an "ignore" state.
start() ->
ok = application:ensure_started(sasl),
ok = application:start(example_server),
io:format("Starting es...").
-spec start(PortNum) -> ok
when PortNum :: inet:port_number().
%% @doc
%% Start the server and begin listening immediately. Slightly more convenient when
%% playing around in the shell.
start(PortNum) ->
ok = start(),
ok = *PREFIX*client_man:listen(PortNum),
io:format("Startup complete, listening on ~w~n", [PortNum]).
-spec start(normal, term()) -> {ok, pid()}.
%% @private
%% Called by OTP to kick things off. This is for the use of the "application" part of
%% OTP, not to be called by user code.
%% See: http://erlang.org/doc/apps/kernel/application.html
start(normal, _Args) ->
*PREFIX*sup:start_link().
-spec stop(term()) -> ok.
%% @private
%% Similar to start/2 above, this is to be called by the "application" part of OTP,
%% not client code. Causes a (hopefully graceful) shutdown of the application.
stop(_State) ->
ok.

View File

@ -0,0 +1,203 @@
%%% @doc
%%% *PROJECT NAME* Client
%%%
%%% An extremely naive (currently Telnet) client handler.
%%% Unlike other modules that represent discrete processes, this one does not adhere
%%% to any OTP behavior. It does, however, adhere to OTP.
%%%
%%% In some cases it is more comfortable to write socket handlers or a certain
%%% category of state machines as "pure" Erlang processes. This approach is made
%%% OTP-able by use of the proc_lib module, which is the underlying library used
%%% to write the stdlib's behaviors like gen_server, gen_statem, gen_fsm, etc.
%%%
%%% http://erlang.org/doc/design_principles/spec_proc.html
%%% @end
-module(*PREFIX*client).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([start/1]).
-export([start_link/1, init/2]).
-export([system_continue/3, system_terminate/4,
system_get_state/1, system_replace_state/2]).
%%% Type and Record Definitions
-record(s, {socket = none :: none | gen_tcp:socket()}).
%% An alias for the state record above. Aliasing state can smooth out annoyances
%% that can arise from using the record directly as its own type all over the code.
-type state() :: #s{}.
%%% Service Interface
-spec start(ListenSocket) -> Result
when ListenSocket :: gen_tcp:socket(),
Result :: {ok, pid()}
| {error, Reason},
Reason :: {already_started, pid()}
| {shutdown, term()}
| term().
%% @private
%% How the *PREFIX*client_man or a prior *PREFIX*client kicks things off.
%% This is called in the context of *PREFIX*client_man or the prior *PREFIX*client.
start(ListenSocket) ->
*PREFIX*client_sup:start_acceptor(ListenSocket).
-spec start_link(ListenSocket) -> Result
when ListenSocket :: gen_tcp:socket(),
Result :: {ok, pid()}
| {error, Reason},
Reason :: {already_started, pid()}
| {shutdown, term()}
| term().
%% @private
%% This is called by the *PREFIX*client_sup. While start/1 is called to iniate a startup
%% (essentially requesting a new worker be started by the supervisor), this is
%% actually called in the context of the supervisor.
start_link(ListenSocket) ->
proc_lib:start_link(?MODULE, init, [self(), ListenSocket]).
-spec init(Parent, ListenSocket) -> no_return()
when Parent :: pid(),
ListenSocket :: gen_tcp:socket().
%% @private
%% This is the first code executed in the context of the new worker itself.
%% This function does not have any return value, as the startup return is
%% passed back to the supervisor by calling proc_lib:init_ack/2.
%% We see the initial form of the typical arity-3 service loop form here in the
%% call to listen/3.
init(Parent, ListenSocket) ->
ok = io:format("~p Listening.~n", [self()]),
Debug = sys:debug_options([]),
ok = proc_lib:init_ack(Parent, {ok, self()}),
listen(Parent, Debug, ListenSocket).
-spec listen(Parent, Debug, ListenSocket) -> no_return()
when Parent :: pid(),
Debug :: [sys:dbg_opt()],
ListenSocket :: gen_tcp:socket().
%% @private
%% This function waits for a TCP connection. The owner of the socket is still
%% the *PREFIX*client_man (so it can still close it on a call to *PREFIX*client_man:ignore/0),
%% but the only one calling gen_tcp:accept/1 on it is this process. Closing the socket
%% is one way a manager process can gracefully unblock child workers that are blocking
%% on a network accept.
%%
%% Once it makes a TCP connection it will call start/1 to spawn its successor.
listen(Parent, Debug, ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
{ok, _} = start(ListenSocket),
{ok, Peer} = inet:peername(Socket),
ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]),
ok = *PREFIX*client_man:enroll(),
State = #s{socket = Socket},
loop(Parent, Debug, State);
{error, closed} ->
ok = io:format("~p Retiring: Listen socket closed.~n", [self()]),
exit(normal)
end.
-spec loop(Parent, Debug, State) -> no_return()
when Parent :: pid(),
Debug :: [sys:dbg_opt()],
State :: state().
%% @private
%% The service loop itself. This is the service state. The process blocks on receive
%% of Erlang messages, TCP segments being received themselves as Erlang messages.
loop(Parent, Debug, State = #s{socket = Socket}) ->
ok = inet:setopts(Socket, [{active, once}]),
receive
{tcp, Socket, <<"bye\r\n">>} ->
ok = io:format("~p Client saying goodbye. Bye!~n", [self()]),
ok = gen_tcp:send(Socket, "Bye!\r\n"),
ok = gen_tcp:shutdown(Socket, read_write),
exit(normal);
{tcp, Socket, Message} ->
ok = io:format("~p received: ~tp~n", [self(), Message]),
ok = *PREFIX*client_man:echo(Message),
loop(Parent, Debug, State);
{relay, Sender, Message} when Sender == self() ->
ok = gen_tcp:send(Socket, ["Message from YOU: ", Message]),
loop(Parent, Debug, State);
{relay, Sender, Message} ->
From = io_lib:format("Message from ~tp: ", [Sender]),
ok = gen_tcp:send(Socket, [From, Message]),
loop(Parent, Debug, State);
{tcp_closed, Socket} ->
ok = io:format("~p Socket closed, retiring.~n", [self()]),
exit(normal);
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State);
Unexpected ->
ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]),
loop(Parent, Debug, State)
end.
-spec system_continue(Parent, Debug, State) -> no_return()
when Parent :: pid(),
Debug :: [sys:dbg_opt()],
State :: state().
%% @private
%% The function called by the OTP internal functions after a system message has been
%% handled. If the worker process has several possible states this is one place
%% resumption of a specific state can be specified and dispatched.
system_continue(Parent, Debug, State) ->
loop(Parent, Debug, State).
-spec system_terminate(Reason, Parent, Debug, State) -> no_return()
when Reason :: term(),
Parent :: pid(),
Debug :: [sys:dbg_opt()],
State :: state().
%% @private
%% Called by the OTP inner bits to allow the process to terminate gracefully.
%% Exactly when and if this is callback gets called is specified in the docs:
%% See: http://erlang.org/doc/design_principles/spec_proc.html#msg
system_terminate(Reason, _Parent, _Debug, _State) ->
exit(Reason).
-spec system_get_state(State) -> {ok, State}
when State :: state().
%% @private
%% This function allows the runtime (or anything else) to inspect the running state
%% of the worker process at any arbitrary time.
system_get_state(State) -> {ok, State}.
-spec system_replace_state(StateFun, State) -> {ok, NewState, State}
when StateFun :: fun(),
State :: state(),
NewState :: term().
%% @private
%% This function allows the system to update the process state in-place. This is most
%% useful for state transitions between code types, like when performing a hot update
%% (very cool, but sort of hard) or hot patching a running system (living on the edge!).
system_replace_state(StateFun, State) ->
{ok, StateFun(State), State}.

View File

@ -0,0 +1,296 @@
%%% @doc
%%% *PROJECT NAME* Client Manager
%%%
%%% This is the "manager" part of the service->worker pattern.
%%% It keeps track of who is connected and can act as a router among the workers.
%%% Having this process allows us to abstract and customize service-level concepts
%%% (the high-level ideas we care about in terms of solving an external problem in the
%%% real world) and keep them separate from the lower-level details of supervision that
%%% OTP should take care of for us.
%%% @end
-module(*PREFIX*client_man).
-behavior(gen_server).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([listen/1, ignore/0]).
-export([enroll/0, echo/1]).
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
%%% Type and Record Definitions
-record(s, {port_num = none :: none | inet:port_number(),
listener = none :: none | gen_tcp:socket(),
clients = [] :: [pid()]}).
-type state() :: #s{}.
%%% Service Interface
-spec listen(PortNum) -> Result
when PortNum :: inet:port_number(),
Result :: ok
| {error, Reason},
Reason :: {listening, inet:port_number()}.
%% @doc
%% Tell the service to start listening on a given port.
%% Only one port can be listened on at a time in the current implementation, so
%% an error is returned if the service is already listening.
listen(PortNum) ->
gen_server:call(?MODULE, {listen, PortNum}).
-spec ignore() -> ok.
%% @doc
%% Tell the service to stop listening.
%% It is not an error to call this function when the service is not listening.
ignore() ->
gen_server:cast(?MODULE, ignore).
%%% Client Process Interface
-spec enroll() -> ok.
%% @doc
%% Clients register here when they establish a connection.
%% Other processes can enroll as well.
enroll() ->
gen_server:cast(?MODULE, {enroll, self()}).
-spec echo(Message) -> ok
when Message :: string().
%% @doc
%% The function that tells the manager to broadcast a message to all clients.
%% This can broadcast arbitrary strings to clients from non-clients as well.
echo(Message) ->
gen_server:cast(?MODULE, {echo, Message, self()}).
%%% Startup Functions
-spec start_link() -> Result
when Result :: {ok, pid()}
| {error, Reason :: term()}.
%% @private
%% This should only ever be called by *PREFIX*clients (the service-level supervisor).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
-spec init(none) -> {ok, state()}.
%% @private
%% Called by the supervisor process to give the process a chance to perform any
%% preparatory work necessary for proper function.
init(none) ->
ok = io:format("Starting.~n"),
State = #s{},
{ok, State}.
%%% gen_server Message Handling Callbacks
-spec handle_call(Message, From, State) -> Result
when Message :: term(),
From :: {pid(), reference()},
State :: state(),
Result :: {reply, Response, NewState}
| {noreply, State},
Response :: ok
| {error, {listening, inet:port_number()}},
NewState :: state().
%% @private
%% The gen_server:handle_call/3 callback.
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3
handle_call({listen, PortNum}, _, State) ->
{Response, NewState} = do_listen(PortNum, State),
{reply, Response, NewState};
handle_call(Unexpected, From, State) ->
ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]),
{noreply, State}.
-spec handle_cast(Message, State) -> {noreply, NewState}
when Message :: term(),
State :: state(),
NewState :: state().
%% @private
%% The gen_server:handle_cast/2 callback.
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2
handle_cast({enroll, Pid}, State) ->
NewState = do_enroll(Pid, State),
{noreply, NewState};
handle_cast({echo, Message, Sender}, State) ->
ok = do_echo(Message, Sender, State),
{noreply, State};
handle_cast(ignore, State) ->
NewState = do_ignore(State),
{noreply, NewState};
handle_cast(Unexpected, State) ->
ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]),
{noreply, State}.
-spec handle_info(Message, State) -> {noreply, NewState}
when Message :: term(),
State :: state(),
NewState :: state().
%% @private
%% The gen_server:handle_info/2 callback.
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2
handle_info({'DOWN', Mon, process, Pid, Reason}, State) ->
NewState = handle_down(Mon, Pid, Reason, State),
{noreply, NewState};
handle_info(Unexpected, State) ->
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
{noreply, State}.
%%% OTP Service Functions
-spec code_change(OldVersion, State, Extra) -> Result
when OldVersion :: {down, Version} | Version,
Version :: term(),
State :: state(),
Extra :: term(),
Result :: {ok, NewState}
| {error, Reason :: term()},
NewState :: state().
%% @private
%% The gen_server:code_change/3 callback.
%% See: http://erlang.org/doc/man/gen_server.html#Module:code_change-3
code_change(_, State, _) ->
{ok, State}.
-spec terminate(Reason, State) -> no_return()
when Reason :: normal
| shutdown
| {shutdown, term()}
| term(),
State :: state().
%% @private
%% The gen_server:terminate/2 callback.
%% See: http://erlang.org/doc/man/gen_server.html#Module:terminate-2
terminate(_, _) ->
ok.
%%% Doer Functions
-spec do_listen(PortNum, State) -> {Result, NewState}
when PortNum :: inet:port_number(),
State :: state(),
Result :: ok
| {error, Reason :: {listening, inet:port_number()}},
NewState :: state().
%% @private
%% The "doer" procedure called when a "listen" message is received.
do_listen(PortNum, State = #s{port_num = none}) ->
SocketOptions =
[inet6,
{active, once},
{mode, binary},
{keepalive, true},
{reuseaddr, true}],
{ok, Listener} = gen_tcp:listen(PortNum, SocketOptions),
{ok, _} = *PREFIX*client:start(Listener),
{ok, State#s{port_num = PortNum, listener = Listener}};
do_listen(_, State = #s{port_num = PortNum}) ->
ok = io:format("~p Already listening on ~p~n", [self(), PortNum]),
{{error, {listening, PortNum}}, State}.
-spec do_ignore(State) -> NewState
when State :: state(),
NewState :: state().
%% @private
%% The "doer" procedure called when an "ignore" message is received.
do_ignore(State = #s{listener = none}) ->
State;
do_ignore(State = #s{listener = Listener}) ->
ok = gen_tcp:close(Listener),
State#s{listener = none}.
-spec do_enroll(Pid, State) -> NewState
when Pid :: pid(),
State :: state(),
NewState :: state().
do_enroll(Pid, State = #s{clients = Clients}) ->
case lists:member(Pid, Clients) of
false ->
Mon = monitor(process, Pid),
ok = io:format("Monitoring ~tp @ ~tp~n", [Pid, Mon]),
State#s{clients = [Pid | Clients]};
true ->
State
end.
-spec do_echo(Message, Sender, State) -> ok
when Message :: string(),
Sender :: pid(),
State :: state().
%% @private
%% The "doer" procedure called when an "echo" message is received.
do_echo(Message, Sender, #s{clients = Clients}) ->
Send = fun(Client) -> Client ! {relay, Sender, Message} end,
lists:foreach(Send, Clients).
-spec handle_down(Mon, Pid, Reason, State) -> NewState
when Mon :: reference(),
Pid :: pid(),
Reason :: term(),
State :: state(),
NewState :: state().
%% @private
%% Deal with monitors. When a new process enrolls as a client a monitor is set and
%% the process is added to the client list. When the process terminates we receive
%% a 'DOWN' message from the monitor. More sophisticated work managers typically have
%% an "unenroll" function, but this echo service doesn't need one.
handle_down(Mon, Pid, Reason, State = #s{clients = Clients}) ->
case lists:member(Pid, Clients) of
true ->
NewClients = lists:delete(Pid, Clients),
State#s{clients = NewClients};
false ->
Unexpected = {'DOWN', Mon, process, Pid, Reason},
ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]),
State
end.

View File

@ -0,0 +1,68 @@
%%% @doc
%%% *PROJECT NAME* Client Supervisor
%%%
%%% This process supervises the client socket handlers themselves. It is a peer of the
%%% *PREFIX*client_man (the manager interface to this network service component), and a
%%% child of the supervisor named *PREFIX*clients.
%%%
%%% Because we don't know (or care) how many client connections the server may end up
%%% handling this is a simple_one_for_one supervisor which can spawn and manage as
%%% many identically defined workers as required, but cannot supervise any other types
%%% of processes (one of the tradeoffs of the "simple" in `simple_one_for_one').
%%%
%%% http://erlang.org/doc/design_principles/sup_princ.html#id79244
%%% @end
-module(*PREFIX*client_sup).
-behaviour(supervisor).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([start_acceptor/1]).
-export([start_link/0]).
-export([init/1]).
-spec start_acceptor(ListenSocket) -> Result
when ListenSocket :: gen_tcp:socket(),
Result :: {ok, pid()}
| {error, Reason},
Reason :: {already_started, pid()}
| {shutdown, term()}
| term().
%% @private
%% Spawns the first listener at the request of the *PREFIX*client_man when es:listen/1
%% is called, or the next listener at the request of the currently listening *PREFIX*client
%% when a connection is made.
%%
%% Error conditions, supervision strategies and other important issues are
%% explained in the supervisor module docs:
%% http://erlang.org/doc/man/supervisor.html
start_acceptor(ListenSocket) ->
supervisor:start_child(?MODULE, [ListenSocket]).
-spec start_link() -> {ok, pid()}.
%% @private
%% This supervisor's own start function.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, none).
-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
%% @private
%% The OTP init/1 function.
init(none) ->
RestartStrategy = {simple_one_for_one, 1, 60},
Client = {*PREFIX*client,
{*PREFIX*client, start_link, []},
temporary,
brutal_kill,
worker,
[*PREFIX*client]},
{ok, {RestartStrategy, [Client]}}.

View File

@ -0,0 +1,47 @@
%%% @doc
%%% *PROJECT NAME* Client Service Supervisor
%%%
%%% This is the service-level supervisor of the system. It is the parent of both the
%%% client connection handlers and the client manager (which manages the client
%%% connection handlers). This is the child of *PREFIX*sup.
%%%
%%% See: http://erlang.org/doc/apps/kernel/application.html
%%% @end
-module(*PREFIX*clients).
-behavior(supervisor).
*AUTHOR*
*COPYRIGHT*
*LICENSE*
-export([start_link/0]).
-export([init/1]).
-spec start_link() -> {ok, pid()}.
%% @private
%% This supervisor's own start function.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, none).
-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
%% @private
%% The OTP init/1 function.
init(none) ->
RestartStrategy = {rest_for_one, 1, 60},
ClientSup = {*PREFIX*client_sup,
{*PREFIX*client_sup, start_link, []},
permanent,
5000,
supervisor,
[*PREFIX*client_sup]},
ClientMan = {*PREFIX*client_man,
{*PREFIX*client_man, start_link, []},
permanent,
5000,
worker,
[*PREFIX*client_man]},
Children = [ClientSup, ClientMan],
{ok, {RestartStrategy, Children}}.

View File

@ -0,0 +1,43 @@
%%% @doc
%%% *PROJECT NAME* Top-level Supervisor
%%%
%%% The very top level supervisor in the system. It only has one service branch: the
%%% client handling service. In a more complex system the client handling service would
%%% only be one part of a larger system. Were this a game system, for example, the
%%% item data management service would be a peer, as would a login credential provision
%%% service, game world event handling, and so on.
%%%
%%% See: http://erlang.org/doc/design_principles/applications.html
%%% See: http://zxq9.com/archives/1311
%%% @end
-module(*PREFIX*sup).
-behaviour(supervisor).
*LICENSE*
-export([start_link/0]).
-export([init/1]).
-spec start_link() -> {ok, pid()}.
%% @private
%% This supervisor's own start function.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
%% @private
%% The OTP init/1 function.
init([]) ->
RestartStrategy = {one_for_one, 1, 60},
Clients = {*PREFIX*clients,
{*PREFIX*clients, start_link, []},
permanent,
5000,
supervisor,
[*PREFIX*clients]},
Children = [Clients],
{ok, {RestartStrategy, Children}}.

View File

@ -0,0 +1,4 @@
{deps,[]}.
{package_id,{"otpr","zx",{0,1,0}}}.
{prefix,"zx_"}.
{type,app}.