feat: 保存并登记当前 Client (#11)
This commit is contained in:
+243
-26
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user