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.
Files changed (58) hide show
  1. devhelmkit/__init__.py +44 -0
  2. devhelmkit/android/__init__.py +7 -0
  3. devhelmkit/android/driver.py +18 -0
  4. devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
  5. devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
  6. devhelmkit/assets/so/x86_64/agent.so +0 -0
  7. devhelmkit/core/__init__.py +3 -0
  8. devhelmkit/core/base_component.py +214 -0
  9. devhelmkit/core/base_driver.py +414 -0
  10. devhelmkit/core/base_window.py +25 -0
  11. devhelmkit/core/selector_spec.py +102 -0
  12. devhelmkit/entry.py +90 -0
  13. devhelmkit/exceptions.py +56 -0
  14. devhelmkit/harmony/__init__.py +3 -0
  15. devhelmkit/harmony/agent/__init__.py +7 -0
  16. devhelmkit/harmony/agent/so_manager.py +300 -0
  17. devhelmkit/harmony/config.py +124 -0
  18. devhelmkit/harmony/device/__init__.py +6 -0
  19. devhelmkit/harmony/device/hdc.py +430 -0
  20. devhelmkit/harmony/driver.py +1416 -0
  21. devhelmkit/harmony/finder/__init__.py +12 -0
  22. devhelmkit/harmony/finder/component_finder.py +336 -0
  23. devhelmkit/harmony/finder/popup_handler.py +81 -0
  24. devhelmkit/harmony/finder/selector_adapter.py +101 -0
  25. devhelmkit/harmony/finder/xpath_query.py +110 -0
  26. devhelmkit/harmony/rpc/__init__.py +12 -0
  27. devhelmkit/harmony/rpc/client.py +126 -0
  28. devhelmkit/harmony/rpc/proxy_v2.py +106 -0
  29. devhelmkit/harmony/rpc/remote_object.py +48 -0
  30. devhelmkit/harmony/uiobject.py +246 -0
  31. devhelmkit/harmony/uiwindow.py +43 -0
  32. devhelmkit/harmony/webview/__init__.py +17 -0
  33. devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
  34. devhelmkit/harmony/webview/devtools_finder.py +131 -0
  35. devhelmkit/harmony/webview/webview_driver.py +326 -0
  36. devhelmkit/model/__init__.py +3 -0
  37. devhelmkit/model/action.py +147 -0
  38. devhelmkit/model/app_state.py +50 -0
  39. devhelmkit/model/constants.py +22 -0
  40. devhelmkit/model/display.py +32 -0
  41. devhelmkit/model/format_string.py +24 -0
  42. devhelmkit/model/input.py +50 -0
  43. devhelmkit/model/json_base.py +42 -0
  44. devhelmkit/model/keys.py +375 -0
  45. devhelmkit/model/match_pattern.py +15 -0
  46. devhelmkit/model/page.py +13 -0
  47. devhelmkit/model/params.py +42 -0
  48. devhelmkit/model/rect.py +58 -0
  49. devhelmkit/model/runnable.py +18 -0
  50. devhelmkit/utils/__init__.py +3 -0
  51. devhelmkit/utils/logger.py +72 -0
  52. devhelmkit/utils/retry.py +46 -0
  53. devhelmkit/utils/timeout.py +64 -0
  54. devhelmkit-0.1.0.dist-info/METADATA +411 -0
  55. devhelmkit-0.1.0.dist-info/RECORD +58 -0
  56. devhelmkit-0.1.0.dist-info/WHEEL +5 -0
  57. devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
  58. 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