devhelmkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devhelmkit/__init__.py +44 -0
- devhelmkit/android/__init__.py +7 -0
- devhelmkit/android/driver.py +18 -0
- devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
- devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
- devhelmkit/assets/so/x86_64/agent.so +0 -0
- devhelmkit/core/__init__.py +3 -0
- devhelmkit/core/base_component.py +214 -0
- devhelmkit/core/base_driver.py +414 -0
- devhelmkit/core/base_window.py +25 -0
- devhelmkit/core/selector_spec.py +102 -0
- devhelmkit/entry.py +90 -0
- devhelmkit/exceptions.py +56 -0
- devhelmkit/harmony/__init__.py +3 -0
- devhelmkit/harmony/agent/__init__.py +7 -0
- devhelmkit/harmony/agent/so_manager.py +300 -0
- devhelmkit/harmony/config.py +124 -0
- devhelmkit/harmony/device/__init__.py +6 -0
- devhelmkit/harmony/device/hdc.py +430 -0
- devhelmkit/harmony/driver.py +1416 -0
- devhelmkit/harmony/finder/__init__.py +12 -0
- devhelmkit/harmony/finder/component_finder.py +336 -0
- devhelmkit/harmony/finder/popup_handler.py +81 -0
- devhelmkit/harmony/finder/selector_adapter.py +101 -0
- devhelmkit/harmony/finder/xpath_query.py +110 -0
- devhelmkit/harmony/rpc/__init__.py +12 -0
- devhelmkit/harmony/rpc/client.py +126 -0
- devhelmkit/harmony/rpc/proxy_v2.py +106 -0
- devhelmkit/harmony/rpc/remote_object.py +48 -0
- devhelmkit/harmony/uiobject.py +246 -0
- devhelmkit/harmony/uiwindow.py +43 -0
- devhelmkit/harmony/webview/__init__.py +17 -0
- devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
- devhelmkit/harmony/webview/devtools_finder.py +131 -0
- devhelmkit/harmony/webview/webview_driver.py +326 -0
- devhelmkit/model/__init__.py +3 -0
- devhelmkit/model/action.py +147 -0
- devhelmkit/model/app_state.py +50 -0
- devhelmkit/model/constants.py +22 -0
- devhelmkit/model/display.py +32 -0
- devhelmkit/model/format_string.py +24 -0
- devhelmkit/model/input.py +50 -0
- devhelmkit/model/json_base.py +42 -0
- devhelmkit/model/keys.py +375 -0
- devhelmkit/model/match_pattern.py +15 -0
- devhelmkit/model/page.py +13 -0
- devhelmkit/model/params.py +42 -0
- devhelmkit/model/rect.py +58 -0
- devhelmkit/model/runnable.py +18 -0
- devhelmkit/utils/__init__.py +3 -0
- devhelmkit/utils/logger.py +72 -0
- devhelmkit/utils/retry.py +46 -0
- devhelmkit/utils/timeout.py +64 -0
- devhelmkit-0.1.0.dist-info/METADATA +411 -0
- devhelmkit-0.1.0.dist-info/RECORD +58 -0
- devhelmkit-0.1.0.dist-info/WHEEL +5 -0
- devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""chromedriver 进程管理:启动、停止、版本匹配。
|
|
4
|
+
|
|
5
|
+
chromedriver 是 selenium webdriver 与 webview 之间的桥梁。
|
|
6
|
+
本模块负责:
|
|
7
|
+
1. 按平台选择正确的二进制文件名(chromedriver.exe / chromedriver / chromedriver.mac)
|
|
8
|
+
2. 按设备 webview 版本匹配 chromedriver 版本
|
|
9
|
+
3. 启动 chromedriver 进程并监听指定端口
|
|
10
|
+
4. 版本不匹配时停止旧进程并重启
|
|
11
|
+
5. 资源释放时停止 chromedriver 进程
|
|
12
|
+
|
|
13
|
+
chromedriver 二进制需用户自行下载放置于 search_path 目录下:
|
|
14
|
+
search_path/
|
|
15
|
+
├── chromedriver_114/
|
|
16
|
+
│ ├── chromedriver.exe # Windows
|
|
17
|
+
│ ├── chromedriver # Linux
|
|
18
|
+
│ └── chromedriver.mac # macOS
|
|
19
|
+
└── chromedriver_132/
|
|
20
|
+
├── chromedriver.exe
|
|
21
|
+
├── chromedriver
|
|
22
|
+
└── chromedriver.mac
|
|
23
|
+
"""
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import platform
|
|
27
|
+
import stat
|
|
28
|
+
import subprocess
|
|
29
|
+
import time
|
|
30
|
+
import urllib.request
|
|
31
|
+
from typing import Optional
|
|
32
|
+
|
|
33
|
+
from devhelmkit.exceptions import DevhelmError
|
|
34
|
+
from devhelmkit.utils import logger
|
|
35
|
+
|
|
36
|
+
# chromedriver 默认监听端口
|
|
37
|
+
DEFAULT_CHROMEDRIVER_PORT = 9515
|
|
38
|
+
|
|
39
|
+
# chromedriver 日志级别环境变量名
|
|
40
|
+
CHROME_DRIVER_LOG_LEVEL_ENV = "CHROME_DRIVER_LOG_LEVEL"
|
|
41
|
+
|
|
42
|
+
# 进程状态检查重试次数
|
|
43
|
+
KILL_RETRY = 3
|
|
44
|
+
|
|
45
|
+
# kill 后等待时间(秒)
|
|
46
|
+
KILL_WAIT = 1.0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ChromedriverManager:
|
|
50
|
+
"""chromedriver 进程生命周期管理。"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, search_path: str = "",
|
|
53
|
+
exe_path: str = "",
|
|
54
|
+
port: int = DEFAULT_CHROMEDRIVER_PORT):
|
|
55
|
+
"""
|
|
56
|
+
Args:
|
|
57
|
+
search_path: chromedriver 存放目录(多版本目录结构)
|
|
58
|
+
exe_path: 直接指定 chromedriver 可执行文件路径(优先于 search_path)
|
|
59
|
+
port: chromedriver 监听端口
|
|
60
|
+
"""
|
|
61
|
+
self._search_path = search_path
|
|
62
|
+
self._exe_path = exe_path
|
|
63
|
+
self._port = port
|
|
64
|
+
self._process: Optional[subprocess.Popen] = None
|
|
65
|
+
self._log_path = ""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def host(self) -> str:
|
|
69
|
+
"""chromedriver 访问地址。"""
|
|
70
|
+
return "http://localhost:%d" % self._port
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def port(self) -> int:
|
|
74
|
+
return self._port
|
|
75
|
+
|
|
76
|
+
def set_exe_path(self, path: str) -> None:
|
|
77
|
+
"""直接指定 chromedriver 可执行文件路径。"""
|
|
78
|
+
self._exe_path = path
|
|
79
|
+
|
|
80
|
+
def set_search_path(self, path: str) -> None:
|
|
81
|
+
"""设置 chromedriver 存放目录。"""
|
|
82
|
+
self._search_path = path
|
|
83
|
+
|
|
84
|
+
def start(self, webview_version: int) -> None:
|
|
85
|
+
"""启动与 webview 版本匹配的 chromedriver。
|
|
86
|
+
|
|
87
|
+
若已有 chromedriver 运行且版本匹配则复用;
|
|
88
|
+
版本不匹配则停止旧进程后重启。
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
webview_version: 设备端 webview 内核版本号(如 114)
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
DevhelmError: chromedriver 未找到或启动失败
|
|
95
|
+
"""
|
|
96
|
+
chrome_path = self._resolve_path(webview_version)
|
|
97
|
+
if self._is_running():
|
|
98
|
+
current_version = self._get_version()
|
|
99
|
+
if current_version < webview_version:
|
|
100
|
+
logger.info(
|
|
101
|
+
"chromedriver 版本 %d 低于 webview 版本 %d,重启",
|
|
102
|
+
current_version, webview_version
|
|
103
|
+
)
|
|
104
|
+
self.stop()
|
|
105
|
+
time.sleep(KILL_WAIT)
|
|
106
|
+
self._start_process(chrome_path)
|
|
107
|
+
else:
|
|
108
|
+
logger.debug("chromedriver 已运行,版本 %d,复用", current_version)
|
|
109
|
+
else:
|
|
110
|
+
self._start_process(chrome_path)
|
|
111
|
+
|
|
112
|
+
def stop(self) -> None:
|
|
113
|
+
"""停止 chromedriver 进程。"""
|
|
114
|
+
if self._process is not None:
|
|
115
|
+
try:
|
|
116
|
+
self._process.terminate()
|
|
117
|
+
self._process.wait(timeout=5)
|
|
118
|
+
except subprocess.TimeoutExpired:
|
|
119
|
+
self._process.kill()
|
|
120
|
+
self._process.wait(timeout=3)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.warn("停止 chromedriver 进程异常: %s", e)
|
|
123
|
+
finally:
|
|
124
|
+
self._process = None
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
# 进程非本实例启动(残留),按名称 kill
|
|
128
|
+
proc_name = self._get_process_name()
|
|
129
|
+
for _ in range(KILL_RETRY):
|
|
130
|
+
self._kill_by_name(proc_name)
|
|
131
|
+
time.sleep(KILL_WAIT)
|
|
132
|
+
if not self._is_running_by_name(proc_name):
|
|
133
|
+
return
|
|
134
|
+
logger.warn("未能停止 chromedriver 进程")
|
|
135
|
+
|
|
136
|
+
def _resolve_path(self, version: int) -> str:
|
|
137
|
+
"""解析 chromedriver 可执行文件路径。
|
|
138
|
+
|
|
139
|
+
优先级:exe_path > search_path/chromedriver_{version}/{name} > 内置
|
|
140
|
+
"""
|
|
141
|
+
proc_name = self._get_process_name()
|
|
142
|
+
if self._exe_path:
|
|
143
|
+
path = self._exe_path
|
|
144
|
+
elif self._search_path:
|
|
145
|
+
path = os.path.join(
|
|
146
|
+
self._search_path, "chromedriver_%d" % version, proc_name
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
raise DevhelmError(
|
|
150
|
+
"未配置 chromedriver 路径,请通过 set_exe_path 或 set_search_path 设置"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not os.path.isfile(path):
|
|
154
|
+
raise DevhelmError("chromedriver 不存在: %s" % path)
|
|
155
|
+
return path
|
|
156
|
+
|
|
157
|
+
def _start_process(self, chrome_path: str) -> None:
|
|
158
|
+
"""启动 chromedriver 子进程。"""
|
|
159
|
+
if not _is_windows():
|
|
160
|
+
os.chmod(chrome_path, stat.S_IRWXU)
|
|
161
|
+
|
|
162
|
+
log_path = os.path.join(
|
|
163
|
+
_get_temp_dir(), "chromedriver.log"
|
|
164
|
+
)
|
|
165
|
+
log_level = os.getenv(CHROME_DRIVER_LOG_LEVEL_ENV, "info")
|
|
166
|
+
cmd = [
|
|
167
|
+
chrome_path,
|
|
168
|
+
"--log-level=%s" % log_level,
|
|
169
|
+
"--log-path=%s" % log_path,
|
|
170
|
+
"--port=%d" % self._port,
|
|
171
|
+
]
|
|
172
|
+
self._log_path = log_path
|
|
173
|
+
logger.info("启动 chromedriver: %s", " ".join(cmd))
|
|
174
|
+
self._process = subprocess.Popen(cmd)
|
|
175
|
+
|
|
176
|
+
def _is_running(self) -> bool:
|
|
177
|
+
"""检查 chromedriver 是否在运行(通过 HTTP /status)。"""
|
|
178
|
+
try:
|
|
179
|
+
urllib.request.urlopen(
|
|
180
|
+
self.host + "/status", timeout=2
|
|
181
|
+
).read()
|
|
182
|
+
return True
|
|
183
|
+
except Exception:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def _is_running_by_name(self, name: str) -> bool:
|
|
187
|
+
"""通过进程名检查是否运行。"""
|
|
188
|
+
if _is_windows():
|
|
189
|
+
try:
|
|
190
|
+
result = subprocess.run(
|
|
191
|
+
["tasklist"],
|
|
192
|
+
capture_output=True, text=True, timeout=10
|
|
193
|
+
)
|
|
194
|
+
return name in result.stdout
|
|
195
|
+
except Exception:
|
|
196
|
+
return False
|
|
197
|
+
else:
|
|
198
|
+
try:
|
|
199
|
+
result = subprocess.run(
|
|
200
|
+
["pgrep", "-f", name],
|
|
201
|
+
capture_output=True, text=True, timeout=10
|
|
202
|
+
)
|
|
203
|
+
return bool(result.stdout.strip())
|
|
204
|
+
except Exception:
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def _kill_by_name(self, name: str) -> None:
|
|
208
|
+
"""按进程名 kill。"""
|
|
209
|
+
if _is_windows():
|
|
210
|
+
subprocess.run(["taskkill", "/F", "/IM", name],
|
|
211
|
+
capture_output=True, timeout=10)
|
|
212
|
+
else:
|
|
213
|
+
subprocess.run(["pkill", "-f", name],
|
|
214
|
+
capture_output=True, timeout=10)
|
|
215
|
+
|
|
216
|
+
def _get_version(self) -> int:
|
|
217
|
+
"""查询运行中 chromedriver 的版本号。"""
|
|
218
|
+
for _ in range(3):
|
|
219
|
+
try:
|
|
220
|
+
resp = urllib.request.urlopen(
|
|
221
|
+
self.host + "/status", timeout=5
|
|
222
|
+
).read().decode("utf-8", errors="ignore")
|
|
223
|
+
match = __import__("re").search(r'"version":"(\d+)\.', resp)
|
|
224
|
+
if match:
|
|
225
|
+
return int(match.group(1))
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.debug("查询 chromedriver 版本失败: %s", e)
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _get_process_name() -> str:
|
|
232
|
+
"""获取当前平台的 chromedriver 进程名。"""
|
|
233
|
+
if _is_windows():
|
|
234
|
+
return "chromedriver.exe"
|
|
235
|
+
elif _is_mac():
|
|
236
|
+
return "chromedriver.mac"
|
|
237
|
+
else:
|
|
238
|
+
return "chromedriver"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _is_windows() -> bool:
|
|
242
|
+
return platform.system() == "Windows"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _is_mac() -> bool:
|
|
246
|
+
return platform.system() == "Darwin"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _get_temp_dir() -> str:
|
|
250
|
+
"""获取系统临时目录。"""
|
|
251
|
+
return os.environ.get("TEMP") or os.environ.get("TMP") or "/tmp"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""devtools 端口探测:定位设备端 webview 调试端口。
|
|
4
|
+
|
|
5
|
+
HarmonyOS webview 调试端口有两种形态:
|
|
6
|
+
1. domain socket:webview_devtools_remote_{pid}(系统 web 内核,默认)
|
|
7
|
+
2. tcp 端口:9222 等(自定义 web 内核,需应用主动开启)
|
|
8
|
+
|
|
9
|
+
探测流程:
|
|
10
|
+
1. 读取 /proc/net/unix 判断是否使用 domain socket
|
|
11
|
+
2. 通过 ps -ef | grep {bundle_name} 获取应用 PID
|
|
12
|
+
3. 在 /proc/net/unix 中匹配包含该 PID 的 devtools socket 名
|
|
13
|
+
"""
|
|
14
|
+
import re
|
|
15
|
+
import time
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
19
|
+
from devhelmkit.utils import logger
|
|
20
|
+
|
|
21
|
+
# domain socket 名称前缀
|
|
22
|
+
DOMAIN_SOCKET_PREFIX = "webview_devtools_remote_"
|
|
23
|
+
|
|
24
|
+
# 默认探测超时(秒)
|
|
25
|
+
DEFAULT_STARTUP_TIMEOUT = 8
|
|
26
|
+
|
|
27
|
+
# 轮询间隔(秒)
|
|
28
|
+
POLL_INTERVAL = 0.8
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DevtoolsFinder:
|
|
32
|
+
"""设备端 webview devtools 端口探测。"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, device: HdcDevice):
|
|
35
|
+
self._device = device
|
|
36
|
+
|
|
37
|
+
def is_using_domain_socket(self, timeout: float = DEFAULT_STARTUP_TIMEOUT) -> bool:
|
|
38
|
+
"""检查设备是否使用 domain socket 形式的 devtools。
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
timeout: 探测超时(秒),轮询直到发现或超时
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True 表示使用 domain socket,False 表示需用 tcp 端口
|
|
45
|
+
"""
|
|
46
|
+
deadline = time.time() + timeout
|
|
47
|
+
while time.time() < deadline:
|
|
48
|
+
result = self._device.shell(
|
|
49
|
+
"cat /proc/net/unix | grep %s" % DOMAIN_SOCKET_PREFIX
|
|
50
|
+
)
|
|
51
|
+
if DOMAIN_SOCKET_PREFIX in result:
|
|
52
|
+
return True
|
|
53
|
+
time.sleep(POLL_INTERVAL)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def find_devtools_socket(self, bundle_name: str,
|
|
57
|
+
timeout: float = DEFAULT_STARTUP_TIMEOUT) -> Optional[str]:
|
|
58
|
+
"""查找指定应用的 devtools domain socket 名称。
|
|
59
|
+
|
|
60
|
+
通过应用 PID 与 /proc/net/unix 中的 devtools socket 匹配。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
bundle_name: 应用包名
|
|
64
|
+
timeout: 探测超时(秒)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
socket 名称(如 webview_devtools_remote_12345),未找到返回 None
|
|
68
|
+
"""
|
|
69
|
+
deadline = time.time() + timeout
|
|
70
|
+
while time.time() < deadline:
|
|
71
|
+
devtools_sockets = self._get_devtools_sockets()
|
|
72
|
+
if not devtools_sockets:
|
|
73
|
+
time.sleep(POLL_INTERVAL)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
process_pids = self._get_bundle_pids(bundle_name)
|
|
77
|
+
if not process_pids:
|
|
78
|
+
time.sleep(POLL_INTERVAL)
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# 匹配 PID 与 socket 名称
|
|
82
|
+
for socket_name in devtools_sockets:
|
|
83
|
+
for pid in process_pids:
|
|
84
|
+
if pid in socket_name:
|
|
85
|
+
logger.debug(
|
|
86
|
+
"找到 devtools socket: %s (pid=%s, bundle=%s)",
|
|
87
|
+
socket_name, pid, bundle_name
|
|
88
|
+
)
|
|
89
|
+
return socket_name
|
|
90
|
+
time.sleep(POLL_INTERVAL)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def check_tcp_port(self, port: int) -> bool:
|
|
94
|
+
"""检查设备端 tcp devtools 端口是否开放。
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
port: 设备端 tcp 端口号
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True 表示端口已开放
|
|
101
|
+
"""
|
|
102
|
+
result = self._device.shell("netstat -tlnp | grep :%d" % port)
|
|
103
|
+
return str(port) in result
|
|
104
|
+
|
|
105
|
+
def _get_devtools_sockets(self) -> List[str]:
|
|
106
|
+
"""读取 /proc/net/unix 中所有 devtools socket 名称。"""
|
|
107
|
+
result = self._device.shell("cat /proc/net/unix | grep devtools")
|
|
108
|
+
sockets = []
|
|
109
|
+
for line in result.split("\n"):
|
|
110
|
+
items = line.split()
|
|
111
|
+
if not items:
|
|
112
|
+
continue
|
|
113
|
+
# 最后一列为 socket 名称,去掉前导 @
|
|
114
|
+
name = items[-1].strip("@")
|
|
115
|
+
if name:
|
|
116
|
+
sockets.append(name)
|
|
117
|
+
return sockets
|
|
118
|
+
|
|
119
|
+
def _get_bundle_pids(self, bundle_name: str) -> List[str]:
|
|
120
|
+
"""获取指定包名应用的所有进程 PID。"""
|
|
121
|
+
result = self._device.shell("ps -ef | grep %s" % bundle_name)
|
|
122
|
+
pids = []
|
|
123
|
+
for line in result.split("\n"):
|
|
124
|
+
items = line.split()
|
|
125
|
+
if len(items) < 8:
|
|
126
|
+
continue
|
|
127
|
+
pid = items[1]
|
|
128
|
+
actual_name = items[7]
|
|
129
|
+
if bundle_name in actual_name:
|
|
130
|
+
pids.append(pid)
|
|
131
|
+
return pids
|