初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,19 @@
from .manager import (
FrpConflictError,
FrpController,
FrpError,
FrpServiceError,
FrpValidationError,
MockFrpBackend,
SystemdFrpBackend,
)
__all__ = [
"FrpConflictError",
"FrpController",
"FrpError",
"FrpServiceError",
"FrpValidationError",
"MockFrpBackend",
"SystemdFrpBackend",
]
+496
View File
@@ -0,0 +1,496 @@
from __future__ import annotations
import json
import os
import re
import subprocess
import threading
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Protocol
from uuid import UUID, uuid4
from app.persistence import atomic_write_bytes
FRP_CATALOG_SCHEMA_VERSION = 1
MAX_FRP_CONFIG_BYTES = 1024 * 1024
SUPPORTED_FRP_SUFFIXES = {".toml", ".yaml", ".yml", ".json", ".ini"}
_SAFE_NAME_RE = re.compile(r"[^\w\-. ()\u4e00-\u9fff]+", re.UNICODE)
class FrpError(RuntimeError):
"""Base error whose text is safe to return to the browser."""
class FrpValidationError(FrpError):
pass
class FrpConflictError(FrpError):
pass
class FrpServiceError(FrpError):
pass
class FrpBackend(Protocol):
def validate(self, path: Path) -> None: ...
def status(self) -> dict[str, Any]: ...
def select(self, path: Path) -> None: ...
def start(self) -> None: ...
def stop(self) -> None: ...
def restart(self) -> None: ...
def _utc_now() -> str:
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
def _checked_uuid(value: str) -> str:
try:
checked = str(UUID(str(value)))
except (ValueError, TypeError, AttributeError) as exc:
raise FrpValidationError("FRP config id is invalid") from exc
if checked != str(value):
raise FrpValidationError("FRP config id is invalid")
return checked
def _checked_suffix(filename: str) -> str:
suffix = Path(str(filename)).suffix.casefold()
if suffix not in SUPPORTED_FRP_SUFFIXES:
raise FrpValidationError("FRP config must be TOML, YAML, JSON or INI")
return suffix
def _checked_content(content: bytes) -> bytes:
if not content:
raise FrpValidationError("FRP config must not be empty")
if len(content) > MAX_FRP_CONFIG_BYTES:
raise FrpValidationError("FRP config exceeds the 1 MiB limit")
try:
text = content.decode("utf-8")
except UnicodeDecodeError as exc:
raise FrpValidationError("FRP config must be UTF-8 text") from exc
if "\x00" in text:
raise FrpValidationError("FRP config must not contain NUL bytes")
return content
def _display_name(name: str, filename: str) -> str:
candidate = str(name or "").strip() or Path(str(filename)).stem.strip()
candidate = _SAFE_NAME_RE.sub("_", candidate).strip(" ._")
if not candidate:
candidate = "frpc"
if len(candidate) > 80:
candidate = candidate[:80].rstrip()
return candidate
def _checked_timestamp(value: str) -> str:
try:
parsed = datetime.fromisoformat(value)
except (TypeError, ValueError) as exc:
raise FrpValidationError("FRP config timestamp is invalid") from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise FrpValidationError("FRP config timestamp is invalid")
return value
class FrpCatalog:
def __init__(self, data_root: Path, backend: FrpBackend) -> None:
self.root = Path(data_root) / "frp"
self.catalog_path = self.root / "catalog.json"
self.backend = backend
self._lock = threading.RLock()
self._error: str | None = None
self._document = {
"schema_version": FRP_CATALOG_SCHEMA_VERSION,
"selected_id": None,
"profiles": [],
}
try:
self._load()
except FrpError as exc:
self._error = str(exc)
@property
def available(self) -> bool:
return self._error is None
@property
def error(self) -> str | None:
return self._error
def _require_available(self) -> None:
if self._error is not None:
raise FrpServiceError(self._error)
def _load(self) -> None:
if not self.catalog_path.exists():
return
try:
raw = json.loads(self.catalog_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise FrpServiceError("FRP config catalog is damaged; the original file was preserved") from exc
if not isinstance(raw, dict) or set(raw) != {"schema_version", "selected_id", "profiles"}:
raise FrpServiceError("FRP config catalog fields are unsupported; the original file was preserved")
if raw["schema_version"] != FRP_CATALOG_SCHEMA_VERSION:
raise FrpServiceError("FRP config catalog version is unsupported; the original file was preserved")
if not isinstance(raw["profiles"], list):
raise FrpServiceError("FRP config catalog profiles are invalid; the original file was preserved")
seen: set[str] = set()
profiles: list[dict[str, str]] = []
for item in raw["profiles"]:
if not isinstance(item, dict) or set(item) != {"id", "name", "filename", "created_at", "updated_at"}:
raise FrpServiceError("FRP config catalog profile is invalid; the original file was preserved")
profile_id = _checked_uuid(item["id"])
if profile_id in seen or not all(isinstance(item[key], str) and item[key] for key in item):
raise FrpServiceError("FRP config catalog profile is invalid; the original file was preserved")
suffix = _checked_suffix(item["filename"])
if item["filename"] != f"frpc{suffix}" or _display_name(item["name"], item["filename"]) != item["name"]:
raise FrpServiceError("FRP config catalog profile is invalid; the original file was preserved")
try:
_checked_timestamp(item["created_at"])
_checked_timestamp(item["updated_at"])
except FrpValidationError as exc:
raise FrpServiceError("FRP config catalog profile is invalid; the original file was preserved") from exc
path = self._profile_path(item)
if not path.is_file():
raise FrpServiceError("FRP config catalog references a missing file; the original catalog was preserved")
seen.add(profile_id)
profiles.append(dict(item))
selected = raw["selected_id"]
if selected is not None and (not isinstance(selected, str) or selected not in seen):
raise FrpServiceError("FRP selected config is invalid; the original catalog was preserved")
self._document = {"schema_version": FRP_CATALOG_SCHEMA_VERSION, "selected_id": selected, "profiles": profiles}
def _profile_path(self, profile: dict[str, str]) -> Path:
return self.root / "profiles" / profile["id"] / profile["filename"]
@staticmethod
def _verification_path(path: Path) -> Path:
return path.with_name(f".{path.stem}.verify{path.suffix}")
def _save(self) -> None:
content = (json.dumps(self._document, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
atomic_write_bytes(self.catalog_path, content)
self._protect(self.catalog_path)
@staticmethod
def _protect(path: Path) -> None:
if os.name != "nt":
path.chmod(0o640)
def _unique_name(self, requested: str, *, excluding: str | None = None) -> str:
existing = {
item["name"].casefold()
for item in self._document["profiles"]
if item["id"] != excluding
}
if requested.casefold() not in existing:
return requested
counter = 2
while f"{requested} ({counter})".casefold() in existing:
counter += 1
return f"{requested} ({counter})"
def list_document(self) -> dict[str, Any]:
with self._lock:
return {
"available": self.available,
"error": self.error,
"selected_id": self._document["selected_id"],
"profiles": deepcopy(self._document["profiles"]),
}
def get(self, profile_id: str) -> tuple[dict[str, str], Path]:
self._require_available()
checked = _checked_uuid(profile_id)
for profile in self._document["profiles"]:
if profile["id"] == checked:
return deepcopy(profile), self._profile_path(profile)
raise FrpValidationError("FRP config was not found")
def create(self, *, filename: str, name: str, content: bytes) -> dict[str, str]:
self._require_available()
checked_content = _checked_content(content)
suffix = _checked_suffix(filename)
with self._lock:
profile_id = str(uuid4())
now = _utc_now()
profile = {
"id": profile_id,
"name": self._unique_name(_display_name(name, filename)),
"filename": f"frpc{suffix}",
"created_at": now,
"updated_at": now,
}
path = self._profile_path(profile)
path.parent.mkdir(parents=True, exist_ok=False)
if os.name != "nt":
path.parent.chmod(0o2750)
try:
temporary = self._verification_path(path)
atomic_write_bytes(temporary, checked_content)
self.backend.validate(temporary)
os.replace(temporary, path)
self._protect(path)
self._document["profiles"].append(profile)
if self._document["selected_id"] is None:
self._document["selected_id"] = profile_id
self.backend.select(path)
self._save()
except BaseException:
temporary = self._verification_path(path)
temporary.unlink(missing_ok=True)
if path.exists():
path.unlink(missing_ok=True)
try:
path.parent.rmdir()
except OSError:
pass
self._document["profiles"] = [item for item in self._document["profiles"] if item["id"] != profile_id]
if self._document["selected_id"] == profile_id:
self._document["selected_id"] = None
raise
return deepcopy(profile)
def update(self, profile_id: str, *, name: str, content: bytes) -> dict[str, str]:
self._require_available()
checked_content = _checked_content(content)
with self._lock:
profile, path = self.get(profile_id)
old_content = path.read_bytes()
temporary = self._verification_path(path)
atomic_write_bytes(temporary, checked_content)
try:
self.backend.validate(temporary)
os.replace(temporary, path)
self._protect(path)
profile["name"] = self._unique_name(_display_name(name, profile["filename"]), excluding=profile_id)
profile["updated_at"] = _utc_now()
index = next(i for i, item in enumerate(self._document["profiles"]) if item["id"] == profile_id)
previous = self._document["profiles"][index]
self._document["profiles"][index] = profile
try:
self._save()
if self._document["selected_id"] == profile_id and self.backend.status()["running"]:
self.backend.restart()
except BaseException:
atomic_write_bytes(path, old_content)
self._protect(path)
self._document["profiles"][index] = previous
self._save()
if self._document["selected_id"] == profile_id and self.backend.status()["running"]:
try:
self.backend.restart()
except FrpError:
pass
raise
finally:
temporary.unlink(missing_ok=True)
return deepcopy(profile)
def select(self, profile_id: str) -> dict[str, str]:
self._require_available()
with self._lock:
profile, path = self.get(profile_id)
previous_id = self._document["selected_id"]
previous_path = self.get(previous_id)[1] if previous_id else None
was_running = bool(self.backend.status()["running"])
try:
if was_running:
self.backend.stop()
self.backend.select(path)
self._document["selected_id"] = profile_id
self._save()
if was_running:
self.backend.start()
except BaseException:
self._document["selected_id"] = previous_id
if previous_path is not None:
self.backend.select(previous_path)
self._save()
if was_running:
try:
self.backend.start()
except FrpError:
pass
raise
return profile
def delete(self, profile_id: str) -> None:
self._require_available()
with self._lock:
profile, path = self.get(profile_id)
selected = self._document["selected_id"] == profile_id
if selected and self.backend.status()["running"]:
raise FrpConflictError("stop FRP before deleting the active config")
old_content = path.read_bytes()
old_document = deepcopy(self._document)
self._document["profiles"] = [item for item in self._document["profiles"] if item["id"] != profile_id]
if selected:
self._document["selected_id"] = None
try:
self._save()
path.unlink()
path.parent.rmdir()
except BaseException:
self._document = old_document
atomic_write_bytes(path, old_content)
self._protect(path)
self._save()
raise
class SystemdFrpBackend:
def __init__(
self,
data_root: Path,
*,
binary: Path = Path("/usr/local/bin/frpc"),
unit: str = "matrix-screen-frpc.service",
runner=subprocess.run,
) -> None:
self.binary = Path(binary)
self.unit = unit
self.environment_path = Path(data_root) / "frp" / "active.env"
self.runner = runner
def _run(self, command: list[str], *, check: bool = True, timeout: int = 15) -> subprocess.CompletedProcess[str]:
try:
result = self.runner(command, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=timeout)
except (OSError, subprocess.TimeoutExpired) as exc:
raise FrpServiceError("FRP system service command failed") from exc
if check and result.returncode != 0:
raise FrpServiceError("FRP system service rejected the operation")
return result
def validate(self, path: Path) -> None:
if not self.binary.is_file():
raise FrpServiceError("frpc is not installed")
result = self._run([
str(self.binary), "verify", "-c", str(path),
"--allow-unsafe=TokenSourceExec",
], check=False)
if result.returncode != 0:
message = (result.stderr or result.stdout or "FRP config validation failed").strip()
message = re.sub(r"(?i)(token|password|secret)(\s*[=:]\s*)\S+", r"\1\2[redacted]", message)
raise FrpValidationError(message[:1000])
def status(self) -> dict[str, Any]:
version = None
if self.binary.is_file():
result = self._run([str(self.binary), "--version"], check=False, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()[:80]
enabled_result = self._run(["/usr/bin/systemctl", "is-enabled", self.unit], check=False, timeout=5)
active_result = self._run(["/usr/bin/systemctl", "is-active", self.unit], check=False, timeout=5)
enabled = enabled_result.stdout.strip() == "enabled"
state = active_result.stdout.strip() or "unknown"
return {
"installed": self.binary.is_file(),
"version": version,
"enabled": enabled,
"running": state == "active",
"state": state,
"last_error": None if state in {"active", "inactive"} else "FRP service is not running normally",
}
def select(self, path: Path) -> None:
resolved = path.resolve()
content = f"FRPC_CONFIG={resolved}\n".encode("utf-8")
atomic_write_bytes(self.environment_path, content)
if os.name != "nt":
self.environment_path.chmod(0o600)
def start(self) -> None:
self._run(["/usr/bin/systemctl", "enable", "--now", self.unit])
if not self.status()["running"]:
raise FrpServiceError("FRP service did not enter the running state")
def stop(self) -> None:
self._run(["/usr/bin/systemctl", "disable", "--now", self.unit])
def restart(self) -> None:
self._run(["/usr/bin/systemctl", "restart", self.unit])
if not self.status()["running"]:
raise FrpServiceError("FRP service did not restart successfully")
class MockFrpBackend:
def __init__(self) -> None:
self.installed = True
self.enabled = False
self.running = False
self.selected: Path | None = None
self.fail_validation = False
self.fail_start = False
self.fail_restart = False
def validate(self, path: Path) -> None:
text = path.read_text(encoding="utf-8")
if self.fail_validation or "INVALID_FRP_CONFIG" in text:
raise FrpValidationError("FRP config validation failed")
def status(self) -> dict[str, Any]:
return {
"installed": self.installed,
"version": "0.71.0-mock",
"enabled": self.enabled,
"running": self.running,
"state": "active" if self.running else "inactive",
"last_error": None,
}
def select(self, path: Path) -> None:
self.selected = Path(path)
def start(self) -> None:
if self.fail_start:
raise FrpServiceError("FRP service did not enter the running state")
self.enabled = True
self.running = True
def stop(self) -> None:
self.enabled = False
self.running = False
def restart(self) -> None:
if self.fail_restart:
raise FrpServiceError("FRP service did not restart successfully")
self.running = True
class FrpController:
def __init__(self, data_root: Path, backend: FrpBackend) -> None:
self.backend = backend
self.catalog = FrpCatalog(data_root, backend)
def status(self) -> dict[str, Any]:
catalog = self.catalog.list_document()
service = self.backend.status()
if not catalog["available"]:
service = {**service, "enabled": False, "running": False, "last_error": catalog["error"]}
return {**catalog, "service": service}
def content(self, profile_id: str) -> tuple[dict[str, str], bytes]:
profile, path = self.catalog.get(profile_id)
return profile, path.read_bytes()
def start(self) -> dict[str, Any]:
state = self.catalog.list_document()
if not state["selected_id"]:
raise FrpConflictError("select an FRP config before starting")
_profile, path = self.catalog.get(state["selected_id"])
self.backend.validate(path)
self.backend.select(path)
self.backend.start()
return self.status()
def stop(self) -> dict[str, Any]:
self.backend.stop()
return self.status()