Files
browser-cli/browser_cli/registry.py
T
daniel156161 69387c1054
Testing / test (3.10) (push) Successful in 48s
Testing / test (3.12) (push) Successful in 47s
Testing / test (3.14) (push) Successful in 1m7s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 6m59s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 6m47s
Testing / test (3.15) (push) Successful in 6m20s
fix: restore Python 3.10/3.11 support and report MCP version
- Drop backslash from f-string expression in markdown/html.py
- That syntax needs 3.12+ (PEP 701) and broke import on 3.10/3.11
- Add Python matrix 3.10/3.12/3.14/3.15 to the Gitea testing workflow
- Pass project_version() to MCPServer; serverInfo.version was empty
- Cover the reported version with an in-memory MCP client test
- Swap deprecated click.get_text_stream for sys.stdin.isatty()
- Add explicit encoding to native host shim and manifest writes
- Add explicit encoding to the registry lock file open
- Add explicit encoding to every read_text/write_text call in tests
- Bump version to 0.16.10 in pyproject.toml and extension/manifest.json
- Move .python-version to 3.15 and refresh uv.lock

Extension code is unchanged; only the manifest version string moved.

Verified with 778 tests on 3.10 through 3.15, plus a strict run using -X warn_default_encoding with EncodingWarning and DeprecationWarning as errors.
2026-10-20 12:41:25 +02:00

100 lines
3.3 KiB
Python

"""Runtime registry helpers for active browser-cli native host endpoints."""
import contextlib
import json
import os
import tempfile
from pathlib import Path
from typing import Iterator
from browser_cli.platform import registry_path
REGISTRY_PATH = registry_path()
@contextlib.contextmanager
def _file_lock(path: Path) -> Iterator[None]:
"""Best-effort cross-process lock for registry read/modify/write updates."""
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
lock_path = path.with_suffix(path.suffix + ".lock")
with lock_path.open("a+", encoding="utf-8") as lock_file:
if os.name == "nt":
try:
import msvcrt
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
yield
finally:
try:
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
except OSError:
pass
else:
try:
import fcntl
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
yield
finally:
try:
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
except OSError:
pass
def _coerce_registry(data) -> dict[str, str]:
if not isinstance(data, dict):
return {}
return {str(alias): str(endpoint) for alias, endpoint in data.items() if alias and endpoint}
def load_registry(path: Path | None = None) -> dict[str, str]:
"""Load the active browser registry.
Older native hosts wrote this file non-atomically, so tolerate trailing
garbage from interrupted/concurrent writes and keep the first valid JSON
object when possible.
"""
registry = path or REGISTRY_PATH
if not registry.exists():
return {}
try:
text = registry.read_text(encoding="utf-8")
except OSError:
return {}
if not text.strip():
return {}
try:
return _coerce_registry(json.loads(text))
except json.JSONDecodeError:
try:
data, _ = json.JSONDecoder().raw_decode(text)
return _coerce_registry(data)
except json.JSONDecodeError:
return {}
def save_registry(data: dict[str, str], path: Path | None = None) -> None:
"""Atomically write the active browser registry."""
registry = path or REGISTRY_PATH
registry.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
payload = json.dumps(_coerce_registry(data), sort_keys=True)
fd, tmp_name = tempfile.mkstemp(prefix=registry.name + ".", suffix=".tmp", dir=registry.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as tmp:
tmp.write(payload)
tmp.flush()
os.fsync(tmp.fileno())
os.replace(tmp_name, registry)
finally:
try:
os.unlink(tmp_name)
except FileNotFoundError:
pass
def update_registry(alias: str, endpoint: str | None, path: Path | None = None) -> None:
"""Add/update an alias, or remove it when endpoint is None."""
registry = path or REGISTRY_PATH
with _file_lock(registry):
data = load_registry(registry)
if endpoint is None:
data.pop(alias, None)
else:
data[alias] = endpoint
save_registry(data, registry)