fix(client): fail closed on polling configuration drift

This commit is contained in:
QiuSW
2026-08-05 01:59:35 +08:00
parent dfd88c3336
commit 3a27225977
6 changed files with 168 additions and 16 deletions
+13 -3
View File
@@ -353,14 +353,24 @@ class LocalStateStore:
row = connection.execute(
"""SELECT service_url,device_id,adb_path,adb_serial,transport,
poll_interval_seconds,failure_threshold,http_timeout_seconds,step_timeout_seconds,
CASE WHEN length(device_token_cipher) > 0 THEN 1 ELSE 0 END
typeof(device_token_cipher),length(device_token_cipher)
FROM profiles WHERE profile_id=?""",
(profile_id,),
).fetchone()
if row is None:
raise StateError("profile_not_found")
settings = ProfileSettings(profile_id, *row[:9])
return ProfileSummary(settings, bool(row[9]))
# summary 不解密,但也不能把损坏的密文降级成“尚未保存”。否则 UI 会
# 允许覆盖本应进入人工恢复的本地状态。DPAPI 密文长度不固定,只要求
# SQLite storage class 确为 BLOB 且非空。
if row[9] != "blob" or type(row[10]) is not int or row[10] <= 0:
raise StateError("invalid_device_token_cipher")
try:
settings = ProfileSettings(profile_id, *row[:9])
except (TypeError, ValueError, ValidationError):
# 存储字段损坏不得把裸模型异常或实际值带到 UI;也不得以默认配置
# 继续启动。配置修复必须显式进行。
raise StateError("stored_profile_invalid") from None
return ProfileSummary(settings, True)
def start_or_resume_polling(self, profile_id: str) -> PollingSession:
now = self._utc_now()
@@ -51,7 +51,7 @@ class ClaimGateway(Protocol):
class ExecutionConsumer(Protocol):
def accept_claim(self, claimed: ClaimedTask) -> None: ...
def accept_claim(self, claimed: ClaimedTask, profile: ProfileSettings) -> None: ...
@dataclass(frozen=True)
@@ -469,9 +469,12 @@ class PollingCoordinator(QObject):
return
try:
consumer = self._consumer
if consumer is None:
frozen_profile = self._frozen_profile
if consumer is None or frozen_profile is None:
raise RuntimeError("execution_consumer_missing")
consumer.accept_claim(claimed)
# consumer 只能使用本次显式 Start 冻结的不可变配置;不得在
# 已领取后回读可变 UI/store,否则 ADB 身份和超时会发生趟内漂移。
consumer.accept_claim(claimed, frozen_profile)
except Exception:
self._request_stop(
PollingState.RECOVERY_REQUIRED,
+6 -4
View File
@@ -5,8 +5,8 @@ from __future__ import annotations
from pathlib import Path
from typing import Protocol
from PySide6.QtCore import QRegularExpression, Qt, Signal, Slot
from PySide6.QtGui import QAction, QKeySequence, QRegularExpressionValidator
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtGui import QAction, QKeySequence
from PySide6.QtWidgets import (
QComboBox,
QFormLayout,
@@ -77,9 +77,11 @@ class SettingsPage(QScrollArea):
self.device_token = QLineEdit()
self.device_token.setObjectName("deviceToken")
self.device_token.setEchoMode(QLineEdit.EchoMode.Password)
self.device_token.setMaxLength(64)
# 控件只负责给输入设置合理上限;长度与字符集必须在 save() 中显式
# 验证。若这里限制为 64,粘贴 65 位 token 会被 Qt 静默截成合法
# 64 位并覆盖原凭据。
self.device_token.setMaxLength(256)
self.device_token.setPlaceholderText("首次必填;已有凭据时留空表示保留")
self.device_token.setValidator(QRegularExpressionValidator(QRegularExpression("[0-9a-f]{0,64}"), self))
form.addRow("设备 token", self.device_token)
self.token_status = QLabel()