Files
chenmingxuan be6e8c2abd feat: release 2.2.0 with PC-only tests and execution reliability fixes
Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
2026-10-27 01:13:41 +08:00

52 lines
1.6 KiB
Python

"""One observation deadline shared with synchronous screen-client worker threads."""
from contextvars import ContextVar
from dataclasses import dataclass, field
from threading import Event
from time import monotonic
class ObservationExpired(TimeoutError):
"""The shared observation deadline has elapsed."""
class ObservationCancelled(RuntimeError):
"""The owning driver has ended this observation."""
@dataclass
class ObservationBudget:
deadline: float
cancelled: Event = field(default_factory=Event)
phase: str = "获取屏幕"
last_failure: str | None = None
def remaining(self) -> float:
if self.cancelled.is_set():
raise ObservationCancelled("设备观测已取消,禁止继续重连")
remaining = self.deadline - monotonic()
if remaining <= 0:
raise ObservationExpired(f"设备观测预算耗尽:{self.detail}")
return remaining
@property
def detail(self) -> str:
return self.phase + (f";最近错误:{self.last_failure}" if self.last_failure else "")
current_observation: ContextVar[ObservationBudget | None] = ContextVar(
"screen_observation_budget", default=None
)
def check_observation(phase: str | None = None, failure: BaseException | None = None) -> None:
budget = current_observation.get()
if budget is not None:
if failure is not None and not isinstance(
failure, (ObservationExpired, ObservationCancelled)
):
budget.last_failure = f"{type(failure).__name__}: {failure}"
budget.remaining()
if phase is not None:
budget.phase = phase