fix: use bitcoind --version for Bitcoin tile version display (works during IBD/startup)

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/63b5dc59-a630-4c14-a6a7-99a71ee517b7

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-07 04:22:19 +00:00
committed by GitHub
parent d0bf878555
commit 44a7b2a8ab

View File

@@ -1502,6 +1502,9 @@ _BTC_SYNC_CACHE_TTL = 5 # seconds
_btc_version_cache: tuple[float, dict | None] = (0.0, None)
_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
# ── Generic service version detection (NixOS store path) ─────────
@@ -1616,6 +1619,57 @@ def _get_bitcoin_version_info() -> dict | None:
return None
def _get_bitcoind_version() -> str | None:
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
Parses the first output line to extract the token after "version ".
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1"
returns "v29.3.knots20260210+bip110-v0.4.1".
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
"""
global _btcd_version_cache
now = time.monotonic()
cached_at, cached_val = _btcd_version_cache
if now - cached_at < _BTC_VERSION_CACHE_TTL:
return cached_val
try:
result = subprocess.run(
["bitcoind", "--version"],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0 and result.stdout.strip():
first_line = result.stdout.splitlines()[0]
m = re.search(r"version\s+(v?\S+)", first_line, re.IGNORECASE)
if m:
ver = m.group(1)
_btcd_version_cache = (now, ver)
return ver
except Exception:
pass
_btcd_version_cache = (now, None)
return None
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
"""Format a raw version string from ``bitcoind --version`` for tile display.
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1""v29.3.knots20260210").
For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended.
"""
# Remove the +bip110... patch suffix that appears in BIP-110 builds
display = re.sub(r"\+bip110\S*", "", raw_version)
if icon == "bip110" and "(bip110)" not in display.lower():
display += " (bip110)"
return display
def _get_bitcoin_sync_info() -> dict | None:
"""Call bitcoin-cli getblockchaininfo and return parsed JSON, or None on error.
@@ -1668,16 +1722,15 @@ async def api_bitcoin_sync():
async def api_bitcoin_version():
"""Return the version string of the active bitcoind implementation."""
loop = asyncio.get_event_loop()
info = await loop.run_in_executor(None, _get_bitcoin_version_info)
if info is None:
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
if raw_ver is None:
return JSONResponse(
status_code=503,
content={"error": "bitcoin-cli unavailable or bitcoind not running"},
content={"error": "bitcoind --version failed or bitcoind not on PATH"},
)
subversion = info.get("subversion", "")
return {
"version": _parse_bitcoin_subversion(subversion),
"subversion": subversion,
"version": _format_bitcoin_version(raw_ver),
"raw_version": raw_ver,
}
@@ -1802,12 +1855,9 @@ async def api_services():
service_data["sync_blocks"] = sync_blocks
service_data["sync_headers"] = sync_headers
if unit == "bitcoind.service" and enabled:
ver_info = await loop.run_in_executor(None, _get_bitcoin_version_info)
if ver_info is not None:
subversion = ver_info.get("subversion", "")
btc_ver = _parse_bitcoin_subversion(subversion)
if icon == "bip110" and "(bip110)" not in btc_ver.lower():
btc_ver += " (bip110)"
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
if raw_ver is not None:
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_data["bitcoin_version"] = btc_ver # backwards compat
service_data["version"] = btc_ver
elif unit != "bitcoind.service":
@@ -2088,12 +2138,9 @@ async def api_service_detail(unit: str, icon: str | None = None):
service_detail["sync_headers"] = sync_headers
if unit == "bitcoind.service" and enabled:
loop = asyncio.get_event_loop()
ver_info = await loop.run_in_executor(None, _get_bitcoin_version_info)
if ver_info is not None:
subversion = ver_info.get("subversion", "")
btc_ver = _parse_bitcoin_subversion(subversion)
if icon == "bip110" and "(bip110)" not in btc_ver.lower():
btc_ver += " (bip110)"
raw_ver = await loop.run_in_executor(None, _get_bitcoind_version)
if raw_ver is not None:
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
service_detail["bitcoin_version"] = btc_ver # backwards compat
service_detail["version"] = btc_ver
elif unit != "bitcoind.service":