feat: 内置并统一使用项目 ADB (#149)
This commit is contained in:
@@ -108,6 +108,13 @@ VSVersionInfo(
|
|||||||
$distPath = Join-Path $buildRoot "dist"
|
$distPath = Join-Path $buildRoot "dist"
|
||||||
$workPath = Join-Path $buildRoot "work"
|
$workPath = Join-Path $buildRoot "work"
|
||||||
$specPath = Join-Path $buildRoot "spec"
|
$specPath = Join-Path $buildRoot "spec"
|
||||||
|
$bundledAdbSource = Join-Path $clientRoot "vendor\android-platform-tools\windows"
|
||||||
|
foreach ($adbFileName in @("adb.exe", "AdbWinApi.dll", "AdbWinUsbApi.dll")) {
|
||||||
|
$adbSourceFile = Join-Path $bundledAdbSource $adbFileName
|
||||||
|
if (-not (Test-Path -LiteralPath $adbSourceFile -PathType Leaf)) {
|
||||||
|
throw "A required bundled ADB source file is missing: $adbSourceFile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Write-Host "[4/7] Building the main application..."
|
Write-Host "[4/7] Building the main application..."
|
||||||
Invoke-Checked -FailureMessage "Failed to build the main application" -Command {
|
Invoke-Checked -FailureMessage "Failed to build the main application" -Command {
|
||||||
@@ -118,6 +125,7 @@ VSVersionInfo(
|
|||||||
--collect-all qfluentwidgets `
|
--collect-all qfluentwidgets `
|
||||||
--collect-all uiautomator2 `
|
--collect-all uiautomator2 `
|
||||||
--collect-all adbutils `
|
--collect-all adbutils `
|
||||||
|
--add-binary "$bundledAdbSource;vendor/android-platform-tools/windows" `
|
||||||
(Join-Path $clientRoot "buyer_main.py")
|
(Join-Path $clientRoot "buyer_main.py")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,11 +139,13 @@ VSVersionInfo(
|
|||||||
|
|
||||||
$mainDist = Join-Path $distPath "CMAutoBuy"
|
$mainDist = Join-Path $distPath "CMAutoBuy"
|
||||||
$launcherExe = Join-Path $distPath "Launcher.exe"
|
$launcherExe = Join-Path $distPath "Launcher.exe"
|
||||||
$adbExe = Join-Path $mainDist "dependencies\adbutils\binaries\adb.exe"
|
$bundledAdbDist = Join-Path $mainDist "dependencies\vendor\android-platform-tools\windows"
|
||||||
foreach ($requiredPath in @(
|
foreach ($requiredPath in @(
|
||||||
(Join-Path $mainDist "CMAutoBuy.exe"),
|
(Join-Path $mainDist "CMAutoBuy.exe"),
|
||||||
(Join-Path $mainDist "dependencies\PyQt5\Qt5\plugins\platforms\qwindows.dll"),
|
(Join-Path $mainDist "dependencies\PyQt5\Qt5\plugins\platforms\qwindows.dll"),
|
||||||
$adbExe,
|
(Join-Path $bundledAdbDist "adb.exe"),
|
||||||
|
(Join-Path $bundledAdbDist "AdbWinApi.dll"),
|
||||||
|
(Join-Path $bundledAdbDist "AdbWinUsbApi.dll"),
|
||||||
$launcherExe
|
$launcherExe
|
||||||
)) {
|
)) {
|
||||||
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
if (-not (Test-Path -LiteralPath $requiredPath -PathType Leaf)) {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""项目内置 ADB 的路径、完整性检查和 adbutils 环境配置。"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from .db import app_dir
|
||||||
|
|
||||||
|
|
||||||
|
ADB_RELATIVE_DIRECTORY = Path("vendor/android-platform-tools/windows")
|
||||||
|
REQUIRED_ADB_FILES: Tuple[str, ...] = (
|
||||||
|
"adb.exe",
|
||||||
|
"AdbWinApi.dll",
|
||||||
|
"AdbWinUsbApi.dll",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BundledAdbError(RuntimeError):
|
||||||
|
"""项目内置 ADB 缺失或不完整。"""
|
||||||
|
|
||||||
|
|
||||||
|
def bundled_adb_directory() -> Path:
|
||||||
|
"""返回项目内置 ADB 目录,不创建目录。"""
|
||||||
|
|
||||||
|
return app_dir() / ADB_RELATIVE_DIRECTORY
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bundled_adb() -> Path:
|
||||||
|
"""检查 ADB 和两个配套 DLL,完整时返回 adb.exe 绝对路径。"""
|
||||||
|
|
||||||
|
directory = bundled_adb_directory()
|
||||||
|
missing_files = [
|
||||||
|
name for name in REQUIRED_ADB_FILES
|
||||||
|
if not (directory / name).is_file()
|
||||||
|
]
|
||||||
|
if missing_files:
|
||||||
|
names = "、".join(missing_files)
|
||||||
|
raise BundledAdbError(
|
||||||
|
f"内置 ADB 文件缺失:{names}。请重新安装完整的软件包。"
|
||||||
|
)
|
||||||
|
return (directory / "adb.exe").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def bundled_adb_path() -> Path:
|
||||||
|
"""返回预期的项目内 adb.exe 路径;不访问系统 PATH。"""
|
||||||
|
|
||||||
|
return (bundled_adb_directory() / "adb.exe").resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def configure_bundled_adb_environment() -> Path:
|
||||||
|
"""校验内置 ADB,并强制 adbutils/uiautomator2 使用同一文件。"""
|
||||||
|
|
||||||
|
adb_path = validate_bundled_adb()
|
||||||
|
os.environ["ADBUTILS_ADB_PATH"] = str(adb_path)
|
||||||
|
return adb_path
|
||||||
@@ -4,8 +4,15 @@ from dataclasses import dataclass
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from typing import Callable, List, Optional, Sequence
|
from typing import Callable, List, Optional, Sequence
|
||||||
|
|
||||||
|
from .adb_runtime import (
|
||||||
|
BundledAdbError,
|
||||||
|
bundled_adb_path,
|
||||||
|
validate_bundled_adb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ADB_TIMEOUT_SECONDS = 5.0
|
DEFAULT_ADB_TIMEOUT_SECONDS = 5.0
|
||||||
DEFAULT_DEVICE_READY_CHECKS = 8
|
DEFAULT_DEVICE_READY_CHECKS = 8
|
||||||
@@ -78,19 +85,24 @@ class AndroidDeviceService:
|
|||||||
command_runner: CommandRunner = run_adb_command,
|
command_runner: CommandRunner = run_adb_command,
|
||||||
timeout_seconds: float = DEFAULT_ADB_TIMEOUT_SECONDS,
|
timeout_seconds: float = DEFAULT_ADB_TIMEOUT_SECONDS,
|
||||||
sleeper: Sleeper = time.sleep,
|
sleeper: Sleeper = time.sleep,
|
||||||
|
adb_executable: Optional[Path] = None,
|
||||||
):
|
):
|
||||||
if timeout_seconds <= 0:
|
if timeout_seconds <= 0:
|
||||||
raise ValueError("ADB 超时时间必须大于 0")
|
raise ValueError("ADB 超时时间必须大于 0")
|
||||||
self._run_command = command_runner
|
self._run_command = command_runner
|
||||||
self._timeout_seconds = timeout_seconds
|
self._timeout_seconds = timeout_seconds
|
||||||
self._sleep = sleeper
|
self._sleep = sleeper
|
||||||
|
self._uses_bundled_adb = adb_executable is None
|
||||||
|
self._adb_executable = str(adb_executable or bundled_adb_path())
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, is_cancelled: Optional[Callable[[], bool]] = None
|
self, is_cancelled: Optional[Callable[[], bool]] = None
|
||||||
) -> List[AndroidDevice]:
|
) -> List[AndroidDevice]:
|
||||||
"""返回当前 ADB 设备;取消后停止补充设备属性。"""
|
"""返回当前 ADB 设备;取消后停止补充设备属性。"""
|
||||||
|
|
||||||
result = self._run(["adb", "devices", "-l"], "搜索设备")
|
result = self._run(
|
||||||
|
[self._adb_executable, "devices", "-l"], "搜索设备"
|
||||||
|
)
|
||||||
devices = self.parse_devices(result.stdout or "")
|
devices = self.parse_devices(result.stdout or "")
|
||||||
|
|
||||||
enriched = []
|
enriched = []
|
||||||
@@ -158,7 +170,15 @@ class AndroidDeviceService:
|
|||||||
raise AndroidDeviceSearchError("应用包名格式不正确")
|
raise AndroidDeviceSearchError("应用包名格式不正确")
|
||||||
|
|
||||||
result = self._run(
|
result = self._run(
|
||||||
["adb", "-s", serial, "shell", "pm", "path", package_name],
|
[
|
||||||
|
self._adb_executable,
|
||||||
|
"-s",
|
||||||
|
serial,
|
||||||
|
"shell",
|
||||||
|
"pm",
|
||||||
|
"path",
|
||||||
|
package_name,
|
||||||
|
],
|
||||||
"检测 PDD 应用",
|
"检测 PDD 应用",
|
||||||
)
|
)
|
||||||
return any(
|
return any(
|
||||||
@@ -193,7 +213,7 @@ class AndroidDeviceService:
|
|||||||
self._check_cancelled(is_cancelled)
|
self._check_cancelled(is_cancelled)
|
||||||
self._report(on_progress, "正在读取勾选设备的 Wi-Fi 地址…")
|
self._report(on_progress, "正在读取勾选设备的 Wi-Fi 地址…")
|
||||||
route = self._run(
|
route = self._run(
|
||||||
["adb", "-s", usb_serial, "shell", "ip", "route"],
|
[self._adb_executable, "-s", usb_serial, "shell", "ip", "route"],
|
||||||
"读取 Wi-Fi 地址",
|
"读取 Wi-Fi 地址",
|
||||||
)
|
)
|
||||||
ip_address = self.parse_wifi_ipv4(route.stdout or "")
|
ip_address = self.parse_wifi_ipv4(route.stdout or "")
|
||||||
@@ -202,7 +222,7 @@ class AndroidDeviceService:
|
|||||||
self._check_cancelled(is_cancelled)
|
self._check_cancelled(is_cancelled)
|
||||||
self._report(on_progress, "正在开启 ADB 5555 端口…")
|
self._report(on_progress, "正在开启 ADB 5555 端口…")
|
||||||
self._run(
|
self._run(
|
||||||
["adb", "-s", usb_serial, "tcpip", "5555"],
|
[self._adb_executable, "-s", usb_serial, "tcpip", "5555"],
|
||||||
"开启 Wi-Fi 调试",
|
"开启 Wi-Fi 调试",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -312,7 +332,14 @@ class AndroidDeviceService:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
result = self._run(
|
result = self._run(
|
||||||
["adb", "-s", serial, "shell", "getprop", property_name],
|
[
|
||||||
|
self._adb_executable,
|
||||||
|
"-s",
|
||||||
|
serial,
|
||||||
|
"shell",
|
||||||
|
"getprop",
|
||||||
|
property_name,
|
||||||
|
],
|
||||||
"读取设备信息",
|
"读取设备信息",
|
||||||
)
|
)
|
||||||
except AndroidDeviceSearchError:
|
except AndroidDeviceSearchError:
|
||||||
@@ -343,12 +370,14 @@ class AndroidDeviceService:
|
|||||||
self._sleep(DEFAULT_DEVICE_READY_INTERVAL_SECONDS)
|
self._sleep(DEFAULT_DEVICE_READY_INTERVAL_SECONDS)
|
||||||
|
|
||||||
def _list_devices(self) -> List[AndroidDevice]:
|
def _list_devices(self) -> List[AndroidDevice]:
|
||||||
result = self._run(["adb", "devices", "-l"], "读取设备列表")
|
result = self._run(
|
||||||
|
[self._adb_executable, "devices", "-l"], "读取设备列表"
|
||||||
|
)
|
||||||
return self.parse_devices(result.stdout or "")
|
return self.parse_devices(result.stdout or "")
|
||||||
|
|
||||||
def _connect_wifi(self, wifi_serial: str) -> None:
|
def _connect_wifi(self, wifi_serial: str) -> None:
|
||||||
result = self._run(
|
result = self._run(
|
||||||
["adb", "connect", wifi_serial],
|
[self._adb_executable, "connect", wifi_serial],
|
||||||
"连接 Wi-Fi 设备",
|
"连接 Wi-Fi 设备",
|
||||||
)
|
)
|
||||||
detail = " ".join(
|
detail = " ".join(
|
||||||
@@ -401,11 +430,16 @@ class AndroidDeviceService:
|
|||||||
callback(message)
|
callback(message)
|
||||||
|
|
||||||
def _run(self, command: Sequence[str], action: str):
|
def _run(self, command: Sequence[str], action: str):
|
||||||
|
if self._uses_bundled_adb:
|
||||||
|
try:
|
||||||
|
validate_bundled_adb()
|
||||||
|
except BundledAdbError as exc:
|
||||||
|
raise AndroidDeviceSearchError(str(exc)) from exc
|
||||||
try:
|
try:
|
||||||
result = self._run_command(command, self._timeout_seconds)
|
result = self._run_command(command, self._timeout_seconds)
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
raise AndroidDeviceSearchError(
|
raise AndroidDeviceSearchError(
|
||||||
"未找到 adb,请安装 Android platform-tools 并把 adb 加入 PATH 后重试"
|
"内置 adb.exe 无法启动,请重新安装完整的软件包"
|
||||||
) from exc
|
) from exc
|
||||||
except subprocess.TimeoutExpired as exc:
|
except subprocess.TimeoutExpired as exc:
|
||||||
raise AndroidDeviceSearchError(
|
raise AndroidDeviceSearchError(
|
||||||
@@ -413,7 +447,7 @@ class AndroidDeviceService:
|
|||||||
) from exc
|
) from exc
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise AndroidDeviceSearchError(
|
raise AndroidDeviceSearchError(
|
||||||
f"无法启动 adb:{str(exc) or '系统拒绝执行'}"
|
f"无法启动内置 adb:{str(exc) or '系统拒绝执行'}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
|
|||||||
@@ -34,6 +34,14 @@ def data_dir() -> Path:
|
|||||||
return directory
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
def app_dir() -> Path:
|
||||||
|
"""返回只读资源根目录;源码和打包运行使用同一相对结构。"""
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
return Path(sys._MEIPASS).resolve()
|
||||||
|
return Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
def default_database_path() -> Path:
|
def default_database_path() -> Path:
|
||||||
"""返回默认数据库文件路径。"""
|
"""返回默认数据库文件路径。"""
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from contextlib import AbstractContextManager, contextmanager
|
|||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from typing import Any, Callable, Iterator, Optional
|
from typing import Any, Callable, Iterator, Optional
|
||||||
|
|
||||||
|
from .adb_runtime import BundledAdbError, configure_bundled_adb_environment
|
||||||
from .performance_timing import current_performance_trace
|
from .performance_timing import current_performance_trace
|
||||||
|
|
||||||
|
|
||||||
@@ -80,6 +81,10 @@ class PddDeviceService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _default_connector(serial: str) -> Any:
|
def _default_connector(serial: str) -> Any:
|
||||||
|
try:
|
||||||
|
configure_bundled_adb_environment()
|
||||||
|
except BundledAdbError as exc:
|
||||||
|
raise PddDeviceError("DEVICE_ADB_MISSING", str(exc)) from exc
|
||||||
try:
|
try:
|
||||||
import uiautomator2 as u2
|
import uiautomator2 as u2
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from PyQt5.QtCore import (
|
|||||||
pyqtSlot,
|
pyqtSlot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .adb_runtime import BundledAdbError, validate_bundled_adb
|
||||||
from .android_device_service import (
|
from .android_device_service import (
|
||||||
AndroidDeviceConversionCancelled,
|
AndroidDeviceConversionCancelled,
|
||||||
AndroidDeviceSearchError,
|
AndroidDeviceSearchError,
|
||||||
@@ -359,6 +360,12 @@ class SettingsPageEventBinder(QObject):
|
|||||||
self._live_purchase_adapter_ready = bool(
|
self._live_purchase_adapter_ready = bool(
|
||||||
live_purchase_adapter_ready
|
live_purchase_adapter_ready
|
||||||
)
|
)
|
||||||
|
self._adb_unavailable_message = ""
|
||||||
|
if android_device_service is None:
|
||||||
|
try:
|
||||||
|
validate_bundled_adb()
|
||||||
|
except BundledAdbError as exc:
|
||||||
|
self._adb_unavailable_message = str(exc)
|
||||||
self._android_device_service = (
|
self._android_device_service = (
|
||||||
android_device_service or AndroidDeviceService()
|
android_device_service or AndroidDeviceService()
|
||||||
)
|
)
|
||||||
@@ -416,8 +423,12 @@ class SettingsPageEventBinder(QObject):
|
|||||||
|
|
||||||
self._load_current_client()
|
self._load_current_client()
|
||||||
self._load_selected_android_device()
|
self._load_selected_android_device()
|
||||||
|
if self._adb_unavailable_message:
|
||||||
|
self._page.deviceStatusLabel.setText(
|
||||||
|
self._adb_unavailable_message
|
||||||
|
)
|
||||||
self._sync_button_state()
|
self._sync_button_state()
|
||||||
if self._saved_android_serial:
|
if self._saved_android_serial and not self._adb_unavailable_message:
|
||||||
QTimer.singleShot(0, self._request_restore_saved_android_device)
|
QTimer.singleShot(0, self._request_restore_saved_android_device)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
@@ -910,7 +921,8 @@ class SettingsPageEventBinder(QObject):
|
|||||||
and not self._android_setting_busy
|
and not self._android_setting_busy
|
||||||
)
|
)
|
||||||
device_commands_enabled = (
|
device_commands_enabled = (
|
||||||
not self._busy
|
not self._adb_unavailable_message
|
||||||
|
and not self._busy
|
||||||
and not self._search_busy
|
and not self._search_busy
|
||||||
and not self._wifi_conversion_busy
|
and not self._wifi_conversion_busy
|
||||||
and not self._android_setting_busy
|
and not self._android_setting_busy
|
||||||
|
|||||||
+24
-1
@@ -17,16 +17,18 @@
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
FluentIcon as FIF,
|
FluentIcon as FIF,
|
||||||
FluentWindow,
|
FluentWindow,
|
||||||
|
MessageBox,
|
||||||
NavigationItemPosition,
|
NavigationItemPosition,
|
||||||
Theme,
|
Theme,
|
||||||
setTheme,
|
setTheme,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .adb_runtime import BundledAdbError, configure_bundled_adb_environment
|
||||||
from .pdd_ui import PDDTaskPage
|
from .pdd_ui import PDDTaskPage
|
||||||
from .pdd_ui_event import PDDTaskPageEvent
|
from .pdd_ui_event import PDDTaskPageEvent
|
||||||
from .pdd_u2_purchase_adapter import (
|
from .pdd_u2_purchase_adapter import (
|
||||||
@@ -147,8 +149,19 @@ def ui_main():
|
|||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
setTheme(Theme.AUTO)
|
setTheme(Theme.AUTO)
|
||||||
|
|
||||||
|
adb_error = ""
|
||||||
|
try:
|
||||||
|
configure_bundled_adb_environment()
|
||||||
|
except BundledAdbError as exc:
|
||||||
|
adb_error = str(exc)
|
||||||
|
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.showMaximized()
|
window.showMaximized()
|
||||||
|
if adb_error:
|
||||||
|
QTimer.singleShot(
|
||||||
|
0,
|
||||||
|
lambda: _show_adb_missing_dialog(window, adb_error),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
mark_current_version_healthy()
|
mark_current_version_healthy()
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -157,5 +170,15 @@ def ui_main():
|
|||||||
return app.exec_()
|
return app.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
def _show_adb_missing_dialog(parent: MainWindow, message: str) -> None:
|
||||||
|
"""在主窗口中央提示内置 ADB 缺失,用户可用大按钮关闭。"""
|
||||||
|
|
||||||
|
dialog = MessageBox("Android 调试组件缺失", message, parent)
|
||||||
|
dialog.yesButton.setText("知道了")
|
||||||
|
dialog.yesButton.setMinimumSize(120, 40)
|
||||||
|
dialog.cancelButton.hide()
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sys.exit(ui_main())
|
sys.exit(ui_main())
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""项目内置 ADB 路径与完整性检查测试。"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from src.adb_runtime import (
|
||||||
|
BundledAdbError,
|
||||||
|
REQUIRED_ADB_FILES,
|
||||||
|
bundled_adb_path,
|
||||||
|
configure_bundled_adb_environment,
|
||||||
|
validate_bundled_adb,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BundledAdbRuntimeTest(unittest.TestCase):
|
||||||
|
def test_project_adb_is_complete(self):
|
||||||
|
adb_path = validate_bundled_adb()
|
||||||
|
|
||||||
|
self.assertEqual(adb_path, bundled_adb_path())
|
||||||
|
self.assertTrue(adb_path.is_absolute())
|
||||||
|
for file_name in REQUIRED_ADB_FILES:
|
||||||
|
self.assertTrue((adb_path.parent / file_name).is_file())
|
||||||
|
|
||||||
|
def test_missing_file_reports_name_without_path_fallback(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory, patch(
|
||||||
|
"src.adb_runtime.app_dir", return_value=Path(directory)
|
||||||
|
):
|
||||||
|
with self.assertRaisesRegex(BundledAdbError, "adb.exe"):
|
||||||
|
validate_bundled_adb()
|
||||||
|
|
||||||
|
def test_configure_forces_adbutils_to_use_project_adb(self):
|
||||||
|
old_value = os.environ.get("ADBUTILS_ADB_PATH")
|
||||||
|
try:
|
||||||
|
configured = configure_bundled_adb_environment()
|
||||||
|
self.assertEqual(
|
||||||
|
os.environ["ADBUTILS_ADB_PATH"],
|
||||||
|
str(configured),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if old_value is None:
|
||||||
|
os.environ.pop("ADBUTILS_ADB_PATH", None)
|
||||||
|
else:
|
||||||
|
os.environ["ADBUTILS_ADB_PATH"] = old_value
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -3,12 +3,16 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from src.adb_runtime import bundled_adb_path
|
||||||
from src.android_device_service import (
|
from src.android_device_service import (
|
||||||
AndroidDeviceSearchError,
|
AndroidDeviceSearchError,
|
||||||
AndroidDeviceService,
|
AndroidDeviceService,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ADB = str(bundled_adb_path())
|
||||||
|
|
||||||
|
|
||||||
def completed(command, stdout="", stderr="", returncode=0):
|
def completed(command, stdout="", stderr="", returncode=0):
|
||||||
"""创建一条假的 ADB 命令结果。"""
|
"""创建一条假的 ADB 命令结果。"""
|
||||||
|
|
||||||
@@ -56,7 +60,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
|
|
||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
commands.append(list(command))
|
commands.append(list(command))
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\n"
|
"List of devices attached\n"
|
||||||
@@ -111,12 +115,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
command = list(command)
|
command = list(command)
|
||||||
commands.append(command)
|
commands.append(command)
|
||||||
if command[:2] == ["adb", "connect"]:
|
if command[:2] == [ADB, "connect"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"already connected to 192.168.0.173:5555\n",
|
"already connected to 192.168.0.173:5555\n",
|
||||||
)
|
)
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\n"
|
"List of devices attached\n"
|
||||||
@@ -131,7 +135,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
self.assertEqual(devices[0].serial, "192.168.0.173:5555")
|
self.assertEqual(devices[0].serial, "192.168.0.173:5555")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
commands[0],
|
commands[0],
|
||||||
["adb", "connect", "192.168.0.173:5555"],
|
[ADB, "connect", "192.168.0.173:5555"],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_restore_saved_wifi_waits_for_authorization_handshake(self):
|
def test_restore_saved_wifi_waits_for_authorization_handshake(self):
|
||||||
@@ -141,9 +145,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
nonlocal device_list_count
|
nonlocal device_list_count
|
||||||
command = list(command)
|
command = list(command)
|
||||||
if command[:2] == ["adb", "connect"]:
|
if command[:2] == [ADB, "connect"]:
|
||||||
return completed(command, "connected to 192.168.0.173:5555\n")
|
return completed(command, "connected to 192.168.0.173:5555\n")
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
device_list_count += 1
|
device_list_count += 1
|
||||||
status = "unauthorized" if device_list_count == 1 else "device"
|
status = "unauthorized" if device_list_count == 1 else "device"
|
||||||
return completed(
|
return completed(
|
||||||
@@ -167,7 +171,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
command = list(command)
|
command = list(command)
|
||||||
commands.append(command)
|
commands.append(command)
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\n"
|
"List of devices attached\n"
|
||||||
@@ -181,12 +185,12 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
|
|
||||||
self.assertEqual(devices[0].serial, "USB-001")
|
self.assertEqual(devices[0].serial, "USB-001")
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
any(command[:2] == ["adb", "connect"] for command in commands)
|
any(command[:2] == [ADB, "connect"] for command in commands)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_missing_model_is_read_from_device(self):
|
def test_missing_model_is_read_from_device(self):
|
||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\nUSB-001 device transport_id:1\n",
|
"List of devices attached\nUSB-001 device transport_id:1\n",
|
||||||
@@ -202,7 +206,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
|
|
||||||
def test_property_failure_keeps_discovered_device(self):
|
def test_property_failure_keeps_discovered_device(self):
|
||||||
def runner(command, _timeout):
|
def runner(command, _timeout):
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\nUSB-001 device model:Phone\n",
|
"List of devices attached\nUSB-001 device model:Phone\n",
|
||||||
@@ -218,7 +222,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
def runner(_command, _timeout):
|
def runner(_command, _timeout):
|
||||||
raise FileNotFoundError("adb")
|
raise FileNotFoundError("adb")
|
||||||
|
|
||||||
with self.assertRaisesRegex(AndroidDeviceSearchError, "未找到 adb"):
|
with self.assertRaisesRegex(AndroidDeviceSearchError, "内置 adb.exe"):
|
||||||
AndroidDeviceService(runner).search()
|
AndroidDeviceService(runner).search()
|
||||||
|
|
||||||
def test_timeout_has_recovery_message(self):
|
def test_timeout_has_recovery_message(self):
|
||||||
@@ -253,7 +257,7 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
commands,
|
commands,
|
||||||
[[
|
[[
|
||||||
"adb",
|
ADB,
|
||||||
"-s",
|
"-s",
|
||||||
"USB-001",
|
"USB-001",
|
||||||
"shell",
|
"shell",
|
||||||
@@ -296,9 +300,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
"192.168.0.0/24 dev wlan0 scope link "
|
"192.168.0.0/24 dev wlan0 scope link "
|
||||||
"src 192.168.0.173\n",
|
"src 192.168.0.173\n",
|
||||||
)
|
)
|
||||||
if command[:2] == ["adb", "connect"]:
|
if command[:2] == [ADB, "connect"]:
|
||||||
return completed(command, "connected to 192.168.0.173:5555\n")
|
return completed(command, "connected to 192.168.0.173:5555\n")
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\n"
|
"List of devices attached\n"
|
||||||
@@ -323,14 +327,14 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
["USB-001", "192.168.0.173:5555"],
|
["USB-001", "192.168.0.173:5555"],
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
["adb", "-s", "USB-001", "tcpip", "5555"],
|
[ADB, "-s", "USB-001", "tcpip", "5555"],
|
||||||
commands,
|
commands,
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
commands.count(["adb", "connect", "192.168.0.173:5555"]),
|
commands.count([ADB, "connect", "192.168.0.173:5555"]),
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
self.assertEqual(commands.count(["adb", "devices", "-l"]), 3)
|
self.assertEqual(commands.count([ADB, "devices", "-l"]), 3)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
any("保存后拔掉 USB" in message for message in progress)
|
any("保存后拔掉 USB" in message for message in progress)
|
||||||
)
|
)
|
||||||
@@ -346,9 +350,9 @@ USB-002 unauthorized usb:1-2 transport_id:3
|
|||||||
command = list(command)
|
command = list(command)
|
||||||
if command[-3:] == ["shell", "ip", "route"]:
|
if command[-3:] == ["shell", "ip", "route"]:
|
||||||
return completed(command, "local src 192.168.0.173\n")
|
return completed(command, "local src 192.168.0.173\n")
|
||||||
if command[:2] == ["adb", "connect"]:
|
if command[:2] == [ADB, "connect"]:
|
||||||
return completed(command, "connected to 192.168.0.173:5555\n")
|
return completed(command, "connected to 192.168.0.173:5555\n")
|
||||||
if command == ["adb", "devices", "-l"]:
|
if command == [ADB, "devices", "-l"]:
|
||||||
return completed(
|
return completed(
|
||||||
command,
|
command,
|
||||||
"List of devices attached\n"
|
"List of devices attached\n"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from build_tools.release_manifest import (
|
|||||||
versioned_manifest_file_name,
|
versioned_manifest_file_name,
|
||||||
write_manifest,
|
write_manifest,
|
||||||
)
|
)
|
||||||
from src.db import data_dir
|
from src.db import app_dir, data_dir
|
||||||
|
|
||||||
|
|
||||||
class LauncherPathTest(unittest.TestCase):
|
class LauncherPathTest(unittest.TestCase):
|
||||||
@@ -139,6 +139,26 @@ class PackagedDataPathTest(unittest.TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(data_dir(), root / "data")
|
self.assertEqual(data_dir(), root / "data")
|
||||||
|
|
||||||
|
def test_packaged_resource_path_uses_pyinstaller_directory(self):
|
||||||
|
with tempfile.TemporaryDirectory() as directory, patch(
|
||||||
|
"src.db.sys.frozen", True, create=True
|
||||||
|
), patch("src.db.sys._MEIPASS", directory, create=True):
|
||||||
|
self.assertEqual(app_dir(), Path(directory).resolve())
|
||||||
|
|
||||||
|
|
||||||
|
class BundledAdbPackagingTest(unittest.TestCase):
|
||||||
|
def test_build_script_packages_and_checks_all_adb_files(self):
|
||||||
|
client_root = Path(__file__).resolve().parents[1]
|
||||||
|
script = (client_root / "packaging" / "build.ps1").read_text(
|
||||||
|
encoding="utf-8-sig"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("--add-binary", script)
|
||||||
|
self.assertIn("vendor/android-platform-tools/windows", script)
|
||||||
|
self.assertIn("adb.exe", script)
|
||||||
|
self.assertIn("AdbWinApi.dll", script)
|
||||||
|
self.assertIn("AdbWinUsbApi.dll", script)
|
||||||
|
|
||||||
|
|
||||||
class ReleaseManifestTest(unittest.TestCase):
|
class ReleaseManifestTest(unittest.TestCase):
|
||||||
def test_manifest_name_and_hash_match_release_files(self):
|
def test_manifest_name_and_hash_match_release_files(self):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from PyQt5.QtCore import Qt
|
|||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from qfluentwidgets import InfoBarPosition
|
from qfluentwidgets import InfoBarPosition
|
||||||
|
|
||||||
|
from src.adb_runtime import BundledAdbError
|
||||||
from src.android_device_service import AndroidDeviceSearchError
|
from src.android_device_service import AndroidDeviceSearchError
|
||||||
from src.db import open_database
|
from src.db import open_database
|
||||||
from src.pdd_ui import PDDTaskPage
|
from src.pdd_ui import PDDTaskPage
|
||||||
@@ -1174,6 +1175,40 @@ class PDDTaskPageEventTest(unittest.TestCase):
|
|||||||
self.assertEqual(result, 0)
|
self.assertEqual(result, 0)
|
||||||
self.assertTrue(window.maximized)
|
self.assertTrue(window.maximized)
|
||||||
|
|
||||||
|
def test_application_startup_shows_dialog_when_bundled_adb_is_missing(self):
|
||||||
|
class FakeApplication:
|
||||||
|
@staticmethod
|
||||||
|
def setAttribute(*_args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, _args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def exec_(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
class FakeWindow:
|
||||||
|
def showMaximized(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
window = FakeWindow()
|
||||||
|
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
|
||||||
|
with patch("src.ui_main.QApplication", FakeApplication), patch(
|
||||||
|
"src.ui_main.MainWindow", return_value=window
|
||||||
|
), patch("src.ui_main.setTheme"), patch(
|
||||||
|
"src.ui_main.mark_current_version_healthy"
|
||||||
|
), patch(
|
||||||
|
"src.ui_main.configure_bundled_adb_environment",
|
||||||
|
side_effect=BundledAdbError(message),
|
||||||
|
), patch(
|
||||||
|
"src.ui_main.QTimer.singleShot",
|
||||||
|
side_effect=lambda _delay, callback: callback(),
|
||||||
|
), patch("src.ui_main._show_adb_missing_dialog") as show_dialog:
|
||||||
|
result = ui_main()
|
||||||
|
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
show_dialog.assert_called_once_with(window, message)
|
||||||
|
|
||||||
def test_main_window_marks_and_clears_update_navigation(self):
|
def test_main_window_marks_and_clears_update_navigation(self):
|
||||||
window = MainWindow(
|
window = MainWindow(
|
||||||
task_repository=self.repository,
|
task_repository=self.repository,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ from PyQt5.QtCore import Qt, QTimer
|
|||||||
from PyQt5.QtTest import QTest
|
from PyQt5.QtTest import QTest
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from src.adb_runtime import BundledAdbError
|
||||||
from src.android_device_service import (
|
from src.android_device_service import (
|
||||||
AndroidDevice,
|
AndroidDevice,
|
||||||
AndroidDeviceConversionCancelled,
|
AndroidDeviceConversionCancelled,
|
||||||
@@ -218,6 +220,24 @@ class SettingsPageEventTest(unittest.TestCase):
|
|||||||
page.eventBinder.shutdown()
|
page.eventBinder.shutdown()
|
||||||
page.deleteLater()
|
page.deleteLater()
|
||||||
|
|
||||||
|
def test_missing_bundled_adb_disables_device_commands(self):
|
||||||
|
message = "内置 ADB 文件缺失:adb.exe。请重新安装完整的软件包。"
|
||||||
|
with patch(
|
||||||
|
"src.settings_ui_event.validate_bundled_adb",
|
||||||
|
side_effect=BundledAdbError(message),
|
||||||
|
):
|
||||||
|
page = SettingsPage(
|
||||||
|
settings_repository=self.repository,
|
||||||
|
admin_gateway=MockAdminGateway(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(page.deviceStatusLabel.text(), message)
|
||||||
|
self.assertFalse(page.searchButton.isEnabled())
|
||||||
|
self.assertFalse(page.convertWifiButton.isEnabled())
|
||||||
|
self.assertTrue(page.currentDeviceSaveButton.isEnabled())
|
||||||
|
page.eventBinder.shutdown()
|
||||||
|
page.deleteLater()
|
||||||
|
|
||||||
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
|
def test_compact_form_layout_keeps_related_fields_on_same_row(self):
|
||||||
page = SettingsPage(
|
page = SettingsPage(
|
||||||
settings_repository=self.repository,
|
settings_repository=self.repository,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import xml.etree.ElementTree as ET
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from src.adb_runtime import configure_bundled_adb_environment
|
||||||
|
|
||||||
|
|
||||||
PDD_PACKAGE_NAME = "com.xunmeng.pinduoduo"
|
PDD_PACKAGE_NAME = "com.xunmeng.pinduoduo"
|
||||||
READY_MARKERS = ("发起拼单", "立即购买", "单独购买", "免拼购买", "快要抢光")
|
READY_MARKERS = ("发起拼单", "立即购买", "单独购买", "免拼购买", "快要抢光")
|
||||||
@@ -59,11 +61,20 @@ def wait_goods_ready(device: Any, first_xml: str, timeout: float) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def measure_once(serial: str, goods_url: str, cold: bool) -> dict[str, int]:
|
def measure_once(serial: str, goods_url: str, cold: bool) -> dict[str, int]:
|
||||||
|
adb_executable = str(configure_bundled_adb_environment())
|
||||||
import uiautomator2 as u2
|
import uiautomator2 as u2
|
||||||
|
|
||||||
if cold:
|
if cold:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["adb", "-s", serial, "shell", "am", "force-stop", PDD_PACKAGE_NAME],
|
[
|
||||||
|
adb_executable,
|
||||||
|
"-s",
|
||||||
|
serial,
|
||||||
|
"shell",
|
||||||
|
"am",
|
||||||
|
"force-stop",
|
||||||
|
PDD_PACKAGE_NAME,
|
||||||
|
],
|
||||||
check=True,
|
check=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -80,7 +91,7 @@ def measure_once(serial: str, goods_url: str, cold: bool) -> dict[str, int]:
|
|||||||
_, result["adb_device_check"] = measure_call(
|
_, result["adb_device_check"] = measure_call(
|
||||||
"adb_device_check",
|
"adb_device_check",
|
||||||
lambda: subprocess.run(
|
lambda: subprocess.run(
|
||||||
["adb", "-s", serial, "get-state"],
|
[adb_executable, "-s", serial, "get-state"],
|
||||||
check=True,
|
check=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from src.pdd_page_classifier import ( # noqa: E402
|
|||||||
PAGE_HOME,
|
PAGE_HOME,
|
||||||
classify_pdd_page,
|
classify_pdd_page,
|
||||||
)
|
)
|
||||||
|
from src.adb_runtime import configure_bundled_adb_environment # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
TEST_URL = "https://mobile.yangkeduo.com/goods.html"
|
TEST_URL = "https://mobile.yangkeduo.com/goods.html"
|
||||||
@@ -143,15 +144,16 @@ def run_diagnostic(
|
|||||||
address = str(device_address or "").strip()
|
address = str(device_address or "").strip()
|
||||||
if not address:
|
if not address:
|
||||||
raise ValueError("必须提供 Android 设备地址")
|
raise ValueError("必须提供 Android 设备地址")
|
||||||
|
adb_executable = str(configure_bundled_adb_environment())
|
||||||
adb_runner(
|
adb_runner(
|
||||||
["adb", "-s", address, "get-state"],
|
[adb_executable, "-s", address, "get-state"],
|
||||||
check=True,
|
check=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
adb_runner(
|
adb_runner(
|
||||||
[
|
[
|
||||||
"adb", "-s", address, "shell", "am", "start",
|
adb_executable, "-s", address, "shell", "am", "start",
|
||||||
"-a", "android.intent.action.VIEW", "-d", TEST_URL,
|
"-a", "android.intent.action.VIEW", "-d", TEST_URL,
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
|||||||
|
# 内置 ADB 文件说明
|
||||||
|
|
||||||
|
- 来源目录:用户指定的 `D:\Portable\adb`
|
||||||
|
- 复制日期:2026-08-11
|
||||||
|
- ADB 版本:1.0.32
|
||||||
|
|
||||||
|
运行所需文件及 SHA-256:
|
||||||
|
|
||||||
|
| 文件 | SHA-256 |
|
||||||
|
|---|---|
|
||||||
|
| `adb.exe` | `AD5D27384D1B5BCEB6342BB3204FEE977217422BCAFD252AB18F763A3DE43931` |
|
||||||
|
| `AdbWinApi.dll` | `14A51482AA003DB79A400F4B15C158397FE6D57EE6606B3D633FA431A7BFDF4B` |
|
||||||
|
| `AdbWinUsbApi.dll` | `041C6859BB4FC78D3A903DD901298CD1ECFB75B6BE0646B74954CD722280A407` |
|
||||||
|
|
||||||
|
升级 ADB 时必须整体替换这三个文件,重新记录版本和哈希,并完成 USB、Wi-Fi 和 uiautomator2 真机验证。
|
||||||
Binary file not shown.
@@ -15,12 +15,12 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Python | 3.10 | 打开 PowerShell 执行 `C:/Python310/python.exe --version`,输出 `Python 3.10.x` |
|
| Python | 3.10 | 打开 PowerShell 执行 `C:/Python310/python.exe --version`,输出 `Python 3.10.x` |
|
||||||
| Git | 任意较新版本 | `git --version` 有输出 |
|
| Git | 任意较新版本 | `git --version` 有输出 |
|
||||||
| adb(安卓调试工具) | 任意较新版本 | `adb version` 有输出 |
|
| adb(安卓调试工具) | 项目内置 1.0.32 | `./vendor/android-platform-tools/windows/adb.exe version` 有输出 |
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
- 项目固定使用 `C:/Python310/python.exe`。**不要用 `python` 这个命令**,因为电脑上可能装了多个 Python,`python` 指向哪一个不确定。本文档所有命令都写全路径。
|
- 项目固定使用 `C:/Python310/python.exe`。**不要用 `python` 这个命令**,因为电脑上可能装了多个 Python,`python` 指向哪一个不确定。本文档所有命令都写全路径。
|
||||||
- adb 一般随 Android SDK Platform Tools 安装。装好后要把它所在目录加入系统环境变量 `Path`,否则 `adb` 命令找不到。
|
- Client 固定使用 `client/vendor/android-platform-tools/windows/` 中的 ADB,不读取系统环境变量 `Path`。三个内置文件必须同时存在;缺失时程序会弹窗提示重新安装。
|
||||||
|
|
||||||
## 2. 安装依赖
|
## 2. 安装依赖
|
||||||
|
|
||||||
@@ -50,12 +50,12 @@ C:/Python310/python.exe -m pip freeze | Select-String "xxx"
|
|||||||
只做采集/采购的界面开发时**可以跳过这一步**,界面不连手机也能打开。
|
只做采集/采购的界面开发时**可以跳过这一步**,界面不连手机也能打开。
|
||||||
|
|
||||||
1. 手机打开「开发者选项」→「USB 调试」。
|
1. 手机打开「开发者选项」→「USB 调试」。
|
||||||
2. 用数据线连电脑,执行 `adb devices`,能看到一行设备号 + `device`。
|
2. 用数据线连电脑,在 `client/` 目录执行 `./vendor/android-platform-tools/windows/adb.exe devices`,能看到一行设备号 + `device`。
|
||||||
3. 打开无线调试(这样后面不用一直插着线):
|
3. 打开无线调试(这样后面不用一直插着线):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
adb tcpip 5555
|
./vendor/android-platform-tools/windows/adb.exe tcpip 5555
|
||||||
adb connect 192.168.0.173:5555
|
./vendor/android-platform-tools/windows/adb.exe connect 192.168.0.173:5555
|
||||||
```
|
```
|
||||||
|
|
||||||
把 `192.168.0.173` 换成手机的实际 IP(手机「设置 → 关于手机 → 状态信息」里能看到)。
|
把 `192.168.0.173` 换成手机的实际 IP(手机「设置 → 关于手机 → 状态信息」里能看到)。
|
||||||
@@ -95,7 +95,8 @@ C:/Python310/python.exe buyer_main.py
|
|||||||
| `ModuleNotFoundError: No module named 'qfluentwidgets'` | 依赖没装,或装到了别的 Python 上 | 回到第 2 步,注意用 `C:/Python310/python.exe -m pip` |
|
| `ModuleNotFoundError: No module named 'qfluentwidgets'` | 依赖没装,或装到了别的 Python 上 | 回到第 2 步,注意用 `C:/Python310/python.exe -m pip` |
|
||||||
| `ImportError: DLL load failed`(导入 PyQt5 时) | 装了多个 Qt 绑定互相冲突 | 卸载干净再装:`pip uninstall PyQt5 PyQt6 PySide2 PySide6 -y`,然后重新执行第 2 步 |
|
| `ImportError: DLL load failed`(导入 PyQt5 时) | 装了多个 Qt 绑定互相冲突 | 卸载干净再装:`pip uninstall PyQt5 PyQt6 PySide2 PySide6 -y`,然后重新执行第 2 步 |
|
||||||
| 窗口一闪就没了 | 直接双击 .py 文件运行的 | 必须在 PowerShell 里跑,才能看到错误信息 |
|
| 窗口一闪就没了 | 直接双击 .py 文件运行的 | 必须在 PowerShell 里跑,才能看到错误信息 |
|
||||||
| `adb: device offline` / `connect failed` | 手机和电脑不在同一个 WiFi,或手机重启过 | 重新执行 `adb connect <手机IP>:5555` |
|
| `adb: device offline` / `connect failed` | 手机和电脑不在同一个 WiFi,或手机重启过 | 使用项目内 adb 重新执行 `./vendor/android-platform-tools/windows/adb.exe connect <手机IP>:5555` |
|
||||||
|
| `内置 ADB 文件缺失` | 发布包不完整,或三个 ADB 文件没有一起复制 | 重新安装完整软件包;不要改用系统 PATH 中的 adb |
|
||||||
| 跑 demo 后目录里多出 `xxx_home.xml`、`xxx_home.png` | `src/demo1/auto_v1.py` 会把控件树和截图写到当前目录 | 正常现象,这些是调试产物,**不要提交到 Git** |
|
| 跑 demo 后目录里多出 `xxx_home.xml`、`xxx_home.png` | `src/demo1/auto_v1.py` 会把控件树和截图写到当前目录 | 正常现象,这些是调试产物,**不要提交到 Git** |
|
||||||
|
|
||||||
界面相关的问题排查不了时,先确认是不是 Qt 主线程被卡住了 —— 见 `02-architecture.md` §5。
|
界面相关的问题排查不了时,先确认是不是 Qt 主线程被卡住了 —— 见 `02-architecture.md` §5。
|
||||||
|
|||||||
@@ -198,7 +198,10 @@ CMAutoBuy/ 整个文件夹拷到任何机器都能用
|
|||||||
│ │ ├── bin/ Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll …
|
│ │ ├── bin/ Qt5Core.dll / Qt5Gui.dll / Qt5Widgets.dll …
|
||||||
│ │ └── plugins/platforms/qwindows.dll
|
│ │ └── plugins/platforms/qwindows.dll
|
||||||
│ ├── qfluentwidgets/ qss 样式、字体、图标资源
|
│ ├── qfluentwidgets/ qss 样式、字体、图标资源
|
||||||
│ ├── adbutils/binaries/adb.exe
|
│ ├── vendor/android-platform-tools/windows/
|
||||||
|
│ │ ├── adb.exe
|
||||||
|
│ │ ├── AdbWinApi.dll
|
||||||
|
│ │ └── AdbWinUsbApi.dll
|
||||||
│ └── …
|
│ └── …
|
||||||
└── data/ 本地数据,升级时保留(见 [03 数据模型](03-data-model.md) §2.1)
|
└── data/ 本地数据,升级时保留(见 [03 数据模型](03-data-model.md) §2.1)
|
||||||
├── client.db
|
├── client.db
|
||||||
@@ -246,7 +249,7 @@ CMAutoBuy/ 整个文件夹拷到任何机器都能用
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Qt 平台插件没收集到 | 启动即报 `could not find or load the Qt platform plugin "windows"` |
|
| Qt 平台插件没收集到 | 启动即报 `could not find or load the Qt platform plugin "windows"` |
|
||||||
| `qfluentwidgets` 资源没收集到 | 能启动,但界面全白、控件没样式 |
|
| `qfluentwidgets` 资源没收集到 | 能启动,但界面全白、控件没样式 |
|
||||||
| `adbutils` 的 adb 二进制没收集到 | 界面正常,但连不上手机 |
|
| 项目内置 ADB 或配套 DLL 没收集到 | 启动时弹窗提示,设备操作保持中断 |
|
||||||
| `uiautomator2` 初始化流程 | 打包后没有 `python -m uiautomator2 init` 这条命令,需确认辅助 App 怎么装 |
|
| `uiautomator2` 初始化流程 | 打包后没有 `python -m uiautomator2 init` 这条命令,需确认辅助 App 怎么装 |
|
||||||
| 杀软误报 | 空白版本信息的 exe 最容易被拦。必须给 exe 加图标和版本信息(产品名、版本号)。根治需要代码签名证书 |
|
| 杀软误报 | 空白版本信息的 exe 最容易被拦。必须给 exe 加图标和版本信息(产品名、版本号)。根治需要代码签名证书 |
|
||||||
| 装到 `C:\Program Files\` | 无写权限,数据被重定向到 VirtualStore,设置改了不生效 |
|
| 装到 `C:\Program Files\` | 无写权限,数据被重定向到 VirtualStore,设置改了不生效 |
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ class TaskTableModel(QAbstractTableModel):
|
|||||||
|
|
||||||
### Android 设备
|
### Android 设备
|
||||||
|
|
||||||
|
- Client 只使用发布包内置的 ADB,不回退系统 `Path`;启动时缺少 `adb.exe` 或配套 DLL,应在主窗口中央弹出带明显关闭按钮的中文提示,并禁用需要 ADB 的设备命令;
|
||||||
- ADB 地址;
|
- ADB 地址;
|
||||||
- PDD 包名;
|
- PDD 包名;
|
||||||
- “测试连接”;
|
- “测试连接”;
|
||||||
|
|||||||
Reference in New Issue
Block a user