From 9ce0a1794b32a2c46e848e8232d65e39862524e7 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Fri, 3 Aug 2018 14:55:33 +0000 Subject: [PATCH] Half un-borked --- test_prep | 6 +- zomp/etc/otpr/realm.conf | 2 +- zomp/lib/otpr/zx/0.1.0/ebin/zx.app | 19 +- zomp/lib/otpr/zx/0.1.0/include/zx_logger.hrl | 2 +- zomp/lib/otpr/zx/0.1.0/make_zx | 11 +- zomp/lib/otpr/zx/0.1.0/src/zx.erl | 624 +++++---- zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl | 869 +++++++----- zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl | 198 +-- zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl | 332 ++--- zomp/lib/otpr/zx/0.1.0/src/zx_key.erl | 27 +- zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl | 91 +- zomp/lib/otpr/zx/0.1.0/src/zx_local.erl | 1200 ++++++++++++----- zomp/lib/otpr/zx/0.1.0/src/zx_net.erl | 214 ++- zomp/lib/otpr/zx/0.1.0/src/zx_sys_conf.erl | 12 +- zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl | 64 +- zomp/lib/otpr/zx/0.1.0/templates/Emakefile | 1 + .../zx/0.1.0/templates/boringlib/funfile.erl | 22 + zomp/lib/otpr/zx/0.1.0/templates/escript | 17 + .../0.1.0/templates/example_server/appmod.erl | 75 ++ .../0.1.0/templates/example_server/client.erl | 203 +++ .../templates/example_server/client_man.erl | 296 ++++ .../templates/example_server/client_sup.erl | 68 + .../templates/example_server/clients.erl | 47 + .../zx/0.1.0/templates/example_server/sup.erl | 43 + zomp/lib/otpr/zx/0.1.0/zomp.meta | 4 + 25 files changed, 2988 insertions(+), 1459 deletions(-) create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/Emakefile create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/boringlib/funfile.erl create mode 100755 zomp/lib/otpr/zx/0.1.0/templates/escript create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/appmod.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/client.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/client_man.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/client_sup.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/clients.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/templates/example_server/sup.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/zomp.meta diff --git a/test_prep b/test_prep index 103e18e..b95d95d 100755 --- a/test_prep +++ b/test_prep @@ -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" diff --git a/zomp/etc/otpr/realm.conf b/zomp/etc/otpr/realm.conf index fbff7c3..7e30e52 100644 --- a/zomp/etc/otpr/realm.conf +++ b/zomp/etc/otpr/realm.conf @@ -1,4 +1,4 @@ {realm, "otpr"}. -{prime, {"otpr.psychobitch.party",11311}}. +{prime, {"zomp.psychobitch.party",11311}}. {sysop, "zxq9"}. {key, "zxq9-root"}. diff --git a/zomp/lib/otpr/zx/0.1.0/ebin/zx.app b/zomp/lib/otpr/zx/0.1.0/ebin/zx.app index 3f56945..9f0b007 100644 --- a/zomp/lib/otpr/zx/0.1.0/ebin/zx.app +++ b/zomp/lib/otpr/zx/0.1.0/ebin/zx.app @@ -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}}]}. diff --git a/zomp/lib/otpr/zx/0.1.0/include/zx_logger.hrl b/zomp/lib/otpr/zx/0.1.0/include/zx_logger.hrl index df4ea57..393e720 100644 --- a/zomp/lib/otpr/zx/0.1.0/include/zx_logger.hrl +++ b/zomp/lib/otpr/zx/0.1.0/include/zx_logger.hrl @@ -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). diff --git a/zomp/lib/otpr/zx/0.1.0/make_zx b/zomp/lib/otpr/zx/0.1.0/make_zx index 1ca8077..57bd198 100755 --- a/zomp/lib/otpr/zx/0.1.0/make_zx +++ b/zomp/lib/otpr/zx/0.1.0/make_zx @@ -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") -> diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx.erl b/zomp/lib/otpr/zx/0.1.0/src/zx.erl index d323c53..93cb123 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx.erl @@ -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,9 +371,9 @@ unsubscribe() -> %%% Execution of application --spec run(Identifier, RunArgs) -> no_return() - when Identifier :: string(), - RunArgs :: [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 %% dependencies and run the program. This implies determining whether the program and @@ -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(), - ok = file:set_cwd(zx_lib:zomp_dir()), - prepare(PackageID, Meta, Dir, RunArgs). + 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()), + 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". diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl index 1932681..146c66e 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl @@ -13,9 +13,13 @@ -copyright("Craig Everett "). -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,453 +29,570 @@ %%% 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) + case zx_lib:package_id(PackageName) of + {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} -> - Print = - fun(PackageID) -> - {ok, PackageString} = zx_lib:package_string(PackageID), - 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) +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({Name, Version}) -> + {ok, PackageString} = zx_lib:package_string({Realm, Name, Version}), + io:format("~ts~n", [PackageString]) + end, + lists:foreach(Print, PackageIDs). + -spec submit(ZspPath :: file:filename()) -> zx:outcome(). %% @private %% Submit a package to the appropriate "prime" server for the given realm. submit(ZspPath) -> - {ok, ZspBin} = file:read_file(ZspPath), - <> = ZspBin, - <> = Signed, - {ok, {PackageID, SigKeyName, _}} = zx_lib:b_to_ts(MetaBin), - {ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}), - true = zx_key:verify(Signed, Sig, PubKey), - {ok, Socket} = connect_auth(element(1, PackageID)), - ok = send(Socket, {submit, PackageID}), - ok = recv_or_die(Socket), - ok = gen_tcp:send(Socket, 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 = <>) -> + <> = 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 file:make_dir(PackageString) of - ok -> - log(info, "Will unpack to directory ./~ts", [PackageString]); - {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). + 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, <>} -> + ok = zx_net:disconnect(Socket), + review5(PackageID, Sig, Signed); + Error -> + done(Socket, Error) + end. + +review5(PackageID, Sig, Signed = <>) -> + 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. -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). +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 -> + review9(PackageString, TgzBin); + {error, Error} -> + Message = "Creating dir ./~ts failed with ~ts. Aborting.", + ok = log(error, Message, [PackageString, Error]), + {error, Error} + end. + +review9(PackageString, TgzBin) -> + ok = erl_tar:extract({binary, TgzBin}, [compressed, {cwd, PackageString}]), + log(info, "Sources unpacked to ./~ts", [PackageString]). -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). +approve(Package) -> package_operation(9, Package). +reject(Package) -> package_operation(10, Package). + + +-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, <>} -> + accept5(PackageID, {KeyName, Key, Socket}, Sig, Signed); + Error -> + done(Socket, Error) + end. + +accept5(PackageID, + Auth, + Sig, + Signed = <>) -> + 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 = <>, + Sig = public_key:sign(SignMe, sha512, Key), + SigSize = byte_size(Sig), + ZspData = <>, + 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()}. + when Realm :: zx:realm(), + 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() - when Realm :: zx:realm(), - RealmConf :: [term()], - User :: zx:user_id(), - KeyID :: zx:key_id(), - Key :: term(). +-spec prep_auth(Realm) -> {User, KeyName, Key} + when Realm :: zx:realm(), + User :: zx:user_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 = <>, + Sig = public_key:sign(Signed, sha512, Key), + SSize = byte_size(Sig), + <>. -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. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl index 598990b..ea43462 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl @@ -11,7 +11,7 @@ -copyright("Craig Everett "). -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, <>} -> - {ok, Bin}; - {tcp, Socket, <>} -> - 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 = <>, - 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. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl index fabd50e..0325c95 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl @@ -144,11 +144,11 @@ -copyright("Craig Everett "). -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, State}; - Error -> - Requestor ! {result, ID, Error}, - {ok, State} - end; + ok = do_fetch2(Bin, Requestor, ID), + {ok, State}; {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 = <>) -> <> = 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 = <>) -> 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. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl index a3cb119..05c6ba8 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl @@ -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 diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl index 3edaa4c..438e71d 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl @@ -25,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. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl index 22283b9..d941b34 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl @@ -10,17 +10,20 @@ -copyright("Craig Everett "). -license("GPL-3.0"). --export([initialize/2, set_version/1, +-export([initialize/0, set_version/1, list_realms/0, list_packages/1, list_versions/1, + latest/1, set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1, update_app_file/0, import_realm/1, drop_realm/1, takeover/1, abdicate/1, set_timeout/1, add_mirror/0, drop_mirror/0, - create_plt/0, dialyze/0, - grow_a_pair/0, drop_key/1, + create_project/0, create_plt/0, dialyze/0, + grow_a_pair/0, grow_a_pair/2, drop_key/1, create_user/0, create_userfile/0, export_user/0, import_user/1, create_realm/0, create_realmfile/0, create_realmfile/1]). +-export([select_user/1, select_private_key/1]). + -include("zx_logger.hrl"). @@ -38,101 +41,115 @@ sysop = none :: none | #user_data{}, url = none :: none | string()}). +-record(project, + {type = none :: none | app | lib | escript, + license = none :: none | none | undefined | string(), + name = none :: none | string(), + id = none :: none | zx:package_id() | zx:name(), + prefix = none :: none | zx:lower0_9(), + appmod = none :: none | zx:lower0_9(), + module = none :: none | zx:lower0_9(), + author = none :: none | string(), + a_email = none :: none | string(), + copyright = none :: none | string(), + c_email = none :: none | string(), + deps = [] :: [zx:package_id()]}). + %%% Functions --spec initialize(Type, PackageString) -> zx:outcome() - when Type :: app | lib, - PackageString :: string(). +-spec initialize() -> zx:outcome(). %% @private -%% Initialize an application in the local directory based on the PackageID provided -%% and interaction with the user to determine a few details. +%% Initialize a project in the local directory based on user input. -initialize(Type, RawPackageString) -> +initialize() -> case filelib:is_file("zomp.meta") of - false -> initialize2(Type, RawPackageString); + false -> initialize(#project{}); true -> {error, "This project is already Zompified.", 17} end. - -initialize2(Type, RawPackageString) -> - case zx_lib:package_id(RawPackageString) of - {ok, {R, N, {z, z, z}}} -> - initialize3(Type, {R, N, {0, 1, 0}}); - {ok, {R, N, {X, z, z}}} -> - initialize3(Type, {R, N, {X, 0, 0}}); - {ok, {R, N, {X, Y, z}}} -> - initialize3(Type, {R, N, {X, Y, 0}}); - {ok, ID} -> - initialize3(Type, ID); - {error, invalid_package_string} -> - {error, "Invalid package string.", 22} +initialize(P = #project{type = none}) -> + initialize(P#project{type = ask_project_type()}); +initialize(P = #project{type = escript}) -> + ok = log(warning, "escripts cannot be initialized."), + initialize(P#project{type = ask_project_type()}); +initialize(P = #project{type = app, id = none}) -> + ID = {_, AppMod, _} = ask_package_id(), + initialize(P#project{id = ID, appmod = AppMod}); +initialize(P = #project{id = none}) -> + initialize(P#project{id = ask_package_id()}); +initialize(P = #project{prefix = none}) -> + initialize(P#project{prefix = ask_prefix()}); +initialize(P = #project{type = app, appmod = none}) -> + initialize(P#project{appmod = ask_appmod()}); +initialize(P = #project{type = app, + id = ID, + prefix = Prefix, + appmod = AppMod}) -> + {ok, PS} = zx_lib:package_string(ID), + Instructions = + "~nAPPLICATION DATA CONFIRMATION~n" + "[1] Type : application~n" + "[2] Package ID : ~ts~n" + "[3] Prefix : ~ts~n" + "[4] AppMod : ~ts~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, [PS, Prefix, AppMod]), + case zx_tty:get_input() of + "1" -> initialize(P#project{type = none}); + "2" -> initialize(P#project{id = none}); + "3" -> initialize(P#project{prefix = none}); + "4" -> initialize(P#project{appmod = none}); + "" -> initialize_check(P); + _ -> + ok = zx_tty:derp(), + initialize(P) + end; +initialize(P = #project{type = lib, + id = ID, + prefix = Prefix}) -> + {ok, PS} = zx_lib:package_string(ID), + Instructions = + "~nLIBRARY DATA CONFIRMATION~n" + "[1] Type : library~n" + "[2] Package ID : ~ts~n" + "[3] Prefix : ~ts~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, [PS, Prefix]), + case zx_tty:get_input() of + "1" -> initialize(P#project{type = none}); + "2" -> initialize(P#project{id = none}); + "3" -> initialize(P#project{prefix = none}); + "" -> initialize_check(P); + _ -> + ok = zx_tty:derp(), + initialize(P) end. -initialize3(Type, PackageID) -> +initialize_check(P = #project{id = PackageID}) -> case package_exists(PackageID) of false -> - Prefix = ask_prefix(), - initialize4(Type, PackageID, Prefix); + zompify(P); true -> - Message = "Package already exists. Try another.", - {error, Message, 17} + Message = + "~nDHOH!~n" + "Sadly this package already exists.~n" + "Two packages with the same name cannot exist in a single realm.~n" + "Please pick another name.~n", + ok = io:format(Message), + initialize(P#project{id = none}) end. -initialize4(app, PackageID, Prefix) -> - Instructions = - "The OTP application controller has to know what module to call start/2 on to " - "start your program.~n" - "If this is not an OTP application or if there is some special setting you " - "need then ignore this by just pressing [ENTER] to bypass this and go back " - "later and edit the 'mod' attribute in your ebin/[AppName].app file by hand.~n" - "this blank.~n" - "Enter the name of your main/start interface module where the application " - "callback start/2 has been defined.~n", - ok = io:format(Instructions), - case zx_tty:get_input() of - "" -> - initialize5(lib, PackageID, Prefix, none); - String -> - case zx_lib:valid_lower0_9(String) of - true -> - AppStart = {list_to_atom(String), []}, - initialize5(app, PackageID, Prefix, AppStart); - false -> - Message = "The name \"~ts\" doesn't seem valid. Try \"[a-z_]*\".~n", - ok = io:format(Message, String), - initialize4(app, PackageID, Prefix) - end - end; -initialize4(lib, PackageID, Prefix) -> - initialize5(lib, PackageID, Prefix, none). - - -initialize5(Type, PackageID, Prefix, AppStart) -> - ok = update_source_vsn(element(3, PackageID)), - ok = initialize_app_file(PackageID, AppStart), - MetaList = - [{package_id, PackageID}, - {deps, []}, - {type, Type}, - {prefix, Prefix}, - {appmod, AppStart}], - Meta = maps:from_list(MetaList), - ok = zx_lib:write_project_meta(Meta), - ok = - case filelib:is_regular("Emakefile") of - true -> - ok; - false -> - EM = [{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}, - {"test/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}], - zx_lib:write_terms("Emakefile", EM) - end, - {ok, PackageString} = zx_lib:package_string(PackageID), - ok = log(info, "Project ~tp initialized.", [PackageString]), +zompify(#project{type = Type, id = ID, prefix = Prefix, appmod = AM, deps = Deps}) -> + ok = initialize_app_file(ID, AM), + Data = #{package_id => ID, deps => Deps, type => Type, prefix => Prefix}, + ok = zx_lib:write_project_meta(Data), + ok = ensure_emakefile(), + {ok, PackageString} = zx_lib:package_string(ID), + ok = log(info, "Project ~ts initialized.", [PackageString]), Message = "~nNOTICE:~n" "This project is currently listed as having no dependencies.~n" @@ -141,53 +158,16 @@ initialize5(Type, PackageID, Prefix, AppStart) -> io:format(Message). --spec package_exists(zx:package_id()) -> boolean(). -%% @private -%% Check the realm's upstream for the existence of a package that already has the same -%% name, or the name of any modules in src/ and report them to the user. Give the user -%% a chance to change their package's name or ignore the conflict, and report all module -%% naming conflicts. +-spec ensure_emakefile() -> ok. -package_exists(PackageID) -> - ok = log(info, "TODO: This is where the intended realm is checked for conflicts " - "and the user is given a chance to rename or ignore the conflict. " - "This check will have to wait for the network protocol fix."), - ok = log(info, "Pretending that the existence of ~tp was checked...", [PackageID]), - false. - - --spec ask_prefix() -> string(). -%% @private -%% Get a valid module prefix to use as a namespace for new modules. - -ask_prefix() -> - Instructions = - "~nPICKING A PREFIX~n" - "Most Erlang applications have a prefix on the front of their modules. This " - "is a way of namespacing modules from different applications so they don't " - "conflict in very large execution environments.~n" - "A common way of coming up with a prefix is to make a lower-case acronym of " - "your application name and add an underscore at the end, unless your app " - "name is so short that it isn't a problem to use it as its own prefix.~n" - "For example: the prefix for all ZX modules is 'zx_', and the prefix for " - "otpr-example_server is 'es_'.~n" - "If you already have a prefix, just type it here (including the trailing " - "underscore if there is one). If you are making up a new one in an existing " - "project you'll need to rename your modules and fix up call sites with sed.~n" - "Enter a blank prefix (just [ENTER]) to ignore this and use no prefix.~n", - ok = io:format(Instructions), - case zx_tty:get_input() of - "" -> - ""; - Prefix -> - case zx_lib:valid_lower0_9(Prefix) of - true -> - Prefix; - false -> - Message = "The string \"~tp\" is problematic. Try \"[a-z]*_\".~n", - ok = io:format(Message, [Prefix]), - ask_prefix() - end +ensure_emakefile() -> + case filelib:is_regular("Emakefile") of + true -> + ok; + false -> + Path = filename:join([os:getenv("ZX_DIR"), "templates", "Emakefile"]), + {ok, _} = file:copy(Path, "Emakefile"), + ok end. @@ -222,15 +202,14 @@ update_app_vsn(Name, Version) -> zx_lib:write_terms(AppFile, [{application, AppName, NewAppData}]). --spec initialize_app_file(PackageID, AppStart) -> ok +-spec initialize_app_file(PackageID, AppMod) -> ok when PackageID :: zx:package_id(), - AppStart :: {StartMod :: module(), Args :: term()} - | none. + AppMod :: module(). %% @private %% Update the app file or create it if it is missing. %% TODO: If the app file is missing and an app/*.src exists, interpret that. -initialize_app_file({_, Name, Version}, AppStart) -> +initialize_app_file({_, Name, Version}, AppMod) -> {ok, VersionString} = zx_lib:version_to_string(Version), AppName = list_to_atom(Name), AppFile = filename:join("ebin", Name ++ ".app"), @@ -249,24 +228,19 @@ initialize_app_file({_, Name, Version}, AppStart) -> Grep = "grep -oP '^-module\\(\\K[^)]+' src/* | cut -d: -f2", Modules = [list_to_atom(M) || M <- string:lexemes(os:cmd(Grep), "\n")], Properties = - case (AppStart == none) or lists:keymember(mod, 1, RawAppData) of + case (AppMod == none) or lists:keymember(mod, 1, RawAppData) of true -> [{vsn, VersionString}, {modules, Modules}]; false -> [{vsn, VersionString}, {modules, Modules}, - {mod, AppStart}] + {mod, {AppMod, []}}] end, Store = fun(T, L) -> lists:keystore(element(1, T), 1, L, T) end, AppData = lists:foldl(Store, RawAppData, Properties), AppProfile = {application, AppName, AppData}, ok = log(info, "Writing app file: ~ts", [AppFile]), - Notice = - "NOTE: From this point on ZX will only update the module list and version " - "number of your .app file. Any additional changes (start module/args, start " - "phases, etc.) will need to be done by hand.~n", - ok = io:format(Notice), zx_lib:write_terms(AppFile, [AppProfile]). @@ -315,7 +289,7 @@ update_app_file(Meta) -> set_version(VersionString) -> case zx_lib:string_to_version(VersionString) of {error, invalid_version_string} -> - Message = "Invalid version string: ~tp", + Message = "Invalid version string: ~ts", {error, Message, 22}; {ok, {_, _, z}} -> Message = "'set version' arguments must be complete, ex: 1.2.3", @@ -351,7 +325,7 @@ update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> {ok, NewVS} = zx_lib:version_to_string(NewVersion), log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]); Error -> - ok = log(error, "Write to zomp.meta failed with: ~tp", [Error]), + ok = log(error, "Write to zomp.meta failed with: ~160tp", [Error]), Error end. @@ -375,18 +349,15 @@ list_realms() -> list_packages(Realm) -> {ok, ID} = zx_daemon:list(Realm), case wait_result(ID) of - {ok, []} -> - io:format("Realm ~tp has no packages available.~n", [Realm]); - {ok, Packages} -> - Print = fun({R, N}) -> io:format("~ts-~ts~n", [R, N]) end, - lists:foreach(Print, Packages); - {error, bad_realm} -> - {error, "Unconfigured realm or bad realm name.", 22}; - {error, network} -> - Message = "Network issues are preventing connection to the realm.", - {error, Message, 101} + {ok, []} -> io:format("No packages available.~n"); + {ok, Packages} -> lists:foreach(print_package(Realm), Packages); + {error, bad_realm} -> {error, "Unconfigured realm or bad realm name.", 22}; + {error, timeout} -> {error, "Request timed out.", 62}; + {error, network} -> {error, "Network problem connecting to realm.", 101} end. +print_package(Realm) -> fun(Name) -> io:format("~ts-~ts~n", [Realm, Name]) end. + -spec list_versions(PackageName :: string()) -> zx:outcome(). %% @private @@ -396,37 +367,48 @@ list_packages(Realm) -> list_versions(PackageString) -> case zx_lib:package_id(PackageString) of - {ok, PackageID} -> - list_versions2(PackageID); - {error, invalid_package_string} -> - {error, "Invalid package name.", 22} + {ok, PackageID} -> list_versions2(PackageID); + {error, invalid_package_string} -> {error, "Invalid package name.", 22} end. list_versions2({Realm, Name, Version}) -> {ok, ID} = zx_daemon:list(Realm, Name, Version), case wait_result(ID) of - {ok, []} -> - io:format("No versions available.~n"); - {ok, Versions} -> - Print = - fun(V) -> - {ok, VS} = zx_lib:version_to_string(V), - io:format("~ts~n", [VS]) - end, - lists:foreach(Print, Versions); - {error, bad_realm} -> - {error, "Bad realm name.", 22}; - {error, bad_package} -> - {error, "Bad package name.", 22}; - {error, network} -> - Message = "Network issues are preventing connection to the realm.", - {error, Message, 101} + {ok, []} -> io:format("No versions available.~n"); + {ok, Versions} -> lists:foreach(fun print_version/1, Versions); + {error, bad_realm} -> {error, "Bad realm name.", 22}; + {error, bad_package} -> {error, "Bad package name.", 22}; + {error, timeout} -> {error, "Request timed out.", 62}; + {error, network} -> {error, "Network problem connecting to realm.", 101} + end. + +print_version(Version) -> + {ok, String} = zx_lib:version_to_string(Version), + io:format("~ts~n", [String]). + + +-spec latest(PackageString :: string()) -> zx:outcome(). + +latest(PackageString) -> + case zx_lib:package_id(PackageString) of + {ok, PackageID} -> latest2(PackageID); + {error, invalid_package_string} -> {error, "Invalid package name.", 22} + end. + +latest2(PackageID) -> + {ok, ID} = zx_daemon:latest(PackageID), + case wait_result(ID) of + {ok, Version} -> print_version(Version); + Error -> Error end. wait_result(ID) -> - receive {result, ID, Result} -> Result end. + receive + {result, ID, Result} -> Result + after 5000 -> {error, timeout} + end. -spec import_realm(Path) -> zx:outcome() @@ -462,7 +444,7 @@ import_realm2(Data) -> KeyName = maps:get(key, RealmConf), KeyPath = zx_key:path(public, {Realm, KeyName}), ok = file:write_file(KeyPath, KeyDER), - log(info, "Added realm ~tp.", [Realm]); + log(info, "Added realm ~ts.", [Realm]); error -> {error, "Invalid .zrf file.", 84} end. @@ -559,7 +541,7 @@ list_deps(PackageString) -> {ok, {_, _, {_, _, z}}} -> {error, "Packages must be fully specified; no partial versions.", 22}; {ok, PackageID} -> - log(info, "Phooey! list_deps(~tp) isn't yet implemented!", [PackageID]); + log(info, "Phooey! list_deps(~16tp) isn't yet implemented!", [PackageID]); {error, invalid_package_string} -> {error, "Invalid package string.", 22} end. @@ -658,64 +640,19 @@ version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> package(TargetDir) -> ok = log(info, "Packaging ~ts", [TargetDir]), {ok, Meta} = zx_lib:read_project_meta(TargetDir), - {Realm, _, _} = maps:get(package_id, Meta), - case list_users(Realm) of - [] -> - UserName = create_user(#user_data{realm = Realm}), - package(TargetDir, Meta, UserName); - [UserName] -> - ok = log(info, "Signing as user ~tp", [UserName]), - package(TargetDir, Meta, UserName); - UserNames -> - UserName = zx_tty:select_string(UserNames), - package(TargetDir, Meta, UserName) - end. - -package(TargetDir, Meta, UserName) -> - {Realm, _, _} = maps:get(package_id, Meta), - KeyDir = zx_lib:path(key, Realm), - ok = zx_lib:force_dir(KeyDir), - Pattern = filename:join(KeyDir, UserName ++ ".*.key.der"), - case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of - [] -> - ok = log(info, "No private keys found. Need to generate a keypair."), - KeyTag = zx_key:prompt_keygen(), - KeyName = UserName ++ "." ++ KeyTag, - KeyID = {Realm, KeyName}, - ok = zx_key:generate_rsa(KeyID), - package(KeyID, TargetDir); - [KeyName] -> - KeyID = {Realm, KeyName}, - ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), - package(KeyID, TargetDir); - KeyNames -> - KeyName = zx_tty:select_string(KeyNames), - package(TargetDir, Meta, UserName, {Realm, KeyName}) - end. - - --spec package(TargetDir, Meta, UserName, KeyID) -> zx:outcome() - when TargetDir :: file:filename(), - Meta :: zx:meta(), - UserName :: zx:user_name(), - KeyID :: zx:key_id(). -%% @private -%% Accept a KeyPrefix for signing and a TargetDir containing a project to package and -%% build a zsp package file ready to be submitted to a repository. - -package(KeyID, TargetDir) -> - {ok, Meta} = zx_lib:read_project_meta(TargetDir), - PackageID = maps:get(package_id, Meta), - true = element(1, PackageID) == element(1, KeyID), + PackageID = {Realm, _, _} = maps:get(package_id, Meta), + Deps = maps:get(deps, Meta), + UserName = select_user(Realm), + KeyName = select_private_key({Realm, UserName}), + ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), {ok, PackageString} = zx_lib:package_string(PackageID), ZspFile = PackageString ++ ".zsp", case filelib:is_regular(ZspFile) of - true -> {error, "Package file already exists. Aborting", 17}; - false -> package(KeyID, TargetDir, PackageID, ZspFile) + false -> package2(TargetDir, PackageID, Deps, KeyName, ZspFile); + true -> {error, "Package file already exists. Aborting", 17} end. - -package(KeyID, TargetDir, PackageID, ZspFile) -> +package2(TargetDir, PackageID, Deps, KeyName, ZspFile) -> ok = remove_binaries(TargetDir), CrashDump = filename:join(TargetDir, "erl_crash.dump"), ok = @@ -734,10 +671,10 @@ package(KeyID, TargetDir, PackageID, ZspFile) -> ok = erl_tar:create(TarGzPath, Targets, [compressed]), {ok, TgzBin} = file:read_file(TarGzPath), ok = file:delete(TarGzPath), - MetaBin = term_to_binary({PackageID, element(2, KeyID), Modules}), + MetaBin = term_to_binary({PackageID, KeyName, Deps, Modules}), MetaSize = byte_size(MetaBin), SignMe = <>, - {ok, Key} = zx_key:load(private, KeyID), + {ok, Key} = zx_key:load(private, {element(1, PackageID), KeyName}), Sig = public_key:sign(SignMe, sha512, Key), SigSize = byte_size(Sig), ZspData = <>, @@ -758,6 +695,435 @@ remove_binaries(TargetDir) -> lists:foreach(fun file:delete/1, ToDelete). +-spec create_project() -> ok. +%% @private +%% Interact with the user to determine what kind of project they want, and template +%% it based on the outcome. + +create_project() -> + create(#project{}). + +create(P = #project{type = none}) -> + create(P#project{type = ask_project_type()}); +create(P = #project{name = none}) -> + create(P#project{name = ask_project_name()}); +create(P = #project{type = escript, id = none}) -> + create(P#project{id = ask_script_name()}); +create(P = #project{type = app, id = none}) -> + ID = {_, AppMod, _} = ask_package_id(), + create(P#project{id = ID, appmod = list_to_atom(AppMod)}); +create(P = #project{type = lib, id = none}) -> + ID = {_, Module, _} = ask_package_id(), + create(P#project{id = ID, module = list_to_atom(Module)}); +create(P = #project{type = app, prefix = none}) -> + create(P#project{prefix = ask_prefix()}); +create(P = #project{type = lib, prefix = none}) -> + create(P#project{prefix = ask_prefix()}); +create(P = #project{type = app, appmod = none}) -> + create(P#project{appmod = ask_appmod()}); +create(P = #project{type = lib, module = none}) -> + create(P#project{module = ask_module()}); +create(P = #project{author = none, copyright = none}) -> + {A, E} = ask_author(), + create(P#project{author = A, a_email = E, copyright = A, c_email = E}); +create(P = #project{author = none}) -> + {Author, Email} = ask_author(), + create(P#project{author = Author, a_email = Email}); +create(P = #project{a_email = none}) -> + create(P#project{a_email = ask_email_optional()}); +create(P = #project{copyright = none}) -> + {Holder, Email} = ask_copyright(), + create(P#project{copyright = Holder, c_email = Email}); +create(P = #project{c_email = none}) -> + create(P#project{c_email = ask_email_optional()}); +create(P = #project{license = none}) -> + create(P#project{license = ask_license()}); +create(P = #project{type = app, + license = License, + name = Name, + id = ID, + prefix = Prefix, + appmod = AppMod, + author = Author, + a_email = AEmail, + copyright = CR, + c_email = CEmail}) -> + {ok, PS} = zx_lib:package_string(ID), + Instructions = + "~nAPPLICATION DATA CONFIRMATION~n" + "[ 1] Type : application~n" + "[ 2] Project Name : ~ts~n" + "[ 3] Package ID : ~ts~n" + "[ 4] Author : ~ts~n" + "[ 5] Author's Email : ~ts~n" + "[ 6] Copyright Holder : ~ts~n" + "[ 7] Copyright Holder's Email: ~ts~n" + "[ 8] License : ~ts~n" + "[ 9] Prefix : ~ts~n" + "[10] AppMod : ~tw~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, + [Name, PS, Author, AEmail, CR, CEmail, License, Prefix, AppMod]), + case zx_tty:get_input() of + "1" -> create(P#project{type = none}); + "2" -> create(P#project{name = none}); + "3" -> create(P#project{id = none}); + "4" -> create(P#project{author = none}); + "5" -> create(P#project{a_email = none}); + "6" -> create(P#project{copyright = none}); + "7" -> create(P#project{c_email = none}); + "8" -> create(P#project{license = none}); + "9" -> create(P#project{prefix = none}); + "10" -> create(P#project{appmod = none}); + "" -> create_check(P); + _ -> + ok = zx_tty:derp(), + create(P) + end; +create(P = #project{type = lib, + license = License, + name = Name, + id = ID, + prefix = Prefix, + module = Module, + author = Author, + a_email = AEmail, + copyright = CR, + c_email = CEmail}) -> + {ok, PS} = zx_lib:package_string(ID), + Instructions = + "~nLIBRARY DATA CONFIRMATION~n" + "[ 1] Type : library~n" + "[ 2] Project Name : ~ts~n" + "[ 3] Package ID : ~ts~n" + "[ 4] Author : ~ts~n" + "[ 5] Author's Email : ~ts~n" + "[ 6] Copyright Holder : ~ts~n" + "[ 7] Copyright Holder's Email: ~ts~n" + "[ 8] License : ~ts~n" + "[ 9] Prefix : ~ts~n" + "[10] Module : ~tw~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, + [Name, PS, Author, AEmail, CR, CEmail, License, Prefix, Module]), + case zx_tty:get_input() of + "1" -> create(P#project{type = none}); + "2" -> create(P#project{name = none}); + "3" -> create(P#project{id = none}); + "4" -> create(P#project{author = none}); + "5" -> create(P#project{a_email = none}); + "6" -> create(P#project{copyright = none}); + "7" -> create(P#project{c_email = none}); + "8" -> create(P#project{license = none}); + "9" -> create(P#project{prefix = none}); + "10" -> create(P#project{module = none}); + "" -> create_check(P); + _ -> + ok = zx_tty:derp(), + create(P) + end; +create(P = #project{type = escript, + license = License, + name = Name, + id = ID, + author = Author, + a_email = AEmail, + copyright = CR, + c_email = CEmail}) -> + Instructions = + "~nLIBRARY DATA CONFIRMATION~n" + "[1] Type : escript~n" + "[2] Project Name : ~ts~n" + "[3] Script : ~ts~n" + "[4] Author : ~ts~n" + "[5] Author's Email : ~ts~n" + "[6] Copyright Holder : ~ts~n" + "[7] Copyright Holder's Email: ~ts~n" + "[8] License : ~ts~n" + "Press a number to select something to change, or [ENTER] to continue.~n", + ok = io:format(Instructions, [Name, ID, Author, AEmail, CR, CEmail, License]), + case zx_tty:get_input() of + "1" -> create(P#project{type = none}); + "2" -> create(P#project{name = none}); + "3" -> create(P#project{id = none}); + "4" -> create(P#project{author = none}); + "5" -> create(P#project{a_email = none}); + "6" -> create(P#project{copyright = none}); + "7" -> create(P#project{c_email = none}); + "8" -> create(P#project{license = none}); + "" -> create_project(P); + _ -> + ok = zx_tty:derp(), + create(P) + end. + + +create_check(P = #project{id = PackageID}) -> + case package_exists(PackageID) of + false -> + create_project(P); + true -> + Message = + "~nDHOH!~n" + "Sadly this package already exists.~n" + "Two packages with the same name cannot exist in a single realm.~n" + "Please pick another name.~n", + ok = io:format(Message), + create(P#project{id = none}) + end. + + +create_project(#project{type = escript, + license = Title, + name = Name, + id = File, + author = Credit, + a_email = AEmail, + copyright = Holder, + c_email = CEmail}) -> + Source = filename:join(os:getenv("ZX_DIR"), "templates/escript"), + Substitutions = + [{"〘\*PROJECT NAME\*〙", Name}, + {"〘\*SCRIPT\*〙", script(File)}, + {"〘\*AUTHOR\*〙", author(Credit, AEmail)}, + {"〘\*COPYRIGHT\*〙", copyright(Holder, CEmail)}, + {"〘\*LICENSE\*〙", license(Title)}], + {ok, Raw} = file:read_file(Source), + UTF8 = unicode:characters_to_list(Raw, utf8), + Cooked = substitute(UTF8, Substitutions), + ok = file:write_file(File, Cooked), + Message = + "Escript \"~ts\" written to ~ts.~n" + "You may want to change the file mode to add \"execute\" permissions.", + log(info, Message, [Name, File]); +create_project(P = #project{id = {_, Name, _}}) -> + case file:make_dir(Name) of + ok -> + ok = file:set_cwd(Name), + munge_sources(P), + zompify(P); + {error, Reason} -> + Message = "Creating directory ~ts failed with ~tp. Aborting.", + Info = io_lib:format(Message, [Name, Reason]), + {error, Info, 1} + end. + + +munge_sources(#project{type = app, + name = Name, + license = Title, + prefix = Prefix, + appmod = AppMod, + author = Credit, + a_email = AEmail, + copyright = Holder, + c_email = CEmail}) -> + ok = file:make_dir("src"), + ok = file:make_dir("ebin"), + {ok, ProjectDir} = file:get_cwd(), + Substitutions = + [{"〘\*PROJECT NAME\*〙", Name}, + {"〘\*PREFIX\*〙", Prefix}, + {"〘\*APP MOD\*〙", atom_to_list(AppMod)}, + {"〘\*AUTHOR\*〙", author(Credit, AEmail)}, + {"〘\*COPYRIGHT\*〙", copyright(Holder, CEmail)}, + {"〘\*LICENSE\*〙", license(Title)}], + TemplateDir = filename:join([os:getenv("ZX_DIR"), "templates", "example_server"]), + ok = file:set_cwd("src"), + AppModFile = atom_to_list(AppMod) ++ ".erl", + {ok, RawAppMod} = file:read_file(filename:join(TemplateDir, "appmod.erl")), + UTF8AppMod = unicode:characters_to_list(RawAppMod, utf8), + CookedAppMod = substitute(UTF8AppMod, Substitutions), + ok = file:write_file(AppModFile, CookedAppMod), + ModuleTemplates = + ["sup.erl", "clients.erl", "client_man.erl", "client_sup.erl", "client.erl"], + Manifest = [filename:join(TemplateDir, T) || T <- ModuleTemplates], + ok = transform(Manifest, Prefix, Substitutions), + file:set_cwd(ProjectDir); +munge_sources(#project{type = lib, + name = Name, + license = Title, + module = Module, + author = Credit, + a_email = AEmail, + copyright = Holder, + c_email = CEmail}) -> + ok = file:make_dir("src"), + ok = file:make_dir("ebin"), + {ok, ProjectDir} = file:get_cwd(), + Substitutions = + [{"〘\*PROJECT NAME\*〙", Name}, + {"〘\*MODULE\*〙", atom_to_list(Module)}, + {"〘\*AUTHOR\*〙", author(Credit, AEmail)}, + {"〘\*COPYRIGHT\*〙", copyright(Holder, CEmail)}, + {"〘\*LICENSE\*〙", license(Title)}], + TemplateDir = filename:join([os:getenv("ZX_DIR"), "templates", "boringlib"]), + ok = file:set_cwd("src"), + ModFile = atom_to_list(Module) ++ ".erl", + {ok, RawMod} = file:read_file(filename:join(TemplateDir, "funfile.erl")), + UTF8Mod = unicode:characters_to_list(RawMod, utf8), + CookedMod = substitute(UTF8Mod, Substitutions), + ok = file:write_file(ModFile, CookedMod), + file:set_cwd(ProjectDir). + + +transform([Template | Rest], Prefix, Substitutions) -> + {ok, Raw} = file:read_file(Template), + Data = substitute(Raw, Substitutions), + Path = Prefix ++ filename:basename(Template), + ok = file:write_file(Path, Data), + transform(Rest, Prefix, Substitutions); +transform([], _, _) -> + ok. + + +substitute(Raw, [{Pattern, Value} | Rest]) -> + Cooking = string:replace(Raw, Pattern, Value, all), + substitute(Cooking, Rest); +substitute(Cooked, []) -> + unicode:characters_to_list(Cooked, utf8). + + +license("") -> ""; +license(Title) -> "-license(\""++ Title ++ "\").". + +script("") -> ""; +script(Name) -> "-script(\"" ++ Name ++ "\").". + +copyright("", "") -> ""; +copyright(Holder, "") -> "-copyright(\"" ++ Holder ++ "\")."; +copyright("", Email) -> "-copyright(\"<" ++ Email ++ ">\")."; +copyright(Holder, Email) -> "-copyright(\"" ++ Holder ++ " <" ++ Email ++ ">\").". + +author("", "") -> ""; +author(Credit, "") -> "-author(\"" ++ Credit ++ "\")."; +author("", Email) -> "-author(\"<" ++ Email ++ ">\")."; +author(Credit, Email) -> "-author(\"" ++ Credit ++ " <" ++ Email ++">\").". + + + +-spec ask_project_type() -> app | lib | escript | no_return(). +%% @private +%% Determine whether the user wants to create an app, a lib, or nothing. + +ask_project_type() -> + Instructions = + "~nPROJECT TYPE~n" + "Select a project type.~n" + "Remember, an \"application\" in Erlang is anything that needs to be started " + "to work, even if it acts in a supporting role.~n" + "(Note that escripts cannot be initialized as packaged projects.~n" + "[1] Application~n" + "[2] Library~n" + "[3] Escript~n", + ok = io:format(Instructions), + case zx_tty:get_input() of + "1" -> app; + "2" -> lib; + "3" -> escript; + _ -> + ok = zx_tty:derp(), + ask_project_type() + end. + + +-spec ask_script_name() -> zx:lower0_9() | no_return(). + +ask_script_name() -> + Instructions = + "~nESCRIPT NAME~n" + "Enter the name of your escript. This will be its callable filename.~n" + "[a-z0-9_]*~n", + case zx_tty:get_lower0_9(Instructions) of + "" -> + ok = io:format("The script has to be called something. Try again. ~n"), + ask_script_name(); + Script -> + Script + end. + + +-spec ask_package_id() -> zx:package_id() | no_return(). + +ask_package_id() -> + Instructions = + "~nPACKAGE ID~n" + "A package ID is a set of three strings separated by dashes that represent " + "a project's realm, package name, and version number.~n" + "Package IDs typically look like this: \"otpr-my_project-1.2.3\" or similar.~n" + "Omitted realms default to \"otpr\". Omitted versions default to \"0.1.0\".~n" + "(A formal definition is available here: " + "http://zxq9.com/projects/zomp/zx_usage.en.html#identifiers)~n", + RawPackageString = zx_tty:get_input(Instructions), + case zx_lib:package_id(RawPackageString) of + {ok, {R, N, {z, z, z}}} -> {R, N, {0, 1, 0}}; + {ok, {R, N, {X, z, z}}} -> {R, N, {X, 0, 0}}; + {ok, {R, N, {X, Y, z}}} -> {R, N, {X, Y, 0}}; + {ok, ID} -> ID; + {error, invalid_package_string} -> + Message = "Sorry, \"~tp\" is an invalid package ID. Try again.~n", + ok = io:format(Message, [RawPackageString]), + ask_package_id() + end. + + +-spec ask_prefix() -> string(). +%% @private +%% Get a valid module prefix to use as a namespace for new modules. + +ask_prefix() -> + Instructions = + "~nPICKING A PREFIX~n" + "Most projects use a prefix with a trailing underscore to namespace their " + "modules. For example, example_server uses the module prefix \"es_\" " + "internally.~n" + "This is optional.~n", + case zx_tty:get_input(Instructions, [], "[ENTER] to leave blank") of + "" -> + ""; + String -> + 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]), + ask_prefix() + end + end. + + +-spec ask_appmod() -> atom() | no_return(). + +ask_appmod() -> + Instructions = + "~nAPPLICATION MODULE~n" + "Enter the name of your main/start interface module where the application " + "callback start/2 has been defined.~n", + list_to_atom(zx_tty:get_lower0_9(Instructions)). + + +-spec ask_module() -> atom() | no_return(). + +ask_module() -> + Instructions = + "~nINTERFACE MODULE~n" + "Enter the name of the library's initial interface module.~n", + list_to_atom(zx_tty:get_lower0_9(Instructions)). + + +-spec package_exists(zx:package_id()) -> boolean(). +%% @private +%% TODO: Change this from a stub into a for-real check via zx_daemon. +%% Check the realm's upstream for the existence of a package that already has the same +%% name, or the name of any modules in src/ and report them to the user. Give the user +%% a chance to change their package's name or ignore the conflict, and report all module +%% naming conflicts. + +package_exists(_) -> + false. + + -spec create_plt() -> ok. %% @private %% Build a general plt file for Dialyzer based on the core Erland distro. @@ -774,7 +1140,7 @@ create_plt() -> " kernel mnesia public_key sasl ssh ssl stdlib", Command = io_lib:format(Template, [PLT]), Message = - "Generating PLT file and writing to: ~tp~n" + "Generating PLT file and writing to: ~ts~n" " There will be a list of \"unknown functions\" in the final output.~n" " Don't panic. This is normal. Turtles all the way down, after all...", ok = log(info, Message, [PLT]), @@ -799,12 +1165,12 @@ dialyze() -> PLT = default_plt(), ok = case filelib:is_regular(PLT) of - true -> log(info, "Using PLT: ~tp", [PLT]); + true -> log(info, "Using PLT: ~ts", [PLT]); false -> create_plt() end, Me = escript:script_name(), EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")), - ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]), + ok = log(info, "Temporarily reconstructing ~ts as ~ts", [Me, EvilTwin]), Sed = io_lib:format("sed 's/^#!.*$//' ~s > ~s", [Me, EvilTwin]), ok = zx_lib:exec_shell(Sed), ok = case dialyzer:run([{init_plt, PLT}, {from, src_code}, {files, [EvilTwin]}]) of @@ -836,7 +1202,7 @@ grow_a_pair(Realm) -> grow_a_pair(Realm, UserName) -> KeyTag = zx_key:prompt_keygen(), - KeyName = UserName ++ "." ++ KeyTag, + KeyName = UserName ++ "-" ++ KeyTag, case zx_key:generate_rsa({Realm, KeyName}) of ok -> ok; @@ -890,7 +1256,7 @@ create_realm(R = #realm_init{realm = Realm, addr = Addr, port = Port, url = URL} Instructions = "~nREALM DATA CONFIRMATION~n" "That's everything! Please confirm these settings:~n" - "[1] Realm Name : ~ts ~n" + "[1] Realm Name : ~ts~n" "[2] Host Address: ~ts~n" "[3] Port Number : ~w~n" "[4] URL : ~ts~n" @@ -903,7 +1269,7 @@ create_realm(R = #realm_init{realm = Realm, addr = Addr, port = Port, url = URL} "4" -> create_realm(R#realm_init{url = none}); "" -> store_realm(R); _ -> - ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + ok = zx_tty:derp(), create_realm(R) end. @@ -925,26 +1291,31 @@ store_realm(#realm_init{realm = Realm, RealmConfPath = filename:join(zx_lib:path(etc, Realm), "realm.conf"), ok = zx_lib:write_terms(RealmConfPath, RealmConf), ok = create_realmfile(Realm), + ok = create_userfile(Realm, UserName), + ZPUF = Realm ++ "-" ++ UserName ++ ".zpuf", ZRF = Realm ++ ".zrf", Message = - "===========================================================================~n" - "DONE!~n" - "~n" - "The realm ~tp has been created and is accessible from this system.~n" - "~n" - "Other zomp nodes and zx users will need the new realm file, ~ts, to~n" - "access the realm. It does not include any public keys.~n" - "~n" - "On the PRIME NODE you need to run two command:~n" - "1. `zx add realm ~ts` to install the new .zrf there~n" - "2. `zx make prime ~ts` to tell the node that it is prime for that realm~n" - "~n" - "On all ZX clients that want to access your new realm and on all subordinate~n" - "Zomp nodes the command `zx add realm ~ts` will need to be run.~n" - "~n" - "How to distribute ~ts is up to you.~n" - "===========================================================================~n", - Substitutions = [Realm, ZRF, ZRF, Realm, ZRF, ZRF], + "=============================================================================~n" + "DONE!~n" + "The realm ~ts has been created and is accessible from this system.~n" + "~n" + "Realm configuration file has been written to: ~ts~n" + "Sysop userfile has been written to: ~ts~n" + "~n" + "-----------------------------------------------------------------------------~n" + "NODE AND CLIENT CONFIGURATION~n" + "~n" + "PRIME NODE configuration requires three commands:~n" + "1. Configure the realm: `zx import realm ~ts`~n" + "2. Add the sysop user: `zx import user ~ts`~n" + "3. Become the prime node: `zx takeover ~ts`~n" + "~n" + "ZX CLIENT and ZOMP DISTRIBUTION NODE configuration requires one command:~n" + "1. Configure the realm: `zx add realm ~ts`~n" + "~n" + "How to distribute ~ts is up to you.~n" + "=============================================================================~n", + Substitutions = [Realm, ZRF, ZPUF, ZRF, ZPUF, Realm, ZRF, ZRF], io:format(Message, Substitutions). @@ -956,19 +1327,12 @@ ask_realm() -> "Enter a name for your new realm.~n" "Names can contain only lower-case letters, numbers and the underscore.~n" "Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - Realm = zx_tty:get_input(), - case zx_lib:valid_lower0_9(Realm) of - true -> - case realm_exists(Realm) of - false -> - Realm; - true -> - ok = io:format("That realm already exists. Be more original.~n"), - ask_realm() - end; + Realm = zx_tty:get_lower0_9(Instructions), + case realm_exists(Realm) of false -> - ok = io:format("Bad realm name \"~ts\". Try again.~n", [Realm]), + Realm; + true -> + ok = io:format("That realm already exists. Be more original.~n"), ask_realm() end. @@ -978,9 +1342,7 @@ ask_realm() -> ask_addr() -> Message = "~nHOST ADDRESS~n" - "Enter a static, valid hostname or IPv4 or IPv6 address at which this host " - "can be reached from the public internet (or internal network if it will never " - "need to be reached from the internet).~n" + "Enter the hostname, IPv4 or IPv6 address of the realm's prime Zomp node.~n" "DO NOT INCLUDE A PORT NUMBER IN THIS STEP~n", ok = io:format(Message), case zx_tty:get_input() of @@ -1029,7 +1391,7 @@ ask_port() -> prompt_port_number(Current) -> ok = io:format("A valid port is any number from 1 to 65535.~n"), - case zx_tty:get_input("[~tw]", [Current]) of + case zx_tty:get_input("[~w]", [Current]) of "" -> Current; S -> @@ -1038,12 +1400,12 @@ prompt_port_number(Current) -> Port when 16#ffff >= Port, Port > 0 -> Port; Illegal -> - Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n", + Whoops = "Whoops! ~w is out of bounds (1~65535). Try again.~n", ok = io:format(Whoops, [Illegal]), prompt_port_number(Current) end catch error:badarg -> - ok = io:format("~tp is not a port number. Try again...", [S]), + ok = io:format("~160tp is not a port number. Try again...", [S]), prompt_port_number(Current) end end. @@ -1057,9 +1419,9 @@ ask_url() -> "Most public realms have a website, IRC channel, or similar location where its " "community (or customers) communicate. If you have such a URL enter it here.~n" "NOTE: No checking is performed on the input here. Confuse your users at " - "your own peril!~n", - ok = io:format(Message), - zx_tty:get_input("[ENTER] to leave blank"). + "your own peril!~n" + "[ENTER] to leave blank.~n", + zx_tty:get_input(Message). -spec create_sysop(InitUser) -> FullUser @@ -1068,15 +1430,15 @@ ask_url() -> create_sysop(U = #user_data{username = none}) -> UserName = ask_username(), - KeyName = UserName ++ ".root", + KeyName = UserName ++ "-root", create_sysop(U#user_data{username = UserName, keys = [KeyName]}); create_sysop(U = #user_data{realname = none}) -> create_sysop(U#user_data{realname = ask_realname()}); create_sysop(U = #user_data{contact_info = none}) -> - create_sysop(U#user_data{contact_info = [ask_email()]}); -create_sysop(U = #user_data{username = UserName, - realname = RealName, - contact_info = [{"email", Email}]}) -> + create_sysop(U#user_data{contact_info = [{"email", ask_email()}]}); +create_sysop(U = #user_data{username = UserName, + realname = RealName, + contact_info = [{"email", Email}]}) -> Instructions = "~nSYSOP DATA CONFIRMATION~n" "Please correct or confirm the sysop's data:~n" @@ -1086,12 +1448,12 @@ create_sysop(U = #user_data{username = UserName, "Press a number to select something to change, or [ENTER] to accept.~n", ok = io:format(Instructions, [UserName, RealName, Email]), case zx_tty:get_input() of - "1" -> create_sysop(U#user_data{username = none}); - "2" -> create_sysop(U#user_data{realname = none}); + "1" -> create_sysop(U#user_data{username = none}); + "2" -> create_sysop(U#user_data{realname = none}); "3" -> create_sysop(U#user_data{contact_info = none}); "" -> U; _ -> - ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + ok = zx_tty:derp(), create_sysop(U) end. @@ -1112,12 +1474,12 @@ create_user(U = #user_data{realm = none}) -> end; create_user(U = #user_data{username = none}) -> UserName = ask_username(), - KeyName = UserName ++ ".1", + KeyName = UserName ++ "-1", create_user(U#user_data{username = UserName, keys = [KeyName]}); create_user(U = #user_data{realname = none}) -> create_user(U#user_data{realname = ask_realname()}); create_user(U = #user_data{contact_info = none}) -> - create_user(U#user_data{contact_info = [ask_email()]}); + create_user(U#user_data{contact_info = [{"email", ask_email()}]}); create_user(U = #user_data{realm = Realm, username = UserName, realname = RealName, @@ -1132,19 +1494,15 @@ create_user(U = #user_data{realm = Realm, "Press a number to select something to change, or [ENTER] to accept.~n", ok = io:format(Instructions, [Realm, UserName, RealName, Email]), case zx_tty:get_input() of - "1" -> - create_user(U#user_data{realm = none}); - "2" -> - create_user(U#user_data{username = none}); - "3" -> - create_user(U#user_data{realname = none}); - "4" -> - create_user(U#user_data{contact_info = none}); + "1" -> create_user(U#user_data{realm = none}); + "2" -> create_user(U#user_data{username = none}); + "3" -> create_user(U#user_data{realname = none}); + "4" -> create_user(U#user_data{contact_info = none}); "" -> ok = store_user(U), UserName; _ -> - ok = io:format("~nArglebargle, glop-glyf!?!~n~n"), + ok = zx_tty:derp(), create_user(U) end. @@ -1164,8 +1522,7 @@ store_user(#user_data{realm = Realm, {keys, KeyNames}], ok = gen_keys(Realm, KeyNames), UserConfPath = filename:join(zx_lib:path(etc, Realm), UserName ++ ".user"), - ok = zx_lib:write_terms(UserConfPath, UserConf), - log(info, "User ~tp created.", [{Realm, UserName}]). + zx_lib:write_terms(UserConfPath, UserConf). -spec create_userfile() -> ok. @@ -1179,14 +1536,22 @@ create_userfile() -> create_userfile(Realm) -> UserName = select_user(Realm), + create_userfile(Realm, UserName). + + +create_userfile(Realm, UserName) -> UserConf = filename:join(zx_lib:path(etc, Realm), UserName ++ ".user"), {ok, UserData} = file:consult(UserConf), Keys = proplists:get_value(keys, UserData), Load = fun(KeyName, Acc) -> case file:read_file(zx_key:path(public, {Realm, KeyName})) of - {ok, Data} -> [{KeyName, Data} | Acc]; - _ -> Acc + {ok, Der} -> + SHA512 = crypto:hash(sha512, Der), + Public = {SHA512, Der}, + [{KeyName, none, Public} | Acc]; + _ -> + Acc end end, PubKeyData = lists:foldl(Load, [], Keys), @@ -1194,8 +1559,8 @@ create_userfile(Realm) -> Bin = term_to_binary({UserData, PubKeyData}), ok = file:write_file(UserFile, Bin), Message = - "Wrote Zomp public user file to ~tp.~n" - "This file can be given to a sysop from ~tp and added to the realm.~n" + "Wrote Zomp public user file to ~ts.~n" + "This file can be given to a sysop from ~ts and added to the realm.~n" "It ONLY contains PUBLIC KEY data.~n", io:format(Message, [UserFile, Realm]). @@ -1216,15 +1581,15 @@ export_user(Realm) -> Keys = proplists:get_value(keys, UserData), Load = fun(KeyName, Acc) -> - Pub = - case file:read_file(zx_key:path(public, {Realm, KeyName})) of - {ok, PD} -> PD; - _ -> none - end, Key = case file:read_file(zx_key:path(private, {Realm, KeyName})) of - {ok, KD} -> KD; - _ -> none + {ok, KDer} -> {crypto:hash(sha512, KDer), KDer}; + _ -> none + end, + Pub = + case file:read_file(zx_key:path(public, {Realm, KeyName})) of + {ok, PDer} -> {crypto:hash(sha512, PDer), PDer}; + _ -> none end, [{KeyName, Key, Pub} | Acc] end, @@ -1233,16 +1598,16 @@ export_user(Realm) -> Bin = term_to_binary({UserData, KeyData}), ok = file:write_file(UserFile, Bin), Message = - "Wrote Zomp DANGEROUS user file to ~tp.~n" + "Wrote Zomp DANGEROUS user file to ~ts.~n" "WARNING: This file contains your PRIVATE KEYS and should NEVER be shared with " - "anyone. Its only use is for the \"import user [.zduf]\" command!~n", + "anyone. Its only use is for the `import user [.zduf]` command!~n", io:format(Message, [UserFile]). -spec import_user(file:filename()) -> zx:outcome(). -import_user(ZDUF) -> - case file:read_file(ZDUF) of +import_user(Path) -> + case file:read_file(Path) of {ok, Bin} -> import_user2(Bin); {error, enoent} -> {error, "Bad path/missing file.", 2}; {error, eacces} -> {error, "Can't read file: bad permissions.", 13}; @@ -1259,7 +1624,7 @@ import_user2(Bin) -> {ok, {UserData, KeyData}} -> import_user3(UserData, KeyData); error -> - {error, "Bad .zduf data. Is this really a legitimate .zduf?", 1} + {error, "Bad data. Is this really a legitimate .zduf or .zpuf?", 1} end. @@ -1279,20 +1644,20 @@ import_user3(UserData, KeyData) -> import_user4(Realm, UserName, KeyData) -> Write = fun - ({KeyName, KeyBin, none}) -> + ({KeyName, {KeySHA512, KeyDer}, none}) -> + KeySHA512 = crypto:hash(sha512, KeyDer), KeyID = {Realm, KeyName}, - file:write_file(zx_key:path(private, KeyID), KeyBin); - ({KeyName, none, PubBin}) -> + file:write_file(zx_key:path(private, KeyID), KeyDer); + ({KeyName, none, {PubSHA512, PubDer}}) -> + PubSHA512 = crypto:hash(sha512, PubDer), KeyID = {Realm, KeyName}, - file:write_file(zx_key:path(public, KeyID), PubBin); - ({KeyName, KeyBin, PubBin}) -> + file:write_file(zx_key:path(public, KeyID), PubDer); + ({KeyName, {KeySHA512, KeyDer}, {PubSHA512, PubDer}}) -> + KeySHA512 = crypto:hash(sha512, KeyDer), + PubSHA512 = crypto:hash(sha512, PubDer), KeyID = {Realm, KeyName}, - file:write_file(zx_key:path(private, KeyID), KeyBin), - file:write_file(zx_key:path(public, KeyID), PubBin); - ({KeyName, PubBin}) -> - ok = log(info, "This file is probably a .zpuf, not a .zduf"), - KeyID = {Realm, KeyName}, - file:write_file(zx_key:path(public, KeyID), PubBin) + file:write_file(zx_key:path(private, KeyID), KeyDer), + file:write_file(zx_key:path(public, KeyID), PubDer) end, ok = lists:foreach(Write, KeyData), log(info, "Imported user ~ts to realm ~ts.", [UserName, Realm]). @@ -1334,6 +1699,22 @@ pick_realm() -> end. +-spec ask_project_name() -> string(). + +ask_project_name() -> + Instructions = + "~nPROJECT NAME~n" + "Enter the natural, readable name of your project.~n" + "(Any valid UTF-8 printables are legal.)~n", + case zx_tty:get_input(Instructions) of + "" -> + ok = io:format("Please enter a project name."), + ask_project_name(); + String -> + unicode:characters_to_list(String, utf8) + end. + + -spec ask_username() -> zx:user_name(). ask_username() -> @@ -1342,21 +1723,7 @@ ask_username() -> "Enter a username.~n" "Names can contain only lower-case letters, numbers and the underscore.~n" "Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - case zx_tty:get_input() of - "" -> - Message = "You have to be called *something*. Let's try that again.~n", - ok = io:format(Message), - ask_username(); - UserName -> - case zx_lib:valid_lower0_9(UserName) of - true -> - UserName; - false -> - ok = io:format("Bad username ~tp. Try again.~n", [UserName]), - ask_username() - end - end. + zx_tty:get_lower0_9(Instructions). -spec ask_realname() -> string(). @@ -1365,46 +1732,139 @@ ask_realname() -> Instructions = "~nREAL NAME~n" "Enter the user's real name (or whatever name people recognize).~n" - "There are no rules for this one. Any valid UTF-8 printables are legal.~n", - ok = io:format(Instructions), - zx_tty:get_input(). + "(Any valid UTF-8 printables are legal.)~n", + String = zx_tty:get_input(Instructions, [], "[ENTER] to leave blank"), + unicode:characters_to_list(String, utf8). --spec ask_email() -> {Type :: string(), Email :: string()}. +-spec ask_author() -> {string(), string()}. + +ask_author() -> + Instructions = + "~nAUTHOR'S NAME~n" + "Enter the authors's real name (or whatever name people recognize).~n" + "(Any valid UTF-8 printables are legal.)~n", + String = zx_tty:get_input(Instructions, [], "[ENTER] to leave blank"), + Author = unicode:characters_to_list(String, utf8), + Email = ask_email_optional(), + {Author, Email}. + + +-spec ask_copyright() -> {string(), string()}. + +ask_copyright() -> + Instructions = + "~nCOPYRIGHT HOLDER'S NAME~n" + "Enter the copyright's real name. This may be a person, company, group, etc.~n" + "There are no rules for this one. Any valid UTF-8 printables are legal.~n" + "This can also be left blank.~n", + String = zx_tty:get_input(Instructions, [], "[ENTER] to leave blank"), + Holder = unicode:characters_to_list(String, utf8), + Email = ask_email_optional(), + {Holder, Email}. + + +-spec ask_license() -> string(). + +ask_license() -> + Message = + "~nLICENSE SELECTION~n" + "Note that many more FOSS license identifiers are available at " + "https://spdx.org/licenses/~n", + ok = io:format(Message), + Options = + [{"Apache License 2.0", "Apache-2.0"}, + {"BSD 3-Clause with Attribution", "BSD-3-Clause-Attribution"}, + {"BSD 2-Clause / FreeBSD", "BSD-2-Clause-FreeBSD"}, + {"GNU General Public License (GPL) v3.0 only", "GPL-3.0-only"}, + {"GNU General Public License (GPL) v3.0 or later", "GPL-3.0-or-later"}, + {"GNU Library (LGPL) v3.0 only", "LGPL-3.0-only"}, + {"GNU Library (LGPL) v3.0 or later", "LGPL-3.0-or-later"}, + {"MIT license", "MIT"}, + {"Mozilla Public License 2.0", "MPL-2.0"}, + {"Public Domain", "Public-Domain"}, + {"[proprietary]", proprietary}, + {"[skip and leave blank]", skip}], + case zx_tty:select(Options) of + skip -> + ""; + proprietary -> + Notice = + "~nNOTE ON PROPRIETARY USE~n" + "It is recommended that you check with your legal department or " + "advisor to determine whether your proprietary project is compatible " + "with the GPL version of Zomp/ZX.~n" + "If not (mostly when deploying client-side code that links to ZX or " + "other GPL/dual-license libraries), Zomp/ZX should be dual-licensed.~n", + ok = io:format(Notice), + ""; + License -> + License + end. + + + +-spec ask_email() -> string(). ask_email() -> - Instructions = - "~nEMAIL~n" - "Enter an email address.~n" - "Valid email address rules apply though the checking done here is quite " - "minimal. Check the address you enter carefully. The only people who will " - "suffer from an invalid address are other realm users.~n", - ok = io:format(Instructions), - Email = zx_tty:get_input(), - case string:lexemes(Email, "@") of + case zx_tty:get_input(email_instructions()) of + "" -> + ok = io:format("An email address is required.~n"), + ask_email(); + String -> + case check_email(String) of + ok -> String; + error -> ask_email() + end + end. + + +-spec ask_email_optional() -> string(). + +ask_email_optional() -> + case zx_tty:get_input(email_instructions(), [], "[ENTER] to leave blank") of + "" -> + ""; + String -> + case check_email(String) of + ok -> String; + error -> ask_email() + end + end. + + +-spec check_email(Address :: string()) -> ok | error. + +check_email(Address) -> + case string:lexemes(Address, "@") of [User, Host] -> case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of {true, true} -> - {"email", Email}; + ok; {false, true} -> Message = "The user part of the email address seems invalid.~n", ok = io:format(Message), - ask_email(); + error; {true, false} -> Message = "The host part of the email address seems invalid.~n", ok = io:format(Message), - ask_email(); + error; {false, false} -> Message = "This email address is totally bonkers. Try again.~n", ok = io:format(Message), - ask_email() + error end; _ -> - ok = io:format("Don't get fresh with me. Try again. For real this time.~n"), - ask_email() + ok = io:format("Don't get fresh. Try again. For real this time.~n"), + error end. +email_instructions() -> + "~nEMAIL~n" + "Enter an email address.~n". + + -spec realm_exists(zx:realm()) -> boolean(). %% @private %% Checks for remnants of a realm. @@ -1424,7 +1884,7 @@ realm_exists(Realm) -> make_realm_dirs(Realm) -> Dirs = [zx_lib:path(D, Realm) || D <- [etc, var, tmp, log, key, zsp, lib]], - Make = fun(D) -> ok = file:make_dir(D) end, + Make = fun(D) -> ok = zx_lib:force_dir(D) end, lists:foreach(Make, Dirs). @@ -1438,7 +1898,7 @@ make_realm_dirs(Realm) -> % {leaf, 256}, % {listen_port, 11311}, % {public_port, 11311}], -% io:format("~tp~n", [ZompSettings]). +% io:format("~tw~n", [ZompSettings]). -spec create_realmfile() -> ok. @@ -1482,7 +1942,7 @@ drop_realm(Realm) -> log(info, "Aborting.") end; false -> - log(info, "Could not find any trace of realm ~tp", [Realm]) + log(info, "Could not find any trace of realm ~ts", [Realm]) end. @@ -1584,9 +2044,27 @@ select_realm() -> -spec select_user(zx:realm()) -> zx:user_name(). select_user(Realm) -> - Pattern = filename:join(zx_lib:path(etc, Realm), "*.user"), - case [filename:basename(F, ".user") || F <- filelib:wildcard(Pattern)] of - [] -> create_user(#user_data{realm = Realm}); - [UserName] -> UserName; - UserNames -> zx_tty:select_string(UserNames) + case list_users(Realm) of + [] -> + Message = + "A user record is required to complete this action. Creating now...", + ok = log(info, Message), + create_user(#user_data{realm = Realm}); + [UserName] -> + UserName; + UserNames -> + zx_tty:select_string(UserNames) + end. + + +-spec select_private_key(zx:user_id()) -> zx:key_name(). + +select_private_key(UserID = {Realm, UserName}) -> + KeyDir = zx_lib:path(key, Realm), + ok = zx_lib:force_dir(KeyDir), + Pattern = filename:join(KeyDir, UserName ++ "-*.key.der"), + case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of + [] -> zx_key:prompt_keygen(UserID); + [KeyName] -> KeyName; + KeyNames -> zx_tty:select_string(KeyNames) end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_net.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_net.erl index f87b707..c74291c 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_net.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_net.erl @@ -9,11 +9,48 @@ -copyright("Craig Everett "). -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}} -> + <> = <>, + {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, <>} -> + 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, <>, 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). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_sys_conf.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_sys_conf.erl index 5c48108..f92929e 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_sys_conf.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_sys_conf.erl @@ -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. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl index 24bbd64..8a7789b 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl @@ -10,7 +10,10 @@ -copyright("Craig Everett "). -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"). diff --git a/zomp/lib/otpr/zx/0.1.0/templates/Emakefile b/zomp/lib/otpr/zx/0.1.0/templates/Emakefile new file mode 100644 index 0000000..68c7b67 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/Emakefile @@ -0,0 +1 @@ +{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/boringlib/funfile.erl b/zomp/lib/otpr/zx/0.1.0/templates/boringlib/funfile.erl new file mode 100644 index 0000000..cfa8577 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/boringlib/funfile.erl @@ -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]). diff --git a/zomp/lib/otpr/zx/0.1.0/templates/escript b/zomp/lib/otpr/zx/0.1.0/templates/escript new file mode 100755 index 0000000..3854e61 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/escript @@ -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]). diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/appmod.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/appmod.erl new file mode 100644 index 0000000..7712ee4 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/appmod.erl @@ -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. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/client.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client.erl new file mode 100644 index 0000000..db70d0b --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client.erl @@ -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}. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_man.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_man.erl new file mode 100644 index 0000000..b2d7a95 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_man.erl @@ -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. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_sup.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_sup.erl new file mode 100644 index 0000000..af4c98f --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/client_sup.erl @@ -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]}}. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/clients.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/clients.erl new file mode 100644 index 0000000..6b88a4c --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/clients.erl @@ -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}}. diff --git a/zomp/lib/otpr/zx/0.1.0/templates/example_server/sup.erl b/zomp/lib/otpr/zx/0.1.0/templates/example_server/sup.erl new file mode 100644 index 0000000..3b2d27d --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/templates/example_server/sup.erl @@ -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}}. diff --git a/zomp/lib/otpr/zx/0.1.0/zomp.meta b/zomp/lib/otpr/zx/0.1.0/zomp.meta new file mode 100644 index 0000000..8188835 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/zomp.meta @@ -0,0 +1,4 @@ +{deps,[]}. +{package_id,{"otpr","zx",{0,1,0}}}. +{prefix,"zx_"}. +{type,app}.