feat: 保存并登记当前 Client (#11)

This commit is contained in:
chengma
2026-08-06 17:57:49 +08:00
parent 262f48c778
commit fab5778049
17 changed files with 1078 additions and 43 deletions
+243 -26
View File
@@ -1,32 +1,92 @@
"""设置页的事件绑定和当前客户端设备号生成。
"""设置页事件、当前 Client 本地保存和后台登记。
本文件把界面操作转换为轻量信号,不直接执行 ADB 或 SQLite。
后续应用服务应在后台线程处理阻塞操作,再通过页面更新方法返回结果。
本文件不直接写 SQL。SQLite 保存和 Admin HTTP 请求由 Worker 在线程中执行,
后台结果只通过信号返回主线程更新页面。
"""
import hashlib
import platform
import uuid
from typing import Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, pyqtSlot
from .admin_gateway import (
AdminGatewayError,
AndroidDeviceInfo,
ClaimCapabilities,
ClientInfo,
ClientRegistrationGateway,
)
from .current_client_service import (
CurrentClientService,
generate_client_device_id,
)
from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway
from .settings_repository import SettingsRepository
DEVICE_ID_PLACEHOLDER = "待生成"
def generate_client_device_id() -> str:
"""根据本机特征生成设备号,不暴露原始系统和硬件信息。"""
class CurrentClientSaveWorker(QObject):
"""在线程中先保存本地身份,再登记到 Admin。"""
fingerprint = "|".join(
(
platform.system(),
platform.node(),
platform.machine(),
f"{uuid.getnode():012x}",
)
)
digest = hashlib.sha256(fingerprint.encode("utf-8")).hexdigest()[:16].upper()
return f"CLIENT-{digest}"
localSaved = pyqtSignal(str, str)
localSaveFailed = pyqtSignal(str)
registrationSucceeded = pyqtSignal(str)
registrationFailed = pyqtSignal(str)
completed = pyqtSignal()
def __init__(
self,
service: CurrentClientService,
gateway: Optional[ClientRegistrationGateway],
client_name: str,
capabilities: ClaimCapabilities,
gateway_error: str = "",
):
super().__init__()
self._service = service
self._gateway = gateway
self._client_name = client_name
self._capabilities = capabilities
self._gateway_error = gateway_error
self._cancelled = False
def cancel(self) -> None:
"""阻止尚未开始的远端登记;已经发出的 HTTP 等超时返回。"""
self._cancelled = True
@pyqtSlot()
def run(self) -> None:
try:
try:
saved = self._service.save(self._client_name)
except Exception as exc:
self.localSaveFailed.emit(str(exc) or "无法写入本地数据库")
return
self.localSaved.emit(saved.client_id, saved.client_name)
if self._cancelled:
return
if self._gateway is None:
self.registrationFailed.emit(
self._gateway_error or "Admin Gateway 尚未配置"
)
return
try:
receipt = self._gateway.register_client(
ClientInfo(saved.client_id, saved.client_name),
self._capabilities,
)
except AdminGatewayError as exc:
self.registrationFailed.emit(str(exc))
except Exception as exc:
self.registrationFailed.emit(str(exc) or "Admin 登记失败")
else:
self.registrationSucceeded.emit(receipt.registered_at)
finally:
self.completed.emit()
class SettingsPageEventBinder(QObject):
@@ -38,36 +98,108 @@ class SettingsPageEventBinder(QObject):
saveRequested = pyqtSignal(str, str)
deleteRequested = pyqtSignal(str)
def __init__(self, page):
def __init__(
self,
page,
settings_repository: Optional[SettingsRepository] = None,
admin_gateway: Optional[ClientRegistrationGateway] = None,
):
super().__init__(page)
self._page = page
self._busy = False
self._current_device_busy = False
self._closing = False
self._thread: Optional[QThread] = None
self._worker: Optional[CurrentClientSaveWorker] = None
repository = settings_repository or SettingsRepository()
self._client_service = CurrentClientService(repository)
self._admin_gateway = admin_gateway
self._gateway_error = ""
if self._admin_gateway is None:
base_url = repository.get("admin.base_url", DEFAULT_ADMIN_BASE_URL)
timeout_value = repository.get("admin.request_timeout_seconds", 3.0)
try:
timeout_seconds = float(timeout_value)
self._admin_gateway = HttpAdminGateway(
base_url if isinstance(base_url, str) else "",
timeout_seconds=timeout_seconds,
)
except (TypeError, ValueError) as exc:
self._gateway_error = str(exc)
page.currentDeviceSaveButton.clicked.connect(
self._request_save_current_device
)
self.currentDeviceSaveRequested.connect(
self._start_save_current_device
)
page.searchButton.clicked.connect(self._request_search)
page.connectButton.clicked.connect(self._request_connect)
page.saveButton.clicked.connect(self._request_save)
page.deleteButton.clicked.connect(self._request_delete)
page.addressInput.textChanged.connect(self._sync_button_state)
page.deviceTableModel.checkedDeviceChanged.connect(self._sync_button_state)
page.destroyed.connect(self.shutdown)
application = QCoreApplication.instance()
if application is not None:
application.aboutToQuit.connect(self.shutdown)
self._load_current_client()
self._sync_button_state()
@pyqtSlot()
def _request_save_current_device(self) -> None:
if self._busy:
if self._busy or self._current_device_busy:
return
device_id = self._page.deviceIdInput.text().strip()
if not device_id or device_id == DEVICE_ID_PLACEHOLDER:
device_id = generate_client_device_id()
self._page.deviceIdInput.setText(device_id)
device_name = self._page.deviceNameInput.text().strip()
self._page.deviceNameInput.setText(device_name)
self.currentDeviceSaveRequested.emit(device_id, device_name)
@pyqtSlot(str, str)
def _start_save_current_device(
self, _displayed_device_id: str, device_name: str
) -> None:
if self._closing or self._current_device_busy:
return
try:
device = self._selected_android_device()
capabilities = ClaimCapabilities(device=device)
except ValueError as exc:
self._page.set_current_device_status(f"保存失败:{exc}")
return
self._current_device_busy = True
self._sync_button_state()
self._page.set_current_device_status("正在保存本地设备信息…")
thread = QThread(self)
worker = CurrentClientSaveWorker(
self._client_service,
self._admin_gateway,
device_name,
capabilities,
self._gateway_error,
)
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.localSaved.connect(self._on_local_saved)
worker.localSaveFailed.connect(self._on_local_save_failed)
worker.registrationSucceeded.connect(self._on_registration_succeeded)
worker.registrationFailed.connect(self._on_registration_failed)
worker.completed.connect(thread.quit)
worker.completed.connect(worker.deleteLater)
thread.finished.connect(self._on_save_thread_finished)
thread.finished.connect(thread.deleteLater)
self._thread = thread
self._worker = worker
thread.start()
@pyqtSlot()
def _request_search(self) -> None:
if self._busy:
@@ -118,7 +250,9 @@ class SettingsPageEventBinder(QObject):
def _sync_button_state(self, *_args) -> None:
serial = self._page.deviceTableModel.checked_serial
has_address = bool(self._page.addressInput.text().strip())
self._page.currentDeviceSaveButton.setEnabled(not self._busy)
self._page.currentDeviceSaveButton.setEnabled(
not self._busy and not self._current_device_busy
)
self._page.searchButton.setEnabled(not self._busy)
self._page.connectButton.setEnabled(not self._busy and has_address)
self._page.saveButton.setEnabled(not self._busy and bool(serial))
@@ -131,3 +265,86 @@ class SettingsPageEventBinder(QObject):
self._sync_button_state()
if message:
self._page.deviceStatusLabel.setText(message)
def _load_current_client(self) -> None:
"""页面创建时恢复本地 Client 信息。"""
try:
saved = self._client_service.load()
except Exception as exc:
self._page.set_current_device_status(
f"读取本地设备信息失败:{str(exc) or '数据库不可用'}"
)
return
self._page.set_client_info(saved.client_id, saved.client_name)
if saved.client_id:
self._page.set_current_device_status("本地设备信息已加载")
def _selected_android_device(self) -> Optional[AndroidDeviceInfo]:
serial = self._page.deviceTableModel.checked_serial.strip()
return AndroidDeviceInfo(serial) if serial else None
@pyqtSlot(str, str)
def _on_local_saved(self, client_id: str, client_name: str) -> None:
if self._closing:
return
self._page.set_client_info(client_id, client_name)
self._page.set_current_device_status(
"本地已保存,正在登记到 Admin…"
)
@pyqtSlot(str)
def _on_local_save_failed(self, message: str) -> None:
if self._closing:
return
self._page.set_current_device_status(f"本地保存失败:{message}")
@pyqtSlot(str)
def _on_registration_succeeded(self, _registered_at: str) -> None:
if self._closing:
return
self._page.set_current_device_status("本地已保存,已登记到 Admin")
@pyqtSlot(str)
def _on_registration_failed(self, message: str) -> None:
if self._closing:
return
self._page.set_current_device_status(
f"本地已保存,Admin 登记失败:{message};可再次点击保存重试"
)
@pyqtSlot()
def _on_save_thread_finished(self) -> None:
self._worker = None
self._thread = None
self._current_device_busy = False
if not self._closing:
self._sync_button_state()
@pyqtSlot()
def shutdown(self) -> None:
"""窗口关闭时忽略迟到结果,并等待短请求安全结束。"""
if self._closing:
return
self._closing = True
worker = self._worker
thread = self._thread
if worker is not None:
worker.cancel()
for signal, slot in (
(worker.localSaved, self._on_local_saved),
(worker.localSaveFailed, self._on_local_save_failed),
(worker.registrationSucceeded, self._on_registration_succeeded),
(worker.registrationFailed, self._on_registration_failed),
):
try:
signal.disconnect(slot)
except TypeError:
pass
if thread is not None and thread.isRunning():
thread.quit()
thread.wait(4000)