Files
ObsidianDragon/tools/fake_dragonxd/run-gui-with-fake.sh
T
DanSandClaude Opus 5.5 7b6e8a8a8e test(daemon): add fake_dragonxd, a slow dragonxd stand-in for stop/kill testing
A scriptable fake dragonxd that reproduces, in minutes, the incident where
the GUI hard-killed a node busy with an hour-long witness rebuild:

- dragonxd's HTTP-RPC queue (8 workers + 16 queued, -rpcthreads /
  -rpcworkqueue honoured): a full queue answers an immediate plain-text
  500 "Work queue depth exceeded", and a request whose client gave up
  keeps its slot until a worker reaches it
- a global cs_main held by a "witness rebuild" that logs the real
  markers; every method except stop/help takes it
- warmup -28 with the daemon's allow-list, an optional startup replay
- dragonxd's shutdown order: stop (or SIGTERM on POSIX) unbinds RPC,
  503s open connections, waits for the rebuild, then logs
  "Shutdown: In progress..." / "Shutdown: done" and exits 0

Everything is appended to <datadir>/fake_dragonxd.events; a missing
"clean-exit" line means the process was killed. One portable C++17 file
(POSIX + Winsock), OFF by default (-DDRAGONX_BUILD_FAKE_DAEMON=ON), built
into <build>/fake_dragonxd/ so it can't shadow the real bin/dragonxd.
run-gui-with-fake.sh runs the GUI against it in a throwaway HOME.

Against master's GUI on Linux it reproduces the bug: closing during a
rebuild queues stop behind cs_main-blocked workers, then the GUI sends
SIGTERM and SIGKILL mid-rebuild, with no clean exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:24:42 -05:00

87 lines
3.4 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run ObsidianDragon against tools/fake_dragonxd in a throwaway, fully isolated HOME (Linux).
#
# tools/fake_dragonxd/run-gui-with-fake.sh [--build-dir DIR] [--scratch DIR] [-- GUI args...]
#
# FAKE_* variables in the environment pass through to the stub (see README.md). It never touches
# the real ~/.hush or ~/.config: HOME, XDG_CONFIG_HOME and TMPDIR all point into the scratch dir.
# Re-using a --scratch dir keeps its settings, conf and GUI log; the stub's events file is appended.
set -euo pipefail
repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
build_dir="$repo/build/linux"
scratch=""
port=21769 # the GUI's external-node / port checks are hard-wired to the default RPC port
usage() { sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'; }
while [[ $# -gt 0 ]]; do
case "$1" in
--build-dir) build_dir="$(cd "$2" && pwd)"; shift 2 ;;
--scratch) scratch="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
gui="$build_dir/bin/ObsidianDragon"
stub="$build_dir/fake_dragonxd/dragonxd"
if [[ ! -x "$gui" ]]; then
echo "missing $gui; build the app first (./build.sh)" >&2
exit 1
fi
if [[ ! -x "$stub" ]]; then
echo "missing $stub; build it with:" >&2
echo " cmake -S . -B ${build_dir#"$repo"/} -DDRAGONX_BUILD_FAKE_DAEMON=ON && cmake --build ${build_dir#"$repo"/} --target fake_dragonxd" >&2
exit 1
fi
if ss -ltn 2>/dev/null | grep -qE "[:.]$port( |$)"; then
echo "port $port is already in use (a real dragonxd?). The GUI would adopt it as an external node; stop it first." >&2
exit 1
fi
scratch="${scratch:-$(mktemp -d "${TMPDIR:-/tmp}/fake-dragonxd.XXXXXX")}"
mkdir -p "$scratch"
scratch="$(cd "$scratch" && pwd)"
app="$scratch/app"
home="$scratch/home"
datadir="$home/.hush/DRAGONX"
config_dir="$home/.config/ObsidianDragon"
mkdir -p "$app" "$datadir/blocks" "$config_dir" "$scratch/tmp"
# Copies, not symlinks: the GUI looks for dragonxd next to /proc/self/exe, which resolves links.
cp -f "$gui" "$app/ObsidianDragon"
cp -f "$stub" "$app/dragonxd"
ln -sfn "$build_dir/bin/res" "$app/res"
for f in sapling-spend.params sapling-output.params asmap.dat; do
if [[ -e "$build_dir/bin/$f" ]]; then ln -sf "$build_dir/bin/$f" "$app/$f"; fi
done
# The GUI also keeps a dragonxd copy in its daemon dir; keep that one the stub too.
if [[ -e "$config_dir/dragonx/dragonxd" ]]; then cp -f "$stub" "$config_dir/dragonx/dragonxd"; fi
# A pre-made conf + blocks/ dir skips the first-run wizard (App::isFirstRun).
if [[ ! -e "$datadir/DRAGONX.conf" ]]; then
printf 'rpcuser=fake\nrpcpassword=fake-%s\nrpcport=%s\n' "$RANDOM$RANDOM" "$port" > "$datadir/DRAGONX.conf"
fi
if [[ ! -e "$config_dir/settings.json" ]]; then
printf '{\n "wizard_completed": true,\n "verbose_logging": true\n}\n' > "$config_dir/settings.json"
fi
cat <<EOF
scratch : $scratch
stub events : $datadir/fake_dragonxd.events
GUI log : $config_dir/dragonx-debug.log
FAKE_* : $(env | grep -E '^FAKE_' | tr '\n' ' ')
EOF
rc=0
HOME="$home" XDG_CONFIG_HOME="$home/.config" TMPDIR="$scratch/tmp" "$app/ObsidianDragon" "$@" || rc=$?
echo "--- GUI exited ($rc). Stub events:"
cat "$datadir/fake_dragonxd.events" 2>/dev/null || echo "(none)"
if pgrep -f "^$app/dragonxd" >/dev/null; then
echo "--- the stub is still running (pid $(pgrep -f "^$app/dragonxd" | tr '\n' ' ')); it exits after its rebuild + flush."
fi
exit "$rc"