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>
87 lines
3.4 KiB
Bash
Executable File
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"
|