Files
pi-stat/CLAUDE.md
T
olieske 602592e304 feat: add ICMP ping column to peer table
- ping_batch() pings each peer's public IP in parallel from the host
  (system ping, 1 packet, 1 s timeout, private IPs skipped)
- New "Ping" column next to stellar-core's "Latency" in the -p/-g table;
  "—" when the peer filters ICMP
- Low ping with high latency hints at a VPN exit / relay in front of
  the actual node

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 08:21:36 +00:00

5.8 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What This Is

pi-stat is a fast status monitoring tool for Stellar Core nodes on the Pi Network. It provides quick system diagnostics as an alternative to the slow pi-node status command.

Running

./pi-stat                        # Standard report
./pi-stat -p / --peers           # Extended peer table
./pi-stat -g / --geo             # With geolocation (implies -p)
./pi-stat -s / --show-ip         # Show IP column (hidden by default)
./pi-stat -x / --extended        # Show full RX/TX/RX-rate/TX-rate columns (default: ratio only)
./pi-stat -i [SEC] / --interactive [SEC]  # Interactive auto-refresh (default: 5s, configurable)

No build step. Requires pip install docker and a running Docker daemon. Runs on Linux, WSL2, and Windows.

Configuration

Optional JSON config file, first match wins: $PI_STAT_CONFIG → ~/.config/pi-stat/config.json → <script dir>/pi-stat.json (see pi-stat.json, shipped with the repo). Loaded by load_config(); missing, unreadable or non-object files fall back to defaults with a warning on stderr.

Key Default Meaning
refresh_interval 5 Interactive auto-refresh interval in seconds (clamped to >= 1)

Precedence for the refresh interval: -i SEC on the command line > config file > DEFAULT_REFRESH (5).

Standard JSON has no comment syntax; document settings with a _comment key instead (any key not read by config_refresh() is ignored), e.g.:

{
  "_comment": "refresh_interval in seconds, min. 1 (default: 5)",
  "refresh_interval": 5
}

Architecture

Single self-contained script (pi-stat). Logic is split into helper functions and a run_cycle() function that collects data and renders one full screen, called either once (normal mode) or in a loop (interactive mode).

Data sources

  • Docker SDK — container CPU/RAM stats; all commands run inside the auto-detected container (mainnet → testnet2 → testnet, first running one) via exec_run
  • Stellar Core HTTP API — exec_json() runs stellar-core http-command <cmd> inside the container
  • Horizon API — curl http://127.0.0.1:8000/ run inside the container via exec_json(), not via the host port 31401 (host→bridge forwarding can hang until timeout if Docker's host firewall rules are broken)
  • PostgreSQL — psql inside the container; used for peer DB counts and pubkey→IP mapping
  • ZFS CLI — zpool/zfs commands on the host for disk health (z01pool); if zpool is not found (HAS_ZFS = False), falls back to shutil.disk_usage() — works on Linux without ZFS, WSL2, and Windows
  • ip-api.com — batch geolocation (Country, City), cached in _geo_cache; only fetched with -g
  • System ping — one ICMP ping per peer IP from the host (ping_batch(), parallel, 1 s timeout, private IPs skipped); shown as Ping next to stellar-core's Latency in the -p/-g table, — if the peer filters ICMP

Output sections (in order)

  1. Container stats — Docker CPU/RAM, Horizon + Core versions (skipped in -p/-g mode)
  2. Disk — ZFS (z01pool) if zpool is available, otherwise shutil.disk_usage() (skipped in -p/-g mode)
  3. Protocol status — Stellar Core state, ledger, quorum phase
  4. API status — Horizon ingest lag
  5. Peer connections — compact counts, or full table with -p/-g

Interactive mode (-i)

interactive_loop() renders via run_cycle() into a StringIO buffer, writes it atomically to avoid flicker, then waits for keypresses using select in setcbreak mode:

  • d — drop a single outbound peer (do_drop_workflow)
  • a — drop all outbound peers (drop_all_workflow)
  • q / Ctrl+C — clean exit with terminal state restore

Outbound peers are numbered in the table. Full node IDs are fetched via peers?fullkeys=true for the droppeer API call.

The refresh deadline is anchored to the start of the cycle (cycle_start), not its end, so the update's own execution time doesn't stack on top of refresh_interval. Execution time is measured and a warning is printed (into the same buffered screen) when a cycle takes >= REFRESH_WARN_RATIO (80%) of the interval, or exceeds it outright.

Key patterns

  • Graceful degradation — every external call is wrapped in try-except; missing data renders as —
  • Dynamic column widths — all table columns sized at runtime from actual data length
  • NAT detection — inbound peers with private IPs are replaced with their DB-stored public IP (db_peer_addr from PostgreSQL)
  • Validator matching — load_validators() parses stellar-core.cfg and indexes by IP, name, and public key; matched peers show a truncated key + (Vn) label
  • Ping vs. Latency — Latency is stellar-core's round trip to the peer process, Ping is ICMP to the peer's IP; a low ping with high latency hints at a VPN exit / relay in front of the actual node
  • Geo caching — _geo_cache is a module-level dict; geo_batch() only fetches IPs not already cached
  • Flag parsing — manual sys.argv parsing; -g implies -p; -i accepts an optional next argument as the refresh interval
  • Config precedence — config_refresh() reads the config value; an explicit -i SEC argument overrides it
  • Cycle timing — interactive_loop() measures cycle duration and warns via warn() when it nears or exceeds refresh_interval, so a slow update doesn't silently push out the actual refresh cadence
  • Persistent cbreak — the tty is switched to cbreak once for the whole session (not reset to canonical between cycles); doing it per-cycle would flush unread input on the mode switch and leave the terminal line-buffered during run_cycle(), silently dropping keypresses (e.g. q) made while a cycle overruns refresh_interval