fix: 任务执行前检查 Android 设备连接 (#84)
This commit is contained in:
@@ -120,6 +120,32 @@ class AndroidDeviceService:
|
||||
)
|
||||
return enriched
|
||||
|
||||
def require_connected(self, serial: str) -> AndroidDevice:
|
||||
"""确认已保存设备仍由 ADB 识别且状态可用。"""
|
||||
|
||||
self._validate_saved_serial(serial)
|
||||
device = next(
|
||||
(item for item in self._list_devices() if item.serial == serial),
|
||||
None,
|
||||
)
|
||||
if device is None:
|
||||
connection = "Wi-Fi" if ":" in serial else "USB"
|
||||
raise AndroidDeviceSearchError(
|
||||
f"{connection} Android 设备 {serial} 未连接,"
|
||||
"请连接设备后重试"
|
||||
)
|
||||
if device.status == "unauthorized":
|
||||
raise AndroidDeviceSearchError(
|
||||
f"Android 设备 {serial} 未授权,"
|
||||
"请在手机上允许 USB 调试后重试"
|
||||
)
|
||||
if device.status != "device":
|
||||
raise AndroidDeviceSearchError(
|
||||
f"Android 设备 {serial} 当前为 {device.status} 状态,"
|
||||
"请重新连接后重试"
|
||||
)
|
||||
return device
|
||||
|
||||
def is_package_installed(
|
||||
self,
|
||||
serial: str,
|
||||
|
||||
@@ -278,6 +278,7 @@ class PDDTaskPage(QWidget):
|
||||
refreshRequested = pyqtSignal()
|
||||
rerunRequested = pyqtSignal(str)
|
||||
rerunCancelRequested = pyqtSignal()
|
||||
openSettingsRequested = pyqtSignal()
|
||||
detailRequested = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
|
||||
+112
-15
@@ -20,7 +20,7 @@
|
||||
只有必须让用户当场做决定时才用模态对话框。
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
QCoreApplication,
|
||||
@@ -32,6 +32,10 @@ from PyQt5.QtCore import (
|
||||
)
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition, MessageBox, PushButton
|
||||
|
||||
from .android_device_service import (
|
||||
AndroidDeviceSearchError,
|
||||
AndroidDeviceService,
|
||||
)
|
||||
from .admin_gateway import (
|
||||
AdminGatewayError,
|
||||
ClientInfo,
|
||||
@@ -101,6 +105,7 @@ class ClaimTaskWorker(QObject):
|
||||
localSaveFailed = pyqtSignal(str, str)
|
||||
retryableFailed = pyqtSignal(str)
|
||||
failed = pyqtSignal(str)
|
||||
deviceUnavailable = pyqtSignal(str)
|
||||
outcome = pyqtSignal(str, str, str)
|
||||
completed = pyqtSignal()
|
||||
|
||||
@@ -114,6 +119,7 @@ class ClaimTaskWorker(QObject):
|
||||
purchase_adapter_factory: Optional[PurchaseAdapterFactory] = None,
|
||||
purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None,
|
||||
selected_task_id: str = "",
|
||||
device_connection_checker: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._gateway = gateway
|
||||
@@ -125,6 +131,10 @@ class ClaimTaskWorker(QObject):
|
||||
self._purchase_adapter_factory = purchase_adapter_factory
|
||||
self._purchase_reconcile_factory = purchase_reconcile_factory
|
||||
self._selected_task_id = selected_task_id
|
||||
self._device_connection_checker = (
|
||||
device_connection_checker
|
||||
or AndroidDeviceService().require_connected
|
||||
)
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""阻止尚未开始的领取;已领取的任务仍必须保存到本地。"""
|
||||
@@ -147,6 +157,10 @@ class ClaimTaskWorker(QObject):
|
||||
client_settings.client_name,
|
||||
)
|
||||
if self._selected_task_id:
|
||||
self._device_connection_checker(android_serial or "")
|
||||
self._task_repository.prepare_collect_rerun(
|
||||
self._selected_task_id
|
||||
)
|
||||
service = CollectTaskService(
|
||||
self._gateway,
|
||||
self._task_repository,
|
||||
@@ -166,10 +180,14 @@ class ClaimTaskWorker(QObject):
|
||||
purchase_adapter_factory=self._purchase_adapter_factory,
|
||||
purchase_reconcile_factory=self._purchase_reconcile_factory,
|
||||
cancelled=lambda: self._cancelled,
|
||||
device_connection_checker=self._device_connection_checker,
|
||||
)
|
||||
result = dispatcher.execute_one()
|
||||
if not self._cancelled or result.kind == "cancelled":
|
||||
self.outcome.emit(result.kind, result.message, result.task_id)
|
||||
except AndroidDeviceSearchError as exc:
|
||||
if not self._cancelled:
|
||||
self.deviceUnavailable.emit(str(exc))
|
||||
except AdminGatewayError as exc:
|
||||
if not self._cancelled:
|
||||
request_hint = (
|
||||
@@ -200,6 +218,7 @@ class PDDTaskPageEvent(QObject):
|
||||
collect_service_factory: Optional[CollectServiceFactory] = None,
|
||||
purchase_adapter_factory: Optional[PurchaseAdapterFactory] = None,
|
||||
purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None,
|
||||
device_connection_checker: Optional[Callable[[str], None]] = None,
|
||||
next_task_delay_ms: int = 500,
|
||||
no_task_delay_ms: int = 5_000,
|
||||
retry_delays_ms: tuple[int, ...] = (5_000, 10_000, 20_000, 30_000),
|
||||
@@ -214,6 +233,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._claim_worker: Optional[ClaimTaskWorker] = None
|
||||
self._rerun_cancel_requested = False
|
||||
self._rerun_feedback: Optional[InfoBar] = None
|
||||
self._device_feedback: Optional[InfoBar] = None
|
||||
self._auto_fetch_running = False
|
||||
self._stop_requested = False
|
||||
self._cycle_next_delay_ms: Optional[int] = None
|
||||
@@ -223,6 +243,10 @@ class PDDTaskPageEvent(QObject):
|
||||
self._collect_service_factory = collect_service_factory
|
||||
self._purchase_adapter_factory = purchase_adapter_factory
|
||||
self._purchase_reconcile_factory = purchase_reconcile_factory
|
||||
self._device_connection_checker = (
|
||||
device_connection_checker
|
||||
or AndroidDeviceService().require_connected
|
||||
)
|
||||
if next_task_delay_ms < 0 or no_task_delay_ms <= 0:
|
||||
raise ValueError("自动获取等待时间配置无效")
|
||||
if not retry_delays_ms or any(value <= 0 for value in retry_delays_ms):
|
||||
@@ -325,8 +349,8 @@ class PDDTaskPageEvent(QObject):
|
||||
)
|
||||
return
|
||||
if not android_serial:
|
||||
self._show_rerun_warning(
|
||||
"重新执行不可用", "请先在设置页选择并保存 Android 设备。"
|
||||
self._show_device_unavailable(
|
||||
"请先在设置页选择并保存 Android 设备"
|
||||
)
|
||||
return
|
||||
try:
|
||||
@@ -353,17 +377,6 @@ class PDDTaskPageEvent(QObject):
|
||||
if not dialog.exec():
|
||||
return
|
||||
|
||||
try:
|
||||
self._repository.prepare_collect_rerun(task_id)
|
||||
except (CollectRerunError, ValueError) as exc:
|
||||
self._show_rerun_warning("不能重新执行", str(exc))
|
||||
self._reload()
|
||||
return
|
||||
except Exception:
|
||||
self._show_rerun_warning(
|
||||
"重新执行失败", "无法更新本地任务状态,请检查数据库后重试。"
|
||||
)
|
||||
return
|
||||
self._start_rerun_worker(task_id)
|
||||
|
||||
def _start_rerun_worker(self, task_id: str) -> None:
|
||||
@@ -371,10 +384,13 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
assert self._claim_gateway is not None
|
||||
self._close_rerun_feedback()
|
||||
self._close_device_feedback()
|
||||
self._rerun_cancel_requested = False
|
||||
self._claim_busy = True
|
||||
self._page.set_rerun_running(True)
|
||||
self._page.set_engine_status(f"正在重新采集任务 {task_id}…")
|
||||
self._page.set_engine_status(
|
||||
f"正在检查 Android 设备并准备重新采集任务 {task_id}…"
|
||||
)
|
||||
self._reload()
|
||||
|
||||
thread = QThread(self)
|
||||
@@ -385,11 +401,13 @@ class PDDTaskPageEvent(QObject):
|
||||
self._selected_android_device_service,
|
||||
self._collect_service_factory,
|
||||
selected_task_id=task_id,
|
||||
device_connection_checker=self._device_connection_checker,
|
||||
)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
worker.retryableFailed.connect(self._on_rerun_failed)
|
||||
worker.failed.connect(self._on_rerun_failed)
|
||||
worker.deviceUnavailable.connect(self._on_rerun_device_unavailable)
|
||||
worker.outcome.connect(self._on_rerun_outcome)
|
||||
worker.completed.connect(thread.quit)
|
||||
worker.completed.connect(worker.deleteLater)
|
||||
@@ -443,6 +461,16 @@ class PDDTaskPageEvent(QObject):
|
||||
self._page.set_engine_status(message)
|
||||
self._show_rerun_error("重新采集失败", message)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_rerun_device_unavailable(self, message: str) -> None:
|
||||
if self._closing or self._rerun_cancel_requested:
|
||||
return
|
||||
content = message or "Android 设备未连接,任务未开始"
|
||||
self._page.set_engine_status(
|
||||
f"Android 设备不可用,重新采集未开始:{content}"
|
||||
)
|
||||
self._show_device_unavailable(content)
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_rerun_thread_finished(self) -> None:
|
||||
cancel_requested = self._rerun_cancel_requested
|
||||
@@ -528,6 +556,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._show_claim_error("领取任务失败", message)
|
||||
return
|
||||
|
||||
self._close_device_feedback()
|
||||
self._auto_fetch_running = True
|
||||
self._stop_requested = False
|
||||
self._retry_count = 0
|
||||
@@ -560,6 +589,7 @@ class PDDTaskPageEvent(QObject):
|
||||
collect_service_factory=self._collect_service_factory,
|
||||
purchase_adapter_factory=self._purchase_adapter_factory,
|
||||
purchase_reconcile_factory=self._purchase_reconcile_factory,
|
||||
device_connection_checker=self._device_connection_checker,
|
||||
)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
@@ -569,6 +599,7 @@ class PDDTaskPageEvent(QObject):
|
||||
worker.localSaveFailed.connect(self._on_claimed_task_save_failed)
|
||||
worker.retryableFailed.connect(self._on_claim_retryable_failed)
|
||||
worker.failed.connect(self._on_claim_failed)
|
||||
worker.deviceUnavailable.connect(self._on_claim_device_unavailable)
|
||||
worker.outcome.connect(self._on_collect_outcome)
|
||||
worker.completed.connect(thread.quit)
|
||||
worker.completed.connect(worker.deleteLater)
|
||||
@@ -682,6 +713,16 @@ class PDDTaskPageEvent(QObject):
|
||||
self._show_claim_error("领取任务失败", content)
|
||||
self._stop_after_current(content)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_claim_device_unavailable(self, message: str) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
content = message or "Android 设备未连接,任务未开始"
|
||||
status = f"自动获取:已停止 · Android 设备不可用:{content}"
|
||||
self._page.set_engine_status(status)
|
||||
self._show_device_unavailable(content)
|
||||
self._stop_after_current(status)
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def _on_collect_outcome(self, kind: str, message: str, task_id: str) -> None:
|
||||
if self._closing:
|
||||
@@ -753,6 +794,53 @@ class PDDTaskPageEvent(QObject):
|
||||
parent=self._page,
|
||||
)
|
||||
|
||||
def _show_device_unavailable(self, content: str) -> None:
|
||||
"""显示一条可进入设置且不会堆叠的设备错误。"""
|
||||
|
||||
self._close_device_feedback()
|
||||
bar = InfoBar.error(
|
||||
title="Android 设备不可用",
|
||||
content=content,
|
||||
isClosable=True,
|
||||
duration=-1,
|
||||
position=InfoBarPosition.TOP_RIGHT,
|
||||
parent=self._page,
|
||||
)
|
||||
self._device_feedback = bar
|
||||
settings_button = PushButton("打开设置", bar)
|
||||
settings_button.setAccessibleName("打开 Android 设备设置")
|
||||
settings_button.clicked.connect(
|
||||
lambda _checked=False, current=bar: self._open_device_settings(current)
|
||||
)
|
||||
close_button = PushButton("关闭提示", bar)
|
||||
close_button.setAccessibleName("关闭 Android 设备提示")
|
||||
close_button.clicked.connect(
|
||||
lambda _checked=False, current=bar: self._close_device_feedback(current)
|
||||
)
|
||||
bar.addWidget(settings_button)
|
||||
bar.addWidget(close_button)
|
||||
bar.destroyed.connect(
|
||||
lambda _object=None, current=bar: self._forget_device_feedback(current)
|
||||
)
|
||||
|
||||
def _open_device_settings(self, bar: InfoBar) -> None:
|
||||
self._page.openSettingsRequested.emit()
|
||||
self._close_device_feedback(bar)
|
||||
|
||||
def _close_device_feedback(self, expected: Optional[InfoBar] = None) -> None:
|
||||
bar = self._device_feedback
|
||||
if bar is None or (expected is not None and bar is not expected):
|
||||
return
|
||||
self._device_feedback = None
|
||||
try:
|
||||
bar.close()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def _forget_device_feedback(self, bar: InfoBar) -> None:
|
||||
if self._device_feedback is bar:
|
||||
self._device_feedback = None
|
||||
|
||||
@pyqtSlot()
|
||||
def _on_claim_thread_finished(self) -> None:
|
||||
self._claim_worker = None
|
||||
@@ -849,6 +937,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._stop_requested = True
|
||||
self._next_cycle_timer.stop()
|
||||
self._close_rerun_feedback()
|
||||
self._close_device_feedback()
|
||||
|
||||
for window in list(self._detail_windows.values()):
|
||||
window.close()
|
||||
@@ -868,6 +957,14 @@ class PDDTaskPageEvent(QObject):
|
||||
self._on_claimed_task_save_failed,
|
||||
),
|
||||
(worker.failed, self._on_claim_failed),
|
||||
(
|
||||
worker.deviceUnavailable,
|
||||
self._on_claim_device_unavailable,
|
||||
),
|
||||
(
|
||||
worker.deviceUnavailable,
|
||||
self._on_rerun_device_unavailable,
|
||||
),
|
||||
(
|
||||
worker.retryableFailed,
|
||||
self._on_claim_retryable_failed,
|
||||
|
||||
@@ -14,6 +14,10 @@ from .admin_gateway import (
|
||||
ClientInfo,
|
||||
)
|
||||
from .collect_task_service import CollectServiceFactory, CollectTaskService
|
||||
from .android_device_service import (
|
||||
AndroidDeviceSearchError,
|
||||
AndroidDeviceService,
|
||||
)
|
||||
from .purchase_task_service import (
|
||||
PurchaseAdapterFactory,
|
||||
PurchaseTaskService,
|
||||
@@ -126,6 +130,7 @@ class TaskDispatcher:
|
||||
purchase_adapter_factory: Optional[PurchaseAdapterFactory] = None,
|
||||
purchase_reconcile_factory: Optional[PurchaseReconcileFactory] = None,
|
||||
cancelled: Callable[[], bool] = lambda: False,
|
||||
device_connection_checker: Optional[Callable[[str], None]] = None,
|
||||
) -> None:
|
||||
self._gateway = gateway
|
||||
self._repository = repository
|
||||
@@ -135,6 +140,10 @@ class TaskDispatcher:
|
||||
self._purchase_factory = purchase_adapter_factory
|
||||
self._reconcile_factory = purchase_reconcile_factory
|
||||
self._cancelled = cancelled
|
||||
self._device_connection_checker = (
|
||||
device_connection_checker
|
||||
or AndroidDeviceService().require_connected
|
||||
)
|
||||
|
||||
@property
|
||||
def purchase_ready(self) -> bool:
|
||||
@@ -169,7 +178,10 @@ class TaskDispatcher:
|
||||
if pending is not None:
|
||||
return self._submit_pending(pending)
|
||||
if not self._device_address:
|
||||
raise ValueError("请先在设置页选择并保存 Android 设备")
|
||||
raise AndroidDeviceSearchError(
|
||||
"请先在设置页选择并保存 Android 设备"
|
||||
)
|
||||
self._device_connection_checker(self._device_address)
|
||||
|
||||
reconcile_task = self._repository.next_purchase_reconcile_task()
|
||||
if reconcile_task is not None:
|
||||
|
||||
@@ -58,6 +58,9 @@ class MainWindow(FluentWindow):
|
||||
purchase_adapter_factory=create_u2_purchase_adapter,
|
||||
)
|
||||
|
||||
self.pddTaskPage.openSettingsRequested.connect(
|
||||
lambda: self.switchTo(self.settingsPage)
|
||||
)
|
||||
self.addSubInterface(self.pddTaskPage, FIF.HOME, "pdd")
|
||||
self.addSubInterface(
|
||||
self.settingsPage,
|
||||
|
||||
Reference in New Issue
Block a user