Skip to main content

priv/bin/barrel_p2p_call.sh

#!/usr/bin/env bash
#
# barrel_p2p_call.sh — erl_call-style one-shot RPC over a barrel_p2p
# (-proto_dist quic) cluster, with full Ed25519 dist authentication.
#
# Boots a hidden probe BEAM with barrel_p2p on the path, configures the
# auth callback and discovery module via -quic_dist_* init args, runs
# rpc:call/5 against the target, asks the target to disconnect so the
# hidden-node entry is reaped immediately, and halts.
#
# Usage:
#   barrel_p2p_call.sh [options] <Node> <Module> <Function> [ArgsTerm]
#
# Options (env var fallback in parens):
#   -c, --cookie COOKIE          dist cookie (COOKIE, default barrel_p2p)
#       --cert FILE              TLS cert (BARREL_P2P_CERT, default data/quic/node.crt)
#       --key  FILE              TLS key  (BARREL_P2P_KEY,  default data/quic/node.key)
#       --key-dir DIR            Ed25519 keypair dir (BARREL_P2P_KEY_DIR, default data/keys)
#       --discovery-dir DIR      filesystem-discovery dir (BARREL_P2P_DISC_DIR, default data/discovery)
#   -t, --timeout MS             rpc:call timeout (default 10000)
#   -n, --name NAME              probe long node name (default barrel_p2p_call_$$@<host>)
#   -h, --help                   show this help
#
# Exit codes: 0 success, 2 usage error, 4 connect failed, 5 badrpc.

set -euo pipefail

usage() {
    sed -n '3,28p' "$0" | sed 's/^# \{0,1\}//'
}

COOKIE="${COOKIE:-barrel_p2p}"
CERT="${BARREL_P2P_CERT:-$PWD/data/quic/node.crt}"
KEY="${BARREL_P2P_KEY:-$PWD/data/quic/node.key}"
KEY_DIR="${BARREL_P2P_KEY_DIR:-$PWD/data/keys}"
DISC_DIR="${BARREL_P2P_DISC_DIR:-$PWD/data/discovery}"
TIMEOUT=10000
NAME=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        -c|--cookie)         COOKIE="$2"; shift 2 ;;
        --cert)              CERT="$2"; shift 2 ;;
        --key)               KEY="$2"; shift 2 ;;
        --key-dir)           KEY_DIR="$2"; shift 2 ;;
        --discovery-dir)     DISC_DIR="$2"; shift 2 ;;
        -t|--timeout)        TIMEOUT="$2"; shift 2 ;;
        -n|--name)           NAME="$2"; shift 2 ;;
        -h|--help)           usage; exit 0 ;;
        --)                  shift; break ;;
        -*)                  echo "barrel_p2p_call: unknown option $1" >&2; exit 2 ;;
        *)                   break ;;
    esac
done

if [[ $# -lt 3 ]]; then
    usage >&2
    exit 2
fi

NODE="$1"; MOD="$2"; FUN="$3"; ARGS="${4:-[]}"

for f in "$CERT" "$KEY"; do
    if [[ ! -r "$f" ]]; then
        echo "barrel_p2p_call: file not readable: $f" >&2
        exit 2
    fi
done

# Resolve script dir and walk up to a rebar3 _build/default/lib tree.
# We support both layouts:
#   1) installed under .../barrel_p2p/priv/bin/  (rebar3-compiled)
#   2) checked-out source tree at /Users/.../erlang-barrel_p2p/priv/bin/
SCRIPT_PATH="$0"
case "$SCRIPT_PATH" in
    /*) ;;
    *)  SCRIPT_PATH="$PWD/$SCRIPT_PATH" ;;
esac
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"

# Possible roots: priv/bin -> barrel_p2p app dir
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Find the surrounding _build/default/lib so we can -pa each dep.
LIB_DIR=""
SEARCH_DIR="$APP_DIR"
for _ in 1 2 3 4 5 6; do
    if [[ -d "$SEARCH_DIR/_build/default/lib" ]]; then
        LIB_DIR="$SEARCH_DIR/_build/default/lib"
        break
    fi
    SEARCH_DIR="$(dirname "$SEARCH_DIR")"
done

PA_ARGS=()
if [[ -n "$LIB_DIR" ]]; then
    for d in "$LIB_DIR"/*/ebin; do
        [[ -d "$d" ]] && PA_ARGS+=( -pa "$d" )
    done
fi
# Always include the barrel_p2p app's own ebin (when running from a
# checkout it lives at $APP_DIR/ebin after compile).
[[ -d "$APP_DIR/ebin" ]] && PA_ARGS+=( -pa "$APP_DIR/ebin" )

if [[ -z "$NAME" ]]; then
    NAME="barrel_p2p_call_$$@$(hostname -s)"
fi

EVAL=$(cat <<ERL
ok = application:load(barrel_p2p),
ok = application:set_env(barrel_p2p, auth_enabled,    true),
ok = application:set_env(barrel_p2p, auth_key_dir,    "${KEY_DIR}"),
ok = application:set_env(barrel_p2p, auth_trust_mode, tofu),
ok = application:set_env(barrel_p2p, discovery_dir,   "${DISC_DIR}"),
ok = application:set_env(barrel_p2p, discovery_backends,
    [barrel_p2p_discovery_static,
     barrel_p2p_discovery_file,
     barrel_p2p_discovery_dns]),
%% Make sure the probe shares the seed's view of the {quic, dist}
%% block so the lookup path resolves the discovery_module correctly.
DistOpts0 = application:get_env(quic, dist, []),
DistOpts1 = lists:keystore(discovery_module, 1, DistOpts0,
                           {discovery_module, barrel_p2p_discovery}),
DistOpts2 = lists:keystore(auth_callback, 1, DistOpts1,
                           {auth_callback,
                            {barrel_p2p_dist_auth_callback, authenticate}}),
ok = application:set_env(quic, dist, DistOpts2),
{ok, _} = barrel_p2p_dist_keys:start_link(),
case net_kernel:connect_node('${NODE}') of
    true -> ok;
    Err  ->
        io:format(standard_error, "connect failed: ~p~n", [Err]),
        erlang:halt(4)
end,
RpcResult = rpc:call('${NODE}', '${MOD}', '${FUN}', ${ARGS}, ${TIMEOUT}),
_ = (catch rpc:call('${NODE}', erlang, disconnect_node, [node()], 2000)),
case RpcResult of
    {badrpc, BadRpc} ->
        io:format(standard_error, "badrpc: ~p~n", [BadRpc]),
        erlang:halt(5);
    Result ->
        io:format("~p~n", [Result]),
        erlang:halt(0)
end.
ERL
)

ERL_ARGS=(
    -name "$NAME"
    -setcookie "$COOKIE"
    -hidden
    -noinput
    -proto_dist quic
    -epmd_module quic_epmd
    -start_epmd false
    -quic_dist_cert "$CERT"
    -quic_dist_key  "$KEY"
    -quic_dist_port 0
    -quic_dist_discovery_module barrel_p2p_discovery
    -quic_dist_auth_callback    barrel_p2p_dist_auth_callback:authenticate
    -kernel net_setuptime 10
)
ERL_ARGS+=( "${PA_ARGS[@]}" )

exec erl "${ERL_ARGS[@]}" -eval "$EVAL"