#!/usr/bin/env bash # One-line p2me install: curl -fsSL https://p2me.in/install | bash # or (no curl): wget -qO- https://p2me.in/install | bash # # Installs only the FIRST CORE: downloads the source archive from the site, builds the image and runs the node in Docker. # After that CORE is updated by the network itself — the site is not needed for updates. # # Requires only bash (Git Bash on Windows), Docker and curl or wget (no compose plugin needed — the node runs via docker run). # Environment variables (optional): # P2ME_DIR — where to install (default ~/p2me). Several nodes on one machine — different directories: the directory name # becomes the node name (container name), free panel and card ports are picked automatically # P2ME_URL — where to download from (default https://p2me.in) # P2ME_FORCE — 1: install over an existing installation (data/ with the wallet is not touched) # Node first run (license and app selection): the script asks whether to answer now in the terminal or later on # the first-run screen in the panel (P2ME_SETUP_NOW=1/0 — no question). If the node will have no panel (a pre-placed # data/machine.json with "role": "main"), questions are asked only in the terminal. Without a terminal — via variables: # P2ME_ACCEPT_LICENSE=1 (required), P2ME_VPN, P2ME_SOCKS5, P2ME_PUBLIC (default 0), P2ME_ARCHIVE (1) # Without P2ME_ACCEPT_LICENSE and without a terminal the choice is left to the first-run screen in the panel (its own or, for role # main, the owner's panel managing the node over p2me). # The whole script is in a function called on the last line: if a curl | bash download is cut off, # the partially downloaded script executes nothing main() { set -euo pipefail local base="${P2ME_URL:-https://p2me.in}" local dir="${P2ME_DIR:-$HOME/p2me}" dir="${dir%/}" local name # Container name — only lowercase letters, digits, _ and - name="$(basename "$dir" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9_\n-' '-')" say "p2me: installing node $name into $dir" command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "neither curl nor wget found" need tar need docker docker_ready \ || die "Docker is not responding. Start Docker (on Linux add your user to the docker group: sudo usermod -aG docker \$USER, then log in again)" docker_desktop_setup if [ -d "$dir/src" ] && [ "${P2ME_FORCE:-}" != "1" ]; then say "p2me is already installed in $dir — CORE updates arrive over the network." say "To reinstall on top (the wallet in $dir/data is kept): $(fetcher) $base/install | P2ME_FORCE=1 bash" exit 0 fi # Container name taken by a node from another directory — a second node on the machine goes into its own directory local owner owner="$(docker inspect -f '{{index .Config.Labels "p2me.dir"}}' "$name" 2>/dev/null || true)" # A node installed earlier via compose — found by its label (project directory is /src/docker) [ -n "$owner" ] || owner="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$name" 2>/dev/null || true)" owner="${owner%/src/docker}" owner="${owner%\\src\\docker}" # On Windows Docker stores the path as C:\Users\…, while Git Bash sees it as /c/Users/… [ -n "$owner" ] && command -v cygpath >/dev/null 2>&1 && owner="$(cygpath -u "$owner")" if [ -n "$owner" ] && [ "$owner" != "$dir" ]; then die "a node named $name already runs from $owner — install another one into its own folder: $(fetcher) $base/install | P2ME_DIR=~/p2me-2 bash" fi local tmp tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT say "Downloading $base/p2me.tar.gz" download "$base/p2me.tar.gz" "$tmp/p2me.tar.gz" || die "download failed: $base/p2me.tar.gz" download "$base/p2me.tar.gz.sha256" "$tmp/p2me.tar.gz.sha256" || die "download failed: $base/p2me.tar.gz.sha256" local want got want="$(cut -d' ' -f1 <"$tmp/p2me.tar.gz.sha256")" got="$(sha256 "$tmp/p2me.tar.gz")" [ -n "$want" ] && [ "$want" = "$got" ] || die "Archive checksum mismatch — try again later" # Panel port: on reinstall — the previous one (saved by the node in machine.json, takes precedence over the variable), otherwise the first # free from 8000 (the card is in data/machine.json) local port="" [ -f "$dir/data/machine.json" ] \ && port="$(tr -d '\n' <"$dir/data/machine.json" | sed -n 's/.*"control": *{[^}]*"port": *\([0-9]*\).*/\1/p')" [ -n "$port" ] || { [ -f "$dir/src/docker/.env" ] && port="$(sed -n 's/^P2ME_PORT=//p' "$dir/src/docker/.env")"; } || true [ -n "$port" ] || port="$(free_port tcp 8000 8009)" || die "no free panel port in 8000-8009" mkdir -p "$dir/data" # Old sources are replaced entirely, data/ (wallet, settings) stays rm -rf "$dir/src" tar -xzf "$tmp/p2me.tar.gz" -C "$dir" # Node mode from a pre-placed data/machine.json: main — no web panel (managed by the owner's panel over # p2me), panel — panel only, no own server (it has no first-run screen) local role="" [ -f "$dir/data/machine.json" ] \ && role="$(tr -d '\n' <"$dir/data/machine.json" | sed -n 's/.*"role": *"\([a-z]*\)".*/\1/p')" # First run — before the build, so the rest of the install proceeds without the user. The node takes the answers from # data/first_run.json at startup (p2me/installer.py) — so they also reach a node without a panel if [ ! -f "$dir/data/wallet.json" ] && [ ! -f "$dir/data/first_run.json" ] && [ "$role" != "panel" ]; then local first_run first_run="$(ask_first_run "$dir/LICENSE" "$role")" || exit 1 [ -z "$first_run" ] || printf '%s\n' "$first_run" >"$dir/data/first_run.json" fi # New node: card port (UDP) — the first free from 8010; the node does not start with a busy one if [ ! -f "$dir/data/machine.json" ]; then local listener listener="$(free_port udp 8010 8099)" || die "no free card port in UDP 8010-8099" printf '{"version": 1, "listener": {"port": %s}}\n' "$listener" >"$dir/data/machine.json" fi # Network files: seed nodes (without them a new node knows nobody — there is intentionally no broadcast discovery) # and genesis (initial distribution; with a different one the node ends up in its own separate network) local f for f in peers.json genesis.json; do [ -f "$dir/data/$f" ] || download "$base/$f" "$dir/data/$f" || rm -f "$dir/data/$f" done # Node parameters (same as src/docker/compose.yaml for development): name, panel port, Docker socket group # and data/ owner; reinstall also takes the port from here { echo "P2ME_NAME=$name" echo "P2ME_PORT=$port" echo "DOCKER_GID=$(docker_gid)" echo "APP_UID=$(id -u)" echo "APP_GID=$(id -g)" } >"$dir/src/docker/.env" say "Building and starting the container (takes a few minutes the first time)" # The image is built from the downloaded archive, not the unpacked directory: file permissions come from tar. Building # from a directory on Windows would give all files 755 — the exec bit is part of the app package hash, and the signatures # of built-in packages would not match (archive_mismatch) # Context goes through a pipe, not a file redirect: newer Docker buildx versions do not read the context from # a regular file on stdin ("failed to peek context header from STDIN: bad file descriptor") cat "$tmp/p2me.tar.gz" | docker build -f src/docker/Dockerfile -t p2me - || die "Failed to build the p2me image" run_node "$dir" "$name" "$port" local cli cli="$(cli_wrapper "$dir" "$name")" local i if [ "$role" = "main" ]; then # No panel — nothing to wait for except a running container sleep 5 [ "$(docker inspect -f '{{.State.Running}}' "$name" 2>/dev/null)" = true ] \ || die "The node did not start. Logs: docker logs $name" say "" say "Done. The node runs without a local panel (role main) — manage it from your panel over p2me." say "Command line: $cli help" say "Logs: docker logs $name" say "Stop: docker stop $name (start again: docker start $name)" exit 0 fi say "Waiting for the panel" for i in $(seq 1 60); do if fetch "http://127.0.0.1:$port" /dev/null; then say "" say "Done. Node panel: http://127.0.0.1:$port" say "Command line: $cli help" say "Stop: docker stop $name (start again: docker start $name)" exit 0 fi sleep 2 done die "The panel did not respond within 2 minutes. Logs: docker logs $name" } say() { printf '%s\n' "$*"; } die() { printf 'p2me: %s\n' "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "command not found: $1"; } # Download a URL to a file: curl, or wget where it is missing (stock Ubuntu Desktop) fetch() { if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" 2>/dev/null else wget -qO "$2" "$1" fi } # Download from the site with retries: the network may briefly drop (e.g. while Docker Desktop restarts # and rebuilds network adapters, including the host network) download() { local i for i in 1 2 3 4 5; do fetch "$1" "$2" && return 0 sleep 3 done return 1 } # Yes/no question: value from an environment variable, otherwise from the terminal (stdin is taken by the script under curl | bash), # otherwise the default. Prints 1 or 0 yes_no() { local var="$1" question="$2" default="$3" answer if [ -n "${!var:-}" ]; then case "${!var}" in 1|y|Y|yes|true) echo 1 ;; *) echo 0 ;; esac return fi if [ -r /dev/tty ] && { : /dev/null; then local hint="[y/N]"; [ "$default" = 1 ] && hint="[Y/n]" printf '%s %s ' "$question" "$hint" >/dev/tty read -r answer /dev/null && tty=1 if [ -z "${P2ME_ACCEPT_LICENSE:-}" ] && [ -z "$tty" ]; then if [ "$role" = "main" ]; then say "No terminal: the license and app choices will be asked in the panel that manages this node over p2me (or set P2ME_ACCEPT_LICENSE=1)" >&2 else say "No terminal: the license and app choices will be asked in the panel (or set P2ME_ACCEPT_LICENSE=1)" >&2 fi return fi if [ -z "${P2ME_ACCEPT_LICENSE:-}" ] && [ "$role" != "main" ]; then [ -z "${P2ME_SETUP_NOW:-}" ] \ && echo "Setup questions: the license, VPN, SOCKS5, public files and chain archive." >/dev/tty if [ "$(yes_no P2ME_SETUP_NOW "Answer them now in the terminal? (No — the panel asks on first open)" 1)" != 1 ]; then return fi fi if [ -n "$tty" ] && [ -z "${P2ME_ACCEPT_LICENSE:-}" ]; then { echo echo "Welcome to p2me. The node finds work by itself (relaying, storage, notary), earns points," echo "and with points you can order services from other nodes. Apps run in isolated containers." echo echo "----- License -----" [ -f "$license" ] && cat "$license" || echo "(the license text is not in the distribution yet)" echo "-------------------" } >/dev/tty fi [ "$(yes_no P2ME_ACCEPT_LICENSE "I accept the license (required)" 0)" = 1 ] || die "the license was not accepted" local vpn socks5 compute exchange public archive [ -n "$tty" ] && [ -z "${P2ME_VPN:-}" ] && echo "VPN: other people's traffic goes out to the internet from this node's IP address (complaints go to you; your provider may forbid it)." >/dev/tty vpn="$(yes_no P2ME_VPN "Install VPN?" 0)" [ -n "$tty" ] && [ -z "${P2ME_SOCKS5:-}" ] && echo "SOCKS5: same as VPN — other people's traffic leaves from this node's IP address." >/dev/tty socks5="$(yes_no P2ME_SOCKS5 "Install the SOCKS5 proxy?" 0)" [ -n "$tty" ] && [ -z "${P2ME_COMPUTE:-}" ] && echo "Compute: this node runs other people's Docker images on their data (no network, limits); you do not see what the code does." >/dev/tty compute="$(yes_no P2ME_COMPUTE "Install Compute?" 0)" [ -n "$tty" ] && [ -z "${P2ME_EXCHANGE:-}" ] && echo "Exchange: points for bitcoin between people; currency exchange may be regulated in your country." >/dev/tty exchange="$(yes_no P2ME_EXCHANGE "Install Exchange (points <-> bitcoin)?" 0)" [ -n "$tty" ] && [ -z "${P2ME_PUBLIC:-}" ] && echo "Public files, open Git repositories and Social space mirrors are other people's content served openly from this node: material legal in one country may be banned in yours. Encrypted data is always allowed." >/dev/tty public="$(yes_no P2ME_PUBLIC "Host other people's public content?" 0)" [ -n "$tty" ] && [ -z "${P2ME_ARCHIVE:-}" ] && echo "Chain archive: keep old parts of the chain with Storage holders for points instead of on this disk." >/dev/tty archive="$(yes_no P2ME_ARCHIVE "Keep the chain in Storage (recommended)?" 1)" b() { [ "$1" = 1 ] && echo true || echo false; } # Answers to app questions (manifest "ask" in src/apps//): the other distribution apps install themselves printf '{"license": true, "apps": {"vpn": %s, "socks5": %s, "compute": %s, "exchange": %s}, "public": %s, "archive": %s}' \ "$(b "$vpn")" "$(b "$socks5")" "$(b "$compute")" "$(b "$exchange")" "$(b "$public")" "$(b "$archive")" } # How the script itself was downloaded — for hints with the command fetcher() { if command -v curl >/dev/null 2>&1; then echo "curl -fsSL"; else echo "wget -qO-"; fi } sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 else shasum -a 256 "$1" | cut -d' ' -f1 fi } # First free port of a protocol (tcp/udp) in a range: ss (Linux), lsof (macOS), netstat (Windows) free_port() { local proto="$1" p for p in $(seq "$2" "$3"); do port_busy "$proto" "$p" || { echo "$p"; return 0; } done return 1 } port_busy() { local proto="$1" port="$2" if command -v ss >/dev/null 2>&1; then if [ "$proto" = tcp ]; then ss -Hltn "sport = :$port" | grep -q . else ss -Hlun "sport = :$port" | grep -q . fi elif command -v lsof >/dev/null 2>&1; then if [ "$proto" = tcp ]; then lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1 else lsof -nP -iUDP:"$port" >/dev/null 2>&1 fi else netstat -an 2>/dev/null | grep -iE "^ *$proto.*[.:]$port " | grep -q . fi } # Docker installed but not running (Docker Desktop after reboot without autostart) — start it ourselves and wait docker_ready() { docker info >/dev/null 2>&1 && return 0 say "Docker is not running — starting Docker Desktop" if ! docker desktop start --timeout 300 >/dev/null 2>&1; then # Docker Desktop older than 4.37 — no docker desktop command local exe exe="$(docker_desktop_exe)" && cmd //c start "" "$(cygpath -w "$exe")" >/dev/null 2>&1 \ || open -ga Docker >/dev/null 2>&1 \ || systemctl --user start docker-desktop >/dev/null 2>&1 \ || return 1 fi local i for i in $(seq 1 90); do docker info >/dev/null 2>&1 && return 0 sleep 2 done return 1 } # Docker Desktop settings the node cannot work without (Docker Engine on Linux has none — everything is already there): # - AutoStart: the node lives in Docker — without autostart it won't survive the machine's first reboot. Docker Desktop # applies it on its own start (on Windows a disabled setting marks its startup item # as disabled) — so on Windows the startup item is also enabled right here; # - HostNetworkingEnabled: the node runs on the host network (docker run --network host), and in Docker Desktop it is # off by default — the panel and transport ports would be unreachable from the machine. Applied only # by restarting Docker Desktop. docker_desktop_setup() { local f="" case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) f="$(cygpath -u "${APPDATA:-}")/Docker/settings-store.json" ;; Darwin) f="$HOME/Library/Group Containers/group.com.docker/settings-store.json" ;; Linux) f="$HOME/.docker/desktop/settings-store.json" # Docker Desktop for Linux is started as a user service if systemctl --user cat docker-desktop >/dev/null 2>&1 \ && ! systemctl --user is-enabled docker-desktop >/dev/null 2>&1; then systemctl --user enable docker-desktop >/dev/null 2>&1 \ && say "Docker Desktop will now start at login — the node runs in it" fi ;; esac [ -n "$f" ] && [ -f "$f" ] || return 0 if settings_enable "$f" AutoStart; then local exe if exe="$(docker_desktop_exe)"; then local key='HKCU\Software\Microsoft\Windows\CurrentVersion' MSYS_NO_PATHCONV=1 reg add "$key\\Run" /v "Docker Desktop" /t REG_SZ /d "$(cygpath -w "$exe")" /f >/dev/null MSYS_NO_PATHCONV=1 reg add "$key\\Explorer\\StartupApproved\\Run" /v "Docker Desktop" /t REG_BINARY \ /d 020000000000000000000000 /f >/dev/null fi say "Docker Desktop will now start at login — the node runs in it" fi if settings_enable "$f" HostNetworkingEnabled; then say "Enabling host networking in Docker Desktop (the node runs in the host network) — restarting Docker Desktop" docker desktop restart --timeout 300 >/dev/null 2>&1 && docker_ready \ || die "Restart Docker Desktop yourself and run the installer again" fi } # Enables a boolean setting in Docker Desktop's settings-store.json; success if it was off or unset. # sed without \n in the replacement — portable to BSD sed (macOS) settings_enable() { local f="$1" key="$2" grep -q "\"$key\": *true" "$f" && return 1 if grep -q "\"$key\": *false" "$f"; then sed -i.bak "s/\"$key\": *false/\"$key\": true/" "$f" elif grep -q '^{ *}' "$f"; then printf '{"%s": true}\n' "$key" >"$f" else sed -i.bak "1s/^{/{\"$key\": true, /" "$f" fi rm -f "$f.bak" } # Docker Desktop.exe next to docker from PATH (…\Docker\Docker\resources\bin\docker) — Windows only docker_desktop_exe() { command -v cygpath >/dev/null 2>&1 || return 1 local exe exe="$(cd "$(dirname "$(command -v docker)")/../.." 2>/dev/null && pwd)/Docker Desktop.exe" || return 1 [ -f "$exe" ] && printf '%s\n' "$exe" } # GID of the Docker socket owner: GNU stat (Linux), BSD stat (macOS); in Git Bash there is no socket — 0 # Run the node with one docker run command (no compose plugin): host network — to check published ports # of app containers; the panel listens only on localhost (the unauthenticated interface manages containers); the # container name is the node name (the node finds its container by it); UID/GID of the data/ owner — otherwise # wallet.json can't be written. The node's previous container (from this same directory — checked above) is recreated # Wallet encryption key — outside data/ (a backup or copy of data/ does not reveal keys): ~/.config/p2me/.wallet.key, # owner-only; mounted read-only into the container. Losing the file = losing the node's hot key (the cold one stays in # data/cold.json under the panel password) — keep the file in a backup separate from data/ wallet_key() { local name="$1" kdir="$HOME/.config/p2me" local key="$kdir/$name.wallet.key" mkdir -p "$kdir" && chmod 700 "$kdir" 2>/dev/null || true if [ ! -s "$key" ]; then (umask 077; od -An -tx1 -N32 /dev/urandom | tr -d ' \n' > "$key") || die "Cannot create the wallet key $key" fi chmod 600 "$key" 2>/dev/null || true echo "$key" } run_node() { local dir="$1" name="$2" port="$3" data="$1/data" local key key="$(wallet_key "$name")" # Git Bash rewrites paths in arguments (/var/run/docker.sock → C:/Program Files/Git/…) — disable that, # and pass the data directory in a form Docker Desktop understands (C:/Users/…) command -v cygpath >/dev/null 2>&1 && { data="$(cygpath -m "$data")"; key="$(cygpath -m "$key")"; } docker rm -f "$name" >/dev/null 2>&1 || true # NetworkManager's answer "is this network metered" (a phone's hotspot, a modem) — over the system D-Bus, read only local dbus=() [ -S /run/dbus/system_bus_socket ] && dbus=(-v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro) MSYS_NO_PATHCONV=1 docker run -d --name "$name" --restart unless-stopped --network host \ --cap-add NET_BIND_SERVICE --cap-add NET_RAW \ --label "p2me.dir=$dir" \ -e HOST=127.0.0.1 -e "PORT=$port" -e "P2ME_CONTAINER=$name" \ --user "$(id -u):$(id -g)" --group-add "$(docker_gid)" \ -v /var/run/docker.sock:/var/run/docker.sock -v "$data:/data" -v "$key:/run/secrets/p2me_wallet:ro" \ ${dbus[@]+"${dbus[@]}"} p2me >/dev/null || die "Failed to start the node container" } # The node's command line on the host: /p2me runs `p2me` inside the node container (p2me/cli.py); linked into # ~/.local/bin under the node's name when that directory is on PATH. Prints how to call it. cli_wrapper() { local dir="$1" name="$2" wrapper="$1/p2me" cat > "$wrapper" </dev/null || stat -L -f %g "$sock" 2>/dev/null || echo 0 } main "$@"