Merge pull request #95 from naturallaw777/copilot/fix-version-detection-nixos
[WIP] Fix version detection for NixOS systemd services
This commit is contained in:
@@ -1503,52 +1503,39 @@ _btc_version_cache: tuple[float, dict | None] = (0.0, None)
|
|||||||
_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
|
_BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
|
||||||
|
|
||||||
|
|
||||||
# ── Generic service version detection ────────────────────────────
|
# ── Generic service version detection (NixOS store path) ─────────
|
||||||
|
|
||||||
# Map service unit names to CLI commands that print a version string.
|
# Regex to extract the version from a Nix store ExecStart path.
|
||||||
# Only include services where a reliable --version flag exists.
|
# Pattern: /nix/store/<32-char-hash>-<name-segments>-<version>/...
|
||||||
_SERVICE_VERSION_COMMANDS: dict[str, list[str]] = {
|
# Name segments may begin with a letter or digit (e.g. 'python3', 'gtk3',
|
||||||
"electrs.service": ["electrs", "--version"],
|
# 'lib32-foo') and consist of alphanumeric characters only (no underscores,
|
||||||
"lnd.service": ["lnd", "--version"],
|
# since Nix store paths use hyphens as separators).
|
||||||
"caddy.service": ["caddy", "version"],
|
# The version is identified as the first token starting with digit.digit.
|
||||||
"tor.service": ["tor", "--version"],
|
_NIX_STORE_VERSION_RE = re.compile(
|
||||||
"livekit.service": ["livekit-server", "--version"],
|
r"/nix/store/[a-z0-9]{32}-" # hash prefix
|
||||||
"vaultwarden.service": ["vaultwarden", "--version"],
|
r"(?:[a-zA-Z0-9][a-zA-Z0-9]*(?:-[a-zA-Z0-9][a-zA-Z0-9]*)*)+" # package name
|
||||||
"btcpayserver.service": ["btcpay-server", "--version"],
|
r"-(\d+\.\d+(?:\.\d+)*(?:[+-][a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)?)/" # version (group 1)
|
||||||
"matrix-synapse.service": ["python3", "-c", "import synapse; print(synapse.__version__)"],
|
)
|
||||||
"gnome-remote-desktop.service": ["grdctl", "--version"],
|
|
||||||
}
|
# Nix path suffixes that indicate a wrapper environment, not a real package version.
|
||||||
|
_NIX_WRAPPER_SUFFIX_RE = re.compile(
|
||||||
|
r"-(?:env|wrapper|wrapped|script|hook|setup|compat)$"
|
||||||
|
)
|
||||||
|
|
||||||
# Cache: unit → (monotonic_timestamp, version_str | None)
|
# Cache: unit → (monotonic_timestamp, version_str | None)
|
||||||
_svc_version_cache: dict[str, tuple[float, str | None]] = {}
|
_svc_version_cache: dict[str, tuple[float, str | None]] = {}
|
||||||
_SVC_VERSION_CACHE_TTL = 300 # 5 minutes — versions only change on system update
|
_SVC_VERSION_CACHE_TTL = 300 # 5 minutes — versions only change on system update
|
||||||
|
|
||||||
|
|
||||||
def _parse_version_from_output(output: str) -> str | None:
|
|
||||||
"""Extract the first semver-like version number from command output.
|
|
||||||
|
|
||||||
Handles patterns such as:
|
|
||||||
'electrs 0.10.5'
|
|
||||||
'lnd version 0.18.4-beta commit=v0.18.4-beta'
|
|
||||||
'Tor version 0.4.8.12.'
|
|
||||||
'v2.7.6 h1:...'
|
|
||||||
Returns a string starting with 'v', e.g. 'v0.10.5', or None.
|
|
||||||
"""
|
|
||||||
m = re.search(r"v?(\d+\.\d+(?:\.\d+(?:\.\d+)?)?(?:[+-][a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)?)", output)
|
|
||||||
if m:
|
|
||||||
ver = m.group(0)
|
|
||||||
if not ver.startswith("v"):
|
|
||||||
ver = "v" + ver
|
|
||||||
return ver
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_service_version(unit: str) -> str | None:
|
def _get_service_version(unit: str) -> str | None:
|
||||||
"""Return a version string for *unit*, using a CLI command when available.
|
"""Extract the version of a service from its Nix store ExecStart path.
|
||||||
|
|
||||||
|
Runs ``systemctl show <unit> --property=ExecStart --value`` and parses
|
||||||
|
the Nix store path embedded in the output to obtain the package version.
|
||||||
|
|
||||||
Results are cached for _SVC_VERSION_CACHE_TTL seconds so that repeated
|
Results are cached for _SVC_VERSION_CACHE_TTL seconds so that repeated
|
||||||
/api/services polls don't re-exec binaries on every request. Returns
|
/api/services polls don't spawn extra processes on every request.
|
||||||
None if no version command is configured or if the command fails.
|
Returns None if the version cannot be determined.
|
||||||
"""
|
"""
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
cached = _svc_version_cache.get(unit)
|
cached = _svc_version_cache.get(unit)
|
||||||
@@ -1558,18 +1545,22 @@ def _get_service_version(unit: str) -> str | None:
|
|||||||
return cached_val
|
return cached_val
|
||||||
|
|
||||||
version: str | None = None
|
version: str | None = None
|
||||||
cmd = _SERVICE_VERSION_COMMANDS.get(unit)
|
|
||||||
if cmd:
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
["systemctl", "show", unit, "--property=ExecStart", "--value"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
output_raw = result.stdout.strip() or result.stderr.strip()
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
output = output_raw.splitlines()[0] if output_raw else ""
|
m = _NIX_STORE_VERSION_RE.search(result.stdout)
|
||||||
version = _parse_version_from_output(output)
|
if m:
|
||||||
|
ver = m.group(1)
|
||||||
|
# Strip a single trailing period (defensive; shouldn't appear in store paths)
|
||||||
|
ver = ver[:-1] if ver.endswith(".") else ver
|
||||||
|
# Skip Nix environment/wrapper suffixes that are not real versions
|
||||||
|
if not _NIX_WRAPPER_SUFFIX_RE.search(ver):
|
||||||
|
version = ver if ver.startswith("v") else f"v{ver}"
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user