"""Per-camera durable effects. A retry retrieves a receipt, never replays START.""" import hashlib import json import os import re import tempfile import threading from datetime import UTC, datetime from pathlib import Path from missioncore_plugin_sdk.v0alpha2.operations import OperationRequest from .errors import MESSAGES from .native import parameters UNKNOWN = { "state": "unknown", "error": "Результат команды не подтверждён. Обновите состояние камеры.", } INVALID = {"state": "error", "error": "Параметр или действие недоступны в текущем режиме камеры."} def atomic(path, value): fd, temporary = tempfile.mkstemp(prefix=".receipt-", dir=path.parent) try: with os.fdopen(fd, "w") as stream: os.fchmod(stream.fileno(), 0o600) json.dump(value, stream, ensure_ascii=False, allow_nan=False) stream.flush() os.fsync(stream.fileno()) os.replace(temporary, path) directory = os.open(path.parent, os.O_RDONLY) try: os.fsync(directory) finally: os.close(directory) finally: if os.path.exists(temporary): os.unlink(temporary) class Operations: def __init__(self, device_id, session_id, root, camera): self.device_id, self.session_id = device_id, session_id self.root, self.camera = Path(root), camera self.lock = threading.Lock() self.root.mkdir(mode=0o700, parents=True, exist_ok=True) def execute(self, value): command = OperationRequest.model_validate(value) if not re.fullmatch(r"op_[0-9a-f]{32}", command.operation_id): raise ValueError("Invalid operation identity") if command.session.device_id != self.device_id: raise ValueError("Command belongs to another camera") digest = hashlib.sha256( json.dumps(value, sort_keys=True, allow_nan=False).encode() ).hexdigest() path = self.root / (command.operation_id + ".json") with self.lock: if path.exists(): previous = json.loads(path.read_text()) if previous["digest"] != digest: raise ValueError("Operation identity is already in use") return previous["result"] if command.session.session_id != self.session_id: raise ValueError("Camera session changed") if command.deadline_at <= datetime.now(UTC): raise ValueError("Command deadline expired") params = dict(command.parameters) parameters(command.action_id, params) receipt = {"digest": digest, "result": dict(UNKNOWN)} # This fsync must complete before the SDK receives the command. # A crash between dispatch and acknowledgement remains UNKNOWN. atomic(path, receipt) try: result = self.camera.call(command.action_id, params) if result["state"] == "error": error = result.get("error") if not isinstance(error, str): error = "" result = { "state": "error", "error": error if error in MESSAGES.values() else MESSAGES.get(error, "Не удалось выполнить команду камеры."), } elif result["state"] == "unknown": result = dict(UNKNOWN) # settings.apply must verify the readback, not merely accept # the SDK's SetXXX acknowledgement or its old cached value. if command.action_id == "settings.apply" and result["state"] == "complete": observed = result.get("result", {}) actual = ( observed.get("mode") if params["key"] == "function_mode" else observed.get("values", {}).get(params["key"]) ) if actual != params["value"]: result = dict(UNKNOWN) receipt["result"] = result except (RuntimeError, ValueError, OSError, KeyError, TypeError): receipt["result"] = dict(UNKNOWN) atomic(path, receipt) return receipt["result"]